zuckerbot-mcp 0.4.1 → 0.4.3
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/README.md +10 -10
- package/dist/cli.js +8 -5
- package/dist/cli.js.map +1 -1
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +9 -2
- package/dist/client.js.map +1 -1
- package/dist/tools.d.ts.map +1 -1
- package/dist/tools.js +187 -77
- package/dist/tools.js.map +1 -1
- package/dist/types.d.ts +20 -1
- package/dist/types.d.ts.map +1 -1
- package/package.json +4 -2
package/dist/tools.js
CHANGED
|
@@ -130,14 +130,14 @@ function buildQuickstartPayload(client) {
|
|
|
130
130
|
{
|
|
131
131
|
step: 4,
|
|
132
132
|
tool: "zuckerbot_create_campaign",
|
|
133
|
-
description: "
|
|
133
|
+
description: "Create a reviewed launch-ready campaign draft",
|
|
134
134
|
requires_key: true,
|
|
135
135
|
flow: "quick_launch",
|
|
136
136
|
},
|
|
137
137
|
{
|
|
138
138
|
step: 5,
|
|
139
139
|
tool: "zuckerbot_launch_campaign",
|
|
140
|
-
description: "
|
|
140
|
+
description: "Launch the reviewed legacy draft on Meta",
|
|
141
141
|
requires_key: true,
|
|
142
142
|
flow: "quick_launch",
|
|
143
143
|
},
|
|
@@ -367,7 +367,7 @@ function registerCreativeGenerationTools(server, client) {
|
|
|
367
367
|
if (creative_handoff)
|
|
368
368
|
body.creative_handoff = creative_handoff;
|
|
369
369
|
const result = await client.post(`/campaigns/${campaign_id}/request-creative`, body);
|
|
370
|
-
return formatResult(appendHint(result, "Creative request dispatched. Monitor progress with zuckerbot_get_creative_status.
|
|
370
|
+
return formatResult(appendHint(result, "Creative request dispatched for planning/review. Monitor progress with zuckerbot_get_creative_status. Multi-tier activation is temporarily disabled; use a legacy campaign draft for a live launch."));
|
|
371
371
|
}
|
|
372
372
|
catch (err) {
|
|
373
373
|
return formatError(err);
|
|
@@ -433,26 +433,53 @@ export function registerTools(server, client) {
|
|
|
433
433
|
registerQuickstartTool(server, client);
|
|
434
434
|
registerBillingStatusTool(server, client);
|
|
435
435
|
// ── 0a. Account Audit ───────────────────────────────────────────
|
|
436
|
-
server.tool("zuckerbot_audit_account", "Run a full audit of the connected Meta ad account: wasted spend detection, creative fatigue, opportunity score (0-100
|
|
436
|
+
server.tool("zuckerbot_audit_account", "Run a full audit of the connected Meta ad account: wasted spend detection, creative fatigue, a complete-account opportunity score (0-100 when all inputs return), and prioritised action items. Saves a shareable web report when the API key resolves to one saved business. Read-only and available on every tier — the recommended FIRST call for any new account or when a user asks 'how are my ads doing?'.", {
|
|
437
437
|
meta_ad_account_id: z.string().optional().describe("Meta ad account ID to audit (format: act_XXXXX). Defaults to the connected account."),
|
|
438
438
|
company_name: z.string().optional().describe("Company name used in the audit narrative. Defaults to the connected business name."),
|
|
439
439
|
}, async ({ meta_ad_account_id, company_name }) => {
|
|
440
440
|
try {
|
|
441
441
|
client.requireAuth();
|
|
442
|
-
|
|
442
|
+
// Ask the API to persist when one business is resolvable, but keep the
|
|
443
|
+
// audit itself available when setup/selection is still incomplete.
|
|
444
|
+
// This stays one metered request and one Meta audit in both cases.
|
|
445
|
+
const body = {
|
|
446
|
+
save_report: true,
|
|
447
|
+
save_report_if_available: true,
|
|
448
|
+
};
|
|
443
449
|
if (meta_ad_account_id)
|
|
444
450
|
body.meta_ad_account_id = meta_ad_account_id;
|
|
445
451
|
if (company_name)
|
|
446
452
|
body.company_name = company_name;
|
|
447
453
|
const result = await client.post("/audit", body);
|
|
448
|
-
|
|
454
|
+
const record = asRecord(result);
|
|
455
|
+
const audit = asRecord(record?.audit);
|
|
456
|
+
const raw = asRecord(audit?.rawInsights);
|
|
457
|
+
const dataCompleteness = asRecord(raw?.data_completeness);
|
|
458
|
+
// Raw Meta insight rows drown the agent's context; the summarised
|
|
459
|
+
// findings above them carry everything the report presents.
|
|
460
|
+
if (raw && "campaign_rows" in raw)
|
|
461
|
+
delete raw.campaign_rows;
|
|
462
|
+
const reportUrl = typeof record?.report_url === "string" ? `https://zuckerbot.ai${record.report_url}` : null;
|
|
463
|
+
const partialDataHint = raw?.data_truncated === true
|
|
464
|
+
? " NOTE: Meta returned incomplete data. Check audit.rawInsights.data_completeness and do not present opportunityScore or exact spend/fatigue ratios as complete-account measures."
|
|
465
|
+
: "";
|
|
466
|
+
const scoreHint = typeof audit?.opportunityScore !== "number"
|
|
467
|
+
? " NOTE: No opportunity score is available for this result. Do not present or infer a numeric account rating."
|
|
468
|
+
: "";
|
|
469
|
+
const currencyHint = dataCompleteness?.account_currency_available === false
|
|
470
|
+
? " NOTE: Meta did not return the account currency. Describe money as account-currency units; do not label it AUD or invent another currency code."
|
|
471
|
+
: "";
|
|
472
|
+
const reportHint = reportUrl
|
|
473
|
+
? ` A shareable web report was saved — give the user this link: ${reportUrl}`
|
|
474
|
+
: " No shareable report was saved because this API key does not resolve to one saved business.";
|
|
475
|
+
return formatResult(appendHint(result, `Audit complete.${reportHint}${partialDataHint}${scoreHint}${currencyHint} Present complete observed findings and audit.rawInsights.action_items to the user, then act on them: zuckerbot_get_performance to drill into a specific campaign, zuckerbot_pause_campaign to stop wasted spend, or zuckerbot_creative_analysis to diagnose fatigued creatives.`));
|
|
449
476
|
}
|
|
450
477
|
catch (err) {
|
|
451
478
|
return formatError(err);
|
|
452
479
|
}
|
|
453
480
|
});
|
|
454
481
|
// ── 0b. Redeem Lifetime Licence ─────────────────────────────────
|
|
455
|
-
server.tool("zuckerbot_redeem_license", "Redeem a ZuckerBot lifetime licence code (format ZB-XXXXX-XXXXX-XXXXX) purchased on Dealify or AppSumo.
|
|
482
|
+
server.tool("zuckerbot_redeem_license", "Redeem a ZuckerBot lifetime licence code (format ZB-XXXXX-XXXXX-XXXXX) purchased on Dealify or AppSumo. Each code activates the plan it was purchased for: Tier 1 (1 ad account, 2,500 calls/mo), Tier 2 (3 accounts, 10K calls/mo) or Tier 3 (10 accounts, 30K calls/mo). Codes also stack additively on one account up to Tier 3 — e.g. two Tier 1 codes = Tier 2. Redeeming upgrades ALL of the account's API keys to the new tier immediately.", {
|
|
456
483
|
code: z.string().describe("Lifetime licence code in the format ZB-XXXXX-XXXXX-XXXXX"),
|
|
457
484
|
}, async ({ code }) => {
|
|
458
485
|
try {
|
|
@@ -462,7 +489,7 @@ export function registerTools(server, client) {
|
|
|
462
489
|
const mintedKey = asRecord(record?.api_key);
|
|
463
490
|
const hint = mintedKey
|
|
464
491
|
? "IMPORTANT: a new API key was minted during redemption. Show the full api_key.key value to the user IMMEDIATELY and tell them to store it securely — it will NEVER be shown again. Then follow the next_steps in the response."
|
|
465
|
-
: "Licence redeemed — all of this account's API keys are upgraded to the new tier.
|
|
492
|
+
: "Licence redeemed — all of this account's API keys are upgraded to the new tier. If the tier is below Lifetime Tier 3, stacking another code raises it. Run zuckerbot_audit_account to put the new limits to work.";
|
|
466
493
|
return formatResult(appendHint(result, hint));
|
|
467
494
|
}
|
|
468
495
|
catch (err) {
|
|
@@ -608,15 +635,15 @@ export function registerTools(server, client) {
|
|
|
608
635
|
}
|
|
609
636
|
});
|
|
610
637
|
// ── 7. Create Full Campaign ─────────────────────────────────────
|
|
611
|
-
server.tool("zuckerbot_create_full_campaign", "Build a complete Meta campaign from an approved Campaign Architect session. IMPORTANT: dry_run defaults to TRUE. When dry_run=true, returns the exact campaign structure that WOULD be created without calling Meta — safe, free, no side effects. Present this to the customer first. Only set dry_run=false after explicit customer approval. When dry_run=false, creates
|
|
638
|
+
server.tool("zuckerbot_create_full_campaign", "Build a complete PAUSED Meta campaign from an approved Campaign Architect session. IMPORTANT: dry_run defaults to TRUE. When dry_run=true, returns the exact campaign structure that WOULD be created without calling Meta — safe, free, no side effects. Present this to the customer first. Only set dry_run=false after explicit customer approval. When dry_run=false, creates Meta objects in PAUSED state for review; Architect auto-activation is temporarily disabled. Generated videos get linked for rejection tracking automatically.", {
|
|
612
639
|
session_id: z.string().describe("Campaign Architect session ID with approved strategy and creatives"),
|
|
613
640
|
dry_run: z.boolean().default(true)
|
|
614
641
|
.describe("DEFAULT TRUE. Set to false ONLY after customer has approved the dry-run preview. Live mode creates real Meta objects."),
|
|
615
642
|
meta_access_token: z.string().optional().describe("Meta access token override (live mode only)"),
|
|
616
643
|
meta_ad_account_id: z.string().optional().describe("Meta ad account ID override (live mode only)"),
|
|
617
644
|
meta_page_id: z.string().optional().describe("Facebook Page ID override (live mode only)"),
|
|
618
|
-
activate: z.
|
|
619
|
-
.describe("
|
|
645
|
+
activate: z.literal(false).default(false)
|
|
646
|
+
.describe("Must remain false. Architect auto-activation is temporarily disabled; live mode creates PAUSED Meta objects only."),
|
|
620
647
|
}, async ({ session_id, dry_run, meta_access_token, meta_ad_account_id, meta_page_id, activate }) => {
|
|
621
648
|
try {
|
|
622
649
|
client.requireAuth();
|
|
@@ -632,9 +659,7 @@ export function registerTools(server, client) {
|
|
|
632
659
|
const isDryRun = record.dry_run === true;
|
|
633
660
|
const hint = isDryRun
|
|
634
661
|
? "DRY RUN — no Meta objects were created. Present the would_create structure to the customer. If they approve, call again with dry_run=false to create live Meta objects."
|
|
635
|
-
:
|
|
636
|
-
? "Campaign built and activated on Meta. Monitor performance with zuckerbot_get_performance. Generated videos are linked for rejection tracking."
|
|
637
|
-
: "Campaign built in PAUSED state on Meta. Present the meta_structure to the customer, then resume the campaign on Meta only after approval. Videos linked for rejection tracking.";
|
|
662
|
+
: "Campaign built in PAUSED state on Meta for review. Architect auto-activation and campaign resume are temporarily disabled; use a reviewed legacy draft with zuckerbot_launch_campaign for the supported live path. Videos linked for rejection tracking.";
|
|
638
663
|
return formatResult(appendHint(result, hint));
|
|
639
664
|
}
|
|
640
665
|
catch (err) {
|
|
@@ -657,14 +682,14 @@ export function registerTools(server, client) {
|
|
|
657
682
|
url,
|
|
658
683
|
ad_count,
|
|
659
684
|
});
|
|
660
|
-
return formatResult(appendHint(result, "Preview generated. Call zuckerbot_create_campaign with the same URL to
|
|
685
|
+
return formatResult(appendHint(result, "Preview generated. Call zuckerbot_create_campaign with the same URL; it defaults to the reviewed legacy draft path that can be launched safely."));
|
|
661
686
|
}
|
|
662
687
|
catch (err) {
|
|
663
688
|
return formatError(err);
|
|
664
689
|
}
|
|
665
690
|
});
|
|
666
691
|
// ── 7. Create Campaign ──────────────────────────────────────────
|
|
667
|
-
server.tool("zuckerbot_create_campaign", "Create a new campaign draft for a business.
|
|
692
|
+
server.tool("zuckerbot_create_campaign", "Create a new campaign draft for a business. Defaults to legacy mode, the only launch-ready path during Dealify hardening. Intelligence mode remains available for planning only and cannot be activated. This tool does not spend money or create anything on Meta; review the draft, then use zuckerbot_launch_campaign.", {
|
|
668
693
|
url: z.string().describe("Business website URL"),
|
|
669
694
|
business_id: z.string().optional().describe("Existing ZuckerBot business ID to anchor intelligence mode"),
|
|
670
695
|
architect_session_id: z.string().optional()
|
|
@@ -682,8 +707,8 @@ export function registerTools(server, client) {
|
|
|
682
707
|
.describe("Daily budget in cents (e.g., 2000 = $20/day)"),
|
|
683
708
|
mode: z
|
|
684
709
|
.enum(["auto", "legacy", "intelligence"])
|
|
685
|
-
.
|
|
686
|
-
.describe("Campaign planning mode.
|
|
710
|
+
.default("legacy")
|
|
711
|
+
.describe("Campaign planning mode. Default legacy is the only launch-ready path. auto/intelligence are planning-only while multi-tier activation is disabled."),
|
|
687
712
|
objective: z
|
|
688
713
|
.enum(["leads", "traffic", "conversions", "awareness"])
|
|
689
714
|
.optional()
|
|
@@ -713,8 +738,7 @@ export function registerTools(server, client) {
|
|
|
713
738
|
body.location = location;
|
|
714
739
|
if (budget_daily_cents !== undefined)
|
|
715
740
|
body.budget_daily_cents = budget_daily_cents;
|
|
716
|
-
|
|
717
|
-
body.mode = mode;
|
|
741
|
+
body.mode = mode ?? "legacy";
|
|
718
742
|
if (objective)
|
|
719
743
|
body.objective = objective;
|
|
720
744
|
if (lead_destination)
|
|
@@ -726,7 +750,9 @@ export function registerTools(server, client) {
|
|
|
726
750
|
if (creative_handoff)
|
|
727
751
|
body.creative_handoff = creative_handoff;
|
|
728
752
|
const result = await client.post("/campaigns/create", body);
|
|
729
|
-
return formatResult(appendHint(result,
|
|
753
|
+
return formatResult(appendHint(result, mode === "legacy"
|
|
754
|
+
? "Launch-ready legacy draft created. Review it, confirm Meta credentials and budget, then call zuckerbot_launch_campaign."
|
|
755
|
+
: "Planning-only intelligence draft created. Multi-tier activation is temporarily disabled; create a legacy-mode draft when the user is ready to launch."));
|
|
730
756
|
}
|
|
731
757
|
catch (err) {
|
|
732
758
|
return formatError(err);
|
|
@@ -789,7 +815,7 @@ export function registerTools(server, client) {
|
|
|
789
815
|
}, async ({ campaign_id }) => {
|
|
790
816
|
try {
|
|
791
817
|
const result = await client.get(`/campaigns/${campaign_id}`);
|
|
792
|
-
return formatResult(appendHint(result, "Inspect creative_status and workflow state.
|
|
818
|
+
return formatResult(appendHint(result, "Inspect creative_status and workflow state. Legacy campaigns can launch directly after review. Intelligence campaigns are planning-only while multi-tier activation is disabled."));
|
|
793
819
|
}
|
|
794
820
|
catch (err) {
|
|
795
821
|
return formatError(err);
|
|
@@ -833,7 +859,7 @@ export function registerTools(server, client) {
|
|
|
833
859
|
const creativeStatus = typeof payload?.creative_status === "string" ? payload.creative_status : null;
|
|
834
860
|
const queuedJobs = Array.isArray(payload?.queued_jobs) ? payload.queued_jobs : [];
|
|
835
861
|
if (creativeStatus !== "uploading" || queuedJobs.length === 0) {
|
|
836
|
-
return formatResult(appendHint(result, "Creatives uploaded. Call zuckerbot_get_creative_status to check processing status
|
|
862
|
+
return formatResult(appendHint(result, "Creatives uploaded. Call zuckerbot_get_creative_status to check processing status. Intelligence activation is temporarily disabled; use a reviewed legacy draft for live launch."));
|
|
837
863
|
}
|
|
838
864
|
try {
|
|
839
865
|
const polled = await pollCreativeStatus(client, campaign_id);
|
|
@@ -847,7 +873,7 @@ export function registerTools(server, client) {
|
|
|
847
873
|
interval_seconds: CREATIVE_STATUS_POLL_INTERVAL_MS / 1000,
|
|
848
874
|
},
|
|
849
875
|
final_status: polled.lastStatus,
|
|
850
|
-
}, "All creatives uploaded and processed.
|
|
876
|
+
}, "All creatives uploaded and processed for review. Intelligence activation is temporarily disabled; use a reviewed legacy draft for live launch."));
|
|
851
877
|
}
|
|
852
878
|
return formatResult(appendHint({
|
|
853
879
|
initial_response: payload,
|
|
@@ -860,7 +886,7 @@ export function registerTools(server, client) {
|
|
|
860
886
|
suggested_tool: "zuckerbot_get_creative_status",
|
|
861
887
|
},
|
|
862
888
|
last_status: polled.lastStatus,
|
|
863
|
-
}, "Uploads still processing. Call zuckerbot_get_creative_status in 15–30 seconds to check progress.
|
|
889
|
+
}, "Uploads still processing. Call zuckerbot_get_creative_status in 15–30 seconds to check progress. Completed intelligence creatives remain review-only while activation is disabled."));
|
|
864
890
|
}
|
|
865
891
|
catch (pollError) {
|
|
866
892
|
return formatResult(appendHint({
|
|
@@ -876,40 +902,36 @@ export function registerTools(server, client) {
|
|
|
876
902
|
return formatError(err);
|
|
877
903
|
}
|
|
878
904
|
});
|
|
879
|
-
server.tool("zuckerbot_get_creative_status", "Check the asynchronous upload queue for an intelligence campaign to see if Meta ad-creation jobs are complete. Poll this after zuckerbot_upload_creative when the initial response shows creative_status='uploading'. Returns all_complete=true when every queued job has finished
|
|
905
|
+
server.tool("zuckerbot_get_creative_status", "Check the asynchronous upload queue for an intelligence campaign to see if Meta ad-creation jobs are complete. Poll this after zuckerbot_upload_creative when the initial response shows creative_status='uploading'. Returns all_complete=true when every queued job has finished for review; intelligence activation is temporarily disabled.", {
|
|
880
906
|
campaign_id: z.string().describe("Intelligence campaign ID"),
|
|
881
907
|
}, async ({ campaign_id }) => {
|
|
882
908
|
try {
|
|
883
909
|
const result = await client.get(`/campaigns/${campaign_id}/creative-status`);
|
|
884
|
-
return formatResult(appendHint(result, "If
|
|
910
|
+
return formatResult(appendHint(result, "If still uploading, poll again in 15–30 seconds. Completed intelligence creatives remain available for review, but multi-tier activation is temporarily disabled; use a legacy draft for live launch."));
|
|
885
911
|
}
|
|
886
912
|
catch (err) {
|
|
887
913
|
return formatError(err);
|
|
888
914
|
}
|
|
889
915
|
});
|
|
890
|
-
server.tool("zuckerbot_activate_campaign", "
|
|
916
|
+
server.tool("zuckerbot_activate_campaign", "Temporarily unavailable during Dealify launch hardening. Intelligence campaigns are planning-only; create a legacy-mode draft and use zuckerbot_launch_campaign for the supported live path.", {
|
|
891
917
|
campaign_id: z.string().describe("Intelligence campaign ID"),
|
|
892
918
|
tier_names: z.array(z.string()).optional().describe("Optional subset of approved tiers to activate"),
|
|
893
919
|
meta_access_token: z.string().optional().describe("Optional Meta/Facebook access token override"),
|
|
894
920
|
meta_ad_account_id: z.string().optional().describe("Optional Meta ad account ID override (format: act_XXXXX)"),
|
|
895
921
|
meta_page_id: z.string().optional().describe("Optional Facebook Page ID override"),
|
|
896
922
|
}, async ({ campaign_id, tier_names, meta_access_token, meta_ad_account_id, meta_page_id }) => {
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
}
|
|
910
|
-
catch (err) {
|
|
911
|
-
return formatError(err);
|
|
912
|
-
}
|
|
923
|
+
void tier_names;
|
|
924
|
+
void meta_access_token;
|
|
925
|
+
void meta_ad_account_id;
|
|
926
|
+
void meta_page_id;
|
|
927
|
+
return formatResult({
|
|
928
|
+
error: true,
|
|
929
|
+
code: "intelligence_activation_temporarily_disabled",
|
|
930
|
+
message: "Multi-tier activation is temporarily disabled. Create a campaign with mode='legacy', review it, then call zuckerbot_launch_campaign.",
|
|
931
|
+
campaign_id,
|
|
932
|
+
supported_create_mode: "legacy",
|
|
933
|
+
supported_launch_tool: "zuckerbot_launch_campaign",
|
|
934
|
+
});
|
|
913
935
|
});
|
|
914
936
|
server.tool("zuckerbot_suggest_angles", "Return only the creative angles and audience tiers for a campaign draft — a lightweight alternative to zuckerbot_get_campaign when you need just the strategy summary without the full campaign payload, stored creatives, or tier execution details.", {
|
|
915
937
|
campaign_id: z.string().describe("Campaign ID"),
|
|
@@ -976,19 +998,19 @@ export function registerTools(server, client) {
|
|
|
976
998
|
return formatError(err);
|
|
977
999
|
}
|
|
978
1000
|
});
|
|
979
|
-
// ── 4. Pause
|
|
980
|
-
server.tool("zuckerbot_pause_campaign", "Pause
|
|
1001
|
+
// ── 4. Pause Campaign ──────────────────────────────────────────
|
|
1002
|
+
server.tool("zuckerbot_pause_campaign", "Pause a running Meta ad campaign. Pausing stops ad delivery and spend immediately while leaving the campaign in Meta. Use this when creative needs refreshing, budget is exhausted, entitlement is withdrawn, or the user asks to stop spending. Resume is temporarily disabled during Dealify launch hardening.", {
|
|
981
1003
|
campaign_id: z.string().describe("ZuckerBot campaign ID"),
|
|
982
1004
|
action: z
|
|
983
|
-
.
|
|
1005
|
+
.literal("pause")
|
|
984
1006
|
.default("pause")
|
|
985
|
-
.describe("
|
|
1007
|
+
.describe("Pause the campaign"),
|
|
986
1008
|
}, async ({ campaign_id, action }) => {
|
|
987
1009
|
try {
|
|
988
1010
|
const result = await client.post(`/campaigns/${campaign_id}/pause`, {
|
|
989
1011
|
action,
|
|
990
1012
|
});
|
|
991
|
-
return formatResult(appendHint(result, "Campaign paused
|
|
1013
|
+
return formatResult(appendHint(result, "Campaign paused. Call zuckerbot_get_performance to confirm delivery has stopped, or zuckerbot_get_campaign to inspect the full workflow state."));
|
|
992
1014
|
}
|
|
993
1015
|
catch (err) {
|
|
994
1016
|
return formatError(err);
|
|
@@ -1253,7 +1275,7 @@ export function registerTools(server, client) {
|
|
|
1253
1275
|
return formatError(err);
|
|
1254
1276
|
}
|
|
1255
1277
|
});
|
|
1256
|
-
server.tool("zuckerbot_get_campaign_insights", "Query campaign, ad set, or ad-level performance for any campaign in the connected Meta ad account — including campaigns not created by ZuckerBot. Tags each row with is_zuckerbot so you can benchmark ZuckerBot campaigns against manually managed ones. Supports date range filtering, campaign name search, status filters, time-series breakdowns, and multi-column sorting. For deduped truth use meta_result / cost_per_meta_result (Meta's Ads Manager \"Results\" for any objective) or conversion_leads / meta_leads — not the inflated, deprecated leads / cpl. meta_result and conversion_leads are most reliable on campaign-level, non-time-incremented queries.", {
|
|
1278
|
+
server.tool("zuckerbot_get_campaign_insights", "Query campaign, ad set, or ad-level performance for any campaign in the connected Meta ad account — including campaigns not created by ZuckerBot. Tags each row with is_zuckerbot so you can benchmark ZuckerBot campaigns against manually managed ones. Supports date range filtering, campaign name search, status filters, time-series breakdowns, and multi-column sorting. For deduped truth use meta_result / cost_per_meta_result (Meta's Ads Manager \"Results\" for any objective) or conversion_leads / meta_leads — not the inflated, deprecated leads / cpl (the response's metric_semantics object labels every lead field). meta_result and conversion_leads are most reliable on campaign-level, non-time-incremented queries. Adset/ad rows report their OWN status — status_scope says which entity a row's status belongs to, statuses are as of status_synced_at, and refresh=true re-syncs them; spend-by-date is authoritative for whether delivery actually happened.", {
|
|
1257
1279
|
business_id: z.string().optional().describe("Optional business ID override linked to the connected Meta ad account"),
|
|
1258
1280
|
date_from: z.string().optional().describe("Optional start date in YYYY-MM-DD format. Defaults to 7 days ago."),
|
|
1259
1281
|
date_to: z.string().optional().describe("Optional end date in YYYY-MM-DD format. Defaults to today."),
|
|
@@ -1319,7 +1341,10 @@ export function registerTools(server, client) {
|
|
|
1319
1341
|
})
|
|
1320
1342
|
.optional()
|
|
1321
1343
|
.describe("Optional user data to improve match rate"),
|
|
1322
|
-
|
|
1344
|
+
fbclid: z.string().optional().describe("Optional Facebook click ID; the server builds a well-formed fbc cookie from it"),
|
|
1345
|
+
fbc: z.string().optional().describe("Optional pre-formatted fbc cookie, forwarded raw — never hashed"),
|
|
1346
|
+
fbp: z.string().optional().describe("Optional _fbp browser cookie, forwarded raw — never hashed. Improves match quality"),
|
|
1347
|
+
}, async ({ campaign_id, lead_id, quality, meta_access_token, user_data, fbclid, fbc, fbp }) => {
|
|
1323
1348
|
try {
|
|
1324
1349
|
const body = {
|
|
1325
1350
|
lead_id,
|
|
@@ -1328,6 +1353,12 @@ export function registerTools(server, client) {
|
|
|
1328
1353
|
};
|
|
1329
1354
|
if (user_data)
|
|
1330
1355
|
body.user_data = user_data;
|
|
1356
|
+
if (fbclid)
|
|
1357
|
+
body.fbclid = fbclid;
|
|
1358
|
+
if (fbc)
|
|
1359
|
+
body.fbc = fbc;
|
|
1360
|
+
if (fbp)
|
|
1361
|
+
body.fbp = fbp;
|
|
1331
1362
|
const result = await client.post(`/campaigns/${campaign_id}/conversions`, body);
|
|
1332
1363
|
return formatResult(appendHint(result, "Conversion signal sent. Use zuckerbot_capi_status to view aggregate CAPI delivery and attribution, or zuckerbot_get_performance to see updated CPL and lead metrics."));
|
|
1333
1364
|
}
|
|
@@ -1472,7 +1503,7 @@ export function registerTools(server, client) {
|
|
|
1472
1503
|
}
|
|
1473
1504
|
});
|
|
1474
1505
|
// ── 18. CAPI Config — Read ─────────────────────────────────────
|
|
1475
|
-
server.tool("zuckerbot_get_capi_config", "Fetch the current Conversions API configuration for a business: whether CAPI delivery is enabled, CRM source, currency, stage-to-event mappings, action source, and webhook URL. Use this before configuring CAPI to see what is already set, or to audit the current event mapping.", {
|
|
1506
|
+
server.tool("zuckerbot_get_capi_config", "Fetch the current Conversions API configuration for a business: whether CAPI delivery is enabled, CRM source, currency, stage-to-event mappings, action source, and webhook URL. Use this before configuring CAPI to see what is already set, or to audit the current event mapping. The webhook secret is write-only: reads return webhook_secret_set and webhook_secret_last4, never the full value — use zuckerbot_rotate_webhook_secret to mint a new one.", {
|
|
1476
1507
|
business_id: z.string().optional().describe("Optional business ID override for the authenticated API key"),
|
|
1477
1508
|
}, async ({ business_id }) => {
|
|
1478
1509
|
try {
|
|
@@ -1496,14 +1527,14 @@ export function registerTools(server, client) {
|
|
|
1496
1527
|
.enum(["website", "app", "email", "phone_call", "chat", "physical_store", "system_generated", "business_messaging", "other"])
|
|
1497
1528
|
.optional()
|
|
1498
1529
|
.describe("Meta Conversions API action_source. Defaults to website for CRM events"),
|
|
1499
|
-
rotate_webhook_secret: z.boolean().optional().describe("Rotate the webhook secret on update"),
|
|
1530
|
+
rotate_webhook_secret: z.boolean().optional().describe("Rotate the webhook secret on update. The new secret is returned exactly once in the response; prefer zuckerbot_rotate_webhook_secret for a dedicated rotation"),
|
|
1500
1531
|
event_mapping: z
|
|
1501
1532
|
.record(z.string(), z.object({
|
|
1502
|
-
meta_event: z.string().describe("Meta standard
|
|
1533
|
+
meta_event: z.string().describe("Meta event name: standard names (Lead, Purchase, StartTrial, …) are canonicalised case-insensitively; custom names (e.g. plg_signup_completed) are stored verbatim — start with a letter, letters/digits/underscore/hyphen/period/space only, max 50 chars"),
|
|
1503
1534
|
value: z.number().optional().describe("Event value in major currency units"),
|
|
1504
1535
|
}))
|
|
1505
1536
|
.optional()
|
|
1506
|
-
.describe("CRM stage mapping object keyed by source stage"),
|
|
1537
|
+
.describe("CRM stage mapping object keyed by source stage. Stage keys are normalised (lower-cased, non-alphanumerics stripped: signup_completed → signupcompleted); inbound webhook source_stage values are normalised identically before matching, and any renamed keys are reported back as normalised_keys in the response"),
|
|
1507
1538
|
}, async ({ business_id, is_enabled, currency, crm_source, optimise_for, action_source, rotate_webhook_secret, event_mapping }) => {
|
|
1508
1539
|
try {
|
|
1509
1540
|
const resolvedBusinessId = await client.resolveBusinessId(business_id);
|
|
@@ -1529,6 +1560,19 @@ export function registerTools(server, client) {
|
|
|
1529
1560
|
return formatError(err);
|
|
1530
1561
|
}
|
|
1531
1562
|
});
|
|
1563
|
+
// ── 19b. CAPI Webhook Secret — Rotate ──────────────────────────
|
|
1564
|
+
server.tool("zuckerbot_rotate_webhook_secret", "Rotate the Conversions API webhook secret for a business. The new secret is returned exactly once, in this response only — every other read shows just webhook_secret_set and webhook_secret_last4. The old secret stops authenticating immediately, so update the system that signs your inbound webhooks (for example your CRM workflow's stored secret) in the same sitting.", {
|
|
1565
|
+
business_id: z.string().optional().describe("Optional business ID override for the authenticated API key"),
|
|
1566
|
+
}, async ({ business_id }) => {
|
|
1567
|
+
try {
|
|
1568
|
+
const resolvedBusinessId = await client.resolveBusinessId(business_id);
|
|
1569
|
+
const result = await client.put("/capi/config", { business_id: resolvedBusinessId, rotate_webhook_secret: true });
|
|
1570
|
+
return formatResult(appendHint(result, "Copy webhook_secret from this response now — it is not shown again. The old secret is already invalid, so update your webhook sender's stored secret immediately, then call zuckerbot_capi_test to verify delivery."));
|
|
1571
|
+
}
|
|
1572
|
+
catch (err) {
|
|
1573
|
+
return formatError(err);
|
|
1574
|
+
}
|
|
1575
|
+
});
|
|
1532
1576
|
// ── 20. CAPI Status ────────────────────────────────────────────
|
|
1533
1577
|
server.tool("zuckerbot_capi_status", "Get 7-day and 30-day CAPI delivery statistics for the business: total events sent, events by type (Lead/Contact/Purchase), match quality breakdown, and attribution counts. Use this to confirm CAPI is functioning and that events are being matched by Meta.", {
|
|
1534
1578
|
business_id: z.string().optional().describe("Optional business ID override"),
|
|
@@ -1558,7 +1602,10 @@ export function registerTools(server, client) {
|
|
|
1558
1602
|
})
|
|
1559
1603
|
.optional()
|
|
1560
1604
|
.describe("Optional user data to hash into the test payload"),
|
|
1561
|
-
|
|
1605
|
+
fbclid: z.string().optional().describe("Optional Facebook click ID to verify fbc construction/passthrough"),
|
|
1606
|
+
fbc: z.string().optional().describe("Optional pre-formatted fbc cookie to verify raw passthrough"),
|
|
1607
|
+
fbp: z.string().optional().describe("Optional _fbp cookie to verify raw passthrough"),
|
|
1608
|
+
}, async ({ business_id, source_stage, crm_source, value, user_data, fbclid, fbc, fbp }) => {
|
|
1562
1609
|
try {
|
|
1563
1610
|
const resolvedBusinessId = await client.resolveBusinessId(business_id);
|
|
1564
1611
|
const body = {
|
|
@@ -1572,6 +1619,12 @@ export function registerTools(server, client) {
|
|
|
1572
1619
|
body.value = value;
|
|
1573
1620
|
if (user_data)
|
|
1574
1621
|
body.user_data = user_data;
|
|
1622
|
+
if (fbclid)
|
|
1623
|
+
body.fbclid = fbclid;
|
|
1624
|
+
if (fbc)
|
|
1625
|
+
body.fbc = fbc;
|
|
1626
|
+
if (fbp)
|
|
1627
|
+
body.fbp = fbp;
|
|
1575
1628
|
const result = await client.post("/capi/config/test", body);
|
|
1576
1629
|
return formatResult(appendHint(result, "Test event sent. Check status in the response — if 'sent', CAPI is working. If 'skipped' or 'failed', review the config with zuckerbot_get_capi_config and fix the event mapping."));
|
|
1577
1630
|
}
|
|
@@ -1580,7 +1633,7 @@ export function registerTools(server, client) {
|
|
|
1580
1633
|
}
|
|
1581
1634
|
});
|
|
1582
1635
|
// ── 22. Create Portfolio ───────────────────────────────────────
|
|
1583
|
-
server.tool("zuckerbot_create_portfolio", "Create a multi-tier audience portfolio for a business from a shared template (e.g., 'Local Services', 'eCommerce') or a custom tier array. Portfolios split
|
|
1636
|
+
server.tool("zuckerbot_create_portfolio", "Create a planning and monitoring-only multi-tier audience portfolio for a business from a shared template (e.g., 'Local Services', 'eCommerce') or a custom tier array. Portfolios split a proposed total budget across prospecting, retargeting, and reactivation tiers with per-tier CPA targets. Portfolio launch is temporarily disabled during Dealify hardening.", {
|
|
1584
1637
|
business_id: z.string().optional().describe("Optional business ID override"),
|
|
1585
1638
|
template_id: z.string().optional().describe("Optional portfolio template ID"),
|
|
1586
1639
|
template_name: z.string().optional().describe("Optional portfolio template name, such as 'Local Services'"),
|
|
@@ -1615,19 +1668,19 @@ export function registerTools(server, client) {
|
|
|
1615
1668
|
if (tiers)
|
|
1616
1669
|
body.tiers = tiers;
|
|
1617
1670
|
const result = await client.post("/portfolios/create", body);
|
|
1618
|
-
return formatResult(appendHint(result, "
|
|
1671
|
+
return formatResult(appendHint(result, "Planning portfolio created. Call zuckerbot_get_portfolio to review the tier configuration. Portfolio launch is temporarily disabled; use a reviewed legacy draft for live launch."));
|
|
1619
1672
|
}
|
|
1620
1673
|
catch (err) {
|
|
1621
1674
|
return formatError(err);
|
|
1622
1675
|
}
|
|
1623
1676
|
});
|
|
1624
1677
|
// ── 23. Get Portfolio ─────────────────────────────────────────
|
|
1625
|
-
server.tool("zuckerbot_get_portfolio", "Fetch the configuration and current performance snapshot for an audience portfolio by ID. Returns tier definitions, budget allocations, CPA targets, and
|
|
1678
|
+
server.tool("zuckerbot_get_portfolio", "Fetch the configuration and current performance snapshot for an audience portfolio by ID. Returns tier definitions, budget allocations, CPA targets, and any existing performance data. Use this to inspect a portfolio for planning, monitoring, or rebalancing an already-active portfolio.", {
|
|
1626
1679
|
portfolio_id: z.string().describe("Audience portfolio ID"),
|
|
1627
1680
|
}, async ({ portfolio_id }) => {
|
|
1628
1681
|
try {
|
|
1629
1682
|
const result = await client.get(`/portfolios/${portfolio_id}`);
|
|
1630
|
-
return formatResult(appendHint(result, "Review tier config and performance. To adjust settings, call zuckerbot_update_portfolio.
|
|
1683
|
+
return formatResult(appendHint(result, "Review tier config and performance. To adjust planning settings, call zuckerbot_update_portfolio. Existing active portfolios can be monitored or rebalanced, but new portfolio launch is temporarily disabled."));
|
|
1631
1684
|
}
|
|
1632
1685
|
catch (err) {
|
|
1633
1686
|
return formatError(err);
|
|
@@ -1696,26 +1749,23 @@ export function registerTools(server, client) {
|
|
|
1696
1749
|
}
|
|
1697
1750
|
});
|
|
1698
1751
|
// ── 27. Launch Portfolio ──────────────────────────────────────
|
|
1699
|
-
server.tool("zuckerbot_launch_portfolio", "
|
|
1752
|
+
server.tool("zuckerbot_launch_portfolio", "Temporarily unavailable during Dealify launch hardening. Portfolio planning and monitoring remain available, but new multi-tier launches must not create Meta objects. Create a legacy-mode draft and use zuckerbot_launch_campaign for the supported live path.", {
|
|
1700
1753
|
portfolio_id: z.string().describe("Audience portfolio ID to launch"),
|
|
1701
1754
|
meta_access_token: z.string().optional().describe("Optional Meta/Facebook access token override"),
|
|
1702
1755
|
meta_ad_account_id: z.string().optional().describe("Optional Meta ad account ID override (format: act_XXXXX)"),
|
|
1703
1756
|
meta_page_id: z.string().optional().describe("Optional Facebook Page ID override"),
|
|
1704
1757
|
}, async ({ portfolio_id, meta_access_token, meta_ad_account_id, meta_page_id }) => {
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
}
|
|
1716
|
-
catch (err) {
|
|
1717
|
-
return formatError(err);
|
|
1718
|
-
}
|
|
1758
|
+
void meta_access_token;
|
|
1759
|
+
void meta_ad_account_id;
|
|
1760
|
+
void meta_page_id;
|
|
1761
|
+
return formatResult({
|
|
1762
|
+
error: true,
|
|
1763
|
+
code: "portfolio_launch_temporarily_disabled",
|
|
1764
|
+
message: "Portfolio launch is temporarily disabled. Create a campaign with mode='legacy', review it, then call zuckerbot_launch_campaign.",
|
|
1765
|
+
portfolio_id,
|
|
1766
|
+
supported_create_mode: "legacy",
|
|
1767
|
+
supported_launch_tool: "zuckerbot_launch_campaign",
|
|
1768
|
+
});
|
|
1719
1769
|
});
|
|
1720
1770
|
// ── 28. Tag Creative ──────────────────────────────────────────
|
|
1721
1771
|
server.tool("zuckerbot_tag_creative", "Tag Meta ads with creative attributes (hook type, visual style, product focus, CTA type, copy tone, setting) by providing ad metadata and optional asset URLs. ZuckerBot uses Claude vision to analyze the creative and store structured tags. These tags feed the zuckerbot_creative_analysis pipeline. Run this after launching new ads to keep the creative intelligence database current.", {
|
|
@@ -1759,8 +1809,10 @@ export function registerTools(server, client) {
|
|
|
1759
1809
|
last_name: z.string().optional().describe("Optional last name for identity matching"),
|
|
1760
1810
|
value: z.number().optional().describe("Optional event value override in major currency units"),
|
|
1761
1811
|
event_time: z.string().optional().describe("Optional ISO 8601 event timestamp. Defaults to now."),
|
|
1762
|
-
fbclid: z.string().optional().describe("Optional Facebook click ID
|
|
1763
|
-
|
|
1812
|
+
fbclid: z.string().optional().describe("Optional Facebook click ID; the server builds a well-formed fbc cookie from it"),
|
|
1813
|
+
fbc: z.string().optional().describe("Optional pre-formatted Facebook click cookie (fb.1.<ms>.<fbclid>), forwarded raw — never hashed"),
|
|
1814
|
+
fbp: z.string().optional().describe("Optional Facebook browser ID cookie (_fbp), forwarded raw — never hashed. Improves match quality for every event"),
|
|
1815
|
+
}, async ({ business_id, source_stage, crm_source, lead_id, meta_lead_id, email, phone, first_name, last_name, value, event_time, fbclid, fbc, fbp }) => {
|
|
1764
1816
|
try {
|
|
1765
1817
|
const resolvedBusinessId = await client.resolveBusinessId(business_id);
|
|
1766
1818
|
const body = {
|
|
@@ -1787,6 +1839,10 @@ export function registerTools(server, client) {
|
|
|
1787
1839
|
body.event_time = event_time;
|
|
1788
1840
|
if (fbclid)
|
|
1789
1841
|
body.fbclid = fbclid;
|
|
1842
|
+
if (fbc)
|
|
1843
|
+
body.fbc = fbc;
|
|
1844
|
+
if (fbp)
|
|
1845
|
+
body.fbp = fbp;
|
|
1790
1846
|
const result = await client.post("/capi/events", body);
|
|
1791
1847
|
return formatResult(appendHint(result, "Event dispatched. Check the status field: 'sent' = successfully delivered to Meta, 'skipped' = CAPI disabled or stage unmapped, 'failed' = Meta rejected the event. Use zuckerbot_capi_status for aggregate delivery metrics."));
|
|
1792
1848
|
}
|
|
@@ -1794,5 +1850,59 @@ export function registerTools(server, client) {
|
|
|
1794
1850
|
return formatError(err);
|
|
1795
1851
|
}
|
|
1796
1852
|
});
|
|
1853
|
+
// ── 26. Spec Mode — Campaign From Spec ─────────────────────────
|
|
1854
|
+
server.tool("zuckerbot_create_campaign_from_spec", "Build a complete Meta campaign VERBATIM from a declarative JSON spec — no strategy generation, no copy authoring. Everything is created PAUSED, always; launching remains a separate deliberate call. Recommended flow: send with dry_run=true first to get the fully resolved Graph API payloads without creating anything, review them, then re-send without dry_run to build. Validation failures return per-field errors (path + message). Spec shape: campaign {name, objective OUTCOME_LEADS|OUTCOME_SALES, budget {type CBO_DAILY, amount, bid_strategy HIGHEST_VOLUME|LOWEST_COST_WITHOUT_CAP|COST_CAP}, special_ad_categories}, ad_sets [{name, conversion_location WEBSITE, pixel_id, optimisation_event {type CUSTOM_CONVERSION, id}|{type STANDARD, event}, performance_goal, attribution {click_days, view_days}, targeting {geo, age_min, advantage_audience, excluded_custom_audiences}, placements {mode MANUAL, exclude}}], ads [{name, asset {type IMAGE_SET, refs {1x1,4x5,9x16 — https URLs or uploaded image hashes}}|{type VIDEO, ref — pre-uploaded Meta video id}, primary_text, headline, description, cta, final_url, ad_set_name?}]. Use zuckerbot_list_custom_conversions to find custom conversion ids.", {
|
|
1855
|
+
business_id: z.string().optional().describe("Optional business ID override for the authenticated API key"),
|
|
1856
|
+
spec: z.record(z.string(), z.any()).describe("The declarative campaign spec (see tool description for the shape)"),
|
|
1857
|
+
dry_run: z.boolean().optional().describe("true = return the resolved Graph payloads without creating anything. Strongly recommended before a real build"),
|
|
1858
|
+
}, async ({ business_id, spec, dry_run }) => {
|
|
1859
|
+
try {
|
|
1860
|
+
const resolvedBusinessId = await client.resolveBusinessId(business_id);
|
|
1861
|
+
const body = { business_id: resolvedBusinessId, spec };
|
|
1862
|
+
if (dry_run !== undefined)
|
|
1863
|
+
body.dry_run = dry_run;
|
|
1864
|
+
const result = await client.post("/campaigns/from-spec", body);
|
|
1865
|
+
return formatResult(appendHint(result, "If this was a dry run, review would_create then re-send without dry_run. If it built: the campaign is PAUSED and spends nothing — inspect it in Ads Manager, then launch deliberately when ready."));
|
|
1866
|
+
}
|
|
1867
|
+
catch (err) {
|
|
1868
|
+
return formatError(err);
|
|
1869
|
+
}
|
|
1870
|
+
});
|
|
1871
|
+
// ── 27. Custom Conversions — List ──────────────────────────────
|
|
1872
|
+
server.tool("zuckerbot_list_custom_conversions", "List the custom conversions on the connected ad account: id, name, rule, source event and pixel. Use this to find the custom conversion id a campaign spec's optimisation_event should reference.", {
|
|
1873
|
+
business_id: z.string().optional().describe("Optional business ID override for the authenticated API key"),
|
|
1874
|
+
}, async ({ business_id }) => {
|
|
1875
|
+
try {
|
|
1876
|
+
const resolvedBusinessId = await client.resolveBusinessId(business_id);
|
|
1877
|
+
const params = new URLSearchParams({ business_id: resolvedBusinessId });
|
|
1878
|
+
const result = await client.get(`/custom-conversions?${params.toString()}`);
|
|
1879
|
+
return formatResult(appendHint(result, "Reference a conversion by id in a spec's optimisation_event: { type: 'CUSTOM_CONVERSION', id }. To create one, call zuckerbot_create_custom_conversion."));
|
|
1880
|
+
}
|
|
1881
|
+
catch (err) {
|
|
1882
|
+
return formatError(err);
|
|
1883
|
+
}
|
|
1884
|
+
});
|
|
1885
|
+
// ── 28. Custom Conversions — Create ────────────────────────────
|
|
1886
|
+
server.tool("zuckerbot_create_custom_conversion", "Create a custom conversion on the connected ad account (requires sufficient Graph permissions on the Meta token — returns insufficient_permission if Meta refuses). Provide the pixel, a name, optionally a rule (e.g. URL contains ...) and the source event type.", {
|
|
1887
|
+
business_id: z.string().optional().describe("Optional business ID override for the authenticated API key"),
|
|
1888
|
+
name: z.string().describe("Display name for the custom conversion"),
|
|
1889
|
+
pixel_id: z.string().describe("Pixel (event source) the conversion is based on"),
|
|
1890
|
+
custom_event_type: z.string().optional().describe("Source event type, defaults to OTHER"),
|
|
1891
|
+
rule: z.record(z.string(), z.any()).optional().describe("Optional Meta rule object, e.g. {\"url\":{\"i_contains\":\"/thank-you\"}}"),
|
|
1892
|
+
}, async ({ business_id, name, pixel_id, custom_event_type, rule }) => {
|
|
1893
|
+
try {
|
|
1894
|
+
const resolvedBusinessId = await client.resolveBusinessId(business_id);
|
|
1895
|
+
const body = { business_id: resolvedBusinessId, name, pixel_id };
|
|
1896
|
+
if (custom_event_type)
|
|
1897
|
+
body.custom_event_type = custom_event_type;
|
|
1898
|
+
if (rule)
|
|
1899
|
+
body.rule = rule;
|
|
1900
|
+
const result = await client.post("/custom-conversions", body);
|
|
1901
|
+
return formatResult(appendHint(result, "Custom conversion created. Reference it in a campaign spec as optimisation_event { type: 'CUSTOM_CONVERSION', id }."));
|
|
1902
|
+
}
|
|
1903
|
+
catch (err) {
|
|
1904
|
+
return formatError(err);
|
|
1905
|
+
}
|
|
1906
|
+
});
|
|
1797
1907
|
}
|
|
1798
1908
|
//# sourceMappingURL=tools.js.map
|