@socialneuron/mcp-server 1.8.2 → 1.9.1
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 +37 -0
- package/dist/http.js +1908 -649
- package/dist/index.js +1864 -680
- package/dist/sn.js +1859 -675
- package/package.json +6 -3
- package/tools.lock.json +15 -15
package/dist/index.js
CHANGED
|
@@ -19,7 +19,7 @@ var MCP_VERSION;
|
|
|
19
19
|
var init_version = __esm({
|
|
20
20
|
"src/lib/version.ts"() {
|
|
21
21
|
"use strict";
|
|
22
|
-
MCP_VERSION = "1.
|
|
22
|
+
MCP_VERSION = "1.9.1";
|
|
23
23
|
}
|
|
24
24
|
});
|
|
25
25
|
|
|
@@ -384,6 +384,209 @@ var init_request_context = __esm({
|
|
|
384
384
|
}
|
|
385
385
|
});
|
|
386
386
|
|
|
387
|
+
// src/lib/edge-function.ts
|
|
388
|
+
var edge_function_exports = {};
|
|
389
|
+
__export(edge_function_exports, {
|
|
390
|
+
callEdgeFunction: () => callEdgeFunction
|
|
391
|
+
});
|
|
392
|
+
function safeGatewayError(responseText, status) {
|
|
393
|
+
try {
|
|
394
|
+
const parsed = JSON.parse(responseText);
|
|
395
|
+
const nested = parsed.error && typeof parsed.error === "object" ? parsed.error : null;
|
|
396
|
+
const candidates = [
|
|
397
|
+
nested?.error_type,
|
|
398
|
+
nested?.code,
|
|
399
|
+
parsed.error_type,
|
|
400
|
+
parsed.error_code,
|
|
401
|
+
parsed.code,
|
|
402
|
+
typeof parsed.error === "string" ? parsed.error : null
|
|
403
|
+
];
|
|
404
|
+
for (const value of candidates) {
|
|
405
|
+
if (typeof value !== "string") continue;
|
|
406
|
+
const normalized = value.trim().toLowerCase();
|
|
407
|
+
if (SAFE_GATEWAY_ERROR_CODES.has(normalized)) return normalized;
|
|
408
|
+
const embedded = [...SAFE_GATEWAY_ERROR_CODES].find((code) => normalized.includes(code));
|
|
409
|
+
if (embedded) return embedded;
|
|
410
|
+
}
|
|
411
|
+
} catch {
|
|
412
|
+
}
|
|
413
|
+
return `Backend request failed (HTTP ${status}).`;
|
|
414
|
+
}
|
|
415
|
+
function safeFailureData(responseText) {
|
|
416
|
+
try {
|
|
417
|
+
const parsed = JSON.parse(responseText);
|
|
418
|
+
const metric = (name) => {
|
|
419
|
+
const value = parsed[name];
|
|
420
|
+
return typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : void 0;
|
|
421
|
+
};
|
|
422
|
+
const billingStatus = typeof parsed.billing_status === "string" && SAFE_BILLING_STATUSES.has(parsed.billing_status) ? parsed.billing_status : void 0;
|
|
423
|
+
const failureReason = parsed.failure_reason === "generation_failed" || parsed.failure_reason === "authentication_failed" || parsed.failure_reason === "cancelled_by_user" ? parsed.failure_reason : void 0;
|
|
424
|
+
const jobStatus = typeof parsed.status === "string" && SAFE_JOB_STATUSES.has(parsed.status) ? parsed.status : void 0;
|
|
425
|
+
const safe = {
|
|
426
|
+
...jobStatus ? { status: jobStatus } : {},
|
|
427
|
+
...metric("credits_reserved") !== void 0 ? { credits_reserved: metric("credits_reserved") } : {},
|
|
428
|
+
...metric("credits_charged") !== void 0 ? { credits_charged: metric("credits_charged") } : {},
|
|
429
|
+
...metric("credits_refunded") !== void 0 ? { credits_refunded: metric("credits_refunded") } : {},
|
|
430
|
+
...billingStatus ? { billing_status: billingStatus } : {},
|
|
431
|
+
...failureReason ? { failure_reason: failureReason } : {}
|
|
432
|
+
};
|
|
433
|
+
return Object.keys(safe).length > 0 ? safe : null;
|
|
434
|
+
} catch {
|
|
435
|
+
return null;
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
function getApiKeyOrNull() {
|
|
439
|
+
const envKey = process.env.SOCIALNEURON_API_KEY;
|
|
440
|
+
if (envKey && envKey.trim().length) return envKey.trim();
|
|
441
|
+
const requestToken = getRequestToken();
|
|
442
|
+
if (requestToken) return requestToken;
|
|
443
|
+
return getAuthenticatedApiKey();
|
|
444
|
+
}
|
|
445
|
+
async function callEdgeFunction(functionName, body, options) {
|
|
446
|
+
const supabaseUrl = getSupabaseUrl();
|
|
447
|
+
const apiKey = getApiKeyOrNull();
|
|
448
|
+
const controller = new AbortController();
|
|
449
|
+
const timeoutMs = options?.timeoutMs ?? 6e4;
|
|
450
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
451
|
+
const enrichedBody = { ...body };
|
|
452
|
+
if (!enrichedBody.userId && !enrichedBody.user_id) {
|
|
453
|
+
try {
|
|
454
|
+
const defaultId = await getDefaultUserId();
|
|
455
|
+
enrichedBody.userId = defaultId;
|
|
456
|
+
enrichedBody.user_id = defaultId;
|
|
457
|
+
} catch {
|
|
458
|
+
}
|
|
459
|
+
} else {
|
|
460
|
+
if (enrichedBody.userId && !enrichedBody.user_id) enrichedBody.user_id = enrichedBody.userId;
|
|
461
|
+
if (enrichedBody.user_id && !enrichedBody.userId) enrichedBody.userId = enrichedBody.user_id;
|
|
462
|
+
}
|
|
463
|
+
if (!enrichedBody.projectId && !enrichedBody.project_id) {
|
|
464
|
+
try {
|
|
465
|
+
const { getDefaultProjectId: getDefaultProjectId2 } = await Promise.resolve().then(() => (init_supabase(), supabase_exports));
|
|
466
|
+
const defaultProjectId = await getDefaultProjectId2();
|
|
467
|
+
if (defaultProjectId) {
|
|
468
|
+
enrichedBody.projectId = defaultProjectId;
|
|
469
|
+
enrichedBody.project_id = defaultProjectId;
|
|
470
|
+
}
|
|
471
|
+
} catch {
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
let url;
|
|
475
|
+
let method = options?.method ?? "POST";
|
|
476
|
+
let headers;
|
|
477
|
+
let requestBody;
|
|
478
|
+
if (!apiKey) {
|
|
479
|
+
clearTimeout(timer);
|
|
480
|
+
return {
|
|
481
|
+
data: null,
|
|
482
|
+
error: "Not authenticated. Run: npx @socialneuron/mcp-server login \u2014 Requires a paid plan (Starter+). See https://socialneuron.com/pricing"
|
|
483
|
+
};
|
|
484
|
+
}
|
|
485
|
+
url = new URL(`${supabaseUrl}/functions/v1/mcp-gateway`);
|
|
486
|
+
headers = {
|
|
487
|
+
Authorization: `Bearer ${apiKey}`,
|
|
488
|
+
"Content-Type": "application/json"
|
|
489
|
+
};
|
|
490
|
+
requestBody = {
|
|
491
|
+
functionName,
|
|
492
|
+
body: enrichedBody,
|
|
493
|
+
query: options?.query,
|
|
494
|
+
method: method.toUpperCase(),
|
|
495
|
+
timeoutMs
|
|
496
|
+
};
|
|
497
|
+
method = "POST";
|
|
498
|
+
try {
|
|
499
|
+
const response = await fetch(url.toString(), {
|
|
500
|
+
method,
|
|
501
|
+
headers,
|
|
502
|
+
body: method === "GET" ? void 0 : JSON.stringify(requestBody),
|
|
503
|
+
signal: controller.signal
|
|
504
|
+
});
|
|
505
|
+
clearTimeout(timer);
|
|
506
|
+
const responseText = await response.text();
|
|
507
|
+
if (!response.ok) {
|
|
508
|
+
const errorCode = safeGatewayError(responseText, response.status);
|
|
509
|
+
const failureData = safeFailureData(responseText);
|
|
510
|
+
if (response.status === 401) {
|
|
511
|
+
return {
|
|
512
|
+
data: failureData,
|
|
513
|
+
error: `Authentication failed (HTTP 401). Run 'npx @socialneuron/mcp-server login' to re-authenticate.`
|
|
514
|
+
};
|
|
515
|
+
}
|
|
516
|
+
if (response.status === 403) {
|
|
517
|
+
return {
|
|
518
|
+
data: failureData,
|
|
519
|
+
error: `Forbidden (HTTP 403): ${errorCode}. This action isn't permitted for your account, plan, or scope \u2014 your connection is still valid.`
|
|
520
|
+
};
|
|
521
|
+
}
|
|
522
|
+
if (response.status === 429) {
|
|
523
|
+
const retryAfter = response.headers.get("retry-after") || "60";
|
|
524
|
+
return {
|
|
525
|
+
data: failureData,
|
|
526
|
+
error: `Rate limit exceeded (HTTP 429). Wait ${retryAfter}s before retrying. Reduce request frequency or upgrade your plan.`
|
|
527
|
+
};
|
|
528
|
+
}
|
|
529
|
+
return { data: failureData, error: errorCode };
|
|
530
|
+
}
|
|
531
|
+
try {
|
|
532
|
+
const data = JSON.parse(responseText);
|
|
533
|
+
return { data, error: null };
|
|
534
|
+
} catch {
|
|
535
|
+
return { data: { text: responseText }, error: null };
|
|
536
|
+
}
|
|
537
|
+
} catch (err) {
|
|
538
|
+
clearTimeout(timer);
|
|
539
|
+
if (err instanceof Error && err.name === "AbortError") {
|
|
540
|
+
return {
|
|
541
|
+
data: null,
|
|
542
|
+
error: `Edge Function '${functionName}' timed out after ${timeoutMs}ms`
|
|
543
|
+
};
|
|
544
|
+
}
|
|
545
|
+
return { data: null, error: "Network request failed. Please retry." };
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
var SAFE_GATEWAY_ERROR_CODES, SAFE_BILLING_STATUSES, SAFE_JOB_STATUSES;
|
|
549
|
+
var init_edge_function = __esm({
|
|
550
|
+
"src/lib/edge-function.ts"() {
|
|
551
|
+
"use strict";
|
|
552
|
+
init_supabase();
|
|
553
|
+
init_request_context();
|
|
554
|
+
SAFE_GATEWAY_ERROR_CODES = /* @__PURE__ */ new Set([
|
|
555
|
+
"daily_limit_reached",
|
|
556
|
+
"insufficient_credits",
|
|
557
|
+
"project_scope_mismatch",
|
|
558
|
+
"schedule_conflict",
|
|
559
|
+
"post_not_found",
|
|
560
|
+
"post_not_reschedulable",
|
|
561
|
+
"post_in_progress",
|
|
562
|
+
"not_cancellable",
|
|
563
|
+
"publishing_in_progress",
|
|
564
|
+
"plan_upgrade_required",
|
|
565
|
+
"rate_limited",
|
|
566
|
+
"validation_error",
|
|
567
|
+
"permission_denied",
|
|
568
|
+
"not_found"
|
|
569
|
+
]);
|
|
570
|
+
SAFE_BILLING_STATUSES = /* @__PURE__ */ new Set([
|
|
571
|
+
"reserved",
|
|
572
|
+
"charged",
|
|
573
|
+
"refunded",
|
|
574
|
+
"failed_no_charge",
|
|
575
|
+
"refund_pending",
|
|
576
|
+
"not_charged"
|
|
577
|
+
]);
|
|
578
|
+
SAFE_JOB_STATUSES = /* @__PURE__ */ new Set([
|
|
579
|
+
"queued",
|
|
580
|
+
"pending",
|
|
581
|
+
"processing",
|
|
582
|
+
"completed",
|
|
583
|
+
"failed",
|
|
584
|
+
"cancelled",
|
|
585
|
+
"canceled"
|
|
586
|
+
]);
|
|
587
|
+
}
|
|
588
|
+
});
|
|
589
|
+
|
|
387
590
|
// src/cli/credentials.ts
|
|
388
591
|
var credentials_exports = {};
|
|
389
592
|
__export(credentials_exports, {
|
|
@@ -410,6 +613,10 @@ import {
|
|
|
410
613
|
} from "node:fs";
|
|
411
614
|
import { homedir, platform } from "node:os";
|
|
412
615
|
import { join } from "node:path";
|
|
616
|
+
function loadNativeKeyring() {
|
|
617
|
+
nativeKeyringModule ??= import("@napi-rs/keyring").catch(() => null);
|
|
618
|
+
return nativeKeyringModule;
|
|
619
|
+
}
|
|
413
620
|
function assertSafeCredentialPaths() {
|
|
414
621
|
if (platform() === "win32") return;
|
|
415
622
|
const uid = process.getuid?.();
|
|
@@ -529,50 +736,91 @@ function writeCredentialsFile(data) {
|
|
|
529
736
|
}
|
|
530
737
|
hardenCredentialPermissions();
|
|
531
738
|
}
|
|
532
|
-
function
|
|
739
|
+
function isMacKeychainItemMissing(error) {
|
|
740
|
+
const commandError = error;
|
|
741
|
+
if (commandError?.status === 44) return true;
|
|
742
|
+
const detail = `${commandError?.stderr ?? ""}
|
|
743
|
+
${commandError?.message ?? ""}`;
|
|
744
|
+
return /\berrsecitemnotfound\b/i.test(detail) || /(?:^|:\s*)the specified item could not be found in the keychain\.\s*$/i.test(detail.trim());
|
|
745
|
+
}
|
|
746
|
+
async function inspectMacKeychain(service) {
|
|
747
|
+
const native = await loadNativeKeyring();
|
|
748
|
+
if (native) {
|
|
749
|
+
try {
|
|
750
|
+
const value = new native.Entry(service, KEYCHAIN_ACCOUNT).getPassword();
|
|
751
|
+
if (value) return { status: "found", value };
|
|
752
|
+
} catch {
|
|
753
|
+
}
|
|
754
|
+
}
|
|
533
755
|
try {
|
|
534
756
|
const result = execFileSync(
|
|
535
757
|
"security",
|
|
536
758
|
["find-generic-password", "-a", KEYCHAIN_ACCOUNT, "-s", service, "-w"],
|
|
537
759
|
{ encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }
|
|
538
760
|
);
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
761
|
+
const value = result.trim();
|
|
762
|
+
return value ? { status: "found", value } : { status: "missing" };
|
|
763
|
+
} catch (error) {
|
|
764
|
+
return isMacKeychainItemMissing(error) ? { status: "missing" } : { status: "unavailable" };
|
|
542
765
|
}
|
|
543
766
|
}
|
|
544
|
-
function
|
|
767
|
+
async function macKeychainRead(service) {
|
|
768
|
+
const result = await inspectMacKeychain(service);
|
|
769
|
+
return result.status === "found" ? result.value : null;
|
|
770
|
+
}
|
|
771
|
+
async function macKeychainWrite(service, value) {
|
|
772
|
+
const native = await loadNativeKeyring();
|
|
773
|
+
if (!native) return false;
|
|
545
774
|
try {
|
|
546
|
-
|
|
547
|
-
"security",
|
|
548
|
-
[
|
|
549
|
-
"add-generic-password",
|
|
550
|
-
"-a",
|
|
551
|
-
KEYCHAIN_ACCOUNT,
|
|
552
|
-
"-s",
|
|
553
|
-
service,
|
|
554
|
-
"-w",
|
|
555
|
-
value,
|
|
556
|
-
"-U"
|
|
557
|
-
// update if exists
|
|
558
|
-
],
|
|
559
|
-
{ stdio: ["pipe", "pipe", "pipe"] }
|
|
560
|
-
);
|
|
775
|
+
new native.Entry(service, KEYCHAIN_ACCOUNT).setPassword(value);
|
|
561
776
|
return true;
|
|
562
777
|
} catch {
|
|
563
778
|
return false;
|
|
564
779
|
}
|
|
565
780
|
}
|
|
566
|
-
function macKeychainDelete(service) {
|
|
781
|
+
async function macKeychainDelete(service) {
|
|
782
|
+
const native = await loadNativeKeyring();
|
|
783
|
+
let nativeDeleted = false;
|
|
784
|
+
if (native) {
|
|
785
|
+
try {
|
|
786
|
+
nativeDeleted = new native.Entry(service, KEYCHAIN_ACCOUNT).deletePassword();
|
|
787
|
+
} catch {
|
|
788
|
+
}
|
|
789
|
+
}
|
|
567
790
|
try {
|
|
568
791
|
execFileSync("security", ["delete-generic-password", "-a", KEYCHAIN_ACCOUNT, "-s", service], {
|
|
569
792
|
stdio: ["pipe", "pipe", "pipe"]
|
|
570
793
|
});
|
|
571
|
-
return
|
|
572
|
-
} catch {
|
|
573
|
-
return
|
|
794
|
+
return "deleted";
|
|
795
|
+
} catch (error) {
|
|
796
|
+
if (isMacKeychainItemMissing(error)) return nativeDeleted ? "deleted" : "missing";
|
|
797
|
+
return "unavailable";
|
|
574
798
|
}
|
|
575
799
|
}
|
|
800
|
+
async function clearMacKeychain(service) {
|
|
801
|
+
for (let attempt = 0; attempt < 4; attempt += 1) {
|
|
802
|
+
const deleted = await macKeychainDelete(service);
|
|
803
|
+
if (deleted === "unavailable") break;
|
|
804
|
+
const remaining = await inspectMacKeychain(service);
|
|
805
|
+
if (remaining.status === "missing") return;
|
|
806
|
+
if (remaining.status === "unavailable") break;
|
|
807
|
+
}
|
|
808
|
+
throw new Error(
|
|
809
|
+
"Unable to verify removal of the existing Social Neuron Keychain credential. Unlock Keychain Access, remove the item, and retry."
|
|
810
|
+
);
|
|
811
|
+
}
|
|
812
|
+
async function verifyMacFileFallback(service) {
|
|
813
|
+
const existing = await inspectMacKeychain(service);
|
|
814
|
+
if (existing.status === "missing") return;
|
|
815
|
+
if (existing.status === "found") {
|
|
816
|
+
throw new Error(
|
|
817
|
+
"An existing Social Neuron Keychain credential could not be replaced. The existing value was retained; unlock or remove it in Keychain Access and retry."
|
|
818
|
+
);
|
|
819
|
+
}
|
|
820
|
+
throw new Error(
|
|
821
|
+
"Unable to verify absence of an existing Social Neuron Keychain credential. Unlock Keychain Access and retry."
|
|
822
|
+
);
|
|
823
|
+
}
|
|
576
824
|
function linuxSecretRead(key) {
|
|
577
825
|
try {
|
|
578
826
|
const result = execFileSync(
|
|
@@ -612,7 +860,7 @@ async function loadApiKey() {
|
|
|
612
860
|
if (envKey) return envKey;
|
|
613
861
|
const os = platform();
|
|
614
862
|
if (os === "darwin") {
|
|
615
|
-
const key = macKeychainRead(KEYCHAIN_SERVICE_API);
|
|
863
|
+
const key = await macKeychainRead(KEYCHAIN_SERVICE_API);
|
|
616
864
|
if (key) return key;
|
|
617
865
|
} else if (os === "linux") {
|
|
618
866
|
const key = linuxSecretRead("api-key");
|
|
@@ -629,13 +877,18 @@ async function loadApiKey() {
|
|
|
629
877
|
async function saveApiKey(key) {
|
|
630
878
|
const os = platform();
|
|
631
879
|
let saved = false;
|
|
880
|
+
let fallbackCredentials;
|
|
632
881
|
if (os === "darwin") {
|
|
633
|
-
saved = macKeychainWrite(KEYCHAIN_SERVICE_API, key);
|
|
882
|
+
saved = await macKeychainWrite(KEYCHAIN_SERVICE_API, key);
|
|
883
|
+
if (!saved) {
|
|
884
|
+
fallbackCredentials = readCredentialsFile();
|
|
885
|
+
await verifyMacFileFallback(KEYCHAIN_SERVICE_API);
|
|
886
|
+
}
|
|
634
887
|
} else if (os === "linux") {
|
|
635
888
|
saved = linuxSecretWrite("api-key", key);
|
|
636
889
|
}
|
|
637
890
|
if (!saved) {
|
|
638
|
-
const creds = readCredentialsFile();
|
|
891
|
+
const creds = fallbackCredentials ?? readCredentialsFile();
|
|
639
892
|
creds.apiKey = key;
|
|
640
893
|
writeCredentialsFile(creds);
|
|
641
894
|
if (os === "win32") {
|
|
@@ -658,8 +911,13 @@ async function saveApiKey(key) {
|
|
|
658
911
|
}
|
|
659
912
|
async function deleteApiKey() {
|
|
660
913
|
const os = platform();
|
|
914
|
+
let keychainError;
|
|
661
915
|
if (os === "darwin") {
|
|
662
|
-
|
|
916
|
+
try {
|
|
917
|
+
await clearMacKeychain(KEYCHAIN_SERVICE_API);
|
|
918
|
+
} catch (error) {
|
|
919
|
+
keychainError = error;
|
|
920
|
+
}
|
|
663
921
|
} else if (os === "linux") {
|
|
664
922
|
linuxSecretDelete("api-key");
|
|
665
923
|
}
|
|
@@ -675,13 +933,14 @@ async function deleteApiKey() {
|
|
|
675
933
|
writeCredentialsFile(creds);
|
|
676
934
|
}
|
|
677
935
|
}
|
|
936
|
+
if (keychainError) throw keychainError;
|
|
678
937
|
}
|
|
679
938
|
async function loadSupabaseUrl() {
|
|
680
939
|
const envUrl = process.env.SOCIALNEURON_SUPABASE_URL || process.env.SUPABASE_URL;
|
|
681
940
|
if (envUrl) return envUrl;
|
|
682
941
|
const os = platform();
|
|
683
942
|
if (os === "darwin") {
|
|
684
|
-
const url = macKeychainRead(KEYCHAIN_SERVICE_URL);
|
|
943
|
+
const url = await macKeychainRead(KEYCHAIN_SERVICE_URL);
|
|
685
944
|
if (url) return url;
|
|
686
945
|
} else if (os === "linux") {
|
|
687
946
|
const url = linuxSecretRead("supabase-url");
|
|
@@ -693,18 +952,23 @@ async function loadSupabaseUrl() {
|
|
|
693
952
|
async function saveSupabaseUrl(url) {
|
|
694
953
|
const os = platform();
|
|
695
954
|
let saved = false;
|
|
955
|
+
let fallbackCredentials;
|
|
696
956
|
if (os === "darwin") {
|
|
697
|
-
saved = macKeychainWrite(KEYCHAIN_SERVICE_URL, url);
|
|
957
|
+
saved = await macKeychainWrite(KEYCHAIN_SERVICE_URL, url);
|
|
958
|
+
if (!saved) {
|
|
959
|
+
fallbackCredentials = readCredentialsFile();
|
|
960
|
+
await verifyMacFileFallback(KEYCHAIN_SERVICE_URL);
|
|
961
|
+
}
|
|
698
962
|
} else if (os === "linux") {
|
|
699
963
|
saved = linuxSecretWrite("supabase-url", url);
|
|
700
964
|
}
|
|
701
965
|
if (!saved) {
|
|
702
|
-
const creds = readCredentialsFile();
|
|
966
|
+
const creds = fallbackCredentials ?? readCredentialsFile();
|
|
703
967
|
creds.supabaseUrl = url;
|
|
704
968
|
writeCredentialsFile(creds);
|
|
705
969
|
}
|
|
706
970
|
}
|
|
707
|
-
var KEYCHAIN_ACCOUNT, KEYCHAIN_SERVICE_API, KEYCHAIN_SERVICE_URL, CONFIG_DIR, CREDENTIALS_FILE;
|
|
971
|
+
var KEYCHAIN_ACCOUNT, KEYCHAIN_SERVICE_API, KEYCHAIN_SERVICE_URL, CONFIG_DIR, CREDENTIALS_FILE, nativeKeyringModule;
|
|
708
972
|
var init_credentials = __esm({
|
|
709
973
|
"src/cli/credentials.ts"() {
|
|
710
974
|
"use strict";
|
|
@@ -857,7 +1121,10 @@ __export(supabase_exports, {
|
|
|
857
1121
|
getSupabaseUrl: () => getSupabaseUrl,
|
|
858
1122
|
initializeAuth: () => initializeAuth,
|
|
859
1123
|
isTelemetryDisabled: () => isTelemetryDisabled,
|
|
860
|
-
|
|
1124
|
+
listAccessibleProjectsWithAccountStatus: () => listAccessibleProjectsWithAccountStatus,
|
|
1125
|
+
logMcpToolInvocation: () => logMcpToolInvocation,
|
|
1126
|
+
resolveProjectForConnectedAccountTool: () => resolveProjectForConnectedAccountTool,
|
|
1127
|
+
resolveProjectStrict: () => resolveProjectStrict
|
|
861
1128
|
});
|
|
862
1129
|
import { createClient } from "@supabase/supabase-js";
|
|
863
1130
|
import { randomUUID } from "node:crypto";
|
|
@@ -896,20 +1163,17 @@ async function getDefaultUserId() {
|
|
|
896
1163
|
if (authenticatedUserId) return authenticatedUserId;
|
|
897
1164
|
const envUserId = process.env.SOCIALNEURON_USER_ID;
|
|
898
1165
|
if (envUserId) return envUserId;
|
|
899
|
-
throw new Error(
|
|
1166
|
+
throw new Error(
|
|
1167
|
+
"No user ID available. Set SOCIALNEURON_USER_ID or authenticate via API key."
|
|
1168
|
+
);
|
|
900
1169
|
}
|
|
901
1170
|
async function getDefaultProjectId() {
|
|
902
1171
|
const requestProjectId = getRequestProjectId();
|
|
903
1172
|
if (requestProjectId) return requestProjectId;
|
|
904
1173
|
if (authenticatedProjectId) return authenticatedProjectId;
|
|
905
1174
|
const userId = await getDefaultUserId().catch(() => null);
|
|
906
|
-
if (userId) {
|
|
907
|
-
const cached = projectIdCache.get(userId);
|
|
908
|
-
if (cached) return cached;
|
|
909
|
-
}
|
|
910
1175
|
const envProjectId = process.env.SOCIALNEURON_PROJECT_ID;
|
|
911
1176
|
if (envProjectId) {
|
|
912
|
-
if (userId) projectIdCache.set(userId, envProjectId);
|
|
913
1177
|
return envProjectId;
|
|
914
1178
|
}
|
|
915
1179
|
if (!userId) return null;
|
|
@@ -918,15 +1182,126 @@ async function getDefaultProjectId() {
|
|
|
918
1182
|
const { data: memberships } = await supabase.from("organization_members").select("organization_id").eq("user_id", userId);
|
|
919
1183
|
const orgIds = (memberships ?? []).map((m) => m.organization_id);
|
|
920
1184
|
if (orgIds.length === 0) return null;
|
|
921
|
-
const { data } = await supabase.from("projects").select("id").in("organization_id", orgIds).order("created_at", { ascending: false }).limit(
|
|
922
|
-
if (data?.
|
|
923
|
-
projectIdCache.set(userId, data.id);
|
|
924
|
-
return data.id;
|
|
925
|
-
}
|
|
1185
|
+
const { data } = await supabase.from("projects").select("id").in("organization_id", orgIds).order("created_at", { ascending: false }).limit(2);
|
|
1186
|
+
if (data?.length === 1) return data[0].id;
|
|
926
1187
|
} catch {
|
|
927
1188
|
}
|
|
928
1189
|
return null;
|
|
929
1190
|
}
|
|
1191
|
+
function normalizePlatformFilter(platform3) {
|
|
1192
|
+
if (!platform3) return null;
|
|
1193
|
+
const values = Array.isArray(platform3) ? platform3 : [platform3];
|
|
1194
|
+
const set = new Set(values.filter(Boolean).map((p) => p.toLowerCase()));
|
|
1195
|
+
return set.size > 0 ? set : null;
|
|
1196
|
+
}
|
|
1197
|
+
async function listAccessibleProjectsWithAccountStatusDirect(userId, platform3) {
|
|
1198
|
+
try {
|
|
1199
|
+
const supabase = getSupabaseClient();
|
|
1200
|
+
const { data: memberships } = await supabase.from("organization_members").select("organization_id").eq("user_id", userId);
|
|
1201
|
+
const orgIds = (memberships ?? []).map((m) => m.organization_id);
|
|
1202
|
+
if (orgIds.length === 0) return [];
|
|
1203
|
+
const { data: projects } = await supabase.from("projects").select("id, name").in("organization_id", orgIds).order("created_at", { ascending: false });
|
|
1204
|
+
const projectRows = projects ?? [];
|
|
1205
|
+
if (projectRows.length === 0) return [];
|
|
1206
|
+
const projectIds = projectRows.map((p) => p.id);
|
|
1207
|
+
const { data: accounts } = await supabase.from("connected_accounts").select("project_id, status, platform").eq("user_id", userId).in("project_id", projectIds);
|
|
1208
|
+
const anyAccountByProject = /* @__PURE__ */ new Set();
|
|
1209
|
+
const platformsByProject = /* @__PURE__ */ new Map();
|
|
1210
|
+
for (const row of accounts ?? []) {
|
|
1211
|
+
if (row.status !== "active" && row.status !== "expires_soon") continue;
|
|
1212
|
+
if (!row.project_id) continue;
|
|
1213
|
+
anyAccountByProject.add(row.project_id);
|
|
1214
|
+
if (row.platform) {
|
|
1215
|
+
const set = platformsByProject.get(row.project_id) ?? /* @__PURE__ */ new Set();
|
|
1216
|
+
set.add(row.platform.toLowerCase());
|
|
1217
|
+
platformsByProject.set(row.project_id, set);
|
|
1218
|
+
}
|
|
1219
|
+
}
|
|
1220
|
+
const requestedPlatforms = normalizePlatformFilter(platform3);
|
|
1221
|
+
return projectRows.map((p) => {
|
|
1222
|
+
const platforms = Array.from(platformsByProject.get(p.id) ?? []);
|
|
1223
|
+
const hasConnectedAccounts = requestedPlatforms ? platforms.some((pl) => requestedPlatforms.has(pl)) : anyAccountByProject.has(p.id);
|
|
1224
|
+
return { id: p.id, name: p.name, hasConnectedAccounts, platforms };
|
|
1225
|
+
});
|
|
1226
|
+
} catch {
|
|
1227
|
+
return [];
|
|
1228
|
+
}
|
|
1229
|
+
}
|
|
1230
|
+
async function listAccessibleProjectsWithAccountStatusViaEdgeFunction(userId, platform3) {
|
|
1231
|
+
try {
|
|
1232
|
+
const { callEdgeFunction: callEdgeFunction2 } = await Promise.resolve().then(() => (init_edge_function(), edge_function_exports));
|
|
1233
|
+
const { data, error } = await callEdgeFunction2(
|
|
1234
|
+
"mcp-data",
|
|
1235
|
+
{ action: "projects", userId, user_id: userId },
|
|
1236
|
+
{ timeoutMs: 1e4 }
|
|
1237
|
+
);
|
|
1238
|
+
if (error || !data?.success || !Array.isArray(data.projects)) return [];
|
|
1239
|
+
const requestedPlatforms = normalizePlatformFilter(platform3);
|
|
1240
|
+
return data.projects.map((p) => {
|
|
1241
|
+
const platforms = (p.platforms ?? []).map((pl) => pl.toLowerCase());
|
|
1242
|
+
const hasConnectedAccounts = requestedPlatforms ? platforms.some((pl) => requestedPlatforms.has(pl)) : Boolean(p.hasConnectedAccounts);
|
|
1243
|
+
return { id: p.id, name: p.name, hasConnectedAccounts, platforms };
|
|
1244
|
+
});
|
|
1245
|
+
} catch {
|
|
1246
|
+
return [];
|
|
1247
|
+
}
|
|
1248
|
+
}
|
|
1249
|
+
async function listAccessibleProjectsWithAccountStatus(userId, platform3) {
|
|
1250
|
+
if (getServiceKeyOrNull()) {
|
|
1251
|
+
return listAccessibleProjectsWithAccountStatusDirect(userId, platform3);
|
|
1252
|
+
}
|
|
1253
|
+
return listAccessibleProjectsWithAccountStatusViaEdgeFunction(
|
|
1254
|
+
userId,
|
|
1255
|
+
platform3
|
|
1256
|
+
);
|
|
1257
|
+
}
|
|
1258
|
+
async function resolveProjectForConnectedAccountTool(explicitProjectId, platform3) {
|
|
1259
|
+
if (explicitProjectId) return { projectId: explicitProjectId };
|
|
1260
|
+
const defaultProjectId = await getDefaultProjectId();
|
|
1261
|
+
if (defaultProjectId) return { projectId: defaultProjectId };
|
|
1262
|
+
const genericError = "project_id is required. Configure an explicit project or use an API key scoped to exactly one project.";
|
|
1263
|
+
const userId = await getDefaultUserId().catch(() => null);
|
|
1264
|
+
if (!userId) return { error: genericError };
|
|
1265
|
+
const projects = await listAccessibleProjectsWithAccountStatus(
|
|
1266
|
+
userId,
|
|
1267
|
+
platform3
|
|
1268
|
+
);
|
|
1269
|
+
if (projects.length === 0) return { error: genericError };
|
|
1270
|
+
const withAccounts = projects.filter((p) => p.hasConnectedAccounts);
|
|
1271
|
+
if (withAccounts.length === 1) {
|
|
1272
|
+
const chosen = withAccounts[0];
|
|
1273
|
+
const platformNote = platform3 ? ` for ${Array.isArray(platform3) ? platform3.join("/") : platform3}` : "";
|
|
1274
|
+
return {
|
|
1275
|
+
projectId: chosen.id,
|
|
1276
|
+
autoResolvedNote: `project_id was not provided; auto-resolved to "${chosen.name}" (${chosen.id}) \u2014 the only one of your ${projects.length} project(s) with an active connected account${platformNote}.`,
|
|
1277
|
+
projects
|
|
1278
|
+
};
|
|
1279
|
+
}
|
|
1280
|
+
const projectList = projects.map(
|
|
1281
|
+
(p) => `${p.name} (${p.id}${p.hasConnectedAccounts ? ", has connected accounts" : ""})`
|
|
1282
|
+
).join("; ");
|
|
1283
|
+
return {
|
|
1284
|
+
error: `project_id is required \u2014 your account has ${projects.length} projects and the target could not be auto-resolved. Pass the exact project_id from this list: ${projectList}.`,
|
|
1285
|
+
projects
|
|
1286
|
+
};
|
|
1287
|
+
}
|
|
1288
|
+
async function resolveProjectStrict(explicitProjectId) {
|
|
1289
|
+
if (explicitProjectId) return { projectId: explicitProjectId };
|
|
1290
|
+
const defaultProjectId = await getDefaultProjectId();
|
|
1291
|
+
if (defaultProjectId) return { projectId: defaultProjectId };
|
|
1292
|
+
const genericError = "project_id is required. Configure an explicit project or use an API key scoped to exactly one project.";
|
|
1293
|
+
const userId = await getDefaultUserId().catch(() => null);
|
|
1294
|
+
if (!userId) return { error: genericError };
|
|
1295
|
+
const projects = await listAccessibleProjectsWithAccountStatus(userId);
|
|
1296
|
+
if (projects.length === 0) return { error: genericError };
|
|
1297
|
+
const projectList = projects.map(
|
|
1298
|
+
(p) => `${p.name} (${p.id}${p.hasConnectedAccounts ? ", has connected accounts" : ""})`
|
|
1299
|
+
).join("; ");
|
|
1300
|
+
return {
|
|
1301
|
+
error: `project_id is required \u2014 your account has ${projects.length} projects. Pass the exact project_id from this list: ${projectList}.`,
|
|
1302
|
+
projects
|
|
1303
|
+
};
|
|
1304
|
+
}
|
|
930
1305
|
async function initializeAuth() {
|
|
931
1306
|
const { loadApiKey: loadApiKey2 } = await Promise.resolve().then(() => (init_credentials(), credentials_exports));
|
|
932
1307
|
const apiKey = await loadApiKey2();
|
|
@@ -952,8 +1327,11 @@ async function initializeAuth() {
|
|
|
952
1327
|
}
|
|
953
1328
|
if (authenticatedExpiresAt) {
|
|
954
1329
|
const expiresMs = new Date(authenticatedExpiresAt).getTime();
|
|
955
|
-
const daysLeft = Math.ceil(
|
|
956
|
-
|
|
1330
|
+
const daysLeft = Math.ceil(
|
|
1331
|
+
(expiresMs - Date.now()) / (1e3 * 60 * 60 * 24)
|
|
1332
|
+
);
|
|
1333
|
+
if (!_quietAuth)
|
|
1334
|
+
console.error("[MCP] Key expires: " + authenticatedExpiresAt);
|
|
957
1335
|
if (daysLeft <= 7) {
|
|
958
1336
|
console.error(
|
|
959
1337
|
`[MCP] Warning: API key expires in ${daysLeft} day(s). Run: npx @socialneuron/mcp-server login`
|
|
@@ -1032,7 +1410,7 @@ async function logMcpToolInvocation(args) {
|
|
|
1032
1410
|
Promise.resolve().then(() => (init_posthog(), posthog_exports)).then(({ captureToolEvent: captureToolEvent2 }) => captureToolEvent2(args)).catch(() => {
|
|
1033
1411
|
});
|
|
1034
1412
|
}
|
|
1035
|
-
var SUPABASE_URL, SUPABASE_SERVICE_KEY, client2, _authMode, authenticatedUserId, authenticatedScopes, authenticatedEmail, authenticatedExpiresAt, authenticatedApiKey, authenticatedProjectId, MCP_RUN_ID, CLOUD_SUPABASE_URL, CLOUD_SUPABASE_ANON_KEY
|
|
1413
|
+
var SUPABASE_URL, SUPABASE_SERVICE_KEY, client2, _authMode, authenticatedUserId, authenticatedScopes, authenticatedEmail, authenticatedExpiresAt, authenticatedApiKey, authenticatedProjectId, MCP_RUN_ID, CLOUD_SUPABASE_URL, CLOUD_SUPABASE_ANON_KEY;
|
|
1036
1414
|
var init_supabase = __esm({
|
|
1037
1415
|
"src/lib/supabase.ts"() {
|
|
1038
1416
|
"use strict";
|
|
@@ -1050,7 +1428,6 @@ var init_supabase = __esm({
|
|
|
1050
1428
|
MCP_RUN_ID = randomUUID();
|
|
1051
1429
|
CLOUD_SUPABASE_URL = "https://rhukkjscgzauutioyeei.supabase.co";
|
|
1052
1430
|
CLOUD_SUPABASE_ANON_KEY = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6InJodWtranNjZ3phdXV0aW95ZWVpIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NjQ4NjM4ODYsImV4cCI6MjA4MDQzOTg4Nn0.JVtrviGvN0HaSh0JFS5KNl5FAB5ffG5Y1IMZsQFUrNQ";
|
|
1053
|
-
projectIdCache = /* @__PURE__ */ new Map();
|
|
1054
1431
|
}
|
|
1055
1432
|
});
|
|
1056
1433
|
|
|
@@ -2016,269 +2393,66 @@ var COMPILED;
|
|
|
2016
2393
|
var init_pii = __esm({
|
|
2017
2394
|
"src/lib/agent-harness/detectors/pii.ts"() {
|
|
2018
2395
|
"use strict";
|
|
2019
|
-
init_constants2();
|
|
2020
|
-
COMPILED = Object.entries(CONSTANTS.PII_PATTERNS).map(
|
|
2021
|
-
([name, pattern]) => ({ name, re: new RegExp(pattern, "gi") })
|
|
2022
|
-
);
|
|
2023
|
-
}
|
|
2024
|
-
});
|
|
2025
|
-
|
|
2026
|
-
// src/lib/agent-harness/scanner.ts
|
|
2027
|
-
function scan(text, options) {
|
|
2028
|
-
const flagged = /* @__PURE__ */ new Set();
|
|
2029
|
-
let risk = 0;
|
|
2030
|
-
const maxLength = options.source === "mcp_tool_output" ? CONSTANTS.MAX_OUTPUT_LENGTH : CONSTANTS.MAX_LENGTH;
|
|
2031
|
-
if (text.length > maxLength) {
|
|
2032
|
-
return {
|
|
2033
|
-
passed: false,
|
|
2034
|
-
risk_score: 1,
|
|
2035
|
-
flagged_patterns: ["excessive_length"],
|
|
2036
|
-
pii_redacted: false
|
|
2037
|
-
};
|
|
2038
|
-
}
|
|
2039
|
-
const zw = detectZeroWidth(text);
|
|
2040
|
-
if (zw.found) {
|
|
2041
|
-
flagged.add(zw.pattern);
|
|
2042
|
-
risk = Math.max(risk, 0.95);
|
|
2043
|
-
}
|
|
2044
|
-
const ipRaw = detectInstructionPhrase(text);
|
|
2045
|
-
if (ipRaw.found) {
|
|
2046
|
-
flagged.add(ipRaw.pattern);
|
|
2047
|
-
risk = Math.max(risk, 0.9);
|
|
2048
|
-
}
|
|
2049
|
-
const normalized = normalize(text);
|
|
2050
|
-
const ipNorm = detectInstructionPhrase(normalized);
|
|
2051
|
-
if (ipNorm.found) {
|
|
2052
|
-
flagged.add(ipNorm.pattern);
|
|
2053
|
-
risk = Math.max(risk, 0.9);
|
|
2054
|
-
}
|
|
2055
|
-
const flaggedArr = Array.from(flagged);
|
|
2056
|
-
if ((options.mode === "block" || options.mode === "sanitize") && flaggedArr.length > 0) {
|
|
2057
|
-
return { passed: false, risk_score: risk, flagged_patterns: flaggedArr, pii_redacted: false };
|
|
2058
|
-
}
|
|
2059
|
-
const pii = scrubPii(normalized, options.source);
|
|
2060
|
-
if (pii.redacted) {
|
|
2061
|
-
return {
|
|
2062
|
-
passed: true,
|
|
2063
|
-
risk_score: Math.max(risk, 0.3),
|
|
2064
|
-
flagged_patterns: [...flaggedArr, ...pii.patterns.map((p) => `pii_${p}`)],
|
|
2065
|
-
sanitized_text: pii.text,
|
|
2066
|
-
pii_redacted: true
|
|
2067
|
-
};
|
|
2068
|
-
}
|
|
2069
|
-
return { passed: true, risk_score: risk, flagged_patterns: flaggedArr, pii_redacted: false };
|
|
2070
|
-
}
|
|
2071
|
-
var init_scanner = __esm({
|
|
2072
|
-
"src/lib/agent-harness/scanner.ts"() {
|
|
2073
|
-
"use strict";
|
|
2074
|
-
init_constants2();
|
|
2075
|
-
init_normalize();
|
|
2076
|
-
init_zeroWidth();
|
|
2077
|
-
init_instructionPhrase();
|
|
2078
|
-
init_pii();
|
|
2079
|
-
}
|
|
2080
|
-
});
|
|
2081
|
-
|
|
2082
|
-
// src/lib/edge-function.ts
|
|
2083
|
-
var edge_function_exports = {};
|
|
2084
|
-
__export(edge_function_exports, {
|
|
2085
|
-
callEdgeFunction: () => callEdgeFunction
|
|
2086
|
-
});
|
|
2087
|
-
function safeGatewayError(responseText, status) {
|
|
2088
|
-
try {
|
|
2089
|
-
const parsed = JSON.parse(responseText);
|
|
2090
|
-
const nested = parsed.error && typeof parsed.error === "object" ? parsed.error : null;
|
|
2091
|
-
const candidates = [
|
|
2092
|
-
nested?.error_type,
|
|
2093
|
-
nested?.code,
|
|
2094
|
-
parsed.error_type,
|
|
2095
|
-
parsed.error_code,
|
|
2096
|
-
parsed.code,
|
|
2097
|
-
typeof parsed.error === "string" ? parsed.error : null
|
|
2098
|
-
];
|
|
2099
|
-
for (const value of candidates) {
|
|
2100
|
-
if (typeof value !== "string") continue;
|
|
2101
|
-
const normalized = value.trim().toLowerCase();
|
|
2102
|
-
if (SAFE_GATEWAY_ERROR_CODES.has(normalized)) return normalized;
|
|
2103
|
-
const embedded = [...SAFE_GATEWAY_ERROR_CODES].find((code) => normalized.includes(code));
|
|
2104
|
-
if (embedded) return embedded;
|
|
2105
|
-
}
|
|
2106
|
-
} catch {
|
|
2107
|
-
}
|
|
2108
|
-
return `Backend request failed (HTTP ${status}).`;
|
|
2109
|
-
}
|
|
2110
|
-
function safeFailureData(responseText) {
|
|
2111
|
-
try {
|
|
2112
|
-
const parsed = JSON.parse(responseText);
|
|
2113
|
-
const metric = (name) => {
|
|
2114
|
-
const value = parsed[name];
|
|
2115
|
-
return typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : void 0;
|
|
2116
|
-
};
|
|
2117
|
-
const billingStatus = typeof parsed.billing_status === "string" && SAFE_BILLING_STATUSES.has(parsed.billing_status) ? parsed.billing_status : void 0;
|
|
2118
|
-
const failureReason = parsed.failure_reason === "generation_failed" || parsed.failure_reason === "authentication_failed" || parsed.failure_reason === "cancelled_by_user" ? parsed.failure_reason : void 0;
|
|
2119
|
-
const jobStatus = typeof parsed.status === "string" && SAFE_JOB_STATUSES.has(parsed.status) ? parsed.status : void 0;
|
|
2120
|
-
const safe = {
|
|
2121
|
-
...jobStatus ? { status: jobStatus } : {},
|
|
2122
|
-
...metric("credits_reserved") !== void 0 ? { credits_reserved: metric("credits_reserved") } : {},
|
|
2123
|
-
...metric("credits_charged") !== void 0 ? { credits_charged: metric("credits_charged") } : {},
|
|
2124
|
-
...metric("credits_refunded") !== void 0 ? { credits_refunded: metric("credits_refunded") } : {},
|
|
2125
|
-
...billingStatus ? { billing_status: billingStatus } : {},
|
|
2126
|
-
...failureReason ? { failure_reason: failureReason } : {}
|
|
2127
|
-
};
|
|
2128
|
-
return Object.keys(safe).length > 0 ? safe : null;
|
|
2129
|
-
} catch {
|
|
2130
|
-
return null;
|
|
2131
|
-
}
|
|
2132
|
-
}
|
|
2133
|
-
function getApiKeyOrNull() {
|
|
2134
|
-
const envKey = process.env.SOCIALNEURON_API_KEY;
|
|
2135
|
-
if (envKey && envKey.trim().length) return envKey.trim();
|
|
2136
|
-
const requestToken = getRequestToken();
|
|
2137
|
-
if (requestToken) return requestToken;
|
|
2138
|
-
return getAuthenticatedApiKey();
|
|
2139
|
-
}
|
|
2140
|
-
async function callEdgeFunction(functionName, body, options) {
|
|
2141
|
-
const supabaseUrl = getSupabaseUrl();
|
|
2142
|
-
const apiKey = getApiKeyOrNull();
|
|
2143
|
-
const controller = new AbortController();
|
|
2144
|
-
const timeoutMs = options?.timeoutMs ?? 6e4;
|
|
2145
|
-
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
2146
|
-
const enrichedBody = { ...body };
|
|
2147
|
-
if (!enrichedBody.userId && !enrichedBody.user_id) {
|
|
2148
|
-
try {
|
|
2149
|
-
const defaultId = await getDefaultUserId();
|
|
2150
|
-
enrichedBody.userId = defaultId;
|
|
2151
|
-
enrichedBody.user_id = defaultId;
|
|
2152
|
-
} catch {
|
|
2153
|
-
}
|
|
2154
|
-
} else {
|
|
2155
|
-
if (enrichedBody.userId && !enrichedBody.user_id) enrichedBody.user_id = enrichedBody.userId;
|
|
2156
|
-
if (enrichedBody.user_id && !enrichedBody.userId) enrichedBody.userId = enrichedBody.user_id;
|
|
2157
|
-
}
|
|
2158
|
-
if (!enrichedBody.projectId && !enrichedBody.project_id) {
|
|
2159
|
-
try {
|
|
2160
|
-
const { getDefaultProjectId: getDefaultProjectId2 } = await Promise.resolve().then(() => (init_supabase(), supabase_exports));
|
|
2161
|
-
const defaultProjectId = await getDefaultProjectId2();
|
|
2162
|
-
if (defaultProjectId) {
|
|
2163
|
-
enrichedBody.projectId = defaultProjectId;
|
|
2164
|
-
enrichedBody.project_id = defaultProjectId;
|
|
2165
|
-
}
|
|
2166
|
-
} catch {
|
|
2167
|
-
}
|
|
2396
|
+
init_constants2();
|
|
2397
|
+
COMPILED = Object.entries(CONSTANTS.PII_PATTERNS).map(
|
|
2398
|
+
([name, pattern]) => ({ name, re: new RegExp(pattern, "gi") })
|
|
2399
|
+
);
|
|
2168
2400
|
}
|
|
2169
|
-
|
|
2170
|
-
|
|
2171
|
-
|
|
2172
|
-
|
|
2173
|
-
|
|
2174
|
-
|
|
2401
|
+
});
|
|
2402
|
+
|
|
2403
|
+
// src/lib/agent-harness/scanner.ts
|
|
2404
|
+
function scan(text, options) {
|
|
2405
|
+
const flagged = /* @__PURE__ */ new Set();
|
|
2406
|
+
let risk = 0;
|
|
2407
|
+
const maxLength = options.source === "mcp_tool_output" ? CONSTANTS.MAX_OUTPUT_LENGTH : CONSTANTS.MAX_LENGTH;
|
|
2408
|
+
if (text.length > maxLength) {
|
|
2175
2409
|
return {
|
|
2176
|
-
|
|
2177
|
-
|
|
2410
|
+
passed: false,
|
|
2411
|
+
risk_score: 1,
|
|
2412
|
+
flagged_patterns: ["excessive_length"],
|
|
2413
|
+
pii_redacted: false
|
|
2178
2414
|
};
|
|
2179
2415
|
}
|
|
2180
|
-
|
|
2181
|
-
|
|
2182
|
-
|
|
2183
|
-
|
|
2184
|
-
}
|
|
2185
|
-
|
|
2186
|
-
|
|
2187
|
-
|
|
2188
|
-
|
|
2189
|
-
|
|
2190
|
-
|
|
2191
|
-
|
|
2192
|
-
|
|
2193
|
-
|
|
2194
|
-
|
|
2195
|
-
|
|
2196
|
-
|
|
2197
|
-
|
|
2198
|
-
|
|
2199
|
-
|
|
2200
|
-
|
|
2201
|
-
|
|
2202
|
-
|
|
2203
|
-
|
|
2204
|
-
|
|
2205
|
-
|
|
2206
|
-
|
|
2207
|
-
|
|
2208
|
-
|
|
2209
|
-
};
|
|
2210
|
-
}
|
|
2211
|
-
if (response.status === 403) {
|
|
2212
|
-
return {
|
|
2213
|
-
data: failureData,
|
|
2214
|
-
error: `Forbidden (HTTP 403): ${errorCode}. This action isn't permitted for your account, plan, or scope \u2014 your connection is still valid.`
|
|
2215
|
-
};
|
|
2216
|
-
}
|
|
2217
|
-
if (response.status === 429) {
|
|
2218
|
-
const retryAfter = response.headers.get("retry-after") || "60";
|
|
2219
|
-
return {
|
|
2220
|
-
data: failureData,
|
|
2221
|
-
error: `Rate limit exceeded (HTTP 429). Wait ${retryAfter}s before retrying. Reduce request frequency or upgrade your plan.`
|
|
2222
|
-
};
|
|
2223
|
-
}
|
|
2224
|
-
return { data: failureData, error: errorCode };
|
|
2225
|
-
}
|
|
2226
|
-
try {
|
|
2227
|
-
const data = JSON.parse(responseText);
|
|
2228
|
-
return { data, error: null };
|
|
2229
|
-
} catch {
|
|
2230
|
-
return { data: { text: responseText }, error: null };
|
|
2231
|
-
}
|
|
2232
|
-
} catch (err) {
|
|
2233
|
-
clearTimeout(timer);
|
|
2234
|
-
if (err instanceof Error && err.name === "AbortError") {
|
|
2235
|
-
return {
|
|
2236
|
-
data: null,
|
|
2237
|
-
error: `Edge Function '${functionName}' timed out after ${timeoutMs}ms`
|
|
2238
|
-
};
|
|
2239
|
-
}
|
|
2240
|
-
return { data: null, error: "Network request failed. Please retry." };
|
|
2416
|
+
const zw = detectZeroWidth(text);
|
|
2417
|
+
if (zw.found) {
|
|
2418
|
+
flagged.add(zw.pattern);
|
|
2419
|
+
risk = Math.max(risk, 0.95);
|
|
2420
|
+
}
|
|
2421
|
+
const ipRaw = detectInstructionPhrase(text);
|
|
2422
|
+
if (ipRaw.found) {
|
|
2423
|
+
flagged.add(ipRaw.pattern);
|
|
2424
|
+
risk = Math.max(risk, 0.9);
|
|
2425
|
+
}
|
|
2426
|
+
const normalized = normalize(text);
|
|
2427
|
+
const ipNorm = detectInstructionPhrase(normalized);
|
|
2428
|
+
if (ipNorm.found) {
|
|
2429
|
+
flagged.add(ipNorm.pattern);
|
|
2430
|
+
risk = Math.max(risk, 0.9);
|
|
2431
|
+
}
|
|
2432
|
+
const flaggedArr = Array.from(flagged);
|
|
2433
|
+
if ((options.mode === "block" || options.mode === "sanitize") && flaggedArr.length > 0) {
|
|
2434
|
+
return { passed: false, risk_score: risk, flagged_patterns: flaggedArr, pii_redacted: false };
|
|
2435
|
+
}
|
|
2436
|
+
const pii = scrubPii(normalized, options.source);
|
|
2437
|
+
if (pii.redacted) {
|
|
2438
|
+
return {
|
|
2439
|
+
passed: true,
|
|
2440
|
+
risk_score: Math.max(risk, 0.3),
|
|
2441
|
+
flagged_patterns: [...flaggedArr, ...pii.patterns.map((p) => `pii_${p}`)],
|
|
2442
|
+
sanitized_text: pii.text,
|
|
2443
|
+
pii_redacted: true
|
|
2444
|
+
};
|
|
2241
2445
|
}
|
|
2446
|
+
return { passed: true, risk_score: risk, flagged_patterns: flaggedArr, pii_redacted: false };
|
|
2242
2447
|
}
|
|
2243
|
-
var
|
|
2244
|
-
|
|
2245
|
-
"src/lib/edge-function.ts"() {
|
|
2448
|
+
var init_scanner = __esm({
|
|
2449
|
+
"src/lib/agent-harness/scanner.ts"() {
|
|
2246
2450
|
"use strict";
|
|
2247
|
-
|
|
2248
|
-
|
|
2249
|
-
|
|
2250
|
-
|
|
2251
|
-
|
|
2252
|
-
"project_scope_mismatch",
|
|
2253
|
-
"schedule_conflict",
|
|
2254
|
-
"post_not_found",
|
|
2255
|
-
"post_not_reschedulable",
|
|
2256
|
-
"post_in_progress",
|
|
2257
|
-
"not_cancellable",
|
|
2258
|
-
"publishing_in_progress",
|
|
2259
|
-
"plan_upgrade_required",
|
|
2260
|
-
"rate_limited",
|
|
2261
|
-
"validation_error",
|
|
2262
|
-
"permission_denied",
|
|
2263
|
-
"not_found"
|
|
2264
|
-
]);
|
|
2265
|
-
SAFE_BILLING_STATUSES = /* @__PURE__ */ new Set([
|
|
2266
|
-
"reserved",
|
|
2267
|
-
"charged",
|
|
2268
|
-
"refunded",
|
|
2269
|
-
"failed_no_charge",
|
|
2270
|
-
"refund_pending",
|
|
2271
|
-
"not_charged"
|
|
2272
|
-
]);
|
|
2273
|
-
SAFE_JOB_STATUSES = /* @__PURE__ */ new Set([
|
|
2274
|
-
"queued",
|
|
2275
|
-
"pending",
|
|
2276
|
-
"processing",
|
|
2277
|
-
"completed",
|
|
2278
|
-
"failed",
|
|
2279
|
-
"cancelled",
|
|
2280
|
-
"canceled"
|
|
2281
|
-
]);
|
|
2451
|
+
init_constants2();
|
|
2452
|
+
init_normalize();
|
|
2453
|
+
init_zeroWidth();
|
|
2454
|
+
init_instructionPhrase();
|
|
2455
|
+
init_pii();
|
|
2282
2456
|
}
|
|
2283
2457
|
});
|
|
2284
2458
|
|
|
@@ -2770,6 +2944,92 @@ var init_ideation = __esm({
|
|
|
2770
2944
|
}
|
|
2771
2945
|
});
|
|
2772
2946
|
|
|
2947
|
+
// src/lib/sanitize-error.ts
|
|
2948
|
+
function redactSensitiveIdentifiers(message) {
|
|
2949
|
+
return message.replace(EMAIL_PATTERN, "[redacted-email]").replace(UUID_PATTERN, "[redacted-id]");
|
|
2950
|
+
}
|
|
2951
|
+
function sanitizeError(error) {
|
|
2952
|
+
const msg = error instanceof Error ? error.message : typeof error === "string" ? error : "Unknown error";
|
|
2953
|
+
for (const [pattern, userMessage] of ERROR_PATTERNS) {
|
|
2954
|
+
if (pattern.test(msg)) {
|
|
2955
|
+
return userMessage;
|
|
2956
|
+
}
|
|
2957
|
+
}
|
|
2958
|
+
return "An unexpected error occurred. Please try again.";
|
|
2959
|
+
}
|
|
2960
|
+
var ERROR_PATTERNS, UUID_PATTERN, EMAIL_PATTERN;
|
|
2961
|
+
var init_sanitize_error = __esm({
|
|
2962
|
+
"src/lib/sanitize-error.ts"() {
|
|
2963
|
+
"use strict";
|
|
2964
|
+
ERROR_PATTERNS = [
|
|
2965
|
+
// Postgres / PostgREST
|
|
2966
|
+
[
|
|
2967
|
+
/PGRST301|permission denied/i,
|
|
2968
|
+
"Access denied. Check your account permissions."
|
|
2969
|
+
],
|
|
2970
|
+
[
|
|
2971
|
+
/42P01|does not exist/i,
|
|
2972
|
+
"Service temporarily unavailable. Please try again."
|
|
2973
|
+
],
|
|
2974
|
+
[
|
|
2975
|
+
/23505|unique.*constraint|duplicate key/i,
|
|
2976
|
+
"A duplicate record already exists."
|
|
2977
|
+
],
|
|
2978
|
+
[/23503|foreign key/i, "Referenced record not found."],
|
|
2979
|
+
// Gemini / Google AI
|
|
2980
|
+
[
|
|
2981
|
+
/google.*api.*key|googleapis\.com.*40[13]/i,
|
|
2982
|
+
"Content generation failed. Please try again."
|
|
2983
|
+
],
|
|
2984
|
+
[
|
|
2985
|
+
/RESOURCE_EXHAUSTED|quota.*exceeded|429.*google/i,
|
|
2986
|
+
"AI service rate limit reached. Please wait and retry."
|
|
2987
|
+
],
|
|
2988
|
+
[
|
|
2989
|
+
/SAFETY|prompt.*blocked|content.*filter/i,
|
|
2990
|
+
"Content was blocked by the AI safety filter. Try rephrasing."
|
|
2991
|
+
],
|
|
2992
|
+
[
|
|
2993
|
+
/gemini.*error|generativelanguage/i,
|
|
2994
|
+
"Content generation failed. Please try again."
|
|
2995
|
+
],
|
|
2996
|
+
// Kie.ai
|
|
2997
|
+
[/kie\.ai|kieai|kie_api/i, "Media generation failed. Please try again."],
|
|
2998
|
+
// Stripe
|
|
2999
|
+
[
|
|
3000
|
+
/stripe.*api|sk_live_|sk_test_/i,
|
|
3001
|
+
"Payment processing error. Please try again."
|
|
3002
|
+
],
|
|
3003
|
+
// Network / fetch
|
|
3004
|
+
[
|
|
3005
|
+
/ECONNREFUSED|ENOTFOUND|ETIMEDOUT|ECONNRESET/i,
|
|
3006
|
+
"External service unavailable. Please try again."
|
|
3007
|
+
],
|
|
3008
|
+
[
|
|
3009
|
+
/fetch failed|network error|abort.*timeout/i,
|
|
3010
|
+
"Network request failed. Please try again."
|
|
3011
|
+
],
|
|
3012
|
+
[/CERT_|certificate|SSL|TLS/i, "Secure connection failed. Please try again."],
|
|
3013
|
+
// Supabase Edge Function internals
|
|
3014
|
+
[
|
|
3015
|
+
/FunctionsHttpError|non-2xx status/i,
|
|
3016
|
+
"Backend service error. Please try again."
|
|
3017
|
+
],
|
|
3018
|
+
[
|
|
3019
|
+
/JWT|token.*expired|token.*invalid/i,
|
|
3020
|
+
"Authentication expired. Please re-authenticate."
|
|
3021
|
+
],
|
|
3022
|
+
// Generic sensitive patterns (API keys, URLs with secrets)
|
|
3023
|
+
[
|
|
3024
|
+
/[a-z0-9]{32,}.*key|Bearer [a-zA-Z0-9._-]+/i,
|
|
3025
|
+
"An internal error occurred. Please try again."
|
|
3026
|
+
]
|
|
3027
|
+
];
|
|
3028
|
+
UUID_PATTERN = /[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/gi;
|
|
3029
|
+
EMAIL_PATTERN = /[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/g;
|
|
3030
|
+
}
|
|
3031
|
+
});
|
|
3032
|
+
|
|
2773
3033
|
// src/lib/checkStatusShape.ts
|
|
2774
3034
|
function buildCheckStatusPayload(job, liveStatus) {
|
|
2775
3035
|
const status = liveStatus?.status ?? job.status;
|
|
@@ -2865,9 +3125,22 @@ function checkCreditBudget(estimatedCost) {
|
|
|
2865
3125
|
}
|
|
2866
3126
|
const used = getCreditsUsed();
|
|
2867
3127
|
if (used + estimatedCost > MAX_CREDITS_PER_RUN) {
|
|
3128
|
+
const message = `Per-run credit cap reached \u2014 this is a local spend guard, not a server rate limit, so retrying will not help. The SOCIALNEURON_MAX_CREDITS_PER_RUN environment variable is set to ${MAX_CREDITS_PER_RUN} credits. This run has already spent ${used} credits and this call is estimated at ~${estimatedCost} more (${used} + ${estimatedCost} = ${used + estimatedCost} > ${MAX_CREDITS_PER_RUN}). To proceed, raise or unset SOCIALNEURON_MAX_CREDITS_PER_RUN.`;
|
|
2868
3129
|
return {
|
|
2869
3130
|
ok: false,
|
|
2870
|
-
message
|
|
3131
|
+
message,
|
|
3132
|
+
error: toolError("billing_error", message, {
|
|
3133
|
+
recover_with: [
|
|
3134
|
+
"Raise SOCIALNEURON_MAX_CREDITS_PER_RUN to a higher credit ceiling.",
|
|
3135
|
+
"Unset SOCIALNEURON_MAX_CREDITS_PER_RUN to remove the per-run cap."
|
|
3136
|
+
],
|
|
3137
|
+
details: {
|
|
3138
|
+
env_var: "SOCIALNEURON_MAX_CREDITS_PER_RUN",
|
|
3139
|
+
credits_used_this_run: used,
|
|
3140
|
+
estimated_call_cost: estimatedCost,
|
|
3141
|
+
max_credits_per_run: MAX_CREDITS_PER_RUN
|
|
3142
|
+
}
|
|
3143
|
+
})
|
|
2871
3144
|
};
|
|
2872
3145
|
}
|
|
2873
3146
|
return { ok: true };
|
|
@@ -2890,8 +3163,15 @@ var init_budget = __esm({
|
|
|
2890
3163
|
"src/lib/budget.ts"() {
|
|
2891
3164
|
"use strict";
|
|
2892
3165
|
init_request_context();
|
|
2893
|
-
|
|
2894
|
-
|
|
3166
|
+
init_tool_error();
|
|
3167
|
+
MAX_CREDITS_PER_RUN = Math.max(
|
|
3168
|
+
0,
|
|
3169
|
+
Number(process.env.SOCIALNEURON_MAX_CREDITS_PER_RUN || 0)
|
|
3170
|
+
);
|
|
3171
|
+
MAX_ASSETS_PER_RUN = Math.max(
|
|
3172
|
+
0,
|
|
3173
|
+
Number(process.env.SOCIALNEURON_MAX_ASSETS_PER_RUN || 0)
|
|
3174
|
+
);
|
|
2895
3175
|
_globalCreditsUsed = 0;
|
|
2896
3176
|
_globalAssetsGenerated = 0;
|
|
2897
3177
|
}
|
|
@@ -2918,7 +3198,7 @@ function asEnvelope2(data) {
|
|
|
2918
3198
|
function registerContentTools(server2) {
|
|
2919
3199
|
server2.tool(
|
|
2920
3200
|
"generate_video",
|
|
2921
|
-
"Start an async AI video generation job \u2014 returns a job_id immediately. Poll with check_status every 10-30s until complete. Base credit costs (reference config; dynamic models scale with duration/audio/resolution): seedance-2-fast 264 (8s, best quality/credit) \xB7 kling-3 100 (5s no-audio) \xB7 grok-imagine 30 (6s, cheapest) \xB7 veo3-fast 65 (8s) \xB7 kling-3-pro 135 (5s no-audio) \xB7 seedance-2 328 (8s 720p) \xB7 veo3-quality 1000 (8s) \xB7 wan-2.6 105 (5s) \xB7 gemini-omni-video 126 (multi-reference composites) \xB7 hailuo-02-standard 180 / seedance-1.5-pro 150 / kling 170 (legacy/budget fallbacks). Costs are estimated pre-check and reconciled post-response from the actual charge. audio adds ~1.5x on kling-3/kling-3-pro and ~2.65x on kling
|
|
3201
|
+
"Start an async AI video generation job \u2014 returns a job_id immediately. Poll with check_status every 10-30s until complete. Base credit costs (reference config; dynamic models scale with duration/audio/resolution): seedance-2-fast 264 (8s, best quality/credit) \xB7 kling-3 100 (5s no-audio) \xB7 grok-imagine 30 (6s, cheapest) \xB7 veo3-fast 65 (8s) \xB7 kling-3-pro 135 (5s no-audio) \xB7 seedance-2 328 (8s 720p) \xB7 veo3-quality 1000 (8s) \xB7 wan-2.6 105 (5s) \xB7 gemini-omni-video 126 (multi-reference composites) \xB7 hailuo-02-standard 180 / seedance-1.5-pro 150 / kling 170 (legacy/budget fallbacks). Costs are estimated pre-check and reconciled post-response from the actual charge. audio adds ~1.5x on kling-3/kling-3-pro and ~2.65x on kling; enable_audio defaults to FALSE EXCEPT the seedance-2 family (seedance-2-fast, seedance-2) where audio is ON by default and its cost is already included. Check get_credit_balance first for expensive generations.",
|
|
2922
3202
|
{
|
|
2923
3203
|
prompt: z2.string().max(2500).describe(
|
|
2924
3204
|
'Video prompt \u2014 be specific about visual style, camera movement, lighting, and mood. Example: "Aerial drone shot of coastal cliffs at golden hour, slow dolly forward, cinematic 24fps, warm color grading." Vague prompts produce generic results.'
|
|
@@ -2933,7 +3213,7 @@ function registerContentTools(server2) {
|
|
|
2933
3213
|
"Video aspect ratio. 16:9 for YouTube/landscape, 9:16 for TikTok/Reels/Shorts, 1:1 for Instagram feed/square. Defaults to 16:9."
|
|
2934
3214
|
),
|
|
2935
3215
|
enable_audio: z2.boolean().optional().describe(
|
|
2936
|
-
"Enable native audio generation. DEFAULTS TO FALSE (cost control). Cost multiplier when true: kling 2.6 ~2.65x (17->45 cr/sec), kling-3 1.5x (20->30 cr/sec), kling-3-pro ~1.5x (27->40 cr/sec).
|
|
3216
|
+
"Enable native audio generation. For most models this DEFAULTS TO FALSE (cost control). EXCEPTION: the seedance-2 family (seedance-2-fast, seedance-2) DEFAULTS TO TRUE \u2014 these generate audio natively and their credit cost already includes it, so audio is on unless you explicitly pass false. Cost multiplier when true on other models: kling 2.6 ~2.65x (17->45 cr/sec), kling-3 1.5x (20->30 cr/sec), kling-3-pro ~1.5x (27->40 cr/sec). 5+ languages."
|
|
2937
3217
|
),
|
|
2938
3218
|
image_url: z2.string().optional().describe(
|
|
2939
3219
|
"Start frame image URL for image-to-video (Kling 3.0 frame control)."
|
|
@@ -2969,12 +3249,12 @@ function registerContentTools(server2) {
|
|
|
2969
3249
|
const estimatedCost = VIDEO_CREDIT_ESTIMATES[model] ?? 120;
|
|
2970
3250
|
const budgetCheck = checkCreditBudget(estimatedCost);
|
|
2971
3251
|
if (!budgetCheck.ok) {
|
|
2972
|
-
return
|
|
2973
|
-
content: [{ type: "text", text: budgetCheck.message }],
|
|
2974
|
-
isError: true
|
|
2975
|
-
};
|
|
3252
|
+
return budgetCheck.error;
|
|
2976
3253
|
}
|
|
2977
|
-
const rateLimit = checkRateLimit(
|
|
3254
|
+
const rateLimit = checkRateLimit(
|
|
3255
|
+
"generation",
|
|
3256
|
+
`generate_video:${userId}`
|
|
3257
|
+
);
|
|
2978
3258
|
if (!rateLimit.allowed) {
|
|
2979
3259
|
return {
|
|
2980
3260
|
content: [
|
|
@@ -2993,9 +3273,12 @@ function registerContentTools(server2) {
|
|
|
2993
3273
|
model,
|
|
2994
3274
|
duration: duration ?? 5,
|
|
2995
3275
|
aspectRatio: aspect_ratio ?? "16:9",
|
|
2996
|
-
// Default FALSE (2026-07-13): the old `?? true` default silently
|
|
2997
|
-
// kling-family costs (2.65x on kling 2.6) for callers that never asked
|
|
2998
|
-
|
|
3276
|
+
// Default FALSE for most models (2026-07-13): the old `?? true` default silently
|
|
3277
|
+
// multiplied kling-family costs (2.65x on kling 2.6) for callers that never asked
|
|
3278
|
+
// for audio. Exception (2026-07-17): the seedance-2 family bills audio into its base
|
|
3279
|
+
// cost and generates it natively, so a false default there shipped silent no-audio
|
|
3280
|
+
// mp4s — those models default TRUE when the caller omits enable_audio.
|
|
3281
|
+
enableAudio: enable_audio ?? AUDIO_NATIVE_DEFAULT_MODELS.has(model),
|
|
2999
3282
|
...image_url && { imageUrl: image_url },
|
|
3000
3283
|
...end_frame_url && { endFrameUrl: end_frame_url },
|
|
3001
3284
|
// The server reads projectId and enforces
|
|
@@ -3097,7 +3380,14 @@ function registerContentTools(server2) {
|
|
|
3097
3380
|
project_id: z2.string().optional().describe("Project ID to associate the generated image with."),
|
|
3098
3381
|
response_format: z2.enum(["text", "json"]).optional().describe("Optional response format. Defaults to text.")
|
|
3099
3382
|
},
|
|
3100
|
-
async ({
|
|
3383
|
+
async ({
|
|
3384
|
+
prompt: prompt2,
|
|
3385
|
+
model,
|
|
3386
|
+
aspect_ratio,
|
|
3387
|
+
image_url,
|
|
3388
|
+
project_id,
|
|
3389
|
+
response_format
|
|
3390
|
+
}) => {
|
|
3101
3391
|
const format = response_format ?? "text";
|
|
3102
3392
|
const userId = await getDefaultUserId();
|
|
3103
3393
|
const assetBudget = checkAssetBudget();
|
|
@@ -3110,12 +3400,12 @@ function registerContentTools(server2) {
|
|
|
3110
3400
|
const estimatedCost = IMAGE_CREDIT_ESTIMATES[model] ?? 30;
|
|
3111
3401
|
const budgetCheck = checkCreditBudget(estimatedCost);
|
|
3112
3402
|
if (!budgetCheck.ok) {
|
|
3113
|
-
return
|
|
3114
|
-
content: [{ type: "text", text: budgetCheck.message }],
|
|
3115
|
-
isError: true
|
|
3116
|
-
};
|
|
3403
|
+
return budgetCheck.error;
|
|
3117
3404
|
}
|
|
3118
|
-
const rateLimit = checkRateLimit(
|
|
3405
|
+
const rateLimit = checkRateLimit(
|
|
3406
|
+
"generation",
|
|
3407
|
+
`generate_image:${userId}`
|
|
3408
|
+
);
|
|
3119
3409
|
if (!rateLimit.allowed) {
|
|
3120
3410
|
return {
|
|
3121
3411
|
content: [
|
|
@@ -3246,6 +3536,15 @@ function registerContentTools(server2) {
|
|
|
3246
3536
|
isError: true
|
|
3247
3537
|
};
|
|
3248
3538
|
}
|
|
3539
|
+
let projectsDisclosure;
|
|
3540
|
+
const ownScopedProjectId = await getDefaultProjectId();
|
|
3541
|
+
if (!ownScopedProjectId) {
|
|
3542
|
+
const ownUserId = await getDefaultUserId().catch(() => null);
|
|
3543
|
+
if (ownUserId) {
|
|
3544
|
+
const list = await listAccessibleProjectsWithAccountStatus(ownUserId);
|
|
3545
|
+
if (list.length > 0) projectsDisclosure = list;
|
|
3546
|
+
}
|
|
3547
|
+
}
|
|
3249
3548
|
if (job.external_id && (job.status === "pending" || job.status === "processing")) {
|
|
3250
3549
|
const { data: liveStatus } = await callEdgeFunction(
|
|
3251
3550
|
"kie-task-status",
|
|
@@ -3269,15 +3568,25 @@ function registerContentTools(server2) {
|
|
|
3269
3568
|
if (livePayload.error) {
|
|
3270
3569
|
lines2.push(`Error: ${livePayload.error}`);
|
|
3271
3570
|
}
|
|
3272
|
-
const fallbackDisclosure2 = buildFallbackDisclosureLine(
|
|
3571
|
+
const fallbackDisclosure2 = buildFallbackDisclosureLine(
|
|
3572
|
+
job.result_metadata
|
|
3573
|
+
);
|
|
3273
3574
|
if (fallbackDisclosure2) lines2.push(fallbackDisclosure2);
|
|
3274
3575
|
lines2.push(`Credits: ${job.credits_cost}`);
|
|
3275
3576
|
if (job.billing_status) {
|
|
3276
3577
|
lines2.push(
|
|
3277
|
-
`Billing: ${job.billing_status} (charged ${job.credits_charged ?? 0}, refunded ${job.credits_refunded ?? 0})`
|
|
3578
|
+
`Billing: ${job.billing_status} (charged ${job.credits_charged ?? 0}, refunded ${job.credits_refunded ?? 0})`
|
|
3579
|
+
);
|
|
3580
|
+
}
|
|
3581
|
+
lines2.push(`Created: ${job.created_at}`);
|
|
3582
|
+
if (projectsDisclosure) {
|
|
3583
|
+
lines2.push(
|
|
3584
|
+
"",
|
|
3585
|
+
`Projects (project_id required elsewhere? pick one): ${projectsDisclosure.map(
|
|
3586
|
+
(p) => `${p.name} (${p.id}${p.hasConnectedAccounts ? ", has connected accounts" : ""})`
|
|
3587
|
+
).join("; ")}`
|
|
3278
3588
|
);
|
|
3279
3589
|
}
|
|
3280
|
-
lines2.push(`Created: ${job.created_at}`);
|
|
3281
3590
|
if (format === "json") {
|
|
3282
3591
|
return {
|
|
3283
3592
|
content: [
|
|
@@ -3294,7 +3603,8 @@ function registerContentTools(server2) {
|
|
|
3294
3603
|
// `job` + `liveStatus`; do not duplicate them here.
|
|
3295
3604
|
asEnvelope2({
|
|
3296
3605
|
...liveStatus,
|
|
3297
|
-
...livePayload
|
|
3606
|
+
...livePayload,
|
|
3607
|
+
...projectsDisclosure ? { projects: projectsDisclosure } : {}
|
|
3298
3608
|
}),
|
|
3299
3609
|
null,
|
|
3300
3610
|
2
|
|
@@ -3335,9 +3645,11 @@ function registerContentTools(server2) {
|
|
|
3335
3645
|
);
|
|
3336
3646
|
}
|
|
3337
3647
|
if (job.error_message) {
|
|
3338
|
-
lines.push(`Error: ${job.error_message}`);
|
|
3648
|
+
lines.push(`Error: ${redactSensitiveIdentifiers(job.error_message)}`);
|
|
3339
3649
|
}
|
|
3340
|
-
const fallbackDisclosure = buildFallbackDisclosureLine(
|
|
3650
|
+
const fallbackDisclosure = buildFallbackDisclosureLine(
|
|
3651
|
+
job.result_metadata
|
|
3652
|
+
);
|
|
3341
3653
|
if (fallbackDisclosure) lines.push(fallbackDisclosure);
|
|
3342
3654
|
lines.push(`Credits: ${job.credits_cost}`);
|
|
3343
3655
|
if (job.billing_status) {
|
|
@@ -3349,10 +3661,19 @@ function registerContentTools(server2) {
|
|
|
3349
3661
|
if (job.completed_at) {
|
|
3350
3662
|
lines.push(`Completed: ${job.completed_at}`);
|
|
3351
3663
|
}
|
|
3664
|
+
if (projectsDisclosure) {
|
|
3665
|
+
lines.push(
|
|
3666
|
+
"",
|
|
3667
|
+
`Projects (project_id required elsewhere? pick one): ${projectsDisclosure.map(
|
|
3668
|
+
(p) => `${p.name} (${p.id}${p.hasConnectedAccounts ? ", has connected accounts" : ""})`
|
|
3669
|
+
).join("; ")}`
|
|
3670
|
+
);
|
|
3671
|
+
}
|
|
3352
3672
|
if (format === "json") {
|
|
3353
3673
|
const enriched = {
|
|
3354
3674
|
...job,
|
|
3355
|
-
...buildCheckStatusPayload(job)
|
|
3675
|
+
...buildCheckStatusPayload(job),
|
|
3676
|
+
...projectsDisclosure ? { projects: projectsDisclosure } : {}
|
|
3356
3677
|
};
|
|
3357
3678
|
return {
|
|
3358
3679
|
content: [
|
|
@@ -3478,10 +3799,7 @@ Return ONLY valid JSON in this exact format:
|
|
|
3478
3799
|
const estimatedCost = 10;
|
|
3479
3800
|
const budgetCheck = checkCreditBudget(estimatedCost);
|
|
3480
3801
|
if (!budgetCheck.ok) {
|
|
3481
|
-
return
|
|
3482
|
-
content: [{ type: "text", text: budgetCheck.message }],
|
|
3483
|
-
isError: true
|
|
3484
|
-
};
|
|
3802
|
+
return budgetCheck.error;
|
|
3485
3803
|
}
|
|
3486
3804
|
const { data, error } = await callEdgeFunction(
|
|
3487
3805
|
"social-neuron-ai",
|
|
@@ -3568,10 +3886,7 @@ Return ONLY valid JSON in this exact format:
|
|
|
3568
3886
|
const estimatedCost = 15;
|
|
3569
3887
|
const budgetCheck = checkCreditBudget(estimatedCost);
|
|
3570
3888
|
if (!budgetCheck.ok) {
|
|
3571
|
-
return
|
|
3572
|
-
content: [{ type: "text", text: budgetCheck.message }],
|
|
3573
|
-
isError: true
|
|
3574
|
-
};
|
|
3889
|
+
return budgetCheck.error;
|
|
3575
3890
|
}
|
|
3576
3891
|
const rateLimit = checkRateLimit(
|
|
3577
3892
|
"generation",
|
|
@@ -3734,10 +4049,7 @@ Return ONLY valid JSON in this exact format:
|
|
|
3734
4049
|
const estimatedCost = 10 + slideCount * 2;
|
|
3735
4050
|
const budgetCheck = checkCreditBudget(estimatedCost);
|
|
3736
4051
|
if (!budgetCheck.ok) {
|
|
3737
|
-
return
|
|
3738
|
-
content: [{ type: "text", text: budgetCheck.message }],
|
|
3739
|
-
isError: true
|
|
3740
|
-
};
|
|
4052
|
+
return budgetCheck.error;
|
|
3741
4053
|
}
|
|
3742
4054
|
const userId = await getDefaultUserId();
|
|
3743
4055
|
const rateLimit = checkRateLimit(
|
|
@@ -3840,13 +4152,14 @@ Return ONLY valid JSON in this exact format:
|
|
|
3840
4152
|
}
|
|
3841
4153
|
);
|
|
3842
4154
|
}
|
|
3843
|
-
var VIDEO_CREDIT_ESTIMATES, VIDEO_MODEL_ENUM, IMAGE_CREDIT_ESTIMATES;
|
|
4155
|
+
var VIDEO_CREDIT_ESTIMATES, AUDIO_NATIVE_DEFAULT_MODELS, VIDEO_MODEL_ENUM, IMAGE_CREDIT_ESTIMATES;
|
|
3844
4156
|
var init_content = __esm({
|
|
3845
4157
|
"src/tools/content.ts"() {
|
|
3846
4158
|
"use strict";
|
|
3847
4159
|
init_edge_function();
|
|
3848
4160
|
init_rate_limit();
|
|
3849
4161
|
init_supabase();
|
|
4162
|
+
init_sanitize_error();
|
|
3850
4163
|
init_version();
|
|
3851
4164
|
init_checkStatusShape();
|
|
3852
4165
|
init_budget();
|
|
@@ -3864,6 +4177,10 @@ var init_content = __esm({
|
|
|
3864
4177
|
"seedance-1.5-pro": 150,
|
|
3865
4178
|
kling: 170
|
|
3866
4179
|
};
|
|
4180
|
+
AUDIO_NATIVE_DEFAULT_MODELS = /* @__PURE__ */ new Set([
|
|
4181
|
+
"seedance-2-fast",
|
|
4182
|
+
"seedance-2"
|
|
4183
|
+
]);
|
|
3867
4184
|
VIDEO_MODEL_ENUM = [
|
|
3868
4185
|
"seedance-2-fast",
|
|
3869
4186
|
"kling-3",
|
|
@@ -3892,57 +4209,6 @@ var init_content = __esm({
|
|
|
3892
4209
|
}
|
|
3893
4210
|
});
|
|
3894
4211
|
|
|
3895
|
-
// src/lib/sanitize-error.ts
|
|
3896
|
-
function sanitizeError(error) {
|
|
3897
|
-
const msg = error instanceof Error ? error.message : typeof error === "string" ? error : "Unknown error";
|
|
3898
|
-
for (const [pattern, userMessage] of ERROR_PATTERNS) {
|
|
3899
|
-
if (pattern.test(msg)) {
|
|
3900
|
-
return userMessage;
|
|
3901
|
-
}
|
|
3902
|
-
}
|
|
3903
|
-
return "An unexpected error occurred. Please try again.";
|
|
3904
|
-
}
|
|
3905
|
-
var ERROR_PATTERNS;
|
|
3906
|
-
var init_sanitize_error = __esm({
|
|
3907
|
-
"src/lib/sanitize-error.ts"() {
|
|
3908
|
-
"use strict";
|
|
3909
|
-
ERROR_PATTERNS = [
|
|
3910
|
-
// Postgres / PostgREST
|
|
3911
|
-
[/PGRST301|permission denied/i, "Access denied. Check your account permissions."],
|
|
3912
|
-
[/42P01|does not exist/i, "Service temporarily unavailable. Please try again."],
|
|
3913
|
-
[/23505|unique.*constraint|duplicate key/i, "A duplicate record already exists."],
|
|
3914
|
-
[/23503|foreign key/i, "Referenced record not found."],
|
|
3915
|
-
// Gemini / Google AI
|
|
3916
|
-
[/google.*api.*key|googleapis\.com.*40[13]/i, "Content generation failed. Please try again."],
|
|
3917
|
-
[
|
|
3918
|
-
/RESOURCE_EXHAUSTED|quota.*exceeded|429.*google/i,
|
|
3919
|
-
"AI service rate limit reached. Please wait and retry."
|
|
3920
|
-
],
|
|
3921
|
-
[
|
|
3922
|
-
/SAFETY|prompt.*blocked|content.*filter/i,
|
|
3923
|
-
"Content was blocked by the AI safety filter. Try rephrasing."
|
|
3924
|
-
],
|
|
3925
|
-
[/gemini.*error|generativelanguage/i, "Content generation failed. Please try again."],
|
|
3926
|
-
// Kie.ai
|
|
3927
|
-
[/kie\.ai|kieai|kie_api/i, "Media generation failed. Please try again."],
|
|
3928
|
-
// Stripe
|
|
3929
|
-
[/stripe.*api|sk_live_|sk_test_/i, "Payment processing error. Please try again."],
|
|
3930
|
-
// Network / fetch
|
|
3931
|
-
[
|
|
3932
|
-
/ECONNREFUSED|ENOTFOUND|ETIMEDOUT|ECONNRESET/i,
|
|
3933
|
-
"External service unavailable. Please try again."
|
|
3934
|
-
],
|
|
3935
|
-
[/fetch failed|network error|abort.*timeout/i, "Network request failed. Please try again."],
|
|
3936
|
-
[/CERT_|certificate|SSL|TLS/i, "Secure connection failed. Please try again."],
|
|
3937
|
-
// Supabase Edge Function internals
|
|
3938
|
-
[/FunctionsHttpError|non-2xx status/i, "Backend service error. Please try again."],
|
|
3939
|
-
[/JWT|token.*expired|token.*invalid/i, "Authentication expired. Please re-authenticate."],
|
|
3940
|
-
// Generic sensitive patterns (API keys, URLs with secrets)
|
|
3941
|
-
[/[a-z0-9]{32,}.*key|Bearer [a-zA-Z0-9._-]+/i, "An internal error occurred. Please try again."]
|
|
3942
|
-
];
|
|
3943
|
-
}
|
|
3944
|
-
});
|
|
3945
|
-
|
|
3946
4212
|
// src/lib/ssrf.ts
|
|
3947
4213
|
import { promises as dnsPromises } from "node:dns";
|
|
3948
4214
|
function isBlockedIP(ip) {
|
|
@@ -4229,6 +4495,122 @@ var init_quality = __esm({
|
|
|
4229
4495
|
}
|
|
4230
4496
|
});
|
|
4231
4497
|
|
|
4498
|
+
// src/lib/connected-account-routing.ts
|
|
4499
|
+
function canonicalPlatform(value) {
|
|
4500
|
+
const normalized = value.trim().toLowerCase();
|
|
4501
|
+
return normalized === "x" ? "twitter" : normalized;
|
|
4502
|
+
}
|
|
4503
|
+
function providerPlatform(value) {
|
|
4504
|
+
return PLATFORM_CASE_MAP[canonicalPlatform(value)] ?? value;
|
|
4505
|
+
}
|
|
4506
|
+
function isUsable(account) {
|
|
4507
|
+
const status = account.effective_status ?? account.status;
|
|
4508
|
+
return status === "active" || status === "expires_soon";
|
|
4509
|
+
}
|
|
4510
|
+
function normalizeRequestedIds(requested) {
|
|
4511
|
+
const ids = /* @__PURE__ */ new Map();
|
|
4512
|
+
for (const [platform3, accountId] of Object.entries(requested ?? {})) {
|
|
4513
|
+
const canonical = canonicalPlatform(platform3);
|
|
4514
|
+
const existing = ids.get(canonical);
|
|
4515
|
+
if (existing && existing !== accountId) {
|
|
4516
|
+
return {
|
|
4517
|
+
error: `Conflicting account IDs were supplied for ${platform3} and its platform alias.`
|
|
4518
|
+
};
|
|
4519
|
+
}
|
|
4520
|
+
ids.set(canonical, accountId);
|
|
4521
|
+
}
|
|
4522
|
+
return { ids };
|
|
4523
|
+
}
|
|
4524
|
+
async function resolveConnectedAccountRouting(input) {
|
|
4525
|
+
const normalizedRequested = normalizeRequestedIds(input.requestedAccountIds);
|
|
4526
|
+
if (normalizedRequested.error) return { error: normalizedRequested.error };
|
|
4527
|
+
const targetPlatforms = new Set(input.platforms.map(canonicalPlatform));
|
|
4528
|
+
for (const requestedPlatform of normalizedRequested.ids?.keys() ?? []) {
|
|
4529
|
+
if (!targetPlatforms.has(requestedPlatform)) {
|
|
4530
|
+
return {
|
|
4531
|
+
error: `An account ID was supplied for untargeted platform ${requestedPlatform}.`
|
|
4532
|
+
};
|
|
4533
|
+
}
|
|
4534
|
+
}
|
|
4535
|
+
const { data, error } = await callEdgeFunction(
|
|
4536
|
+
"mcp-data",
|
|
4537
|
+
{
|
|
4538
|
+
action: "connected-accounts",
|
|
4539
|
+
projectId: input.projectId,
|
|
4540
|
+
project_id: input.projectId
|
|
4541
|
+
},
|
|
4542
|
+
{ timeoutMs: 1e4 }
|
|
4543
|
+
);
|
|
4544
|
+
if (error || !Array.isArray(data?.accounts)) {
|
|
4545
|
+
return {
|
|
4546
|
+
error: `Connected-account verification failed: ${error ?? data?.error ?? "no account inventory returned"}.`
|
|
4547
|
+
};
|
|
4548
|
+
}
|
|
4549
|
+
const connectedAccountIds = {};
|
|
4550
|
+
for (const platform3 of input.platforms) {
|
|
4551
|
+
const canonical = canonicalPlatform(platform3);
|
|
4552
|
+
const displayPlatform = providerPlatform(platform3);
|
|
4553
|
+
const platformAccounts = data.accounts.filter(
|
|
4554
|
+
(account) => canonicalPlatform(account.platform) === canonical && account.project_id === input.projectId && isUsable(account)
|
|
4555
|
+
);
|
|
4556
|
+
const requestedId = normalizedRequested.ids?.get(canonical);
|
|
4557
|
+
let selected;
|
|
4558
|
+
if (requestedId) {
|
|
4559
|
+
selected = data.accounts.find((account) => account.id === requestedId);
|
|
4560
|
+
if (!selected) {
|
|
4561
|
+
return {
|
|
4562
|
+
error: `${displayPlatform}: account ${requestedId} is not available for project_id ${input.projectId}.`
|
|
4563
|
+
};
|
|
4564
|
+
}
|
|
4565
|
+
if (canonicalPlatform(selected.platform) !== canonical) {
|
|
4566
|
+
return {
|
|
4567
|
+
error: `${displayPlatform}: account ${requestedId} belongs to ${selected.platform}.`
|
|
4568
|
+
};
|
|
4569
|
+
}
|
|
4570
|
+
if (selected.project_id !== input.projectId) {
|
|
4571
|
+
return {
|
|
4572
|
+
error: `${displayPlatform}: account ${requestedId} is not bound to project_id ${input.projectId}.`
|
|
4573
|
+
};
|
|
4574
|
+
}
|
|
4575
|
+
if (!isUsable(selected)) {
|
|
4576
|
+
return {
|
|
4577
|
+
error: `${displayPlatform}: account ${requestedId} is ${selected.effective_status ?? selected.status}.`
|
|
4578
|
+
};
|
|
4579
|
+
}
|
|
4580
|
+
} else if (platformAccounts.length === 1) {
|
|
4581
|
+
selected = platformAccounts[0];
|
|
4582
|
+
} else if (platformAccounts.length === 0) {
|
|
4583
|
+
return {
|
|
4584
|
+
error: `${displayPlatform}: no active account is bound to project_id ${input.projectId}.`
|
|
4585
|
+
};
|
|
4586
|
+
} else {
|
|
4587
|
+
return {
|
|
4588
|
+
error: `${displayPlatform}: multiple active accounts are bound to project_id ${input.projectId}; pass the exact account ID returned by list_connected_accounts.`
|
|
4589
|
+
};
|
|
4590
|
+
}
|
|
4591
|
+
connectedAccountIds[displayPlatform] = selected.id;
|
|
4592
|
+
}
|
|
4593
|
+
return { connectedAccountIds };
|
|
4594
|
+
}
|
|
4595
|
+
var PLATFORM_CASE_MAP;
|
|
4596
|
+
var init_connected_account_routing = __esm({
|
|
4597
|
+
"src/lib/connected-account-routing.ts"() {
|
|
4598
|
+
"use strict";
|
|
4599
|
+
init_edge_function();
|
|
4600
|
+
PLATFORM_CASE_MAP = {
|
|
4601
|
+
youtube: "YouTube",
|
|
4602
|
+
tiktok: "TikTok",
|
|
4603
|
+
instagram: "Instagram",
|
|
4604
|
+
twitter: "Twitter",
|
|
4605
|
+
x: "Twitter",
|
|
4606
|
+
linkedin: "LinkedIn",
|
|
4607
|
+
facebook: "Facebook",
|
|
4608
|
+
threads: "Threads",
|
|
4609
|
+
bluesky: "Bluesky"
|
|
4610
|
+
};
|
|
4611
|
+
}
|
|
4612
|
+
});
|
|
4613
|
+
|
|
4232
4614
|
// src/tools/distribution.ts
|
|
4233
4615
|
import { z as z3 } from "zod";
|
|
4234
4616
|
import { createHash } from "node:crypto";
|
|
@@ -4341,21 +4723,6 @@ async function validatePublishMediaUrl(url) {
|
|
|
4341
4723
|
function accountEffectiveStatus(account) {
|
|
4342
4724
|
return account.effective_status || account.status;
|
|
4343
4725
|
}
|
|
4344
|
-
function isUsableAccount(account) {
|
|
4345
|
-
const status = accountEffectiveStatus(account);
|
|
4346
|
-
return status === "active" || status === "expires_soon";
|
|
4347
|
-
}
|
|
4348
|
-
function formatAccountChoice(account) {
|
|
4349
|
-
const name = account.username ? `@${account.username}` : "unnamed";
|
|
4350
|
-
const project = account.project_id ? `project_id=${account.project_id}` : "project_id=unassigned";
|
|
4351
|
-
const refresh = account.has_refresh_token ? "OAuth 2.0 refresh" : "no refresh token";
|
|
4352
|
-
return `${account.id} (${name}, ${project}, ${accountEffectiveStatus(account)}, ${refresh})`;
|
|
4353
|
-
}
|
|
4354
|
-
function requestedAccountIdForPlatform(accountId, accountIds, platform3) {
|
|
4355
|
-
if (accountId) return accountId;
|
|
4356
|
-
if (!accountIds) return void 0;
|
|
4357
|
-
return accountIds[platform3] || accountIds[platform3.toLowerCase()];
|
|
4358
|
-
}
|
|
4359
4726
|
async function rehostExternalUrl(mediaUrl, projectId) {
|
|
4360
4727
|
const ssrf = await validateUrlForSSRF(mediaUrl);
|
|
4361
4728
|
if (!ssrf.isValid) {
|
|
@@ -4491,17 +4858,17 @@ function registerDistributionTools(server2) {
|
|
|
4491
4858
|
schedule_at: z3.string().optional().describe(
|
|
4492
4859
|
'ISO 8601 UTC datetime for scheduled posting (e.g. "2026-03-20T14:00:00Z"). Omit to post immediately. Must be in the future.'
|
|
4493
4860
|
),
|
|
4494
|
-
project_id: z3.string().optional().describe(
|
|
4861
|
+
project_id: z3.string().uuid().optional().describe(
|
|
4495
4862
|
"Social Neuron brand/project ID to associate this post with. Provide this when the account has multiple brands so brand voice and connected account routing stay scoped to the right brand."
|
|
4496
4863
|
),
|
|
4497
4864
|
response_format: z3.enum(["text", "json"]).optional().describe("Optional response format. Defaults to text."),
|
|
4498
4865
|
attribution: z3.boolean().optional().describe(
|
|
4499
4866
|
'If true, appends "Created with Social Neuron" to the caption. Default: false.'
|
|
4500
4867
|
),
|
|
4501
|
-
account_id: z3.string().optional().describe(
|
|
4502
|
-
"Connected account ID to post from.
|
|
4868
|
+
account_id: z3.string().uuid().optional().describe(
|
|
4869
|
+
"Connected account ID to post from. Optional when the resolved project has exactly one active account for the target platform \u2014 it is auto-bound. Required (with a clear error listing candidates) when multiple accounts exist for the same platform. Use list_connected_accounts to find the right ID. The account must be active and bound to the exact project_id."
|
|
4503
4870
|
),
|
|
4504
|
-
account_ids: z3.record(z3.string(), z3.string()).optional().describe(
|
|
4871
|
+
account_ids: z3.record(z3.string(), z3.string().uuid()).optional().describe(
|
|
4505
4872
|
'Per-platform account IDs when posting to multiple platforms. Example: {"twitter": "abc123", "instagram": "def456"}. Use list_connected_accounts with the same project_id to find IDs.'
|
|
4506
4873
|
),
|
|
4507
4874
|
auto_rehost: z3.boolean().optional().describe(
|
|
@@ -4545,6 +4912,45 @@ function registerDistributionTools(server2) {
|
|
|
4545
4912
|
isError: true
|
|
4546
4913
|
};
|
|
4547
4914
|
}
|
|
4915
|
+
const projectResolution = await resolveProjectForConnectedAccountTool(
|
|
4916
|
+
project_id,
|
|
4917
|
+
platforms
|
|
4918
|
+
);
|
|
4919
|
+
if (!projectResolution.projectId) {
|
|
4920
|
+
return {
|
|
4921
|
+
content: [
|
|
4922
|
+
{
|
|
4923
|
+
type: "text",
|
|
4924
|
+
text: projectResolution.error ?? "A project_id is required for publishing. Configure an explicit project or use an API key that is scoped to exactly one project."
|
|
4925
|
+
}
|
|
4926
|
+
],
|
|
4927
|
+
isError: true
|
|
4928
|
+
};
|
|
4929
|
+
}
|
|
4930
|
+
const resolvedProjectId = projectResolution.projectId;
|
|
4931
|
+
const projectAutoResolvedNote = projectResolution.autoResolvedNote;
|
|
4932
|
+
if (account_id && account_ids) {
|
|
4933
|
+
return {
|
|
4934
|
+
content: [
|
|
4935
|
+
{
|
|
4936
|
+
type: "text",
|
|
4937
|
+
text: "Pass either account_id or account_ids, not both."
|
|
4938
|
+
}
|
|
4939
|
+
],
|
|
4940
|
+
isError: true
|
|
4941
|
+
};
|
|
4942
|
+
}
|
|
4943
|
+
if (account_id && platforms.length !== 1) {
|
|
4944
|
+
return {
|
|
4945
|
+
content: [
|
|
4946
|
+
{
|
|
4947
|
+
type: "text",
|
|
4948
|
+
text: "account_id is valid only for a single target platform. Use account_ids for multi-platform publishing."
|
|
4949
|
+
}
|
|
4950
|
+
],
|
|
4951
|
+
isError: true
|
|
4952
|
+
};
|
|
4953
|
+
}
|
|
4548
4954
|
const userId = await getDefaultUserId();
|
|
4549
4955
|
const rateLimit = checkRateLimit("posting", `schedule_post:${userId}`);
|
|
4550
4956
|
if (!rateLimit.allowed) {
|
|
@@ -4649,11 +5055,16 @@ function registerDistributionTools(server2) {
|
|
|
4649
5055
|
}
|
|
4650
5056
|
const resolvedJobs = resolved;
|
|
4651
5057
|
resolvedMediaUrls = resolvedJobs.map((item) => item.url);
|
|
4652
|
-
resolvedMediaUrlsAreTrustedR2 = resolvedJobs.map(
|
|
5058
|
+
resolvedMediaUrlsAreTrustedR2 = resolvedJobs.map(
|
|
5059
|
+
(item) => item.trustedR2
|
|
5060
|
+
);
|
|
4653
5061
|
}
|
|
4654
5062
|
const shouldRehost = auto_rehost !== false;
|
|
4655
5063
|
if (shouldRehost && resolvedMediaUrl && !resolvedMediaUrlIsTrustedR2) {
|
|
4656
|
-
const rehost = await rehostExternalUrl(
|
|
5064
|
+
const rehost = await rehostExternalUrl(
|
|
5065
|
+
resolvedMediaUrl,
|
|
5066
|
+
resolvedProjectId
|
|
5067
|
+
);
|
|
4657
5068
|
if ("error" in rehost) {
|
|
4658
5069
|
return {
|
|
4659
5070
|
content: [
|
|
@@ -4671,7 +5082,7 @@ function registerDistributionTools(server2) {
|
|
|
4671
5082
|
if (shouldRehost && resolvedMediaUrls && resolvedMediaUrls.length > 0) {
|
|
4672
5083
|
const rehosted = await Promise.all(
|
|
4673
5084
|
resolvedMediaUrls.map(
|
|
4674
|
-
(u, index) => resolvedMediaUrlsAreTrustedR2[index] ? Promise.resolve({ signedUrl: u, r2Key: "" }) : rehostExternalUrl(u,
|
|
5085
|
+
(u, index) => resolvedMediaUrlsAreTrustedR2[index] ? Promise.resolve({ signedUrl: u, r2Key: "" }) : rehostExternalUrl(u, resolvedProjectId)
|
|
4675
5086
|
)
|
|
4676
5087
|
);
|
|
4677
5088
|
const failIdx = rehosted.findIndex((r) => "error" in r);
|
|
@@ -4738,7 +5149,7 @@ function registerDistributionTools(server2) {
|
|
|
4738
5149
|
}
|
|
4739
5150
|
}
|
|
4740
5151
|
const normalizedPlatforms = platforms.map(
|
|
4741
|
-
(p) =>
|
|
5152
|
+
(p) => PLATFORM_CASE_MAP2[p.toLowerCase()] || p
|
|
4742
5153
|
);
|
|
4743
5154
|
const blockedPlatforms = normalizedPlatforms.filter(
|
|
4744
5155
|
(p) => MCP_NOT_LIVE_FOR_POSTING.has(p)
|
|
@@ -4754,92 +5165,27 @@ function registerDistributionTools(server2) {
|
|
|
4754
5165
|
isError: true
|
|
4755
5166
|
};
|
|
4756
5167
|
}
|
|
4757
|
-
|
|
4758
|
-
"mcp-data",
|
|
4759
|
-
{
|
|
4760
|
-
action: "connected-accounts",
|
|
4761
|
-
...project_id ? { projectId: project_id, project_id } : {}
|
|
4762
|
-
},
|
|
4763
|
-
{ timeoutMs: 1e4 }
|
|
4764
|
-
);
|
|
4765
|
-
if (accountsData?.accounts) {
|
|
4766
|
-
const accounts = accountsData.accounts;
|
|
4767
|
-
const issues = [];
|
|
4768
|
-
for (const platform3 of normalizedPlatforms) {
|
|
4769
|
-
const platformAccounts = accounts.filter(
|
|
4770
|
-
(a) => a.platform.toLowerCase() === platform3.toLowerCase() && isUsableAccount(a)
|
|
4771
|
-
);
|
|
4772
|
-
const requestedAccountId = requestedAccountIdForPlatform(
|
|
4773
|
-
account_id,
|
|
4774
|
-
account_ids,
|
|
4775
|
-
platform3
|
|
4776
|
-
);
|
|
4777
|
-
if (requestedAccountId) {
|
|
4778
|
-
const selected = accounts.find((a) => a.id === requestedAccountId);
|
|
4779
|
-
if (!selected) {
|
|
4780
|
-
issues.push(
|
|
4781
|
-
`${platform3}: Account "${requestedAccountId}" is not available${project_id ? ` for project_id ${project_id}` : ""}. Call \`list_connected_accounts\`${project_id ? " with the same project_id" : ""} and choose one of the returned IDs.`
|
|
4782
|
-
);
|
|
4783
|
-
} else if (selected.platform.toLowerCase() !== platform3.toLowerCase()) {
|
|
4784
|
-
issues.push(
|
|
4785
|
-
`${platform3}: Account "${requestedAccountId}" belongs to ${selected.platform}, not ${platform3}.`
|
|
4786
|
-
);
|
|
4787
|
-
} else if (!isUsableAccount(selected)) {
|
|
4788
|
-
issues.push(
|
|
4789
|
-
`${platform3}: Account "${selected.username || selected.id}" is ${accountEffectiveStatus(selected)}. Reconnect at socialneuron.com/settings/connections.`
|
|
4790
|
-
);
|
|
4791
|
-
} else if (project_id && selected.project_id && selected.project_id !== project_id) {
|
|
4792
|
-
issues.push(
|
|
4793
|
-
`${platform3}: Account "${selected.username || selected.id}" is bound to project_id ${selected.project_id}, not ${project_id}. Use the selected brand's account or reconnect/assign it in Settings > Integrations.`
|
|
4794
|
-
);
|
|
4795
|
-
}
|
|
4796
|
-
continue;
|
|
4797
|
-
}
|
|
4798
|
-
if (platformAccounts.length === 0) {
|
|
4799
|
-
issues.push(
|
|
4800
|
-
`${platform3}: not connected yet. This is a one-time browser setup on socialneuron.com \u2014 NOT another OAuth in Claude. Call \`start_platform_connection\` with platform="${platform3.toLowerCase()}"${project_id ? ` and project_id="${project_id}"` : ""} to get a deep link, ask the user to open it in their browser and approve on the platform, then call \`wait_for_connection\` before retrying schedule_post.`
|
|
4801
|
-
);
|
|
4802
|
-
} else if (platformAccounts.length > 1) {
|
|
4803
|
-
const accountList = platformAccounts.map((a) => ` - ${formatAccountChoice(a)}`).join("\n");
|
|
4804
|
-
issues.push(
|
|
4805
|
-
`${platform3}: Multiple accounts found. Specify account_id or account_ids to choose:
|
|
4806
|
-
${accountList}`
|
|
4807
|
-
);
|
|
4808
|
-
} else if (platformAccounts.length === 1) {
|
|
4809
|
-
const acct = platformAccounts[0];
|
|
4810
|
-
if (acct.expires_at && new Date(acct.expires_at) < /* @__PURE__ */ new Date() && !acct.has_refresh_token) {
|
|
4811
|
-
issues.push(
|
|
4812
|
-
`${platform3}: Account "${acct.username || acct.id}" has expired OAuth and no refresh token. Reconnect at socialneuron.com/settings/connections.`
|
|
4813
|
-
);
|
|
4814
|
-
}
|
|
4815
|
-
}
|
|
4816
|
-
}
|
|
4817
|
-
if (issues.length > 0) {
|
|
4818
|
-
return {
|
|
4819
|
-
content: [
|
|
4820
|
-
{
|
|
4821
|
-
type: "text",
|
|
4822
|
-
text: `Cannot post \u2014 account issues found:
|
|
4823
|
-
|
|
4824
|
-
${issues.join("\n\n")}`
|
|
4825
|
-
}
|
|
4826
|
-
],
|
|
4827
|
-
isError: true
|
|
4828
|
-
};
|
|
4829
|
-
}
|
|
4830
|
-
}
|
|
4831
|
-
let connectedAccountIds;
|
|
5168
|
+
let requestedAccountIds;
|
|
4832
5169
|
if (account_id) {
|
|
4833
|
-
|
|
4834
|
-
for (const p of normalizedPlatforms) {
|
|
4835
|
-
connectedAccountIds[p] = account_id;
|
|
4836
|
-
}
|
|
5170
|
+
requestedAccountIds = { [normalizedPlatforms[0]]: account_id };
|
|
4837
5171
|
} else if (account_ids) {
|
|
4838
|
-
|
|
4839
|
-
|
|
4840
|
-
|
|
4841
|
-
|
|
4842
|
-
|
|
5172
|
+
requestedAccountIds = account_ids;
|
|
5173
|
+
}
|
|
5174
|
+
const routing = await resolveConnectedAccountRouting({
|
|
5175
|
+
projectId: resolvedProjectId,
|
|
5176
|
+
platforms: normalizedPlatforms,
|
|
5177
|
+
requestedAccountIds
|
|
5178
|
+
});
|
|
5179
|
+
if (routing.error || !routing.connectedAccountIds) {
|
|
5180
|
+
return {
|
|
5181
|
+
content: [
|
|
5182
|
+
{
|
|
5183
|
+
type: "text",
|
|
5184
|
+
text: `Cannot post \u2014 ${routing.error ?? "exact connected-account routing could not be established."}`
|
|
5185
|
+
}
|
|
5186
|
+
],
|
|
5187
|
+
isError: true
|
|
5188
|
+
};
|
|
4843
5189
|
}
|
|
4844
5190
|
let finalCaption = caption;
|
|
4845
5191
|
if (attribution && finalCaption) {
|
|
@@ -4893,8 +5239,9 @@ Created with Social Neuron`;
|
|
|
4893
5239
|
title,
|
|
4894
5240
|
hashtags,
|
|
4895
5241
|
scheduledAt: schedule_at,
|
|
4896
|
-
projectId:
|
|
4897
|
-
|
|
5242
|
+
projectId: resolvedProjectId,
|
|
5243
|
+
project_id: resolvedProjectId,
|
|
5244
|
+
connectedAccountIds: routing.connectedAccountIds,
|
|
4898
5245
|
...normalizedPlatformMetadata ? {
|
|
4899
5246
|
platformMetadata: convertPlatformMetadata(
|
|
4900
5247
|
normalizedPlatformMetadata
|
|
@@ -4929,10 +5276,14 @@ Created with Social Neuron`;
|
|
|
4929
5276
|
isError: true
|
|
4930
5277
|
};
|
|
4931
5278
|
}
|
|
5279
|
+
const responseData = projectAutoResolvedNote ? { ...data, project_auto_resolved: projectAutoResolvedNote } : data;
|
|
4932
5280
|
const lines = [
|
|
4933
5281
|
data.success ? "Post scheduled successfully." : "Post scheduling had errors.",
|
|
4934
5282
|
`Scheduled for: ${data.scheduledAt}`
|
|
4935
5283
|
];
|
|
5284
|
+
if (projectAutoResolvedNote) {
|
|
5285
|
+
lines.push("", `Note: ${projectAutoResolvedNote}`);
|
|
5286
|
+
}
|
|
4936
5287
|
if (tiktokAutoInboxApplied) {
|
|
4937
5288
|
lines.push(
|
|
4938
5289
|
"",
|
|
@@ -4950,7 +5301,7 @@ Created with Social Neuron`;
|
|
|
4950
5301
|
}
|
|
4951
5302
|
}
|
|
4952
5303
|
if (format === "json") {
|
|
4953
|
-
const structuredContent = asEnvelope3(
|
|
5304
|
+
const structuredContent = asEnvelope3(responseData);
|
|
4954
5305
|
return {
|
|
4955
5306
|
structuredContent,
|
|
4956
5307
|
content: [
|
|
@@ -4963,7 +5314,7 @@ Created with Social Neuron`;
|
|
|
4963
5314
|
};
|
|
4964
5315
|
}
|
|
4965
5316
|
return {
|
|
4966
|
-
structuredContent: asEnvelope3(
|
|
5317
|
+
structuredContent: asEnvelope3(responseData),
|
|
4967
5318
|
content: [{ type: "text", text: lines.join("\n") }],
|
|
4968
5319
|
isError: !data.success
|
|
4969
5320
|
};
|
|
@@ -4977,7 +5328,9 @@ Created with Social Neuron`;
|
|
|
4977
5328
|
project_id: z3.string().uuid().optional().describe(
|
|
4978
5329
|
"Brand/project ID that owns the post. Defaults to the authenticated key's project or the account default."
|
|
4979
5330
|
),
|
|
4980
|
-
scheduled_at: z3.string().datetime({ offset: true }).describe(
|
|
5331
|
+
scheduled_at: z3.string().datetime({ offset: true }).describe(
|
|
5332
|
+
"New future publish time as an ISO 8601 datetime with timezone."
|
|
5333
|
+
),
|
|
4981
5334
|
expected_scheduled_at: z3.string().datetime({ offset: true }).optional().describe(
|
|
4982
5335
|
"Optional current schedule timestamp. If it changed since you read it, the update is rejected instead of silently overwriting it."
|
|
4983
5336
|
),
|
|
@@ -5033,7 +5386,11 @@ Created with Social Neuron`;
|
|
|
5033
5386
|
projectId: resolvedProjectId,
|
|
5034
5387
|
project_id: resolvedProjectId,
|
|
5035
5388
|
scheduled_at: next.toISOString(),
|
|
5036
|
-
...expected_scheduled_at ? {
|
|
5389
|
+
...expected_scheduled_at ? {
|
|
5390
|
+
expected_scheduled_at: new Date(
|
|
5391
|
+
expected_scheduled_at
|
|
5392
|
+
).toISOString()
|
|
5393
|
+
} : {}
|
|
5037
5394
|
});
|
|
5038
5395
|
if (error || !result?.success) {
|
|
5039
5396
|
const code = result?.error ?? error ?? "reschedule_failed";
|
|
@@ -5066,17 +5423,34 @@ Created with Social Neuron`;
|
|
|
5066
5423
|
"list_connected_accounts",
|
|
5067
5424
|
"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.",
|
|
5068
5425
|
{
|
|
5069
|
-
project_id: z3.string().optional().describe(
|
|
5426
|
+
project_id: z3.string().uuid().optional().describe(
|
|
5070
5427
|
"Brand/project ID to scope connected accounts. Use the same project_id when calling schedule_post."
|
|
5071
5428
|
),
|
|
5072
|
-
include_all: z3.boolean().optional().describe(
|
|
5429
|
+
include_all: z3.boolean().optional().describe(
|
|
5430
|
+
"If true, include expired or inactive accounts as well as usable accounts."
|
|
5431
|
+
),
|
|
5073
5432
|
response_format: z3.enum(["text", "json"]).optional().describe("Optional response format. Defaults to text.")
|
|
5074
5433
|
},
|
|
5075
5434
|
async ({ project_id, include_all, response_format }) => {
|
|
5076
5435
|
const format = response_format ?? "text";
|
|
5436
|
+
const projectResolution = await resolveProjectForConnectedAccountTool(project_id);
|
|
5437
|
+
if (!projectResolution.projectId) {
|
|
5438
|
+
return {
|
|
5439
|
+
content: [
|
|
5440
|
+
{
|
|
5441
|
+
type: "text",
|
|
5442
|
+
text: projectResolution.error ?? "A project_id is required to list connected accounts. Configure an explicit project or use an API key scoped to exactly one project."
|
|
5443
|
+
}
|
|
5444
|
+
],
|
|
5445
|
+
isError: true
|
|
5446
|
+
};
|
|
5447
|
+
}
|
|
5448
|
+
const resolvedProjectId = projectResolution.projectId;
|
|
5449
|
+
const projectAutoResolvedNote = projectResolution.autoResolvedNote;
|
|
5077
5450
|
const { data: result, error: efError } = await callEdgeFunction("mcp-data", {
|
|
5078
5451
|
action: "connected-accounts",
|
|
5079
|
-
|
|
5452
|
+
projectId: resolvedProjectId,
|
|
5453
|
+
project_id: resolvedProjectId,
|
|
5080
5454
|
...include_all ? { includeAll: true } : {}
|
|
5081
5455
|
});
|
|
5082
5456
|
if (efError || !result?.success) {
|
|
@@ -5090,10 +5464,27 @@ Created with Social Neuron`;
|
|
|
5090
5464
|
isError: true
|
|
5091
5465
|
};
|
|
5092
5466
|
}
|
|
5093
|
-
const
|
|
5467
|
+
const parsedAccounts = (result.accounts ?? []).map(publicConnectedAccount).filter((account) => account !== null);
|
|
5468
|
+
if (parsedAccounts.some(
|
|
5469
|
+
(account) => account.project_id !== resolvedProjectId
|
|
5470
|
+
)) {
|
|
5471
|
+
return {
|
|
5472
|
+
content: [
|
|
5473
|
+
{
|
|
5474
|
+
type: "text",
|
|
5475
|
+
text: "Connected-account project attestation failed. No account inventory was returned."
|
|
5476
|
+
}
|
|
5477
|
+
],
|
|
5478
|
+
isError: true
|
|
5479
|
+
};
|
|
5480
|
+
}
|
|
5481
|
+
const accounts = parsedAccounts;
|
|
5094
5482
|
if (accounts.length === 0) {
|
|
5095
5483
|
if (format === "json") {
|
|
5096
|
-
const structuredContent = asEnvelope3({
|
|
5484
|
+
const structuredContent = asEnvelope3({
|
|
5485
|
+
accounts: [],
|
|
5486
|
+
...projectAutoResolvedNote ? { project_auto_resolved: projectAutoResolvedNote } : {}
|
|
5487
|
+
});
|
|
5097
5488
|
return {
|
|
5098
5489
|
structuredContent,
|
|
5099
5490
|
content: [
|
|
@@ -5108,13 +5499,15 @@ Created with Social Neuron`;
|
|
|
5108
5499
|
content: [
|
|
5109
5500
|
{
|
|
5110
5501
|
type: "text",
|
|
5111
|
-
text: "No connected social media accounts found. Connect platforms in Social Neuron Settings > Connections."
|
|
5502
|
+
text: "No connected social media accounts found. Connect platforms in Social Neuron Settings > Connections." + (projectAutoResolvedNote ? `
|
|
5503
|
+
|
|
5504
|
+
Note: ${projectAutoResolvedNote}` : "")
|
|
5112
5505
|
}
|
|
5113
5506
|
]
|
|
5114
5507
|
};
|
|
5115
5508
|
}
|
|
5116
5509
|
const lines = [
|
|
5117
|
-
`${accounts.length} connected account(s)
|
|
5510
|
+
`${accounts.length} connected account(s) for project ${resolvedProjectId}:`,
|
|
5118
5511
|
""
|
|
5119
5512
|
];
|
|
5120
5513
|
for (const account of accounts) {
|
|
@@ -5126,8 +5519,14 @@ Created with Social Neuron`;
|
|
|
5126
5519
|
` ${platformLower}: ${name} | id=${account.id} | ${project} | status=${status} (connected ${account.created_at.split("T")[0]})`
|
|
5127
5520
|
);
|
|
5128
5521
|
}
|
|
5522
|
+
if (projectAutoResolvedNote) {
|
|
5523
|
+
lines.push("", `Note: ${projectAutoResolvedNote}`);
|
|
5524
|
+
}
|
|
5129
5525
|
if (format === "json") {
|
|
5130
|
-
const structuredContent = asEnvelope3({
|
|
5526
|
+
const structuredContent = asEnvelope3({
|
|
5527
|
+
accounts,
|
|
5528
|
+
...projectAutoResolvedNote ? { project_auto_resolved: projectAutoResolvedNote } : {}
|
|
5529
|
+
});
|
|
5131
5530
|
return {
|
|
5132
5531
|
structuredContent,
|
|
5133
5532
|
content: [
|
|
@@ -5322,7 +5721,10 @@ Created with Social Neuron`;
|
|
|
5322
5721
|
if (!Number.isFinite(startDate.getTime())) {
|
|
5323
5722
|
return {
|
|
5324
5723
|
content: [
|
|
5325
|
-
{
|
|
5724
|
+
{
|
|
5725
|
+
type: "text",
|
|
5726
|
+
text: "start_after must be a valid ISO datetime."
|
|
5727
|
+
}
|
|
5326
5728
|
],
|
|
5327
5729
|
isError: true
|
|
5328
5730
|
};
|
|
@@ -5423,11 +5825,14 @@ Created with Social Neuron`;
|
|
|
5423
5825
|
"Schedule all posts in a content plan. Optionally auto-assigns time slots and runs quality checks before scheduling. Supports dry-run mode.",
|
|
5424
5826
|
{
|
|
5425
5827
|
plan: z3.object({
|
|
5828
|
+
project_id: z3.string().uuid().optional(),
|
|
5829
|
+
account_ids: z3.record(z3.string(), z3.string().uuid()).optional().describe("Exact connected-account ID per platform."),
|
|
5426
5830
|
posts: z3.array(
|
|
5427
5831
|
z3.object({
|
|
5428
5832
|
id: z3.string(),
|
|
5429
5833
|
caption: z3.string(),
|
|
5430
5834
|
platform: z3.string(),
|
|
5835
|
+
connected_account_id: z3.string().uuid().optional(),
|
|
5431
5836
|
title: z3.string().optional(),
|
|
5432
5837
|
media_url: z3.string().optional(),
|
|
5433
5838
|
schedule_at: z3.string().optional(),
|
|
@@ -5435,6 +5840,12 @@ Created with Social Neuron`;
|
|
|
5435
5840
|
})
|
|
5436
5841
|
)
|
|
5437
5842
|
}).passthrough().optional(),
|
|
5843
|
+
project_id: z3.string().uuid().optional().describe(
|
|
5844
|
+
"Exact brand/project ID. Defaults only when the authenticated user has one project."
|
|
5845
|
+
),
|
|
5846
|
+
account_ids: z3.record(z3.string(), z3.string().uuid()).optional().describe(
|
|
5847
|
+
"Exact connected-account ID per platform for every post in the plan."
|
|
5848
|
+
),
|
|
5438
5849
|
plan_id: z3.string().uuid().optional().describe("Persisted content plan ID from content_plans table"),
|
|
5439
5850
|
auto_slot: z3.boolean().default(true).describe("Auto-assign time slots for posts without schedule_at"),
|
|
5440
5851
|
dry_run: z3.boolean().default(false).describe("Preview without actually scheduling"),
|
|
@@ -5451,6 +5862,8 @@ Created with Social Neuron`;
|
|
|
5451
5862
|
async ({
|
|
5452
5863
|
plan,
|
|
5453
5864
|
plan_id,
|
|
5865
|
+
project_id,
|
|
5866
|
+
account_ids,
|
|
5454
5867
|
auto_slot,
|
|
5455
5868
|
dry_run,
|
|
5456
5869
|
response_format,
|
|
@@ -5460,9 +5873,11 @@ Created with Social Neuron`;
|
|
|
5460
5873
|
idempotency_seed
|
|
5461
5874
|
}) => {
|
|
5462
5875
|
try {
|
|
5876
|
+
const effectiveBatchSize = batch_size ?? 4;
|
|
5463
5877
|
let workingPlan = plan;
|
|
5464
5878
|
let effectivePlanId = plan_id;
|
|
5465
|
-
let effectiveProjectId;
|
|
5879
|
+
let effectiveProjectId = project_id;
|
|
5880
|
+
let projectAutoResolvedNote;
|
|
5466
5881
|
let approvalSummary;
|
|
5467
5882
|
if (!workingPlan && plan_id) {
|
|
5468
5883
|
const { data: planResult, error: planError } = await callEdgeFunction("mcp-data", { action: "get-content-plan", plan_id });
|
|
@@ -5509,23 +5924,59 @@ Created with Social Neuron`;
|
|
|
5509
5924
|
posts: postsFromPayload
|
|
5510
5925
|
};
|
|
5511
5926
|
effectivePlanId = stored.id;
|
|
5512
|
-
effectiveProjectId
|
|
5927
|
+
if (effectiveProjectId && stored.project_id && effectiveProjectId !== stored.project_id) {
|
|
5928
|
+
return {
|
|
5929
|
+
content: [
|
|
5930
|
+
{
|
|
5931
|
+
type: "text",
|
|
5932
|
+
text: `project_id ${effectiveProjectId} does not own plan ${plan_id}.`
|
|
5933
|
+
}
|
|
5934
|
+
],
|
|
5935
|
+
isError: true
|
|
5936
|
+
};
|
|
5937
|
+
}
|
|
5938
|
+
effectiveProjectId = stored.project_id ?? effectiveProjectId;
|
|
5513
5939
|
}
|
|
5514
5940
|
if (!workingPlan) {
|
|
5515
5941
|
return {
|
|
5516
5942
|
content: [
|
|
5517
5943
|
{
|
|
5518
5944
|
type: "text",
|
|
5519
|
-
text: "Provide either `plan` (inline) or `plan_id` (persisted) to schedule content."
|
|
5945
|
+
text: "Provide either `plan` (inline) or `plan_id` (persisted) to schedule content."
|
|
5946
|
+
}
|
|
5947
|
+
],
|
|
5948
|
+
isError: true
|
|
5949
|
+
};
|
|
5950
|
+
}
|
|
5951
|
+
const planProjectId = workingPlan.project_id;
|
|
5952
|
+
if (effectiveProjectId && typeof planProjectId === "string" && planProjectId.length > 0 && effectiveProjectId !== planProjectId) {
|
|
5953
|
+
return {
|
|
5954
|
+
content: [
|
|
5955
|
+
{
|
|
5956
|
+
type: "text",
|
|
5957
|
+
text: `Conflicting project_id values were supplied for the plan (${planProjectId}) and request (${effectiveProjectId}).`
|
|
5520
5958
|
}
|
|
5521
5959
|
],
|
|
5522
5960
|
isError: true
|
|
5523
5961
|
};
|
|
5524
5962
|
}
|
|
5963
|
+
if (!effectiveProjectId && typeof planProjectId === "string" && planProjectId.length > 0) {
|
|
5964
|
+
effectiveProjectId = planProjectId;
|
|
5965
|
+
}
|
|
5525
5966
|
if (!effectiveProjectId) {
|
|
5526
|
-
const
|
|
5527
|
-
|
|
5528
|
-
|
|
5967
|
+
const projectResolution = await resolveProjectForConnectedAccountTool();
|
|
5968
|
+
effectiveProjectId = projectResolution.projectId;
|
|
5969
|
+
projectAutoResolvedNote = projectResolution.autoResolvedNote;
|
|
5970
|
+
if (!effectiveProjectId) {
|
|
5971
|
+
return {
|
|
5972
|
+
content: [
|
|
5973
|
+
{
|
|
5974
|
+
type: "text",
|
|
5975
|
+
text: projectResolution.error ?? "A project_id is required to schedule a content plan. Configure an explicit project or use an API key scoped to exactly one project."
|
|
5976
|
+
}
|
|
5977
|
+
],
|
|
5978
|
+
isError: true
|
|
5979
|
+
};
|
|
5529
5980
|
}
|
|
5530
5981
|
}
|
|
5531
5982
|
if (effectivePlanId) {
|
|
@@ -5744,6 +6195,61 @@ Created with Social Neuron`;
|
|
|
5744
6195
|
isError: false
|
|
5745
6196
|
};
|
|
5746
6197
|
}
|
|
6198
|
+
const embeddedAccountIds = workingPlan.account_ids ?? {};
|
|
6199
|
+
for (const [platform3, accountId] of Object.entries(account_ids ?? {})) {
|
|
6200
|
+
if (embeddedAccountIds[platform3] && embeddedAccountIds[platform3] !== accountId) {
|
|
6201
|
+
return {
|
|
6202
|
+
content: [
|
|
6203
|
+
{
|
|
6204
|
+
type: "text",
|
|
6205
|
+
text: `Conflicting account_ids values were supplied for ${platform3}.`
|
|
6206
|
+
}
|
|
6207
|
+
],
|
|
6208
|
+
isError: true
|
|
6209
|
+
};
|
|
6210
|
+
}
|
|
6211
|
+
}
|
|
6212
|
+
const requestedPlanAccountIds = {
|
|
6213
|
+
...embeddedAccountIds,
|
|
6214
|
+
...account_ids ?? {}
|
|
6215
|
+
};
|
|
6216
|
+
for (const post of workingPlan.posts) {
|
|
6217
|
+
if (!post.connected_account_id) continue;
|
|
6218
|
+
const key = post.platform.toLowerCase() === "x" ? "twitter" : post.platform.toLowerCase();
|
|
6219
|
+
const existing = requestedPlanAccountIds[key];
|
|
6220
|
+
if (existing && existing !== post.connected_account_id) {
|
|
6221
|
+
return {
|
|
6222
|
+
content: [
|
|
6223
|
+
{
|
|
6224
|
+
type: "text",
|
|
6225
|
+
text: `Plan contains conflicting connected_account_id values for ${post.platform}. Use separate plans when scheduling the same platform through different accounts.`
|
|
6226
|
+
}
|
|
6227
|
+
],
|
|
6228
|
+
isError: true
|
|
6229
|
+
};
|
|
6230
|
+
}
|
|
6231
|
+
requestedPlanAccountIds[key] = post.connected_account_id;
|
|
6232
|
+
}
|
|
6233
|
+
const planPlatforms = Array.from(
|
|
6234
|
+
new Set(workingPlan.posts.map((post) => post.platform))
|
|
6235
|
+
);
|
|
6236
|
+
const planRouting = await resolveConnectedAccountRouting({
|
|
6237
|
+
projectId: effectiveProjectId,
|
|
6238
|
+
platforms: planPlatforms,
|
|
6239
|
+
requestedAccountIds: requestedPlanAccountIds
|
|
6240
|
+
});
|
|
6241
|
+
if (planRouting.error || !planRouting.connectedAccountIds) {
|
|
6242
|
+
return {
|
|
6243
|
+
content: [
|
|
6244
|
+
{
|
|
6245
|
+
type: "text",
|
|
6246
|
+
text: `Cannot schedule plan \u2014 ${planRouting.error ?? "exact connected-account routing could not be established."}`
|
|
6247
|
+
}
|
|
6248
|
+
],
|
|
6249
|
+
isError: true
|
|
6250
|
+
};
|
|
6251
|
+
}
|
|
6252
|
+
const verifiedPlanAccountIds = planRouting.connectedAccountIds;
|
|
5747
6253
|
let scheduled = 0;
|
|
5748
6254
|
let failed = 0;
|
|
5749
6255
|
const results = [];
|
|
@@ -5772,7 +6278,7 @@ Created with Social Neuron`;
|
|
|
5772
6278
|
retryable: false
|
|
5773
6279
|
};
|
|
5774
6280
|
}
|
|
5775
|
-
const normalizedPlatform =
|
|
6281
|
+
const normalizedPlatform = PLATFORM_CASE_MAP2[post.platform.toLowerCase()] ?? post.platform;
|
|
5776
6282
|
const idempotencyKey = buildIdempotencyKey(post);
|
|
5777
6283
|
const { data, error } = await callEdgeFunction(
|
|
5778
6284
|
"schedule-post",
|
|
@@ -5783,6 +6289,13 @@ Created with Social Neuron`;
|
|
|
5783
6289
|
mediaUrl: post.media_url,
|
|
5784
6290
|
scheduledAt: post.schedule_at,
|
|
5785
6291
|
hashtags: post.hashtags,
|
|
6292
|
+
...effectiveProjectId ? {
|
|
6293
|
+
projectId: effectiveProjectId,
|
|
6294
|
+
project_id: effectiveProjectId
|
|
6295
|
+
} : {},
|
|
6296
|
+
connectedAccountIds: {
|
|
6297
|
+
[normalizedPlatform]: verifiedPlanAccountIds[normalizedPlatform]
|
|
6298
|
+
},
|
|
5786
6299
|
...effectivePlanId ? { planId: effectivePlanId } : {},
|
|
5787
6300
|
idempotencyKey
|
|
5788
6301
|
},
|
|
@@ -5841,7 +6354,7 @@ Created with Social Neuron`;
|
|
|
5841
6354
|
const platformBatches = Array.from(grouped.entries()).map(
|
|
5842
6355
|
async ([platform3, platformPosts]) => {
|
|
5843
6356
|
const platformResults = [];
|
|
5844
|
-
const batches = chunk(platformPosts,
|
|
6357
|
+
const batches = chunk(platformPosts, effectiveBatchSize);
|
|
5845
6358
|
for (const batch of batches) {
|
|
5846
6359
|
const settled = await Promise.allSettled(
|
|
5847
6360
|
batch.map((post) => scheduleOne(post))
|
|
@@ -5902,7 +6415,8 @@ Created with Social Neuron`;
|
|
|
5902
6415
|
total_posts: workingPlan.posts.length,
|
|
5903
6416
|
scheduled,
|
|
5904
6417
|
failed
|
|
5905
|
-
}
|
|
6418
|
+
},
|
|
6419
|
+
...projectAutoResolvedNote ? { project_auto_resolved: projectAutoResolvedNote } : {}
|
|
5906
6420
|
}),
|
|
5907
6421
|
null,
|
|
5908
6422
|
2
|
|
@@ -5926,6 +6440,9 @@ Created with Social Neuron`;
|
|
|
5926
6440
|
lines.push(
|
|
5927
6441
|
`Scheduled: ${scheduled}/${workingPlan.posts.length} | Failed: ${failed}/${workingPlan.posts.length}`
|
|
5928
6442
|
);
|
|
6443
|
+
if (projectAutoResolvedNote) {
|
|
6444
|
+
lines.push("", `Note: ${projectAutoResolvedNote}`);
|
|
6445
|
+
}
|
|
5929
6446
|
return {
|
|
5930
6447
|
content: [{ type: "text", text: lines.join("\n") }],
|
|
5931
6448
|
isError: failed > 0
|
|
@@ -5945,7 +6462,7 @@ Created with Social Neuron`;
|
|
|
5945
6462
|
}
|
|
5946
6463
|
);
|
|
5947
6464
|
}
|
|
5948
|
-
var
|
|
6465
|
+
var PLATFORM_CASE_MAP2, TIKTOK_AUDIT_APPROVED, MCP_NOT_LIVE_FOR_POSTING, VIDEO_FILE_EXTENSIONS, IMAGE_FILE_EXTENSIONS;
|
|
5949
6466
|
var init_distribution = __esm({
|
|
5950
6467
|
"src/tools/distribution.ts"() {
|
|
5951
6468
|
"use strict";
|
|
@@ -5956,11 +6473,17 @@ var init_distribution = __esm({
|
|
|
5956
6473
|
init_supabase();
|
|
5957
6474
|
init_quality();
|
|
5958
6475
|
init_version();
|
|
5959
|
-
|
|
6476
|
+
init_connected_account_routing();
|
|
6477
|
+
PLATFORM_CASE_MAP2 = {
|
|
5960
6478
|
youtube: "YouTube",
|
|
5961
6479
|
tiktok: "TikTok",
|
|
5962
6480
|
instagram: "Instagram",
|
|
5963
6481
|
twitter: "Twitter",
|
|
6482
|
+
// 'x' is the platform's current branding but connected_account_routing.ts
|
|
6483
|
+
// (and the DB convention) still key on 'Twitter' — keep both aliases
|
|
6484
|
+
// resolving to the same case so schedule_content_plan's platform:'x' posts
|
|
6485
|
+
// don't fall through to an undefined binding (F8, 2026-07-15).
|
|
6486
|
+
x: "Twitter",
|
|
5964
6487
|
linkedin: "LinkedIn",
|
|
5965
6488
|
facebook: "Facebook",
|
|
5966
6489
|
threads: "Threads",
|
|
@@ -6502,15 +7025,35 @@ function registerAnalyticsTools(server2) {
|
|
|
6502
7025
|
content_id: z5.string().uuid().optional().describe(
|
|
6503
7026
|
"Filter to a specific content_history ID to see performance of one piece of content."
|
|
6504
7027
|
),
|
|
6505
|
-
project_id: z5.string().optional().describe("Project ID. Defaults to the active project context."),
|
|
7028
|
+
project_id: z5.string().uuid().optional().describe("Project ID. Defaults to the active project context."),
|
|
6506
7029
|
limit: z5.number().min(1).max(100).optional().describe("Maximum number of posts to return. Defaults to 20."),
|
|
6507
7030
|
response_format: z5.enum(["text", "json"]).optional().describe("Optional response format. Defaults to text.")
|
|
6508
7031
|
},
|
|
6509
|
-
async ({
|
|
7032
|
+
async ({
|
|
7033
|
+
platform: platform3,
|
|
7034
|
+
days,
|
|
7035
|
+
content_id,
|
|
7036
|
+
project_id,
|
|
7037
|
+
limit,
|
|
7038
|
+
response_format
|
|
7039
|
+
}) => {
|
|
6510
7040
|
const format = response_format ?? "text";
|
|
6511
7041
|
const lookbackDays = days ?? 30;
|
|
6512
7042
|
const maxPosts = limit ?? 20;
|
|
6513
|
-
const
|
|
7043
|
+
const projectResolution = await resolveProjectStrict(project_id);
|
|
7044
|
+
if (!projectResolution.projectId) {
|
|
7045
|
+
return {
|
|
7046
|
+
content: [
|
|
7047
|
+
{
|
|
7048
|
+
type: "text",
|
|
7049
|
+
text: projectResolution.error ?? "A project_id is required to fetch analytics. Configure an explicit project or use an API key scoped to exactly one project."
|
|
7050
|
+
}
|
|
7051
|
+
],
|
|
7052
|
+
isError: true
|
|
7053
|
+
};
|
|
7054
|
+
}
|
|
7055
|
+
const resolvedProjectId = projectResolution.projectId;
|
|
7056
|
+
const projectAutoResolvedNote = projectResolution.autoResolvedNote;
|
|
6514
7057
|
const { data: result, error: efError } = await callEdgeFunction("mcp-data", {
|
|
6515
7058
|
action: "analytics",
|
|
6516
7059
|
platform: platform3,
|
|
@@ -6521,11 +7064,17 @@ function registerAnalyticsTools(server2) {
|
|
|
6521
7064
|
limit: Math.min(maxPosts * 5, 100),
|
|
6522
7065
|
latestOnly: true,
|
|
6523
7066
|
contentId: content_id,
|
|
6524
|
-
|
|
7067
|
+
projectId: resolvedProjectId,
|
|
7068
|
+
project_id: resolvedProjectId
|
|
6525
7069
|
});
|
|
6526
7070
|
if (efError) {
|
|
6527
7071
|
return {
|
|
6528
|
-
content: [
|
|
7072
|
+
content: [
|
|
7073
|
+
{
|
|
7074
|
+
type: "text",
|
|
7075
|
+
text: `Failed to fetch analytics: ${efError}`
|
|
7076
|
+
}
|
|
7077
|
+
],
|
|
6529
7078
|
isError: true
|
|
6530
7079
|
};
|
|
6531
7080
|
}
|
|
@@ -6545,7 +7094,8 @@ function registerAnalyticsTools(server2) {
|
|
|
6545
7094
|
totalViews: 0,
|
|
6546
7095
|
totalEngagement: 0,
|
|
6547
7096
|
postCount: 0,
|
|
6548
|
-
posts: []
|
|
7097
|
+
posts: [],
|
|
7098
|
+
...projectAutoResolvedNote ? { project_auto_resolved: projectAutoResolvedNote } : {}
|
|
6549
7099
|
});
|
|
6550
7100
|
return {
|
|
6551
7101
|
structuredContent,
|
|
@@ -6561,7 +7111,9 @@ function registerAnalyticsTools(server2) {
|
|
|
6561
7111
|
content: [
|
|
6562
7112
|
{
|
|
6563
7113
|
type: "text",
|
|
6564
|
-
text: `No analytics data found for the last ${lookbackDays} days${platform3 ? ` on ${platform3}` : ""}.`
|
|
7114
|
+
text: `No analytics data found for the last ${lookbackDays} days${platform3 ? ` on ${platform3}` : ""}.` + (projectAutoResolvedNote ? `
|
|
7115
|
+
|
|
7116
|
+
Note: ${projectAutoResolvedNote}` : "")
|
|
6565
7117
|
}
|
|
6566
7118
|
]
|
|
6567
7119
|
};
|
|
@@ -6593,20 +7145,27 @@ function registerAnalyticsTools(server2) {
|
|
|
6593
7145
|
postCount: posts.length,
|
|
6594
7146
|
posts
|
|
6595
7147
|
};
|
|
6596
|
-
return formatAnalytics(
|
|
7148
|
+
return formatAnalytics(
|
|
7149
|
+
summary,
|
|
7150
|
+
lookbackDays,
|
|
7151
|
+
format,
|
|
7152
|
+
projectAutoResolvedNote
|
|
7153
|
+
);
|
|
6597
7154
|
}
|
|
6598
7155
|
);
|
|
6599
7156
|
server2.tool(
|
|
6600
7157
|
"refresh_platform_analytics",
|
|
6601
7158
|
"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.",
|
|
6602
7159
|
{
|
|
6603
|
-
project_id: z5.string().optional().describe("Project ID. Defaults to the active project context."),
|
|
7160
|
+
project_id: z5.string().uuid().optional().describe("Project ID. Defaults to the active project context."),
|
|
6604
7161
|
response_format: z5.enum(["text", "json"]).optional().describe("Optional response format. Defaults to text.")
|
|
6605
7162
|
},
|
|
6606
7163
|
async ({ project_id, response_format }) => {
|
|
6607
7164
|
const format = response_format ?? "text";
|
|
6608
7165
|
const userId = await getDefaultUserId();
|
|
6609
|
-
const
|
|
7166
|
+
const projectResolution = await resolveProjectStrict(project_id);
|
|
7167
|
+
const resolvedProjectId = projectResolution.projectId;
|
|
7168
|
+
const projectAutoResolvedNote = projectResolution.autoResolvedNote;
|
|
6610
7169
|
const rateLimit = checkRateLimit(
|
|
6611
7170
|
"posting",
|
|
6612
7171
|
`refresh_platform_analytics:${userId}:${resolvedProjectId ?? "default"}`
|
|
@@ -6622,25 +7181,48 @@ function registerAnalyticsTools(server2) {
|
|
|
6622
7181
|
isError: true
|
|
6623
7182
|
};
|
|
6624
7183
|
}
|
|
7184
|
+
if (!resolvedProjectId) {
|
|
7185
|
+
return {
|
|
7186
|
+
content: [
|
|
7187
|
+
{
|
|
7188
|
+
type: "text",
|
|
7189
|
+
text: projectResolution.error ?? "A project_id is required to refresh analytics. Configure an explicit project or use an API key scoped to exactly one project."
|
|
7190
|
+
}
|
|
7191
|
+
],
|
|
7192
|
+
isError: true
|
|
7193
|
+
};
|
|
7194
|
+
}
|
|
6625
7195
|
const { data, error } = await callEdgeFunction("fetch-analytics", {
|
|
6626
7196
|
userId,
|
|
6627
|
-
|
|
7197
|
+
projectId: resolvedProjectId,
|
|
7198
|
+
project_id: resolvedProjectId
|
|
6628
7199
|
});
|
|
6629
7200
|
if (error) {
|
|
6630
7201
|
return {
|
|
6631
|
-
content: [
|
|
7202
|
+
content: [
|
|
7203
|
+
{
|
|
7204
|
+
type: "text",
|
|
7205
|
+
text: `Error refreshing analytics: ${error}`
|
|
7206
|
+
}
|
|
7207
|
+
],
|
|
6632
7208
|
isError: true
|
|
6633
7209
|
};
|
|
6634
7210
|
}
|
|
6635
7211
|
const result = data;
|
|
6636
7212
|
if (!result.success) {
|
|
6637
7213
|
return {
|
|
6638
|
-
content: [
|
|
7214
|
+
content: [
|
|
7215
|
+
{ type: "text", text: "Analytics refresh failed." }
|
|
7216
|
+
],
|
|
6639
7217
|
isError: true
|
|
6640
7218
|
};
|
|
6641
7219
|
}
|
|
6642
|
-
const queued = (result.results ?? []).filter(
|
|
6643
|
-
|
|
7220
|
+
const queued = (result.results ?? []).filter(
|
|
7221
|
+
(r) => r.status === "queued"
|
|
7222
|
+
).length;
|
|
7223
|
+
const errored = (result.results ?? []).filter(
|
|
7224
|
+
(r) => r.status === "error"
|
|
7225
|
+
).length;
|
|
6644
7226
|
const lines = [
|
|
6645
7227
|
`Analytics refresh triggered successfully.`,
|
|
6646
7228
|
` Posts processed: ${result.postsProcessed}`,
|
|
@@ -6649,13 +7231,17 @@ function registerAnalyticsTools(server2) {
|
|
|
6649
7231
|
if (errored > 0) {
|
|
6650
7232
|
lines.push(` Errors: ${errored}`);
|
|
6651
7233
|
}
|
|
7234
|
+
if (projectAutoResolvedNote) {
|
|
7235
|
+
lines.push("", `Note: ${projectAutoResolvedNote}`);
|
|
7236
|
+
}
|
|
6652
7237
|
if (format === "json") {
|
|
6653
7238
|
const structuredContent = asEnvelope4({
|
|
6654
7239
|
success: true,
|
|
6655
7240
|
postsProcessed: result.postsProcessed,
|
|
6656
7241
|
queued,
|
|
6657
7242
|
errored,
|
|
6658
|
-
projectId: resolvedProjectId ?? null
|
|
7243
|
+
projectId: resolvedProjectId ?? null,
|
|
7244
|
+
...projectAutoResolvedNote ? { project_auto_resolved: projectAutoResolvedNote } : {}
|
|
6659
7245
|
});
|
|
6660
7246
|
return {
|
|
6661
7247
|
structuredContent,
|
|
@@ -6671,12 +7257,21 @@ function registerAnalyticsTools(server2) {
|
|
|
6671
7257
|
}
|
|
6672
7258
|
);
|
|
6673
7259
|
}
|
|
6674
|
-
function formatAnalytics(summary, days, format) {
|
|
6675
|
-
const structuredContent = asEnvelope4({
|
|
7260
|
+
function formatAnalytics(summary, days, format, projectAutoResolvedNote) {
|
|
7261
|
+
const structuredContent = asEnvelope4({
|
|
7262
|
+
...summary,
|
|
7263
|
+
days,
|
|
7264
|
+
...projectAutoResolvedNote ? { project_auto_resolved: projectAutoResolvedNote } : {}
|
|
7265
|
+
});
|
|
6676
7266
|
if (format === "json") {
|
|
6677
7267
|
return {
|
|
6678
7268
|
structuredContent,
|
|
6679
|
-
content: [
|
|
7269
|
+
content: [
|
|
7270
|
+
{
|
|
7271
|
+
type: "text",
|
|
7272
|
+
text: JSON.stringify(structuredContent, null, 2)
|
|
7273
|
+
}
|
|
7274
|
+
]
|
|
6680
7275
|
};
|
|
6681
7276
|
}
|
|
6682
7277
|
const lines = [
|
|
@@ -6700,6 +7295,9 @@ function formatAnalytics(summary, days, format) {
|
|
|
6700
7295
|
lines.push(line);
|
|
6701
7296
|
}
|
|
6702
7297
|
}
|
|
7298
|
+
if (projectAutoResolvedNote) {
|
|
7299
|
+
lines.push("", `Note: ${projectAutoResolvedNote}`);
|
|
7300
|
+
}
|
|
6703
7301
|
return {
|
|
6704
7302
|
structuredContent,
|
|
6705
7303
|
content: [{ type: "text", text: lines.join("\n") }]
|
|
@@ -8129,15 +8727,64 @@ function registerYouTubeAnalyticsTools(server2) {
|
|
|
8129
8727
|
start_date: z10.string().regex(/^\d{4}-\d{2}-\d{2}$/).describe("Start date in YYYY-MM-DD format."),
|
|
8130
8728
|
end_date: z10.string().regex(/^\d{4}-\d{2}-\d{2}$/).describe("End date in YYYY-MM-DD format."),
|
|
8131
8729
|
video_id: z10.string().optional().describe('YouTube video ID. Required when action is "video".'),
|
|
8132
|
-
max_results: z10.number().min(1).max(50).optional().describe(
|
|
8730
|
+
max_results: z10.number().min(1).max(50).optional().describe(
|
|
8731
|
+
'Max videos to return for "topVideos" action. Defaults to 10.'
|
|
8732
|
+
),
|
|
8733
|
+
connected_account_id: z10.string().uuid().optional().describe(
|
|
8734
|
+
"Exact YouTube connected-account ID from list_connections. Optional when exactly one active YouTube account is bound to the resolved project \u2014 auto-resolved. Required (with a clear list of candidates) when the project has multiple YouTube accounts."
|
|
8735
|
+
),
|
|
8736
|
+
project_id: z10.string().uuid().optional().describe(
|
|
8737
|
+
"Exact brand/project ID. Defaults only when the authenticated user has one project."
|
|
8738
|
+
),
|
|
8133
8739
|
response_format: z10.enum(["text", "json"]).optional().describe("Optional response format. Defaults to text.")
|
|
8134
8740
|
},
|
|
8135
|
-
async ({
|
|
8741
|
+
async ({
|
|
8742
|
+
action,
|
|
8743
|
+
start_date,
|
|
8744
|
+
end_date,
|
|
8745
|
+
video_id,
|
|
8746
|
+
max_results,
|
|
8747
|
+
connected_account_id,
|
|
8748
|
+
project_id,
|
|
8749
|
+
response_format
|
|
8750
|
+
}) => {
|
|
8136
8751
|
const format = response_format ?? "text";
|
|
8137
8752
|
if (action === "video" && !video_id) {
|
|
8138
8753
|
return {
|
|
8139
8754
|
content: [
|
|
8140
|
-
{
|
|
8755
|
+
{
|
|
8756
|
+
type: "text",
|
|
8757
|
+
text: 'Error: video_id is required when action is "video".'
|
|
8758
|
+
}
|
|
8759
|
+
],
|
|
8760
|
+
isError: true
|
|
8761
|
+
};
|
|
8762
|
+
}
|
|
8763
|
+
const resolvedProjectId = project_id ?? await getDefaultProjectId() ?? void 0;
|
|
8764
|
+
if (!resolvedProjectId) {
|
|
8765
|
+
return {
|
|
8766
|
+
content: [
|
|
8767
|
+
{
|
|
8768
|
+
type: "text",
|
|
8769
|
+
text: "project_id is required. Configure an explicit project or use an API key scoped to exactly one project."
|
|
8770
|
+
}
|
|
8771
|
+
],
|
|
8772
|
+
isError: true
|
|
8773
|
+
};
|
|
8774
|
+
}
|
|
8775
|
+
const routing = await resolveConnectedAccountRouting({
|
|
8776
|
+
projectId: resolvedProjectId,
|
|
8777
|
+
platforms: ["youtube"],
|
|
8778
|
+
requestedAccountIds: connected_account_id ? { youtube: connected_account_id } : void 0
|
|
8779
|
+
});
|
|
8780
|
+
const resolvedAccountId = routing.connectedAccountIds?.YouTube;
|
|
8781
|
+
if (routing.error || !resolvedAccountId) {
|
|
8782
|
+
return {
|
|
8783
|
+
content: [
|
|
8784
|
+
{
|
|
8785
|
+
type: "text",
|
|
8786
|
+
text: routing.error ?? "YouTube: exact connected-account routing could not be established."
|
|
8787
|
+
}
|
|
8141
8788
|
],
|
|
8142
8789
|
isError: true
|
|
8143
8790
|
};
|
|
@@ -8147,11 +8794,19 @@ function registerYouTubeAnalyticsTools(server2) {
|
|
|
8147
8794
|
startDate: start_date,
|
|
8148
8795
|
endDate: end_date,
|
|
8149
8796
|
videoId: video_id,
|
|
8150
|
-
maxResults: max_results ?? 10
|
|
8797
|
+
maxResults: max_results ?? 10,
|
|
8798
|
+
projectId: resolvedProjectId,
|
|
8799
|
+
project_id: resolvedProjectId,
|
|
8800
|
+
connectedAccountId: resolvedAccountId
|
|
8151
8801
|
});
|
|
8152
8802
|
if (error) {
|
|
8153
8803
|
return {
|
|
8154
|
-
content: [
|
|
8804
|
+
content: [
|
|
8805
|
+
{
|
|
8806
|
+
type: "text",
|
|
8807
|
+
text: `YouTube Analytics error: ${error}`
|
|
8808
|
+
}
|
|
8809
|
+
],
|
|
8155
8810
|
isError: true
|
|
8156
8811
|
};
|
|
8157
8812
|
}
|
|
@@ -8164,7 +8819,12 @@ function registerYouTubeAnalyticsTools(server2) {
|
|
|
8164
8819
|
{
|
|
8165
8820
|
type: "text",
|
|
8166
8821
|
text: JSON.stringify(
|
|
8167
|
-
asEnvelope7({
|
|
8822
|
+
asEnvelope7({
|
|
8823
|
+
action,
|
|
8824
|
+
startDate: start_date,
|
|
8825
|
+
endDate: end_date,
|
|
8826
|
+
analytics: a
|
|
8827
|
+
}),
|
|
8168
8828
|
null,
|
|
8169
8829
|
2
|
|
8170
8830
|
)
|
|
@@ -8191,7 +8851,10 @@ function registerYouTubeAnalyticsTools(server2) {
|
|
|
8191
8851
|
if (days.length === 0) {
|
|
8192
8852
|
return {
|
|
8193
8853
|
content: [
|
|
8194
|
-
{
|
|
8854
|
+
{
|
|
8855
|
+
type: "text",
|
|
8856
|
+
text: "No daily analytics data found for this period."
|
|
8857
|
+
}
|
|
8195
8858
|
]
|
|
8196
8859
|
};
|
|
8197
8860
|
}
|
|
@@ -8214,7 +8877,10 @@ function registerYouTubeAnalyticsTools(server2) {
|
|
|
8214
8877
|
]
|
|
8215
8878
|
};
|
|
8216
8879
|
}
|
|
8217
|
-
const lines = [
|
|
8880
|
+
const lines = [
|
|
8881
|
+
`YouTube Daily Analytics (${start_date} to ${end_date}):`,
|
|
8882
|
+
""
|
|
8883
|
+
];
|
|
8218
8884
|
for (const d of days) {
|
|
8219
8885
|
lines.push(
|
|
8220
8886
|
` ${d.date}: ${d.views.toLocaleString()} views, ${d.watchTimeMinutes.toLocaleString()} min watch, +${d.subscribersGained} subs, ${d.likes} likes, ${d.comments} comments`
|
|
@@ -8261,7 +8927,12 @@ function registerYouTubeAnalyticsTools(server2) {
|
|
|
8261
8927
|
const videos = result.topVideos ?? [];
|
|
8262
8928
|
if (videos.length === 0) {
|
|
8263
8929
|
return {
|
|
8264
|
-
content: [
|
|
8930
|
+
content: [
|
|
8931
|
+
{
|
|
8932
|
+
type: "text",
|
|
8933
|
+
text: "No top videos found for this period."
|
|
8934
|
+
}
|
|
8935
|
+
]
|
|
8265
8936
|
};
|
|
8266
8937
|
}
|
|
8267
8938
|
if (format === "json") {
|
|
@@ -8283,7 +8954,10 @@ function registerYouTubeAnalyticsTools(server2) {
|
|
|
8283
8954
|
]
|
|
8284
8955
|
};
|
|
8285
8956
|
}
|
|
8286
|
-
const lines = [
|
|
8957
|
+
const lines = [
|
|
8958
|
+
`Top ${videos.length} YouTube Videos (${start_date} to ${end_date}):`,
|
|
8959
|
+
""
|
|
8960
|
+
];
|
|
8287
8961
|
for (let i = 0; i < videos.length; i++) {
|
|
8288
8962
|
const v = videos[i];
|
|
8289
8963
|
lines.push(
|
|
@@ -8296,11 +8970,18 @@ function registerYouTubeAnalyticsTools(server2) {
|
|
|
8296
8970
|
}
|
|
8297
8971
|
if (format === "json") {
|
|
8298
8972
|
return {
|
|
8299
|
-
content: [
|
|
8973
|
+
content: [
|
|
8974
|
+
{
|
|
8975
|
+
type: "text",
|
|
8976
|
+
text: JSON.stringify(asEnvelope7(result), null, 2)
|
|
8977
|
+
}
|
|
8978
|
+
]
|
|
8300
8979
|
};
|
|
8301
8980
|
}
|
|
8302
8981
|
return {
|
|
8303
|
-
content: [
|
|
8982
|
+
content: [
|
|
8983
|
+
{ type: "text", text: JSON.stringify(result, null, 2) }
|
|
8984
|
+
]
|
|
8304
8985
|
};
|
|
8305
8986
|
}
|
|
8306
8987
|
);
|
|
@@ -8309,6 +8990,8 @@ var init_youtube_analytics = __esm({
|
|
|
8309
8990
|
"src/tools/youtube-analytics.ts"() {
|
|
8310
8991
|
"use strict";
|
|
8311
8992
|
init_edge_function();
|
|
8993
|
+
init_supabase();
|
|
8994
|
+
init_connected_account_routing();
|
|
8312
8995
|
init_version();
|
|
8313
8996
|
}
|
|
8314
8997
|
});
|
|
@@ -8324,6 +9007,29 @@ function asEnvelope8(data) {
|
|
|
8324
9007
|
data
|
|
8325
9008
|
};
|
|
8326
9009
|
}
|
|
9010
|
+
async function exactYouTubeRoute(projectId, connectedAccountId) {
|
|
9011
|
+
const resolvedProjectId = projectId ?? await getDefaultProjectId() ?? void 0;
|
|
9012
|
+
if (!resolvedProjectId) {
|
|
9013
|
+
return {
|
|
9014
|
+
error: "project_id is required. Configure an explicit project or use an API key scoped to exactly one project."
|
|
9015
|
+
};
|
|
9016
|
+
}
|
|
9017
|
+
const routing = await resolveConnectedAccountRouting({
|
|
9018
|
+
projectId: resolvedProjectId,
|
|
9019
|
+
platforms: ["youtube"],
|
|
9020
|
+
requestedAccountIds: connectedAccountId ? { youtube: connectedAccountId } : void 0
|
|
9021
|
+
});
|
|
9022
|
+
const resolvedAccountId = routing.connectedAccountIds?.YouTube;
|
|
9023
|
+
if (routing.error || !resolvedAccountId) {
|
|
9024
|
+
return {
|
|
9025
|
+
error: routing.error ?? "YouTube: exact connected-account routing could not be established."
|
|
9026
|
+
};
|
|
9027
|
+
}
|
|
9028
|
+
return {
|
|
9029
|
+
projectId: resolvedProjectId,
|
|
9030
|
+
connectedAccountId: resolvedAccountId
|
|
9031
|
+
};
|
|
9032
|
+
}
|
|
8327
9033
|
function registerCommentsTools(server2) {
|
|
8328
9034
|
server2.tool(
|
|
8329
9035
|
"list_comments",
|
|
@@ -8336,19 +9042,42 @@ function registerCommentsTools(server2) {
|
|
|
8336
9042
|
page_token: z11.string().optional().describe(
|
|
8337
9043
|
"Pagination cursor from previous list_comments response nextPageToken field. Omit for first page of results."
|
|
8338
9044
|
),
|
|
9045
|
+
connected_account_id: z11.string().uuid().optional().describe(
|
|
9046
|
+
"Exact YouTube connected-account ID from list_connections. Optional when exactly one active YouTube account is bound to the resolved project \u2014 auto-resolved. Required (with a clear list of candidates) when the project has multiple YouTube accounts."
|
|
9047
|
+
),
|
|
9048
|
+
project_id: PROJECT_ID_SCHEMA,
|
|
8339
9049
|
response_format: z11.enum(["text", "json"]).optional().describe("Optional response format. Defaults to text.")
|
|
8340
9050
|
},
|
|
8341
|
-
async ({
|
|
9051
|
+
async ({
|
|
9052
|
+
video_id,
|
|
9053
|
+
max_results,
|
|
9054
|
+
page_token,
|
|
9055
|
+
connected_account_id,
|
|
9056
|
+
project_id,
|
|
9057
|
+
response_format
|
|
9058
|
+
}) => {
|
|
8342
9059
|
const format = response_format ?? "text";
|
|
9060
|
+
const route = await exactYouTubeRoute(project_id, connected_account_id);
|
|
9061
|
+
if ("error" in route) {
|
|
9062
|
+
return {
|
|
9063
|
+
content: [{ type: "text", text: route.error }],
|
|
9064
|
+
isError: true
|
|
9065
|
+
};
|
|
9066
|
+
}
|
|
8343
9067
|
const { data, error } = await callEdgeFunction("youtube-comments", {
|
|
8344
9068
|
action: "list",
|
|
8345
9069
|
videoId: video_id,
|
|
8346
9070
|
maxResults: max_results ?? 50,
|
|
8347
|
-
pageToken: page_token
|
|
9071
|
+
pageToken: page_token,
|
|
9072
|
+
projectId: route.projectId,
|
|
9073
|
+
project_id: route.projectId,
|
|
9074
|
+
connectedAccountId: route.connectedAccountId
|
|
8348
9075
|
});
|
|
8349
9076
|
if (error) {
|
|
8350
9077
|
return {
|
|
8351
|
-
content: [
|
|
9078
|
+
content: [
|
|
9079
|
+
{ type: "text", text: `Error listing comments: ${error}` }
|
|
9080
|
+
],
|
|
8352
9081
|
isError: true
|
|
8353
9082
|
};
|
|
8354
9083
|
}
|
|
@@ -8360,7 +9089,10 @@ function registerCommentsTools(server2) {
|
|
|
8360
9089
|
{
|
|
8361
9090
|
type: "text",
|
|
8362
9091
|
text: JSON.stringify(
|
|
8363
|
-
asEnvelope8({
|
|
9092
|
+
asEnvelope8({
|
|
9093
|
+
comments,
|
|
9094
|
+
nextPageToken: result.nextPageToken ?? null
|
|
9095
|
+
}),
|
|
8364
9096
|
null,
|
|
8365
9097
|
2
|
|
8366
9098
|
)
|
|
@@ -8397,12 +9129,31 @@ function registerCommentsTools(server2) {
|
|
|
8397
9129
|
"reply_to_comment",
|
|
8398
9130
|
"Reply to a YouTube comment. Get the parent_id from list_comments results. Reply appears as the authenticated channel. Use for community engagement after checking list_comments for questions or feedback.",
|
|
8399
9131
|
{
|
|
8400
|
-
parent_id: z11.string().describe(
|
|
9132
|
+
parent_id: z11.string().describe(
|
|
9133
|
+
"The ID of the parent comment to reply to (from list_comments)."
|
|
9134
|
+
),
|
|
8401
9135
|
text: z11.string().min(1).describe("The reply text."),
|
|
9136
|
+
connected_account_id: z11.string().uuid().optional().describe(
|
|
9137
|
+
"Exact YouTube connected-account ID from list_connections. Optional when exactly one active YouTube account is bound to the resolved project \u2014 auto-resolved. Required (with a clear list of candidates) when the project has multiple YouTube accounts."
|
|
9138
|
+
),
|
|
9139
|
+
project_id: PROJECT_ID_SCHEMA,
|
|
8402
9140
|
response_format: z11.enum(["text", "json"]).optional().describe("Optional response format. Defaults to text.")
|
|
8403
9141
|
},
|
|
8404
|
-
async ({
|
|
9142
|
+
async ({
|
|
9143
|
+
parent_id,
|
|
9144
|
+
text,
|
|
9145
|
+
connected_account_id,
|
|
9146
|
+
project_id,
|
|
9147
|
+
response_format
|
|
9148
|
+
}) => {
|
|
8405
9149
|
const format = response_format ?? "text";
|
|
9150
|
+
const route = await exactYouTubeRoute(project_id, connected_account_id);
|
|
9151
|
+
if ("error" in route) {
|
|
9152
|
+
return {
|
|
9153
|
+
content: [{ type: "text", text: route.error }],
|
|
9154
|
+
isError: true
|
|
9155
|
+
};
|
|
9156
|
+
}
|
|
8406
9157
|
const userId = await getDefaultUserId();
|
|
8407
9158
|
const rateLimit = checkRateLimit("posting", `reply_to_comment:${userId}`);
|
|
8408
9159
|
if (!rateLimit.allowed) {
|
|
@@ -8419,18 +9170,31 @@ function registerCommentsTools(server2) {
|
|
|
8419
9170
|
const { data, error } = await callEdgeFunction("youtube-comments", {
|
|
8420
9171
|
action: "reply",
|
|
8421
9172
|
parentId: parent_id,
|
|
8422
|
-
text
|
|
9173
|
+
text,
|
|
9174
|
+
projectId: route.projectId,
|
|
9175
|
+
project_id: route.projectId,
|
|
9176
|
+
connectedAccountId: route.connectedAccountId
|
|
8423
9177
|
});
|
|
8424
9178
|
if (error) {
|
|
8425
9179
|
return {
|
|
8426
|
-
content: [
|
|
9180
|
+
content: [
|
|
9181
|
+
{
|
|
9182
|
+
type: "text",
|
|
9183
|
+
text: `Error replying to comment: ${error}`
|
|
9184
|
+
}
|
|
9185
|
+
],
|
|
8427
9186
|
isError: true
|
|
8428
9187
|
};
|
|
8429
9188
|
}
|
|
8430
9189
|
const result = data;
|
|
8431
9190
|
if (format === "json") {
|
|
8432
9191
|
return {
|
|
8433
|
-
content: [
|
|
9192
|
+
content: [
|
|
9193
|
+
{
|
|
9194
|
+
type: "text",
|
|
9195
|
+
text: JSON.stringify(asEnvelope8(result), null, 2)
|
|
9196
|
+
}
|
|
9197
|
+
]
|
|
8434
9198
|
};
|
|
8435
9199
|
}
|
|
8436
9200
|
return {
|
|
@@ -8451,10 +9215,27 @@ function registerCommentsTools(server2) {
|
|
|
8451
9215
|
{
|
|
8452
9216
|
video_id: z11.string().describe("The YouTube video ID to comment on."),
|
|
8453
9217
|
text: z11.string().min(1).describe("The comment text."),
|
|
9218
|
+
connected_account_id: z11.string().uuid().optional().describe(
|
|
9219
|
+
"Exact YouTube connected-account ID from list_connections. Optional when exactly one active YouTube account is bound to the resolved project \u2014 auto-resolved. Required (with a clear list of candidates) when the project has multiple YouTube accounts."
|
|
9220
|
+
),
|
|
9221
|
+
project_id: PROJECT_ID_SCHEMA,
|
|
8454
9222
|
response_format: z11.enum(["text", "json"]).optional().describe("Optional response format. Defaults to text.")
|
|
8455
9223
|
},
|
|
8456
|
-
async ({
|
|
9224
|
+
async ({
|
|
9225
|
+
video_id,
|
|
9226
|
+
text,
|
|
9227
|
+
connected_account_id,
|
|
9228
|
+
project_id,
|
|
9229
|
+
response_format
|
|
9230
|
+
}) => {
|
|
8457
9231
|
const format = response_format ?? "text";
|
|
9232
|
+
const route = await exactYouTubeRoute(project_id, connected_account_id);
|
|
9233
|
+
if ("error" in route) {
|
|
9234
|
+
return {
|
|
9235
|
+
content: [{ type: "text", text: route.error }],
|
|
9236
|
+
isError: true
|
|
9237
|
+
};
|
|
9238
|
+
}
|
|
8458
9239
|
const userId = await getDefaultUserId();
|
|
8459
9240
|
const rateLimit = checkRateLimit("posting", `post_comment:${userId}`);
|
|
8460
9241
|
if (!rateLimit.allowed) {
|
|
@@ -8471,18 +9252,28 @@ function registerCommentsTools(server2) {
|
|
|
8471
9252
|
const { data, error } = await callEdgeFunction("youtube-comments", {
|
|
8472
9253
|
action: "post",
|
|
8473
9254
|
videoId: video_id,
|
|
8474
|
-
text
|
|
9255
|
+
text,
|
|
9256
|
+
projectId: route.projectId,
|
|
9257
|
+
project_id: route.projectId,
|
|
9258
|
+
connectedAccountId: route.connectedAccountId
|
|
8475
9259
|
});
|
|
8476
9260
|
if (error) {
|
|
8477
9261
|
return {
|
|
8478
|
-
content: [
|
|
9262
|
+
content: [
|
|
9263
|
+
{ type: "text", text: `Error posting comment: ${error}` }
|
|
9264
|
+
],
|
|
8479
9265
|
isError: true
|
|
8480
9266
|
};
|
|
8481
9267
|
}
|
|
8482
9268
|
const result = data;
|
|
8483
9269
|
if (format === "json") {
|
|
8484
9270
|
return {
|
|
8485
|
-
content: [
|
|
9271
|
+
content: [
|
|
9272
|
+
{
|
|
9273
|
+
type: "text",
|
|
9274
|
+
text: JSON.stringify(asEnvelope8(result), null, 2)
|
|
9275
|
+
}
|
|
9276
|
+
]
|
|
8486
9277
|
};
|
|
8487
9278
|
}
|
|
8488
9279
|
return {
|
|
@@ -8503,10 +9294,27 @@ function registerCommentsTools(server2) {
|
|
|
8503
9294
|
{
|
|
8504
9295
|
comment_id: z11.string().describe("The comment ID to moderate."),
|
|
8505
9296
|
moderation_status: z11.enum(["published", "rejected"]).describe('"published" to approve, "rejected" to hide.'),
|
|
9297
|
+
connected_account_id: z11.string().uuid().optional().describe(
|
|
9298
|
+
"Exact YouTube connected-account ID from list_connections. Optional when exactly one active YouTube account is bound to the resolved project \u2014 auto-resolved. Required (with a clear list of candidates) when the project has multiple YouTube accounts."
|
|
9299
|
+
),
|
|
9300
|
+
project_id: PROJECT_ID_SCHEMA,
|
|
8506
9301
|
response_format: z11.enum(["text", "json"]).optional().describe("Optional response format. Defaults to text.")
|
|
8507
9302
|
},
|
|
8508
|
-
async ({
|
|
9303
|
+
async ({
|
|
9304
|
+
comment_id,
|
|
9305
|
+
moderation_status,
|
|
9306
|
+
connected_account_id,
|
|
9307
|
+
project_id,
|
|
9308
|
+
response_format
|
|
9309
|
+
}) => {
|
|
8509
9310
|
const format = response_format ?? "text";
|
|
9311
|
+
const route = await exactYouTubeRoute(project_id, connected_account_id);
|
|
9312
|
+
if ("error" in route) {
|
|
9313
|
+
return {
|
|
9314
|
+
content: [{ type: "text", text: route.error }],
|
|
9315
|
+
isError: true
|
|
9316
|
+
};
|
|
9317
|
+
}
|
|
8510
9318
|
const userId = await getDefaultUserId();
|
|
8511
9319
|
const rateLimit = checkRateLimit("posting", `moderate_comment:${userId}`);
|
|
8512
9320
|
if (!rateLimit.allowed) {
|
|
@@ -8523,11 +9331,19 @@ function registerCommentsTools(server2) {
|
|
|
8523
9331
|
const { error } = await callEdgeFunction("youtube-comments", {
|
|
8524
9332
|
action: "moderate",
|
|
8525
9333
|
commentId: comment_id,
|
|
8526
|
-
moderationStatus: moderation_status
|
|
9334
|
+
moderationStatus: moderation_status,
|
|
9335
|
+
projectId: route.projectId,
|
|
9336
|
+
project_id: route.projectId,
|
|
9337
|
+
connectedAccountId: route.connectedAccountId
|
|
8527
9338
|
});
|
|
8528
9339
|
if (error) {
|
|
8529
9340
|
return {
|
|
8530
|
-
content: [
|
|
9341
|
+
content: [
|
|
9342
|
+
{
|
|
9343
|
+
type: "text",
|
|
9344
|
+
text: `Error moderating comment: ${error}`
|
|
9345
|
+
}
|
|
9346
|
+
],
|
|
8531
9347
|
isError: true
|
|
8532
9348
|
};
|
|
8533
9349
|
}
|
|
@@ -8564,10 +9380,26 @@ function registerCommentsTools(server2) {
|
|
|
8564
9380
|
"Delete a YouTube comment. Only works for comments owned by the authenticated channel.",
|
|
8565
9381
|
{
|
|
8566
9382
|
comment_id: z11.string().describe("The comment ID to delete."),
|
|
9383
|
+
connected_account_id: z11.string().uuid().optional().describe(
|
|
9384
|
+
"Exact YouTube connected-account ID from list_connections. Optional when exactly one active YouTube account is bound to the resolved project \u2014 auto-resolved. Required (with a clear list of candidates) when the project has multiple YouTube accounts."
|
|
9385
|
+
),
|
|
9386
|
+
project_id: PROJECT_ID_SCHEMA,
|
|
8567
9387
|
response_format: z11.enum(["text", "json"]).optional().describe("Optional response format. Defaults to text.")
|
|
8568
9388
|
},
|
|
8569
|
-
async ({
|
|
9389
|
+
async ({
|
|
9390
|
+
comment_id,
|
|
9391
|
+
connected_account_id,
|
|
9392
|
+
project_id,
|
|
9393
|
+
response_format
|
|
9394
|
+
}) => {
|
|
8570
9395
|
const format = response_format ?? "text";
|
|
9396
|
+
const route = await exactYouTubeRoute(project_id, connected_account_id);
|
|
9397
|
+
if ("error" in route) {
|
|
9398
|
+
return {
|
|
9399
|
+
content: [{ type: "text", text: route.error }],
|
|
9400
|
+
isError: true
|
|
9401
|
+
};
|
|
9402
|
+
}
|
|
8571
9403
|
const userId = await getDefaultUserId();
|
|
8572
9404
|
const rateLimit = checkRateLimit("posting", `delete_comment:${userId}`);
|
|
8573
9405
|
if (!rateLimit.allowed) {
|
|
@@ -8583,11 +9415,16 @@ function registerCommentsTools(server2) {
|
|
|
8583
9415
|
}
|
|
8584
9416
|
const { error } = await callEdgeFunction("youtube-comments", {
|
|
8585
9417
|
action: "delete",
|
|
8586
|
-
commentId: comment_id
|
|
9418
|
+
commentId: comment_id,
|
|
9419
|
+
projectId: route.projectId,
|
|
9420
|
+
project_id: route.projectId,
|
|
9421
|
+
connectedAccountId: route.connectedAccountId
|
|
8587
9422
|
});
|
|
8588
9423
|
if (error) {
|
|
8589
9424
|
return {
|
|
8590
|
-
content: [
|
|
9425
|
+
content: [
|
|
9426
|
+
{ type: "text", text: `Error deleting comment: ${error}` }
|
|
9427
|
+
],
|
|
8591
9428
|
isError: true
|
|
8592
9429
|
};
|
|
8593
9430
|
}
|
|
@@ -8596,24 +9433,38 @@ function registerCommentsTools(server2) {
|
|
|
8596
9433
|
content: [
|
|
8597
9434
|
{
|
|
8598
9435
|
type: "text",
|
|
8599
|
-
text: JSON.stringify(
|
|
9436
|
+
text: JSON.stringify(
|
|
9437
|
+
asEnvelope8({ success: true, commentId: comment_id }),
|
|
9438
|
+
null,
|
|
9439
|
+
2
|
|
9440
|
+
)
|
|
8600
9441
|
}
|
|
8601
9442
|
]
|
|
8602
9443
|
};
|
|
8603
9444
|
}
|
|
8604
9445
|
return {
|
|
8605
|
-
content: [
|
|
9446
|
+
content: [
|
|
9447
|
+
{
|
|
9448
|
+
type: "text",
|
|
9449
|
+
text: `Comment ${comment_id} deleted successfully.`
|
|
9450
|
+
}
|
|
9451
|
+
]
|
|
8606
9452
|
};
|
|
8607
9453
|
}
|
|
8608
9454
|
);
|
|
8609
9455
|
}
|
|
9456
|
+
var PROJECT_ID_SCHEMA;
|
|
8610
9457
|
var init_comments = __esm({
|
|
8611
9458
|
"src/tools/comments.ts"() {
|
|
8612
9459
|
"use strict";
|
|
8613
9460
|
init_edge_function();
|
|
8614
9461
|
init_rate_limit();
|
|
8615
9462
|
init_supabase();
|
|
9463
|
+
init_connected_account_routing();
|
|
8616
9464
|
init_version();
|
|
9465
|
+
PROJECT_ID_SCHEMA = z11.string().uuid().optional().describe(
|
|
9466
|
+
"Exact brand/project ID. Defaults only when the authenticated user has one project."
|
|
9467
|
+
);
|
|
8617
9468
|
}
|
|
8618
9469
|
});
|
|
8619
9470
|
|
|
@@ -11324,6 +12175,14 @@ var init_plan_approvals = __esm({
|
|
|
11324
12175
|
|
|
11325
12176
|
// src/tools/discovery.ts
|
|
11326
12177
|
import { z as z23 } from "zod";
|
|
12178
|
+
function isHostedTransport() {
|
|
12179
|
+
return process.env.MCP_TRANSPORT === "http";
|
|
12180
|
+
}
|
|
12181
|
+
function isPubliclyDiscoverable(tool) {
|
|
12182
|
+
if (tool.internal || tool.hiddenFromPublicCount) return false;
|
|
12183
|
+
if (tool.localOnly && isHostedTransport()) return false;
|
|
12184
|
+
return true;
|
|
12185
|
+
}
|
|
11327
12186
|
function toolKnowledgeDocument(tool) {
|
|
11328
12187
|
const lines = [
|
|
11329
12188
|
`Tool: ${tool.name}`,
|
|
@@ -11334,7 +12193,8 @@ function toolKnowledgeDocument(tool) {
|
|
|
11334
12193
|
if (tool.task_intent) lines.push(`Task intent: ${tool.task_intent}`);
|
|
11335
12194
|
if (tool.use_when) lines.push(`Use when: ${tool.use_when}`);
|
|
11336
12195
|
if (tool.avoid_when) lines.push(`Avoid when: ${tool.avoid_when}`);
|
|
11337
|
-
if (tool.next_tools?.length)
|
|
12196
|
+
if (tool.next_tools?.length)
|
|
12197
|
+
lines.push(`Common next tools: ${tool.next_tools.join(", ")}`);
|
|
11338
12198
|
return {
|
|
11339
12199
|
id: `tool:${tool.name}`,
|
|
11340
12200
|
title: `MCP tool: ${tool.name}`,
|
|
@@ -11351,9 +12211,7 @@ function toolKnowledgeDocument(tool) {
|
|
|
11351
12211
|
function getKnowledgeDocuments() {
|
|
11352
12212
|
return [
|
|
11353
12213
|
...STATIC_KNOWLEDGE_DOCUMENTS,
|
|
11354
|
-
...TOOL_CATALOG.filter(
|
|
11355
|
-
toolKnowledgeDocument
|
|
11356
|
-
)
|
|
12214
|
+
...TOOL_CATALOG.filter(isPubliclyDiscoverable).map(toolKnowledgeDocument)
|
|
11357
12215
|
];
|
|
11358
12216
|
}
|
|
11359
12217
|
function tokenize(input) {
|
|
@@ -11407,7 +12265,9 @@ function registerDiscoveryTools(server2) {
|
|
|
11407
12265
|
};
|
|
11408
12266
|
return {
|
|
11409
12267
|
structuredContent,
|
|
11410
|
-
content: [
|
|
12268
|
+
content: [
|
|
12269
|
+
{ type: "text", text: JSON.stringify(structuredContent) }
|
|
12270
|
+
]
|
|
11411
12271
|
};
|
|
11412
12272
|
}
|
|
11413
12273
|
);
|
|
@@ -11428,10 +12288,14 @@ function registerDiscoveryTools(server2) {
|
|
|
11428
12288
|
}
|
|
11429
12289
|
},
|
|
11430
12290
|
async ({ id }) => {
|
|
11431
|
-
const doc = getKnowledgeDocuments().find(
|
|
12291
|
+
const doc = getKnowledgeDocuments().find(
|
|
12292
|
+
(candidate) => candidate.id === id
|
|
12293
|
+
);
|
|
11432
12294
|
if (!doc) {
|
|
11433
12295
|
return {
|
|
11434
|
-
content: [
|
|
12296
|
+
content: [
|
|
12297
|
+
{ type: "text", text: `Document not found: ${id}` }
|
|
12298
|
+
],
|
|
11435
12299
|
isError: true
|
|
11436
12300
|
};
|
|
11437
12301
|
}
|
|
@@ -11444,7 +12308,9 @@ function registerDiscoveryTools(server2) {
|
|
|
11444
12308
|
};
|
|
11445
12309
|
return {
|
|
11446
12310
|
structuredContent,
|
|
11447
|
-
content: [
|
|
12311
|
+
content: [
|
|
12312
|
+
{ type: "text", text: JSON.stringify(structuredContent) }
|
|
12313
|
+
]
|
|
11448
12314
|
};
|
|
11449
12315
|
}
|
|
11450
12316
|
);
|
|
@@ -11453,7 +12319,9 @@ function registerDiscoveryTools(server2) {
|
|
|
11453
12319
|
'Search available tools by name, description, module, or scope. Use "name" detail (~50 tokens) for quick lookup, "summary" (~500 tokens) for descriptions, "full" for complete input schemas. Start here if unsure which tool to call \u2014 filter by module (e.g. "planning", "content", "analytics") to narrow results.',
|
|
11454
12320
|
{
|
|
11455
12321
|
query: z23.string().optional().describe("Search query to filter tools by name or description"),
|
|
11456
|
-
module: z23.string().optional().describe(
|
|
12322
|
+
module: z23.string().optional().describe(
|
|
12323
|
+
'Filter by module name (e.g. "planning", "content", "analytics")'
|
|
12324
|
+
),
|
|
11457
12325
|
scope: z23.string().optional().describe('Filter by required scope (e.g. "mcp:read", "mcp:write")'),
|
|
11458
12326
|
detail: z23.enum(["name", "summary", "full"]).default("summary").describe(
|
|
11459
12327
|
'Detail level: "name" for just tool names, "summary" for names + descriptions, "full" for complete info including scope and module'
|
|
@@ -11470,14 +12338,18 @@ function registerDiscoveryTools(server2) {
|
|
|
11470
12338
|
if (query) {
|
|
11471
12339
|
results = searchTools(query);
|
|
11472
12340
|
}
|
|
11473
|
-
results = results.filter(
|
|
12341
|
+
results = results.filter(isPubliclyDiscoverable);
|
|
11474
12342
|
if (module) {
|
|
11475
12343
|
const moduleTools = getToolsByModule(module);
|
|
11476
|
-
results = results.filter(
|
|
12344
|
+
results = results.filter(
|
|
12345
|
+
(t) => moduleTools.some((mt) => mt.name === t.name)
|
|
12346
|
+
);
|
|
11477
12347
|
}
|
|
11478
12348
|
if (scope) {
|
|
11479
12349
|
const scopeTools = getToolsByScope(scope);
|
|
11480
|
-
results = results.filter(
|
|
12350
|
+
results = results.filter(
|
|
12351
|
+
(t) => scopeTools.some((st) => st.name === t.name)
|
|
12352
|
+
);
|
|
11481
12353
|
}
|
|
11482
12354
|
if (available_only) {
|
|
11483
12355
|
results = results.filter(isAvailable);
|
|
@@ -11515,7 +12387,12 @@ function registerDiscoveryTools(server2) {
|
|
|
11515
12387
|
text: JSON.stringify(
|
|
11516
12388
|
{
|
|
11517
12389
|
toolCount: results.length,
|
|
11518
|
-
...hasKnownScopes ? {
|
|
12390
|
+
...hasKnownScopes ? {
|
|
12391
|
+
scopes: {
|
|
12392
|
+
available: currentScopes,
|
|
12393
|
+
unavailable_matches: unavailableCount
|
|
12394
|
+
}
|
|
12395
|
+
} : {},
|
|
11519
12396
|
tools: output
|
|
11520
12397
|
},
|
|
11521
12398
|
null,
|
|
@@ -11606,7 +12483,10 @@ var init_discovery = __esm({
|
|
|
11606
12483
|
import { z as z24 } from "zod";
|
|
11607
12484
|
import { randomUUID as randomUUID3 } from "node:crypto";
|
|
11608
12485
|
function asEnvelope20(data) {
|
|
11609
|
-
return {
|
|
12486
|
+
return {
|
|
12487
|
+
_meta: { version: MCP_VERSION, timestamp: (/* @__PURE__ */ new Date()).toISOString() },
|
|
12488
|
+
data
|
|
12489
|
+
};
|
|
11610
12490
|
}
|
|
11611
12491
|
function registerPipelineTools(server2) {
|
|
11612
12492
|
server2.tool(
|
|
@@ -11629,7 +12509,10 @@ function registerPipelineTools(server2) {
|
|
|
11629
12509
|
action: "pipeline-readiness",
|
|
11630
12510
|
platforms,
|
|
11631
12511
|
estimated_posts,
|
|
11632
|
-
...resolvedProjectId ? {
|
|
12512
|
+
...resolvedProjectId ? {
|
|
12513
|
+
projectId: resolvedProjectId,
|
|
12514
|
+
project_id: resolvedProjectId
|
|
12515
|
+
} : {}
|
|
11633
12516
|
},
|
|
11634
12517
|
{ timeoutMs: 15e3 }
|
|
11635
12518
|
);
|
|
@@ -11647,16 +12530,24 @@ function registerPipelineTools(server2) {
|
|
|
11647
12530
|
const blockers = [];
|
|
11648
12531
|
const warnings = [];
|
|
11649
12532
|
if (!isUnlimited && credits < estimatedCost) {
|
|
11650
|
-
blockers.push(
|
|
12533
|
+
blockers.push(
|
|
12534
|
+
`Insufficient credits: ${credits} available, ~${estimatedCost} needed`
|
|
12535
|
+
);
|
|
11651
12536
|
}
|
|
11652
12537
|
if (missingPlatforms.length > 0) {
|
|
11653
|
-
blockers.push(
|
|
12538
|
+
blockers.push(
|
|
12539
|
+
`Missing connected accounts: ${missingPlatforms.join(", ")}`
|
|
12540
|
+
);
|
|
11654
12541
|
}
|
|
11655
12542
|
if (!hasBrand) {
|
|
11656
|
-
warnings.push(
|
|
12543
|
+
warnings.push(
|
|
12544
|
+
"No brand profile found. Content will use generic voice."
|
|
12545
|
+
);
|
|
11657
12546
|
}
|
|
11658
12547
|
if (pendingApprovals > 0) {
|
|
11659
|
-
warnings.push(
|
|
12548
|
+
warnings.push(
|
|
12549
|
+
`${pendingApprovals} pending approval(s) from previous runs.`
|
|
12550
|
+
);
|
|
11660
12551
|
}
|
|
11661
12552
|
if (!insightsFresh) {
|
|
11662
12553
|
warnings.push(
|
|
@@ -11671,7 +12562,10 @@ function registerPipelineTools(server2) {
|
|
|
11671
12562
|
estimated_cost: estimatedCost,
|
|
11672
12563
|
sufficient: credits >= estimatedCost
|
|
11673
12564
|
},
|
|
11674
|
-
connected_accounts: {
|
|
12565
|
+
connected_accounts: {
|
|
12566
|
+
platforms: connectedPlatforms,
|
|
12567
|
+
missing: missingPlatforms
|
|
12568
|
+
},
|
|
11675
12569
|
brand_profile: { exists: hasBrand },
|
|
11676
12570
|
pending_approvals: { count: pendingApprovals },
|
|
11677
12571
|
insights_available: {
|
|
@@ -11685,11 +12579,18 @@ function registerPipelineTools(server2) {
|
|
|
11685
12579
|
};
|
|
11686
12580
|
if (format === "json") {
|
|
11687
12581
|
return {
|
|
11688
|
-
content: [
|
|
12582
|
+
content: [
|
|
12583
|
+
{
|
|
12584
|
+
type: "text",
|
|
12585
|
+
text: JSON.stringify(asEnvelope20(result), null, 2)
|
|
12586
|
+
}
|
|
12587
|
+
]
|
|
11689
12588
|
};
|
|
11690
12589
|
}
|
|
11691
12590
|
const lines = [];
|
|
11692
|
-
lines.push(
|
|
12591
|
+
lines.push(
|
|
12592
|
+
`Pipeline Readiness: ${result.ready ? "READY" : "NOT READY"}`
|
|
12593
|
+
);
|
|
11693
12594
|
lines.push("=".repeat(40));
|
|
11694
12595
|
lines.push(
|
|
11695
12596
|
`Credits: ${credits} available, ~${estimatedCost} needed \u2014 ${credits >= estimatedCost ? "OK" : "BLOCKED"}`
|
|
@@ -11697,7 +12598,9 @@ function registerPipelineTools(server2) {
|
|
|
11697
12598
|
lines.push(
|
|
11698
12599
|
`Accounts: ${connectedPlatforms.length} connected${missingPlatforms.length > 0 ? ` (missing: ${missingPlatforms.join(", ")})` : " \u2014 OK"}`
|
|
11699
12600
|
);
|
|
11700
|
-
lines.push(
|
|
12601
|
+
lines.push(
|
|
12602
|
+
`Brand: ${hasBrand ? "OK" : "Missing (will use generic voice)"}`
|
|
12603
|
+
);
|
|
11701
12604
|
lines.push(`Pending Approvals: ${pendingApprovals}`);
|
|
11702
12605
|
lines.push(
|
|
11703
12606
|
`Insights: ${insightsFresh ? "Fresh" : insightAge === null ? "None available" : `${insightAge} days old`}`
|
|
@@ -11716,7 +12619,12 @@ function registerPipelineTools(server2) {
|
|
|
11716
12619
|
} catch (err) {
|
|
11717
12620
|
const message = sanitizeError(err);
|
|
11718
12621
|
return {
|
|
11719
|
-
content: [
|
|
12622
|
+
content: [
|
|
12623
|
+
{
|
|
12624
|
+
type: "text",
|
|
12625
|
+
text: `Readiness check failed: ${message}`
|
|
12626
|
+
}
|
|
12627
|
+
],
|
|
11720
12628
|
isError: true
|
|
11721
12629
|
};
|
|
11722
12630
|
}
|
|
@@ -11730,6 +12638,9 @@ function registerPipelineTools(server2) {
|
|
|
11730
12638
|
topic: z24.string().optional().describe("Content topic (required if no source_url)"),
|
|
11731
12639
|
source_url: z24.string().optional().describe("URL to extract content from"),
|
|
11732
12640
|
platforms: z24.array(PLATFORM_ENUM2).min(1).describe("Target platforms"),
|
|
12641
|
+
account_ids: z24.record(z24.string(), z24.string().uuid()).optional().describe(
|
|
12642
|
+
"Exact connected-account ID per target platform. Required when a project has multiple accounts on one platform."
|
|
12643
|
+
),
|
|
11733
12644
|
days: z24.number().min(1).max(7).default(5).describe("Days to plan"),
|
|
11734
12645
|
posts_per_day: z24.number().min(1).max(3).default(1).describe("Posts per platform per day"),
|
|
11735
12646
|
approval_mode: z24.enum(["auto", "review_all", "review_low_confidence"]).default("review_low_confidence").describe(
|
|
@@ -11751,6 +12662,7 @@ function registerPipelineTools(server2) {
|
|
|
11751
12662
|
topic,
|
|
11752
12663
|
source_url,
|
|
11753
12664
|
platforms,
|
|
12665
|
+
account_ids,
|
|
11754
12666
|
days,
|
|
11755
12667
|
posts_per_day,
|
|
11756
12668
|
approval_mode,
|
|
@@ -11768,7 +12680,12 @@ function registerPipelineTools(server2) {
|
|
|
11768
12680
|
let creditsUsed = 0;
|
|
11769
12681
|
if (!topic && !source_url) {
|
|
11770
12682
|
return {
|
|
11771
|
-
content: [
|
|
12683
|
+
content: [
|
|
12684
|
+
{
|
|
12685
|
+
type: "text",
|
|
12686
|
+
text: "Either topic or source_url is required."
|
|
12687
|
+
}
|
|
12688
|
+
],
|
|
11772
12689
|
isError: true
|
|
11773
12690
|
};
|
|
11774
12691
|
}
|
|
@@ -11798,6 +12715,35 @@ function registerPipelineTools(server2) {
|
|
|
11798
12715
|
}
|
|
11799
12716
|
try {
|
|
11800
12717
|
const resolvedProjectId = project_id ?? await getDefaultProjectId() ?? void 0;
|
|
12718
|
+
if (schedulingRequested && !resolvedProjectId) {
|
|
12719
|
+
return {
|
|
12720
|
+
content: [
|
|
12721
|
+
{
|
|
12722
|
+
type: "text",
|
|
12723
|
+
text: "A project_id is required to schedule pipeline output. Configure an explicit project or use an API key scoped to exactly one project."
|
|
12724
|
+
}
|
|
12725
|
+
],
|
|
12726
|
+
isError: true
|
|
12727
|
+
};
|
|
12728
|
+
}
|
|
12729
|
+
if (schedulingRequested) {
|
|
12730
|
+
const preflightRouting = await resolveConnectedAccountRouting({
|
|
12731
|
+
projectId: resolvedProjectId,
|
|
12732
|
+
platforms,
|
|
12733
|
+
requestedAccountIds: account_ids
|
|
12734
|
+
});
|
|
12735
|
+
if (preflightRouting.error || !preflightRouting.connectedAccountIds) {
|
|
12736
|
+
return {
|
|
12737
|
+
content: [
|
|
12738
|
+
{
|
|
12739
|
+
type: "text",
|
|
12740
|
+
text: `Cannot run publishing pipeline \u2014 ${preflightRouting.error ?? "exact connected-account routing could not be established."} (checked before any credits were spent.)`
|
|
12741
|
+
}
|
|
12742
|
+
],
|
|
12743
|
+
isError: true
|
|
12744
|
+
};
|
|
12745
|
+
}
|
|
12746
|
+
}
|
|
11801
12747
|
const estimatedCost = BASE_PLAN_CREDITS + (source_url ? SOURCE_EXTRACTION_CREDITS : 0);
|
|
11802
12748
|
const { data: budgetData } = await callEdgeFunction(
|
|
11803
12749
|
"mcp-data",
|
|
@@ -11851,7 +12797,13 @@ function registerPipelineTools(server2) {
|
|
|
11851
12797
|
"social-neuron-ai",
|
|
11852
12798
|
{
|
|
11853
12799
|
type: "generation",
|
|
11854
|
-
prompt: buildPlanPrompt(
|
|
12800
|
+
prompt: buildPlanPrompt(
|
|
12801
|
+
resolvedTopic,
|
|
12802
|
+
platforms,
|
|
12803
|
+
days,
|
|
12804
|
+
posts_per_day,
|
|
12805
|
+
source_url
|
|
12806
|
+
),
|
|
11855
12807
|
model: "gemini-2.5-flash",
|
|
11856
12808
|
responseFormat: "json",
|
|
11857
12809
|
...resolvedProjectId ? { projectId: resolvedProjectId, project_id: resolvedProjectId } : {}
|
|
@@ -11859,7 +12811,10 @@ function registerPipelineTools(server2) {
|
|
|
11859
12811
|
{ timeoutMs: 6e4 }
|
|
11860
12812
|
);
|
|
11861
12813
|
if (planError || !planData) {
|
|
11862
|
-
errors.push({
|
|
12814
|
+
errors.push({
|
|
12815
|
+
stage: "planning",
|
|
12816
|
+
message: planError ?? "No AI response"
|
|
12817
|
+
});
|
|
11863
12818
|
await callEdgeFunction(
|
|
11864
12819
|
"mcp-data",
|
|
11865
12820
|
{
|
|
@@ -11876,7 +12831,10 @@ function registerPipelineTools(server2) {
|
|
|
11876
12831
|
);
|
|
11877
12832
|
return {
|
|
11878
12833
|
content: [
|
|
11879
|
-
{
|
|
12834
|
+
{
|
|
12835
|
+
type: "text",
|
|
12836
|
+
text: `Planning failed: ${planError ?? "No AI response"}`
|
|
12837
|
+
}
|
|
11880
12838
|
],
|
|
11881
12839
|
isError: true
|
|
11882
12840
|
};
|
|
@@ -11906,20 +12864,22 @@ function registerPipelineTools(server2) {
|
|
|
11906
12864
|
const postsArray = extractJsonArray(rawText);
|
|
11907
12865
|
const requestedPlatformSet = new Set(platforms);
|
|
11908
12866
|
const maxPosts = platforms.length * days * posts_per_day;
|
|
11909
|
-
const parsedPosts = (postsArray ?? []).map(
|
|
11910
|
-
|
|
11911
|
-
|
|
11912
|
-
|
|
11913
|
-
|
|
11914
|
-
|
|
11915
|
-
|
|
11916
|
-
|
|
11917
|
-
|
|
11918
|
-
|
|
11919
|
-
|
|
11920
|
-
|
|
11921
|
-
|
|
11922
|
-
|
|
12867
|
+
const parsedPosts = (postsArray ?? []).map(
|
|
12868
|
+
(p) => ({
|
|
12869
|
+
id: String(p.id ?? randomUUID3().slice(0, 8)),
|
|
12870
|
+
day: Number(p.day ?? 1),
|
|
12871
|
+
date: String(p.date ?? ""),
|
|
12872
|
+
platform: String(p.platform ?? ""),
|
|
12873
|
+
content_type: p.content_type ?? "caption",
|
|
12874
|
+
caption: String(p.caption ?? ""),
|
|
12875
|
+
title: p.title ? String(p.title) : void 0,
|
|
12876
|
+
hashtags: Array.isArray(p.hashtags) ? p.hashtags.map(String) : void 0,
|
|
12877
|
+
hook: String(p.hook ?? ""),
|
|
12878
|
+
angle: String(p.angle ?? ""),
|
|
12879
|
+
visual_direction: p.visual_direction ? String(p.visual_direction) : void 0,
|
|
12880
|
+
media_type: p.media_type ? String(p.media_type) : void 0
|
|
12881
|
+
})
|
|
12882
|
+
);
|
|
11923
12883
|
const platformFilteredPosts = parsedPosts.filter(
|
|
11924
12884
|
(post) => requestedPlatformSet.has(post.platform)
|
|
11925
12885
|
);
|
|
@@ -12002,7 +12962,10 @@ function registerPipelineTools(server2) {
|
|
|
12002
12962
|
estimated_credits: estimatedCost,
|
|
12003
12963
|
generated_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
12004
12964
|
},
|
|
12005
|
-
...resolvedProjectId ? {
|
|
12965
|
+
...resolvedProjectId ? {
|
|
12966
|
+
projectId: resolvedProjectId,
|
|
12967
|
+
project_id: resolvedProjectId
|
|
12968
|
+
} : {}
|
|
12006
12969
|
},
|
|
12007
12970
|
{ timeoutMs: 1e4 }
|
|
12008
12971
|
);
|
|
@@ -12055,6 +13018,22 @@ function registerPipelineTools(server2) {
|
|
|
12055
13018
|
}
|
|
12056
13019
|
}
|
|
12057
13020
|
let postsScheduled = 0;
|
|
13021
|
+
const pipelineRouting = schedulingRequested && postsApproved > 0 ? await resolveConnectedAccountRouting({
|
|
13022
|
+
projectId: resolvedProjectId,
|
|
13023
|
+
platforms,
|
|
13024
|
+
requestedAccountIds: account_ids
|
|
13025
|
+
}) : void 0;
|
|
13026
|
+
if (pipelineRouting?.error || schedulingRequested && postsApproved > 0 && !pipelineRouting?.connectedAccountIds) {
|
|
13027
|
+
return {
|
|
13028
|
+
content: [
|
|
13029
|
+
{
|
|
13030
|
+
type: "text",
|
|
13031
|
+
text: `Cannot run publishing pipeline \u2014 ${pipelineRouting?.error ?? "exact connected-account routing could not be established."}`
|
|
13032
|
+
}
|
|
13033
|
+
],
|
|
13034
|
+
isError: true
|
|
13035
|
+
};
|
|
13036
|
+
}
|
|
12058
13037
|
if (!dry_run && !skipSet.has("schedule") && postsApproved > 0) {
|
|
12059
13038
|
const approvedPosts = posts.filter((p) => p.status === "approved");
|
|
12060
13039
|
const scheduleBase = /* @__PURE__ */ new Date();
|
|
@@ -12062,10 +13041,27 @@ function registerPipelineTools(server2) {
|
|
|
12062
13041
|
const scheduleBaseMs = scheduleBase.getTime();
|
|
12063
13042
|
for (const post of approvedPosts) {
|
|
12064
13043
|
if (creditsUsed >= creditLimit) {
|
|
12065
|
-
errors.push({
|
|
13044
|
+
errors.push({
|
|
13045
|
+
stage: "schedule",
|
|
13046
|
+
message: "Credit limit reached"
|
|
13047
|
+
});
|
|
12066
13048
|
break;
|
|
12067
13049
|
}
|
|
12068
|
-
const scheduledAt = post.schedule_at ?? new Date(
|
|
13050
|
+
const scheduledAt = post.schedule_at ?? new Date(
|
|
13051
|
+
scheduleBaseMs + (Math.max(1, post.day) - 1) * 864e5
|
|
13052
|
+
).toISOString();
|
|
13053
|
+
const route = Object.entries(
|
|
13054
|
+
pipelineRouting.connectedAccountIds
|
|
13055
|
+
).find(
|
|
13056
|
+
([platform3]) => platform3.toLowerCase() === post.platform.toLowerCase()
|
|
13057
|
+
);
|
|
13058
|
+
if (!route) {
|
|
13059
|
+
errors.push({
|
|
13060
|
+
stage: "schedule",
|
|
13061
|
+
message: `No verified connected account route for ${post.platform}`
|
|
13062
|
+
});
|
|
13063
|
+
continue;
|
|
13064
|
+
}
|
|
12069
13065
|
try {
|
|
12070
13066
|
const { error: schedError } = await callEdgeFunction(
|
|
12071
13067
|
"schedule-post",
|
|
@@ -12078,7 +13074,11 @@ function registerPipelineTools(server2) {
|
|
|
12078
13074
|
scheduledAt,
|
|
12079
13075
|
planId,
|
|
12080
13076
|
idempotencyKey: `pipeline-${planId}-${post.id}`,
|
|
12081
|
-
...resolvedProjectId ? {
|
|
13077
|
+
...resolvedProjectId ? {
|
|
13078
|
+
projectId: resolvedProjectId,
|
|
13079
|
+
project_id: resolvedProjectId
|
|
13080
|
+
} : {},
|
|
13081
|
+
connectedAccountIds: { [route[0]]: route[1] }
|
|
12082
13082
|
},
|
|
12083
13083
|
{ timeoutMs: 15e3 }
|
|
12084
13084
|
);
|
|
@@ -12144,12 +13144,17 @@ function registerPipelineTools(server2) {
|
|
|
12144
13144
|
if (response_format === "json") {
|
|
12145
13145
|
return {
|
|
12146
13146
|
content: [
|
|
12147
|
-
{
|
|
13147
|
+
{
|
|
13148
|
+
type: "text",
|
|
13149
|
+
text: JSON.stringify(asEnvelope20(resultPayload), null, 2)
|
|
13150
|
+
}
|
|
12148
13151
|
]
|
|
12149
13152
|
};
|
|
12150
13153
|
}
|
|
12151
13154
|
const lines = [];
|
|
12152
|
-
lines.push(
|
|
13155
|
+
lines.push(
|
|
13156
|
+
`Pipeline ${pipelineId.slice(0, 8)}... ${finalStatus.toUpperCase()}`
|
|
13157
|
+
);
|
|
12153
13158
|
lines.push("=".repeat(40));
|
|
12154
13159
|
lines.push(`Posts generated: ${posts.length}`);
|
|
12155
13160
|
lines.push(`Posts approved: ${postsApproved}`);
|
|
@@ -12188,7 +13193,9 @@ function registerPipelineTools(server2) {
|
|
|
12188
13193
|
} catch {
|
|
12189
13194
|
}
|
|
12190
13195
|
return {
|
|
12191
|
-
content: [
|
|
13196
|
+
content: [
|
|
13197
|
+
{ type: "text", text: `Pipeline failed: ${message}` }
|
|
13198
|
+
],
|
|
12192
13199
|
isError: true
|
|
12193
13200
|
};
|
|
12194
13201
|
}
|
|
@@ -12225,7 +13232,12 @@ function registerPipelineTools(server2) {
|
|
|
12225
13232
|
}
|
|
12226
13233
|
if (format === "json") {
|
|
12227
13234
|
return {
|
|
12228
|
-
content: [
|
|
13235
|
+
content: [
|
|
13236
|
+
{
|
|
13237
|
+
type: "text",
|
|
13238
|
+
text: JSON.stringify(asEnvelope20(data), null, 2)
|
|
13239
|
+
}
|
|
13240
|
+
]
|
|
12229
13241
|
};
|
|
12230
13242
|
}
|
|
12231
13243
|
const lines = [];
|
|
@@ -12265,10 +13277,19 @@ function registerPipelineTools(server2) {
|
|
|
12265
13277
|
},
|
|
12266
13278
|
async ({ plan_id, quality_threshold, response_format }) => {
|
|
12267
13279
|
try {
|
|
12268
|
-
const { data: loadResult, error: loadError } = await callEdgeFunction(
|
|
13280
|
+
const { data: loadResult, error: loadError } = await callEdgeFunction(
|
|
13281
|
+
"mcp-data",
|
|
13282
|
+
{ action: "auto-approve-plan", plan_id },
|
|
13283
|
+
{ timeoutMs: 1e4 }
|
|
13284
|
+
);
|
|
12269
13285
|
if (loadError) {
|
|
12270
13286
|
return {
|
|
12271
|
-
content: [
|
|
13287
|
+
content: [
|
|
13288
|
+
{
|
|
13289
|
+
type: "text",
|
|
13290
|
+
text: `Failed to load plan: ${loadError}`
|
|
13291
|
+
}
|
|
13292
|
+
],
|
|
12272
13293
|
isError: true
|
|
12273
13294
|
};
|
|
12274
13295
|
}
|
|
@@ -12276,7 +13297,10 @@ function registerPipelineTools(server2) {
|
|
|
12276
13297
|
if (!stored?.plan_payload) {
|
|
12277
13298
|
return {
|
|
12278
13299
|
content: [
|
|
12279
|
-
{
|
|
13300
|
+
{
|
|
13301
|
+
type: "text",
|
|
13302
|
+
text: `No content plan found for plan_id=${plan_id}`
|
|
13303
|
+
}
|
|
12280
13304
|
],
|
|
12281
13305
|
isError: true
|
|
12282
13306
|
};
|
|
@@ -12303,7 +13327,11 @@ function registerPipelineTools(server2) {
|
|
|
12303
13327
|
blockers: []
|
|
12304
13328
|
};
|
|
12305
13329
|
autoApproved++;
|
|
12306
|
-
details.push({
|
|
13330
|
+
details.push({
|
|
13331
|
+
post_id: post.id,
|
|
13332
|
+
action: "approved",
|
|
13333
|
+
score: quality.total
|
|
13334
|
+
});
|
|
12307
13335
|
} else if (quality.total >= quality_threshold - 5) {
|
|
12308
13336
|
post.status = "needs_edit";
|
|
12309
13337
|
post.quality = {
|
|
@@ -12313,7 +13341,11 @@ function registerPipelineTools(server2) {
|
|
|
12313
13341
|
blockers: quality.blockers
|
|
12314
13342
|
};
|
|
12315
13343
|
flagged++;
|
|
12316
|
-
details.push({
|
|
13344
|
+
details.push({
|
|
13345
|
+
post_id: post.id,
|
|
13346
|
+
action: "flagged",
|
|
13347
|
+
score: quality.total
|
|
13348
|
+
});
|
|
12317
13349
|
} else {
|
|
12318
13350
|
post.status = "rejected";
|
|
12319
13351
|
post.quality = {
|
|
@@ -12323,7 +13355,11 @@ function registerPipelineTools(server2) {
|
|
|
12323
13355
|
blockers: quality.blockers
|
|
12324
13356
|
};
|
|
12325
13357
|
rejected++;
|
|
12326
|
-
details.push({
|
|
13358
|
+
details.push({
|
|
13359
|
+
post_id: post.id,
|
|
13360
|
+
action: "rejected",
|
|
13361
|
+
score: quality.total
|
|
13362
|
+
});
|
|
12327
13363
|
}
|
|
12328
13364
|
}
|
|
12329
13365
|
const newStatus = flagged === 0 && rejected === 0 ? "approved" : "in_review";
|
|
@@ -12359,7 +13395,10 @@ function registerPipelineTools(server2) {
|
|
|
12359
13395
|
if (response_format === "json") {
|
|
12360
13396
|
return {
|
|
12361
13397
|
content: [
|
|
12362
|
-
{
|
|
13398
|
+
{
|
|
13399
|
+
type: "text",
|
|
13400
|
+
text: JSON.stringify(asEnvelope20(resultPayload), null, 2)
|
|
13401
|
+
}
|
|
12363
13402
|
]
|
|
12364
13403
|
};
|
|
12365
13404
|
}
|
|
@@ -12380,7 +13419,9 @@ function registerPipelineTools(server2) {
|
|
|
12380
13419
|
} catch (err) {
|
|
12381
13420
|
const message = sanitizeError(err);
|
|
12382
13421
|
return {
|
|
12383
|
-
content: [
|
|
13422
|
+
content: [
|
|
13423
|
+
{ type: "text", text: `Auto-approve failed: ${message}` }
|
|
13424
|
+
],
|
|
12384
13425
|
isError: true
|
|
12385
13426
|
};
|
|
12386
13427
|
}
|
|
@@ -12414,6 +13455,7 @@ var init_pipeline = __esm({
|
|
|
12414
13455
|
init_quality();
|
|
12415
13456
|
init_version();
|
|
12416
13457
|
init_parse_utils();
|
|
13458
|
+
init_connected_account_routing();
|
|
12417
13459
|
PLATFORM_ENUM2 = z24.enum([
|
|
12418
13460
|
"youtube",
|
|
12419
13461
|
"tiktok",
|
|
@@ -13941,25 +14983,39 @@ function registerCarouselTools(server2) {
|
|
|
13941
14983
|
]).optional().describe("Carousel template. Default: hormozi-authority."),
|
|
13942
14984
|
slide_count: z28.number().min(3).max(10).optional().describe("Number of slides (3-10). Default: 7."),
|
|
13943
14985
|
aspect_ratio: z28.enum(["1:1", "4:5", "9:16"]).optional().describe("Aspect ratio for both carousel and images. Default: 1:1."),
|
|
13944
|
-
style: z28.enum(["minimal", "bold", "professional", "playful", "hormozi"]).optional().describe(
|
|
14986
|
+
style: z28.enum(["minimal", "bold", "professional", "playful", "hormozi"]).optional().describe(
|
|
14987
|
+
"Visual style. Default: hormozi for hormozi-authority template."
|
|
14988
|
+
),
|
|
13945
14989
|
image_style_suffix: z28.string().max(500).optional().describe(
|
|
13946
14990
|
'Style suffix appended to every image prompt for visual consistency across slides. Example: "dark moody lighting, cinematic, 35mm film grain".'
|
|
13947
14991
|
),
|
|
13948
14992
|
hook: z28.string().max(300).optional().describe(
|
|
13949
14993
|
"Explicit hook/opener for slide 1. Overrides any hook derived from topic. Keep under 15 words for strongest scroll-stop."
|
|
13950
14994
|
),
|
|
13951
|
-
hook_family: z28.enum([
|
|
14995
|
+
hook_family: z28.enum([
|
|
14996
|
+
"curiosity",
|
|
14997
|
+
"authority",
|
|
14998
|
+
"pain_point",
|
|
14999
|
+
"contrarian",
|
|
15000
|
+
"data_driven"
|
|
15001
|
+
]).optional().describe(
|
|
13952
15002
|
"Hook family tag. Recorded on the carousel so downstream analytics can attribute engagement to hook pattern."
|
|
13953
15003
|
),
|
|
13954
|
-
cta_text: z28.string().max(200).optional().describe(
|
|
13955
|
-
|
|
15004
|
+
cta_text: z28.string().max(200).optional().describe(
|
|
15005
|
+
"Explicit CTA copy for the final slide. If omitted, derived from topic."
|
|
15006
|
+
),
|
|
15007
|
+
cta_url: z28.string().url().optional().describe(
|
|
15008
|
+
"URL promoted on the CTA slide. Defaults to the project landing page."
|
|
15009
|
+
),
|
|
13956
15010
|
tone: z28.string().max(200).optional().describe(
|
|
13957
15011
|
'Voice/tone override. Composes with the brand profile voice when present. Example: "educational, confident, not arrogant".'
|
|
13958
15012
|
),
|
|
13959
15013
|
constraints: z28.string().max(500).optional().describe(
|
|
13960
15014
|
'Content constraints applied at generation time. Example: "No fabricated statistics. Sentence case only. No ALL CAPS."'
|
|
13961
15015
|
),
|
|
13962
|
-
platform: z28.enum(["linkedin", "instagram", "tiktok", "x"]).optional().describe(
|
|
15016
|
+
platform: z28.enum(["linkedin", "instagram", "tiktok", "x"]).optional().describe(
|
|
15017
|
+
"Target platform. Affects tone conventions and slide-count guardrails."
|
|
15018
|
+
),
|
|
13963
15019
|
brand_id: z28.string().optional().describe(
|
|
13964
15020
|
"Brand/project ID to pull visual context from (colors, logo, mood). Falls back to project_id, then default project."
|
|
13965
15021
|
),
|
|
@@ -13991,7 +15047,9 @@ function registerCarouselTools(server2) {
|
|
|
13991
15047
|
const slideCount = slide_count ?? 7;
|
|
13992
15048
|
const ratio = aspect_ratio ?? "1:1";
|
|
13993
15049
|
let brandContext = null;
|
|
13994
|
-
const
|
|
15050
|
+
const defaultProjectId = await getDefaultProjectId();
|
|
15051
|
+
const brandProjectId = brand_id || project_id || defaultProjectId;
|
|
15052
|
+
const resolvedProjectId = project_id || defaultProjectId;
|
|
13995
15053
|
if (brandProjectId) {
|
|
13996
15054
|
brandContext = await fetchBrandVisualContext(brandProjectId);
|
|
13997
15055
|
}
|
|
@@ -14000,10 +15058,7 @@ function registerCarouselTools(server2) {
|
|
|
14000
15058
|
const totalEstimatedCost = carouselTextCost + slideCount * perImageCost;
|
|
14001
15059
|
const budgetCheck = checkCreditBudget(totalEstimatedCost);
|
|
14002
15060
|
if (!budgetCheck.ok) {
|
|
14003
|
-
return
|
|
14004
|
-
content: [{ type: "text", text: budgetCheck.message }],
|
|
14005
|
-
isError: true
|
|
14006
|
-
};
|
|
15061
|
+
return budgetCheck.error;
|
|
14007
15062
|
}
|
|
14008
15063
|
const assetBudget = checkAssetBudget(slideCount);
|
|
14009
15064
|
if (!assetBudget.ok) {
|
|
@@ -14013,7 +15068,10 @@ function registerCarouselTools(server2) {
|
|
|
14013
15068
|
};
|
|
14014
15069
|
}
|
|
14015
15070
|
const userId = await getDefaultUserId();
|
|
14016
|
-
const rateLimit = checkRateLimit(
|
|
15071
|
+
const rateLimit = checkRateLimit(
|
|
15072
|
+
"generation",
|
|
15073
|
+
`create_carousel:${userId}`
|
|
15074
|
+
);
|
|
14017
15075
|
if (!rateLimit.allowed) {
|
|
14018
15076
|
return {
|
|
14019
15077
|
content: [
|
|
@@ -14033,7 +15091,7 @@ function registerCarouselTools(server2) {
|
|
|
14033
15091
|
slideCount,
|
|
14034
15092
|
aspectRatio: ratio,
|
|
14035
15093
|
style: resolvedStyle,
|
|
14036
|
-
projectId:
|
|
15094
|
+
projectId: resolvedProjectId,
|
|
14037
15095
|
hook,
|
|
14038
15096
|
hookFamily: hook_family,
|
|
14039
15097
|
ctaText: cta_text,
|
|
@@ -14047,7 +15105,12 @@ function registerCarouselTools(server2) {
|
|
|
14047
15105
|
if (carouselError || !carouselData?.carousel) {
|
|
14048
15106
|
const errMsg = carouselError ?? "No carousel data returned";
|
|
14049
15107
|
return {
|
|
14050
|
-
content: [
|
|
15108
|
+
content: [
|
|
15109
|
+
{
|
|
15110
|
+
type: "text",
|
|
15111
|
+
text: `Carousel text generation failed: ${errMsg}`
|
|
15112
|
+
}
|
|
15113
|
+
],
|
|
14051
15114
|
isError: true
|
|
14052
15115
|
};
|
|
14053
15116
|
}
|
|
@@ -14076,7 +15139,8 @@ function registerCarouselTools(server2) {
|
|
|
14076
15139
|
{
|
|
14077
15140
|
prompt: imagePrompt,
|
|
14078
15141
|
model: image_model,
|
|
14079
|
-
aspectRatio: ratio
|
|
15142
|
+
aspectRatio: ratio,
|
|
15143
|
+
...resolvedProjectId && { projectId: resolvedProjectId }
|
|
14080
15144
|
},
|
|
14081
15145
|
{ timeoutMs: 3e4 }
|
|
14082
15146
|
);
|
|
@@ -14133,14 +15197,19 @@ function registerCarouselTools(server2) {
|
|
|
14133
15197
|
type: "text",
|
|
14134
15198
|
text: JSON.stringify(
|
|
14135
15199
|
{
|
|
14136
|
-
_meta: {
|
|
15200
|
+
_meta: {
|
|
15201
|
+
version: MCP_VERSION,
|
|
15202
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
15203
|
+
},
|
|
14137
15204
|
data: {
|
|
14138
15205
|
carouselId: carousel.id,
|
|
14139
15206
|
templateId,
|
|
14140
15207
|
style: resolvedStyle,
|
|
14141
15208
|
slideCount: carousel.slides.length,
|
|
14142
15209
|
slides: carousel.slides.map((s) => {
|
|
14143
|
-
const job = imageJobs.find(
|
|
15210
|
+
const job = imageJobs.find(
|
|
15211
|
+
(j) => j.slideNumber === s.slideNumber
|
|
15212
|
+
);
|
|
14144
15213
|
return {
|
|
14145
15214
|
...s,
|
|
14146
15215
|
imageJobId: job?.jobId ?? null,
|
|
@@ -14167,9 +15236,17 @@ function registerCarouselTools(server2) {
|
|
|
14167
15236
|
credits: {
|
|
14168
15237
|
textGeneration: textCredits,
|
|
14169
15238
|
imagesEstimated: successfulJobs.length * perImageCost,
|
|
14170
|
-
imagesCharged: imageJobs.reduce(
|
|
14171
|
-
|
|
14172
|
-
|
|
15239
|
+
imagesCharged: imageJobs.reduce(
|
|
15240
|
+
(sum, job) => sum + (job.creditsCharged ?? 0),
|
|
15241
|
+
0
|
|
15242
|
+
),
|
|
15243
|
+
imagesRefunded: imageJobs.reduce(
|
|
15244
|
+
(sum, job) => sum + (job.creditsRefunded ?? 0),
|
|
15245
|
+
0
|
|
15246
|
+
),
|
|
15247
|
+
billingUnknownSlides: imageJobs.filter(
|
|
15248
|
+
(job) => job.billingStatus === "unknown"
|
|
15249
|
+
).length,
|
|
14173
15250
|
totalEstimated: textCredits + successfulJobs.length * perImageCost
|
|
14174
15251
|
}
|
|
14175
15252
|
}
|
|
@@ -14197,7 +15274,9 @@ function registerCarouselTools(server2) {
|
|
|
14197
15274
|
for (const slide of carousel.slides) {
|
|
14198
15275
|
const job = imageJobs.find((j) => j.slideNumber === slide.slideNumber);
|
|
14199
15276
|
const status = job?.jobId ? `image: ${job.jobId}` : `image FAILED: ${job?.error}`;
|
|
14200
|
-
lines.push(
|
|
15277
|
+
lines.push(
|
|
15278
|
+
` ${slide.slideNumber}. ${slide.headline || "(no headline)"} [${status}]`
|
|
15279
|
+
);
|
|
14201
15280
|
}
|
|
14202
15281
|
if (failedJobs.length > 0) {
|
|
14203
15282
|
lines.push("");
|
|
@@ -14208,7 +15287,9 @@ function registerCarouselTools(server2) {
|
|
|
14208
15287
|
const jobIdList = successfulJobs.map((j) => j.jobId).join(", ");
|
|
14209
15288
|
lines.push("");
|
|
14210
15289
|
lines.push("Next steps:");
|
|
14211
|
-
lines.push(
|
|
15290
|
+
lines.push(
|
|
15291
|
+
` 1. Poll each job: check_status with job_id for each of: ${jobIdList}`
|
|
15292
|
+
);
|
|
14212
15293
|
lines.push(
|
|
14213
15294
|
" 2. When all complete: schedule_post with job_ids=[...] and media_type=CAROUSEL_ALBUM"
|
|
14214
15295
|
);
|
|
@@ -15078,28 +16159,33 @@ var init_analytics_pulse = __esm({
|
|
|
15078
16159
|
|
|
15079
16160
|
// src/tools/connections.ts
|
|
15080
16161
|
import { z as z33 } from "zod";
|
|
15081
|
-
function
|
|
16162
|
+
function findActiveAccounts(accounts, platform3, projectId) {
|
|
15082
16163
|
const target = platform3.toLowerCase();
|
|
15083
|
-
return accounts.
|
|
16164
|
+
return accounts.filter((a) => {
|
|
15084
16165
|
const effectiveStatus = a.effective_status || a.status;
|
|
15085
|
-
return a.platform.toLowerCase() === target && (effectiveStatus === "active" || effectiveStatus === "expires_soon");
|
|
15086
|
-
})
|
|
16166
|
+
return a.platform.toLowerCase() === target && a.project_id === projectId && (effectiveStatus === "active" || effectiveStatus === "expires_soon");
|
|
16167
|
+
});
|
|
15087
16168
|
}
|
|
15088
16169
|
function registerConnectionTools(server2) {
|
|
15089
16170
|
server2.tool(
|
|
15090
16171
|
"start_platform_connection",
|
|
15091
16172
|
"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.",
|
|
15092
16173
|
{
|
|
15093
|
-
platform: z33.enum(PLATFORM_ENUM4).describe(
|
|
15094
|
-
|
|
15095
|
-
|
|
16174
|
+
platform: z33.enum(PLATFORM_ENUM4).describe(
|
|
16175
|
+
"Platform to connect. Lower-case: instagram, tiktok, youtube, etc."
|
|
16176
|
+
),
|
|
16177
|
+
project_id: z33.string().uuid().optional().describe(
|
|
16178
|
+
"Brand/project ID to bind the new social account to. Required when the account has multiple brands."
|
|
15096
16179
|
),
|
|
15097
16180
|
response_format: z33.enum(["text", "json"]).optional().describe("Response format. Default: text.")
|
|
15098
16181
|
},
|
|
15099
16182
|
async ({ platform: platform3, project_id, response_format }) => {
|
|
15100
16183
|
const format = response_format ?? "text";
|
|
15101
16184
|
const userId = await getDefaultUserId();
|
|
15102
|
-
const rl = checkRateLimit(
|
|
16185
|
+
const rl = checkRateLimit(
|
|
16186
|
+
"posting",
|
|
16187
|
+
`start_platform_connection:${userId}`
|
|
16188
|
+
);
|
|
15103
16189
|
if (!rl.allowed) {
|
|
15104
16190
|
return {
|
|
15105
16191
|
content: [
|
|
@@ -15111,12 +16197,27 @@ function registerConnectionTools(server2) {
|
|
|
15111
16197
|
isError: true
|
|
15112
16198
|
};
|
|
15113
16199
|
}
|
|
16200
|
+
const projectResolution = await resolveProjectStrict(project_id);
|
|
16201
|
+
if (!projectResolution.projectId) {
|
|
16202
|
+
return {
|
|
16203
|
+
content: [
|
|
16204
|
+
{
|
|
16205
|
+
type: "text",
|
|
16206
|
+
text: projectResolution.error ?? "A project_id is required to connect a platform. Configure an explicit project or use an API key scoped to exactly one project."
|
|
16207
|
+
}
|
|
16208
|
+
],
|
|
16209
|
+
isError: true
|
|
16210
|
+
};
|
|
16211
|
+
}
|
|
16212
|
+
const resolvedProjectId = projectResolution.projectId;
|
|
16213
|
+
const projectAutoResolvedNote = projectResolution.autoResolvedNote;
|
|
15114
16214
|
const { data, error } = await callEdgeFunction(
|
|
15115
16215
|
"mcp-data",
|
|
15116
16216
|
{
|
|
15117
16217
|
action: "mint-connection-nonce",
|
|
15118
16218
|
platform: platform3,
|
|
15119
|
-
|
|
16219
|
+
projectId: resolvedProjectId,
|
|
16220
|
+
project_id: resolvedProjectId
|
|
15120
16221
|
},
|
|
15121
16222
|
{ timeoutMs: 1e4 }
|
|
15122
16223
|
);
|
|
@@ -15132,6 +16233,17 @@ function registerConnectionTools(server2) {
|
|
|
15132
16233
|
isError: true
|
|
15133
16234
|
};
|
|
15134
16235
|
}
|
|
16236
|
+
if (data.project_id !== resolvedProjectId) {
|
|
16237
|
+
return {
|
|
16238
|
+
content: [
|
|
16239
|
+
{
|
|
16240
|
+
type: "text",
|
|
16241
|
+
text: "Connection link project attestation failed. No OAuth link was returned."
|
|
16242
|
+
}
|
|
16243
|
+
],
|
|
16244
|
+
isError: true
|
|
16245
|
+
};
|
|
16246
|
+
}
|
|
15135
16247
|
if (format === "json") {
|
|
15136
16248
|
return {
|
|
15137
16249
|
content: [
|
|
@@ -15140,10 +16252,11 @@ function registerConnectionTools(server2) {
|
|
|
15140
16252
|
text: JSON.stringify(
|
|
15141
16253
|
{
|
|
15142
16254
|
platform: data.platform,
|
|
15143
|
-
project_id:
|
|
16255
|
+
project_id: resolvedProjectId,
|
|
15144
16256
|
deep_link: data.deep_link,
|
|
15145
16257
|
expires_at: data.expires_at,
|
|
15146
|
-
next_step: "Open deep_link in a browser, approve on the platform, then call wait_for_connection."
|
|
16258
|
+
next_step: "Open deep_link in a browser, approve on the platform, then call wait_for_connection.",
|
|
16259
|
+
...projectAutoResolvedNote ? { project_auto_resolved: projectAutoResolvedNote } : {}
|
|
15147
16260
|
},
|
|
15148
16261
|
null,
|
|
15149
16262
|
2
|
|
@@ -15159,7 +16272,7 @@ function registerConnectionTools(server2) {
|
|
|
15159
16272
|
type: "text",
|
|
15160
16273
|
text: [
|
|
15161
16274
|
`${data.platform} connection ready.`,
|
|
15162
|
-
|
|
16275
|
+
`Brand/project: ${resolvedProjectId}`,
|
|
15163
16276
|
"",
|
|
15164
16277
|
'Ask the user to open this link in a browser and click "Connect" on the platform:',
|
|
15165
16278
|
` ${data.deep_link}`,
|
|
@@ -15167,7 +16280,8 @@ function registerConnectionTools(server2) {
|
|
|
15167
16280
|
`Link expires at: ${data.expires_at} (~2 minutes).`,
|
|
15168
16281
|
"",
|
|
15169
16282
|
"After they approve, call `wait_for_connection` with the same platform to confirm.",
|
|
15170
|
-
"This is a one-time browser setup \u2014 not another OAuth flow inside Claude."
|
|
16283
|
+
"This is a one-time browser setup \u2014 not another OAuth flow inside Claude.",
|
|
16284
|
+
...projectAutoResolvedNote ? ["", `Note: ${projectAutoResolvedNote}`] : []
|
|
15171
16285
|
].join("\n")
|
|
15172
16286
|
}
|
|
15173
16287
|
],
|
|
@@ -15180,14 +16294,20 @@ function registerConnectionTools(server2) {
|
|
|
15180
16294
|
"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.",
|
|
15181
16295
|
{
|
|
15182
16296
|
platform: z33.enum(PLATFORM_ENUM4).describe("Platform to wait for."),
|
|
15183
|
-
project_id: z33.string().optional().describe(
|
|
16297
|
+
project_id: z33.string().uuid().optional().describe(
|
|
15184
16298
|
"Brand/project ID to scope the connection poll. Use the same project_id passed to start_platform_connection."
|
|
15185
16299
|
),
|
|
15186
16300
|
timeout_s: z33.number().min(5).max(600).optional().describe("How long to wait, in seconds. Default 120."),
|
|
15187
16301
|
poll_interval_s: z33.number().min(2).max(30).optional().describe("Poll interval in seconds. Default 5."),
|
|
15188
16302
|
response_format: z33.enum(["text", "json"]).optional().describe("Response format. Default: text.")
|
|
15189
16303
|
},
|
|
15190
|
-
async ({
|
|
16304
|
+
async ({
|
|
16305
|
+
platform: platform3,
|
|
16306
|
+
project_id,
|
|
16307
|
+
timeout_s,
|
|
16308
|
+
poll_interval_s,
|
|
16309
|
+
response_format
|
|
16310
|
+
}) => {
|
|
15191
16311
|
const format = response_format ?? "text";
|
|
15192
16312
|
const startedAt = Date.now();
|
|
15193
16313
|
const timeoutMs = (timeout_s ?? 120) * 1e3;
|
|
@@ -15206,6 +16326,18 @@ function registerConnectionTools(server2) {
|
|
|
15206
16326
|
isError: true
|
|
15207
16327
|
};
|
|
15208
16328
|
}
|
|
16329
|
+
const resolvedProjectId = project_id ?? await getDefaultProjectId() ?? void 0;
|
|
16330
|
+
if (!resolvedProjectId) {
|
|
16331
|
+
return {
|
|
16332
|
+
content: [
|
|
16333
|
+
{
|
|
16334
|
+
type: "text",
|
|
16335
|
+
text: "A project_id is required to wait for a connection. Use the same project_id used to start the OAuth flow."
|
|
16336
|
+
}
|
|
16337
|
+
],
|
|
16338
|
+
isError: true
|
|
16339
|
+
};
|
|
16340
|
+
}
|
|
15209
16341
|
const activeWaits = inFlightWaitsByUser.get(userId) ?? 0;
|
|
15210
16342
|
if (activeWaits >= MAX_CONCURRENT_WAITS_PER_USER) {
|
|
15211
16343
|
return {
|
|
@@ -15227,12 +16359,62 @@ function registerConnectionTools(server2) {
|
|
|
15227
16359
|
"mcp-data",
|
|
15228
16360
|
{
|
|
15229
16361
|
action: "connected-accounts",
|
|
15230
|
-
|
|
16362
|
+
projectId: resolvedProjectId,
|
|
16363
|
+
project_id: resolvedProjectId
|
|
15231
16364
|
},
|
|
15232
16365
|
{ timeoutMs: 1e4 }
|
|
15233
16366
|
);
|
|
15234
16367
|
if (!error && data?.success) {
|
|
15235
|
-
const
|
|
16368
|
+
const foundAccounts = findActiveAccounts(
|
|
16369
|
+
data.accounts ?? [],
|
|
16370
|
+
platform3,
|
|
16371
|
+
resolvedProjectId
|
|
16372
|
+
);
|
|
16373
|
+
if (foundAccounts.length > 1) {
|
|
16374
|
+
if (format === "json") {
|
|
16375
|
+
return {
|
|
16376
|
+
content: [
|
|
16377
|
+
{
|
|
16378
|
+
type: "text",
|
|
16379
|
+
text: JSON.stringify(
|
|
16380
|
+
{
|
|
16381
|
+
connected: true,
|
|
16382
|
+
platform: platform3,
|
|
16383
|
+
project_id: resolvedProjectId,
|
|
16384
|
+
accounts: foundAccounts.map((a) => ({
|
|
16385
|
+
id: a.id,
|
|
16386
|
+
username: a.username,
|
|
16387
|
+
connected_at: a.created_at
|
|
16388
|
+
})),
|
|
16389
|
+
attempts,
|
|
16390
|
+
message: `${platform3} has ${foundAccounts.length} active accounts for this project. Pass the exact account_id to schedule_post.`
|
|
16391
|
+
},
|
|
16392
|
+
null,
|
|
16393
|
+
2
|
|
16394
|
+
)
|
|
16395
|
+
}
|
|
16396
|
+
],
|
|
16397
|
+
isError: false
|
|
16398
|
+
};
|
|
16399
|
+
}
|
|
16400
|
+
return {
|
|
16401
|
+
content: [
|
|
16402
|
+
{
|
|
16403
|
+
type: "text",
|
|
16404
|
+
text: [
|
|
16405
|
+
`${platform3} is connected \u2014 ${foundAccounts.length} active accounts found for project ${resolvedProjectId}:`,
|
|
16406
|
+
...foundAccounts.map(
|
|
16407
|
+
(a) => ` ${a.username || "(unnamed)"} (id=${a.id})`
|
|
16408
|
+
),
|
|
16409
|
+
"",
|
|
16410
|
+
"Call schedule_post with the exact account_id (or account_ids) for the one you mean."
|
|
16411
|
+
].join("\n")
|
|
16412
|
+
}
|
|
16413
|
+
],
|
|
16414
|
+
isError: false
|
|
16415
|
+
};
|
|
16416
|
+
}
|
|
16417
|
+
const found = foundAccounts[0];
|
|
15236
16418
|
if (found) {
|
|
15237
16419
|
if (format === "json") {
|
|
15238
16420
|
return {
|
|
@@ -15243,7 +16425,7 @@ function registerConnectionTools(server2) {
|
|
|
15243
16425
|
{
|
|
15244
16426
|
connected: true,
|
|
15245
16427
|
platform: found.platform,
|
|
15246
|
-
project_id:
|
|
16428
|
+
project_id: resolvedProjectId,
|
|
15247
16429
|
account_id: found.id,
|
|
15248
16430
|
username: found.username,
|
|
15249
16431
|
connected_at: found.created_at,
|
|
@@ -15263,7 +16445,7 @@ function registerConnectionTools(server2) {
|
|
|
15263
16445
|
type: "text",
|
|
15264
16446
|
text: [
|
|
15265
16447
|
`${found.platform} is connected.`,
|
|
15266
|
-
|
|
16448
|
+
`Brand/project: ${resolvedProjectId}`,
|
|
15267
16449
|
`Account: ${found.username || "(unnamed)"} (id=${found.id})`,
|
|
15268
16450
|
`Detected after ${attempts} poll(s) in ${((Date.now() - startedAt) / 1e3).toFixed(1)}s.`,
|
|
15269
16451
|
"Ready to call `schedule_post`."
|
|
@@ -15276,7 +16458,9 @@ function registerConnectionTools(server2) {
|
|
|
15276
16458
|
}
|
|
15277
16459
|
const remaining = deadline - Date.now();
|
|
15278
16460
|
if (remaining <= 0) break;
|
|
15279
|
-
await new Promise(
|
|
16461
|
+
await new Promise(
|
|
16462
|
+
(resolve3) => setTimeout(resolve3, Math.min(intervalMs, remaining))
|
|
16463
|
+
);
|
|
15280
16464
|
}
|
|
15281
16465
|
const message = `${platform3} did not connect within ${timeout_s ?? 120}s (${attempts} polls). The user may not have completed the browser OAuth yet, or the link expired. Mint a new link with \`start_platform_connection\` and try again, or have the user go directly to socialneuron.com/settings/connections.`;
|
|
15282
16466
|
if (format === "json") {
|
|
@@ -15288,7 +16472,7 @@ function registerConnectionTools(server2) {
|
|
|
15288
16472
|
{
|
|
15289
16473
|
connected: false,
|
|
15290
16474
|
platform: platform3,
|
|
15291
|
-
project_id:
|
|
16475
|
+
project_id: resolvedProjectId,
|
|
15292
16476
|
attempts,
|
|
15293
16477
|
timed_out: true,
|
|
15294
16478
|
message
|