@apex-inc/mcp-server 0.16.0 → 0.18.0
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/dist/resources.d.ts +13 -0
- package/dist/resources.d.ts.map +1 -1
- package/dist/resources.js +21 -0
- package/dist/resources.js.map +1 -1
- package/dist/tools.d.ts +250 -11
- package/dist/tools.d.ts.map +1 -1
- package/dist/tools.js +442 -54
- package/dist/tools.js.map +1 -1
- package/package.json +1 -1
- package/skills/apex-adoption/SKILL.md +47 -0
- package/skills/apex-experimentation/SKILL.md +1 -1
- package/skills/apex-growth-intelligence/SKILL.md +2 -2
- package/skills/apex-integration-cookbook/SKILL.md +9 -2
- package/skills/apex-spec/SKILL.md +1 -1
package/dist/tools.js
CHANGED
|
@@ -53,6 +53,24 @@ function titleCaseEvent(event) {
|
|
|
53
53
|
.map((w) => w.charAt(0).toUpperCase() + w.slice(1))
|
|
54
54
|
.join(" ");
|
|
55
55
|
}
|
|
56
|
+
/**
|
|
57
|
+
* Query string for the pack report windows: relative (`days` / `months`)
|
|
58
|
+
* or explicit `start` + `end` (YYYY-MM-DD, both required together —
|
|
59
|
+
* explicit bounds win server-side).
|
|
60
|
+
*/
|
|
61
|
+
function windowQuery(opts) {
|
|
62
|
+
const params = new URLSearchParams();
|
|
63
|
+
if (opts.days)
|
|
64
|
+
params.set("days", String(opts.days));
|
|
65
|
+
if (opts.months)
|
|
66
|
+
params.set("months", String(opts.months));
|
|
67
|
+
if (opts.start && opts.end) {
|
|
68
|
+
params.set("start", opts.start);
|
|
69
|
+
params.set("end", opts.end);
|
|
70
|
+
}
|
|
71
|
+
const qs = params.toString();
|
|
72
|
+
return qs ? `?${qs}` : "";
|
|
73
|
+
}
|
|
56
74
|
function errMsg(err) {
|
|
57
75
|
return err instanceof Error ? err.message : String(err);
|
|
58
76
|
}
|
|
@@ -437,15 +455,23 @@ export const toolDefinitions = {
|
|
|
437
455
|
guardrailType: z.enum(["rate", "revenue", "duration"]).optional().describe("How the guardrail is measured. 'rate' (default) = how often the harm event fires. 'revenue' = sum a value (e.g. refund $)."),
|
|
438
456
|
guardrailThreshold: z.number().optional().describe("Harm margin for the guardrail, RELATIVE (0.25 = flag a >25% rise). Defaults to 0.25."),
|
|
439
457
|
predictionMagnitude: z.string().optional().describe("Your committed prediction of the effect, e.g. '10% lift'. Recorded with 'agent' provenance and used for calibration. Required before the experiment can be ACTIVATED."),
|
|
440
|
-
mode: z.enum(["sdk", "snippet"]).optional().describe("Experiment mode: 'sdk' for code-level (default via MCP), 'snippet' for runtime DOM"),
|
|
441
458
|
surface: z.enum(["web", "mobile"]).optional().describe("The PROPERTY the experiment runs in (not the SDK used): 'web' for a website, 'mobile' for a native / Capacitor / React-Native app. OPTIONAL — when omitted it is auto-detected from repo signals (capacitor.config.* / @apex-inc/capacitor-plugin → mobile) and the workspace's registered data sources; pass it explicitly only to override or when both web and mobile sources exist. Sets the dashboard data-source label (Website vs Mobile app)."),
|
|
442
459
|
dataSourceId: z.string().optional().describe("Optional explicit Data Source (property) id this experiment runs in, e.g. 'ds_website' or 'ds_ios'. Usually inferred from surface; pass it to bind to a specific registered source. Tenant-validated server-side."),
|
|
443
460
|
randomizationUnit: z.enum(["device", "visitor", "person", "account", "org"]).optional().describe("What to bucket on. Default 'visitor' (per-device). Use 'person' to give one stitched user the same variant across their devices, or 'account'/'org' for B2B account-level tests where everyone in an account should see the same variant."),
|
|
461
|
+
minSamplePerArm: z.number().int().min(30).optional().describe("Minimum exposed subjects PER ARM before a winner can be claimed (the sample gate). Defaults to 50. Raise it to demand more evidence (detect a smaller lift); lower it (floor 30) for low-traffic properties that would otherwise stall. The real winner-gate is still the 95% probability-to-be-best — this only sets the traffic floor. Pre-registration: frozen once the experiment is running."),
|
|
462
|
+
runtimeDays: z.number().int().min(1).max(365).optional().describe("How long to run the experiment, in days (the decision window). Apex force-concludes at this point — decisive winner if reached, else inconclusive. Defaults to 14. Set e.g. 28 to run for four weeks. This is the max runtime, not a minimum: a decisive winner can conclude sooner. Pre-registration: frozen once the experiment is running (fork to change it)."),
|
|
444
463
|
preview: z.boolean().optional().describe("If true, returns a preview without creating. Default: true."),
|
|
445
464
|
}),
|
|
446
465
|
handler: async (args) => {
|
|
447
466
|
const split = args.trafficSplit ?? 50;
|
|
448
|
-
|
|
467
|
+
// MCP experiments are ALWAYS code-native (SDK). Client-side snippet /
|
|
468
|
+
// runtime-DOM experiments are intentionally NOT offered here (founder
|
|
469
|
+
// decision 2026-07-05): the MCP audience is developers in an IDE with the
|
|
470
|
+
// code open, where `useApexVariant` is more reliable (no flicker, no
|
|
471
|
+
// fragile selectors, deterministic exposure) and plugs into the
|
|
472
|
+
// graduation pipeline. Snippet experiments live in the Apex web app's
|
|
473
|
+
// visual editor for the no-code / non-repo case.
|
|
474
|
+
const mode = "sdk";
|
|
449
475
|
// Surface auto-detect (QA 2026-06-18): when the agent doesn't pass a
|
|
450
476
|
// surface, infer it from repo signals + registered data sources instead of
|
|
451
477
|
// silently defaulting to web (which mislabels mobile apps). Ambiguous →
|
|
@@ -464,16 +490,23 @@ export const toolDefinitions = {
|
|
|
464
490
|
}
|
|
465
491
|
const experimentSurface = surfaceResolution.surface;
|
|
466
492
|
const isPreview = args.preview !== false;
|
|
467
|
-
// p1-metric — resolve the primary metric
|
|
468
|
-
//
|
|
469
|
-
//
|
|
493
|
+
// p1-metric — resolve the primary metric (the conversion goal).
|
|
494
|
+
//
|
|
495
|
+
// Founder decision 2026-07-05: DO NOT silently default the conversion
|
|
496
|
+
// event to `form_submit`. A guessed goal ("Signups" that secretly counts
|
|
497
|
+
// form submits) is worse than none. When the caller doesn't name a
|
|
498
|
+
// conversion event, the draft is created UNWIRED (no `source`): it's a
|
|
499
|
+
// valid draft, but the activation gate blocks starting it until a real
|
|
500
|
+
// outcome event is set — and we nudge loudly to set one. An explicit
|
|
501
|
+
// primaryMetricEvent is validated against the workspace event spec.
|
|
502
|
+
const conversionEventSet = Boolean(args.primaryMetricEvent);
|
|
470
503
|
let primaryMetric = {
|
|
471
|
-
key: "
|
|
472
|
-
label: "Conversion
|
|
504
|
+
key: "conversion_goal",
|
|
505
|
+
label: "Conversion goal (not set)",
|
|
473
506
|
type: "rate",
|
|
474
507
|
unit: "%",
|
|
475
508
|
direction: "increase",
|
|
476
|
-
source
|
|
509
|
+
// No `source` — UNWIRED on purpose. Can't start until set.
|
|
477
510
|
};
|
|
478
511
|
if (args.primaryMetricEvent) {
|
|
479
512
|
const event = args.primaryMetricEvent.trim();
|
|
@@ -487,7 +520,7 @@ export const toolDefinitions = {
|
|
|
487
520
|
return {
|
|
488
521
|
content: [{
|
|
489
522
|
type: "text",
|
|
490
|
-
text: `Could not load the event spec to validate primaryMetricEvent "${event}" (${message}). Fix the connection or omit primaryMetricEvent to
|
|
523
|
+
text: `Could not load the event spec to validate primaryMetricEvent "${event}" (${message}). Fix the connection, or omit primaryMetricEvent to create the draft without a conversion goal (you'll set it before starting).`,
|
|
491
524
|
}],
|
|
492
525
|
isError: true,
|
|
493
526
|
};
|
|
@@ -527,7 +560,7 @@ export const toolDefinitions = {
|
|
|
527
560
|
target_url: args.targetUrl,
|
|
528
561
|
target_anchor: args.targetAnchor,
|
|
529
562
|
data_source_id: args.dataSourceId,
|
|
530
|
-
primary_metric_event: primaryMetric.source
|
|
563
|
+
primary_metric_event: primaryMetric.source?.eventType,
|
|
531
564
|
});
|
|
532
565
|
if (cc.data.sameMetric.length || cc.data.differentMetric.length) {
|
|
533
566
|
conflictAdvisory = renderConflictAdvisory(cc.data);
|
|
@@ -546,14 +579,23 @@ export const toolDefinitions = {
|
|
|
546
579
|
targetComponent: args.targetComponent || null,
|
|
547
580
|
mode,
|
|
548
581
|
trafficSplit: { control: split, variant: 100 - split },
|
|
582
|
+
decisionWindowDays: args.runtimeDays ?? 14,
|
|
583
|
+
minSamplePerArm: args.minSamplePerArm ?? 50,
|
|
549
584
|
control: args.controlContent,
|
|
550
585
|
variant: args.variantContent,
|
|
551
|
-
|
|
586
|
+
conversionGoal: conversionEventSet
|
|
587
|
+
? { event: primaryMetric.source?.eventType, type: primaryMetric.type }
|
|
588
|
+
: "NOT SET — required before the experiment can start",
|
|
552
589
|
beliefId: args.beliefId || null,
|
|
553
590
|
beliefStatement: args.beliefStatement || null,
|
|
554
591
|
predictionId: args.predictionId || null,
|
|
555
592
|
...(conflictAdvisory ? { _conflict_advisory: conflictAdvisory } : {}),
|
|
556
|
-
|
|
593
|
+
...(conversionEventSet
|
|
594
|
+
? {}
|
|
595
|
+
: {
|
|
596
|
+
_conversion_goal_warning: "No conversion event set. The draft will be created, but it CANNOT be started until you set the outcome you're optimizing. Call list_events to see what's firing (prefer a real outcome like signup/purchase over form_submit), then pass primaryMetricEvent. Confirm the goal with the user before creating.",
|
|
597
|
+
}),
|
|
598
|
+
_instructions: `Before proposing variant copy, ground every factual claim in .apex/brand-truth.md (read it; cite the line each claim traces to; if a claim isn't backed there, ask the user instead of inventing it).${conflictAdvisory ? " IMPORTANT: _conflict_advisory is set — show it to the user and resolve the overlap (mutex / variant / sequence) before creating." : ""}${conversionEventSet ? "" : " IMPORTANT: _conversion_goal_warning is set — surface it and set a conversion event (list_events → primaryMetricEvent) before creating, unless the user explicitly wants a draft to fill in later."} Then present this as a summary and ask the user to confirm (1), adjust (2), or cancel (3). Do NOT create the experiment until confirmed.`,
|
|
557
599
|
};
|
|
558
600
|
return { content: [{ type: "text", text: JSON.stringify(recipe, null, 2) }] };
|
|
559
601
|
}
|
|
@@ -651,6 +693,16 @@ export const toolDefinitions = {
|
|
|
651
693
|
status: "draft",
|
|
652
694
|
...(args.dataSourceId ? { dataSourceId: args.dataSourceId } : {}),
|
|
653
695
|
...(args.randomizationUnit ? { randomizationUnit: args.randomizationUnit } : {}),
|
|
696
|
+
// Sample-size floor (minPerVariant) and/or run duration (durationDays).
|
|
697
|
+
// The API merges evaluationWindow, so sending one never clobbers the other.
|
|
698
|
+
...(() => {
|
|
699
|
+
const ew = {};
|
|
700
|
+
if (args.minSamplePerArm)
|
|
701
|
+
ew.minPerVariant = args.minSamplePerArm;
|
|
702
|
+
if (args.runtimeDays)
|
|
703
|
+
ew.durationDays = args.runtimeDays;
|
|
704
|
+
return Object.keys(ew).length > 0 ? { evaluationWindow: ew } : {};
|
|
705
|
+
})(),
|
|
654
706
|
hypothesis: resolvedHypothesis,
|
|
655
707
|
// Agent-authored: tagged "agent" provenance so calibration attributes it
|
|
656
708
|
// correctly. When the agent supplies predictionMagnitude it satisfies the
|
|
@@ -692,7 +744,8 @@ export const toolDefinitions = {
|
|
|
692
744
|
],
|
|
693
745
|
}
|
|
694
746
|
: {}),
|
|
695
|
-
|
|
747
|
+
// VJATTR D1: 7-day exposure→conversion attribution window (was 24h).
|
|
748
|
+
attributionWindow: { unit: "days", value: 7, startFrom: "first_interaction" },
|
|
696
749
|
variants,
|
|
697
750
|
});
|
|
698
751
|
let previewUrl = "";
|
|
@@ -742,7 +795,9 @@ export const toolDefinitions = {
|
|
|
742
795
|
`7. Show the user the diff and ask them to preview at: ${previewUrl}`,
|
|
743
796
|
`8. After preview approval, commit and push`,
|
|
744
797
|
`9. Call track_deployment with the experiment ID and commit SHA`,
|
|
745
|
-
`10. Call
|
|
798
|
+
`10. Call activate_experiment (with the user's go-ahead). If the code isn't live yet, the`,
|
|
799
|
+
` experiment holds in "Waiting on deploy" and starts automatically at the first exposure —`,
|
|
800
|
+
` verify with verify_experiment_wiring after the deploy lands.`,
|
|
746
801
|
];
|
|
747
802
|
const mobileSteps = [
|
|
748
803
|
"IMPLEMENT THE EXPERIMENT IN CODE (mobile / Capacitor):",
|
|
@@ -772,6 +827,18 @@ export const toolDefinitions = {
|
|
|
772
827
|
"- Before calling activate_experiment, tell the user the expected metered volume and get their explicit go-ahead.",
|
|
773
828
|
"- Activation also requires a measurable primary goal, a guardrail, and a committed prediction (the server enforces this). If you didn't pass predictionMagnitude, add one first.",
|
|
774
829
|
];
|
|
830
|
+
const conversionGoalNudge = conversionEventSet
|
|
831
|
+
? []
|
|
832
|
+
: [
|
|
833
|
+
"",
|
|
834
|
+
"⚠️ CONVERSION GOAL NOT SET — this draft cannot be started yet:",
|
|
835
|
+
"- No outcome event is wired, so there's nothing to measure a winner on.",
|
|
836
|
+
"- Pick the real outcome: call list_events to see what's firing in this",
|
|
837
|
+
" workspace, then update_experiment({ experiment_id, primary_metric_event }).",
|
|
838
|
+
"- Prefer a true outcome (signup / purchase / checkout_completed) over a",
|
|
839
|
+
" proxy like form_submit. Confirm the goal with the user.",
|
|
840
|
+
"- activate_experiment will refuse until a measurable goal is set.",
|
|
841
|
+
];
|
|
775
842
|
const recipe = {
|
|
776
843
|
_apex: true,
|
|
777
844
|
_type: "experiment_created",
|
|
@@ -780,7 +847,11 @@ export const toolDefinitions = {
|
|
|
780
847
|
surface: experimentSurface,
|
|
781
848
|
status: exp.status,
|
|
782
849
|
mode,
|
|
783
|
-
|
|
850
|
+
conversionGoal: conversionEventSet
|
|
851
|
+
? { event: primaryMetric.source?.eventType, type: primaryMetric.type }
|
|
852
|
+
: "NOT SET — required before start (see _instructions)",
|
|
853
|
+
decisionWindowDays: args.runtimeDays ?? 14,
|
|
854
|
+
minSamplePerArm: args.minSamplePerArm ?? 50,
|
|
784
855
|
beliefId: beliefId || null,
|
|
785
856
|
hypothesisId: hypothesisId || null,
|
|
786
857
|
predictionId: args.predictionId || null,
|
|
@@ -804,20 +875,13 @@ export const toolDefinitions = {
|
|
|
804
875
|
} : null,
|
|
805
876
|
previewUrl,
|
|
806
877
|
trafficSplit: { control: split, variant: 100 - split },
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
.concat([
|
|
815
|
-
"SNIPPET MODE — no code changes needed.",
|
|
816
|
-
"The experiment will be applied via the Apex snippet at runtime.",
|
|
817
|
-
`Preview: ${previewUrl}`,
|
|
818
|
-
])
|
|
819
|
-
.concat(activationPolicy)
|
|
820
|
-
.join("\n"),
|
|
878
|
+
// Always code-native (SDK) — see the `mode` note above.
|
|
879
|
+
_instructions: groundingGuidance
|
|
880
|
+
.concat(isMobile ? mobileSteps : webSteps)
|
|
881
|
+
.concat(styleGuidance)
|
|
882
|
+
.concat(activationPolicy)
|
|
883
|
+
.concat(conversionGoalNudge)
|
|
884
|
+
.join("\n"),
|
|
821
885
|
};
|
|
822
886
|
return { content: [{ type: "text", text: JSON.stringify(recipe, null, 2) }] };
|
|
823
887
|
},
|
|
@@ -871,6 +935,38 @@ export const toolDefinitions = {
|
|
|
871
935
|
};
|
|
872
936
|
},
|
|
873
937
|
},
|
|
938
|
+
recapture_experiment_screenshots: {
|
|
939
|
+
description: `${APEX} — Re-run the hosted screenshot capture for a WEB experiment's arms (renders the target URL for control + variant and stores the images shown on the dashboard). Use when screenshots are missing or stale — e.g. the page changed, or an earlier capture failed. Web experiments only (mobile arms capture on-device via the SDK).`,
|
|
940
|
+
schema: z.object({
|
|
941
|
+
experimentId: z.string().describe("The experiment ID to re-capture screenshots for."),
|
|
942
|
+
}),
|
|
943
|
+
handler: async ({ experimentId }) => {
|
|
944
|
+
try {
|
|
945
|
+
const res = await apiPost(`/api/experiments/${encodeURIComponent(experimentId)}/recapture`, {});
|
|
946
|
+
const { captured, errors } = res.data;
|
|
947
|
+
const lines = [
|
|
948
|
+
`${APEX} Re-captured ${captured} screenshot${captured === 1 ? "" : "s"} for ${experimentId}.`,
|
|
949
|
+
];
|
|
950
|
+
if (errors.length)
|
|
951
|
+
lines.push(` Arm(s) that failed: ${errors.join("; ")}`);
|
|
952
|
+
if (captured === 0) {
|
|
953
|
+
lines.push(` No images were produced — confirm the target URL is public and reachable.`);
|
|
954
|
+
}
|
|
955
|
+
return { content: [{ type: "text", text: lines.join("\n") }] };
|
|
956
|
+
}
|
|
957
|
+
catch (err) {
|
|
958
|
+
return {
|
|
959
|
+
content: [
|
|
960
|
+
{
|
|
961
|
+
type: "text",
|
|
962
|
+
text: `${APEX} ${err instanceof Error ? err.message : String(err)}`,
|
|
963
|
+
},
|
|
964
|
+
],
|
|
965
|
+
isError: true,
|
|
966
|
+
};
|
|
967
|
+
}
|
|
968
|
+
},
|
|
969
|
+
},
|
|
874
970
|
verify_experiment_wiring: {
|
|
875
971
|
description: `${APEX} — Verify an experiment is actually wired up before launch: check whether \`experiment_exposure\` events are arriving for BOTH arms. Use this before activate_experiment to avoid launching an experiment that silently collects no data. Returns a per-arm exposure breakdown and an actionable next step.`,
|
|
876
972
|
schema: z.object({
|
|
@@ -904,8 +1000,14 @@ export const toolDefinitions = {
|
|
|
904
1000
|
case "no_exposures":
|
|
905
1001
|
default:
|
|
906
1002
|
verdictLine = "NO EXPOSURES — no exposures recorded for any arm";
|
|
907
|
-
nextStep =
|
|
908
|
-
"No `experiment_exposure` events
|
|
1003
|
+
nextStep = wiring.sdkMode
|
|
1004
|
+
? "No `experiment_exposure` events have reached Apex yet. This experiment is " +
|
|
1005
|
+
"code-wired (SDK hook), so you can activate it NOW: activate_experiment will " +
|
|
1006
|
+
"hold it in `pending_deployment` (\"Waiting on deploy\") and it starts " +
|
|
1007
|
+
"automatically the moment the deployed `useApexVariant(\"" +
|
|
1008
|
+
wiring.experimentId +
|
|
1009
|
+
"\")` code records its first exposure. Ship the code, and the test starts itself."
|
|
1010
|
+
: "No `experiment_exposure` events are arriving. The variant-serving code " +
|
|
909
1011
|
"(useApexVariant / Apex.getVariant) probably isn't deployed or running yet, " +
|
|
910
1012
|
"so nothing is being assigned. Deploy the experiment code and confirm it runs " +
|
|
911
1013
|
"on the target surface, then re-run verify_experiment_wiring.";
|
|
@@ -940,7 +1042,7 @@ export const toolDefinitions = {
|
|
|
940
1042
|
},
|
|
941
1043
|
},
|
|
942
1044
|
activate_experiment: {
|
|
943
|
-
description: `${APEX} — Launch an experiment. Changes its status from draft to running. Traffic will be split immediately. Before flipping to running, this verifies BOTH arms are producing \`experiment_exposure\` events (wiring check) and
|
|
1045
|
+
description: `${APEX} — Launch an experiment. Changes its status from draft to running. Traffic will be split immediately. Before flipping to running, this verifies BOTH arms are producing \`experiment_exposure\` events (wiring check). Code-wired (SDK-hook) experiments may be activated before their code ships: the server holds them in \`pending_deployment\` ("Waiting on deploy") and starts on the first exposure. Snippet experiments are applied by the Apex snippet at runtime, so they also don't need pre-activation exposures — they launch and wiring is verified afterward. Only wired experiments that genuinely can't self-start are refused unless force is true. Ask the user to confirm before calling.`,
|
|
944
1046
|
schema: z.object({
|
|
945
1047
|
experimentId: z.string().describe("The experiment ID to activate"),
|
|
946
1048
|
force: z
|
|
@@ -959,7 +1061,17 @@ export const toolDefinitions = {
|
|
|
959
1061
|
catch (err) {
|
|
960
1062
|
wiringError = err instanceof Error ? err.message : String(err);
|
|
961
1063
|
}
|
|
962
|
-
|
|
1064
|
+
// Code-wired (SDK-hook) AND snippet-applied experiments skip the hard
|
|
1065
|
+
// refusal. SDK: the server holds them in `pending_deployment` and
|
|
1066
|
+
// auto-starts on first exposure. Snippet: the Apex snippet only applies
|
|
1067
|
+
// the experiment ONCE IT'S RUNNING, so it can't have pre-activation
|
|
1068
|
+
// exposures — blocking on them is a chicken-and-egg. Both are safe to
|
|
1069
|
+
// launch "early"; wiring is verified AFTER via verify_experiment_wiring.
|
|
1070
|
+
if (wiring &&
|
|
1071
|
+
!wiring.bothArmsLive &&
|
|
1072
|
+
force !== true &&
|
|
1073
|
+
!wiring.sdkMode &&
|
|
1074
|
+
!wiring.snippetMode) {
|
|
963
1075
|
const armLines = wiring.perArm.map(formatArmLine);
|
|
964
1076
|
const zeroArms = wiring.perArm
|
|
965
1077
|
.filter((a) => a.exposures === 0)
|
|
@@ -1005,7 +1117,56 @@ export const toolDefinitions = {
|
|
|
1005
1117
|
const exp = await apiPatch("/api/experiments", {
|
|
1006
1118
|
id: experimentId,
|
|
1007
1119
|
status: "running",
|
|
1120
|
+
// Deploy-wait override: the server holds an unwired code-wired
|
|
1121
|
+
// experiment in `pending_deployment` unless the caller forces.
|
|
1122
|
+
...(force === true ? { force: true } : {}),
|
|
1008
1123
|
});
|
|
1124
|
+
// Deploy-wait hold — the server accepted the start but is waiting for
|
|
1125
|
+
// the experiment code to go live. Report the held state precisely
|
|
1126
|
+
// (claiming "LIVE" here would repeat the 2026-07-04 confusion).
|
|
1127
|
+
if (exp.status === "pending_deployment") {
|
|
1128
|
+
const held = [
|
|
1129
|
+
`${APEX} Experiment Waiting on Deploy`,
|
|
1130
|
+
`${"═".repeat(40)}`,
|
|
1131
|
+
``,
|
|
1132
|
+
` "${exp.name}" is armed but NOT collecting yet.`,
|
|
1133
|
+
` ID: ${exp.id}`,
|
|
1134
|
+
` Status: pending_deployment ("Waiting on deploy")`,
|
|
1135
|
+
``,
|
|
1136
|
+
` Apex hasn't received any \`experiment_exposure\` events from this`,
|
|
1137
|
+
` experiment's code, so the start is on hold. Nothing is wrong —`,
|
|
1138
|
+
` the experiment starts AUTOMATICALLY the moment the deployed code`,
|
|
1139
|
+
` records its first exposure.`,
|
|
1140
|
+
``,
|
|
1141
|
+
` To go live:`,
|
|
1142
|
+
` 1. Render the variants through the SDK hook:`,
|
|
1143
|
+
` const variant = useApexVariant("${exp.id}");`,
|
|
1144
|
+
` 2. Deploy that code to the target page.`,
|
|
1145
|
+
` 3. Done — the first visitor flips it to running. Verify with`,
|
|
1146
|
+
` verify_experiment_wiring or the experiment page.`,
|
|
1147
|
+
``,
|
|
1148
|
+
` Certain the code is already live? Re-run activate_experiment`,
|
|
1149
|
+
` with force: true to start immediately.`,
|
|
1150
|
+
``,
|
|
1151
|
+
`${"═".repeat(40)}`,
|
|
1152
|
+
].join("\n");
|
|
1153
|
+
return {
|
|
1154
|
+
content: [
|
|
1155
|
+
{ type: "text", text: held },
|
|
1156
|
+
{
|
|
1157
|
+
type: "text",
|
|
1158
|
+
text: JSON.stringify({
|
|
1159
|
+
_apex: true,
|
|
1160
|
+
_type: "activation_held",
|
|
1161
|
+
reason: "code_not_live",
|
|
1162
|
+
experimentId: exp.id,
|
|
1163
|
+
status: exp.status,
|
|
1164
|
+
autoStartsOnFirstExposure: true,
|
|
1165
|
+
}, null, 2),
|
|
1166
|
+
},
|
|
1167
|
+
],
|
|
1168
|
+
};
|
|
1169
|
+
}
|
|
1009
1170
|
// PATCH returns a minimal/legacy body — re-read the unified record to
|
|
1010
1171
|
// derive the live traffic split from the allocation weights.
|
|
1011
1172
|
let controlPct = 50;
|
|
@@ -1023,6 +1184,9 @@ export const toolDefinitions = {
|
|
|
1023
1184
|
else if (wiring && !wiring.bothArmsLive && force === true) {
|
|
1024
1185
|
notes.push(` ⚠ Launched with force: true despite an incomplete wiring check`, ` (verdict: ${wiring.verdict}, ${wiring.armsLive}/${wiring.totalArms} arm(s) live).`, ` Some arm(s) may not be collecting data yet.`, ``);
|
|
1025
1186
|
}
|
|
1187
|
+
else if (wiring && !wiring.bothArmsLive && wiring.snippetMode) {
|
|
1188
|
+
notes.push(` ℹ Snippet experiment — the Apex snippet applies it to visitors now`, ` that it's running, so exposures start flowing on the next visits.`, ` Confirm both arms record exposures shortly with`, ` verify_experiment_wiring (a mismatched selector would silently`, ` collect nothing).`, ``);
|
|
1189
|
+
}
|
|
1026
1190
|
// Author-time conflict guard (advisory): if this now-running experiment
|
|
1027
1191
|
// overlaps another live one on the same surface, surface the fix.
|
|
1028
1192
|
try {
|
|
@@ -1157,7 +1321,7 @@ export const toolDefinitions = {
|
|
|
1157
1321
|
},
|
|
1158
1322
|
},
|
|
1159
1323
|
archive_experiment: {
|
|
1160
|
-
description: `${APEX} — Archive an experiment. Removes it from active views but preserves all data, learnings, and belief graph connections. Archived experiments still contribute to intelligence scoring and recommendations. Ask the user to confirm before calling.`,
|
|
1324
|
+
description: `${APEX} — Archive an experiment. Removes it from active views but preserves all data, learnings, and belief graph connections. Archived experiments still contribute to intelligence scoring and recommendations. A RUNNING experiment cannot be archived — end it first (conclude_experiment / promote_winner) so its result + belief are captured; the server returns 409 archive_requires_conclusion otherwise. Ask the user to confirm before calling.`,
|
|
1161
1325
|
schema: z.object({
|
|
1162
1326
|
experimentId: z.string().describe("The experiment ID to archive"),
|
|
1163
1327
|
}),
|
|
@@ -1341,6 +1505,64 @@ export const toolDefinitions = {
|
|
|
1341
1505
|
};
|
|
1342
1506
|
},
|
|
1343
1507
|
},
|
|
1508
|
+
list_events: {
|
|
1509
|
+
description: `${APEX} — List the workspace's ACTIVE events: the canonical event names actually firing, with total volume and the platforms they arrive on, from the ingest catalog. Use this to discover which events exist / are wired before choosing a primary metric or guardrail in create_experiment, or to confirm an event is live. Distinct from list_metrics (which lists typed rate/revenue/duration GOALS) — this is the raw observed-event catalog.`,
|
|
1510
|
+
schema: z.object({
|
|
1511
|
+
limit: z
|
|
1512
|
+
.number()
|
|
1513
|
+
.int()
|
|
1514
|
+
.min(1)
|
|
1515
|
+
.max(200)
|
|
1516
|
+
.optional()
|
|
1517
|
+
.describe("Max events to return, by volume descending (default 50)."),
|
|
1518
|
+
}),
|
|
1519
|
+
handler: async (args) => {
|
|
1520
|
+
const catalog = await apiGet("/api/events/catalog");
|
|
1521
|
+
const rows = catalog?.events ?? [];
|
|
1522
|
+
// Aggregate per event name across platforms (the catalog is keyed by
|
|
1523
|
+
// (eventName, platform); the agent wants one row per event).
|
|
1524
|
+
const byName = new Map();
|
|
1525
|
+
for (const r of rows) {
|
|
1526
|
+
const e = byName.get(r.eventName) ?? { count: 0, platforms: new Set() };
|
|
1527
|
+
e.count += r.count ?? 0;
|
|
1528
|
+
if (r.platform)
|
|
1529
|
+
e.platforms.add(r.platform);
|
|
1530
|
+
byName.set(r.eventName, e);
|
|
1531
|
+
}
|
|
1532
|
+
const events = [...byName.entries()]
|
|
1533
|
+
.map(([name, v]) => ({
|
|
1534
|
+
event: name,
|
|
1535
|
+
count: v.count,
|
|
1536
|
+
platforms: [...v.platforms].sort(),
|
|
1537
|
+
}))
|
|
1538
|
+
.sort((a, b) => b.count - a.count)
|
|
1539
|
+
.slice(0, args.limit ?? 50);
|
|
1540
|
+
if (events.length === 0) {
|
|
1541
|
+
return {
|
|
1542
|
+
content: [
|
|
1543
|
+
{
|
|
1544
|
+
type: "text",
|
|
1545
|
+
text: `${APEX} No active events yet — nothing has been tracked in this workspace. Install the snippet / SDK and fire events, then re-check.`,
|
|
1546
|
+
},
|
|
1547
|
+
],
|
|
1548
|
+
};
|
|
1549
|
+
}
|
|
1550
|
+
const lines = events.map((e) => ` • ${e.event} — ${e.count.toLocaleString()} events (${e.platforms.join(", ") || "web"})`);
|
|
1551
|
+
return {
|
|
1552
|
+
content: [
|
|
1553
|
+
{
|
|
1554
|
+
type: "text",
|
|
1555
|
+
text: [
|
|
1556
|
+
`${APEX} Active events (${events.length}, by volume):`,
|
|
1557
|
+
...lines,
|
|
1558
|
+
``,
|
|
1559
|
+
`Pick a measurable OUTCOME event (e.g. purchase/signup) as primaryMetricEvent in create_experiment. Use list_metrics to see which are typed goals.`,
|
|
1560
|
+
].join("\n"),
|
|
1561
|
+
},
|
|
1562
|
+
],
|
|
1563
|
+
};
|
|
1564
|
+
},
|
|
1565
|
+
},
|
|
1344
1566
|
list_metrics: {
|
|
1345
1567
|
description: `${APEX} — List the canonical metric/goal catalog for this workspace: typed metrics (rate/revenue/duration) with whether each is DETECTED (firing, with volume) and its outcome tier. Use this to pick a measurable OUTCOME goal (purchase/signup) over a proxy (email_open) before create_experiment, or role:'guardrail' to see protective harm signals.`,
|
|
1346
1568
|
schema: z.object({
|
|
@@ -1403,6 +1625,8 @@ export const toolDefinitions = {
|
|
|
1403
1625
|
primaryMetricEvent: z.string().optional().describe("Change the primary metric's canonical event — draft only."),
|
|
1404
1626
|
primaryMetricType: z.enum(["rate", "count", "revenue", "duration"]).optional(),
|
|
1405
1627
|
guardrailEvent: z.string().optional().describe("Replace the guardrail with this canonical protective event — draft only."),
|
|
1628
|
+
minSamplePerArm: z.number().int().min(30).optional().describe("Set the minimum exposed subjects PER ARM before a winner can be claimed (the sample gate; floor 30). Draft only — it's part of the pre-registration and freezes at launch (a running experiment returns experiment_locked; fork to change it)."),
|
|
1629
|
+
runtimeDays: z.number().int().min(1).max(365).optional().describe("Set how long to run the experiment, in days (the decision window Apex force-concludes at). Draft only — part of the pre-registration; freezes at launch (fork to change it)."),
|
|
1406
1630
|
}),
|
|
1407
1631
|
handler: async (args) => {
|
|
1408
1632
|
const body = { id: args.experimentId };
|
|
@@ -1412,6 +1636,16 @@ export const toolDefinitions = {
|
|
|
1412
1636
|
body.surface = args.surface;
|
|
1413
1637
|
if (args.hypothesis !== undefined)
|
|
1414
1638
|
body.hypothesis = args.hypothesis;
|
|
1639
|
+
// evaluationWindow patch — the API merges it, so setting one of
|
|
1640
|
+
// minPerVariant / durationDays never clobbers the other.
|
|
1641
|
+
if (args.minSamplePerArm !== undefined || args.runtimeDays !== undefined) {
|
|
1642
|
+
const ew = {};
|
|
1643
|
+
if (args.minSamplePerArm !== undefined)
|
|
1644
|
+
ew.minPerVariant = args.minSamplePerArm;
|
|
1645
|
+
if (args.runtimeDays !== undefined)
|
|
1646
|
+
ew.durationDays = args.runtimeDays;
|
|
1647
|
+
body.evaluationWindow = ew;
|
|
1648
|
+
}
|
|
1415
1649
|
if (args.primaryMetricEvent) {
|
|
1416
1650
|
const t = args.primaryMetricType ?? "rate";
|
|
1417
1651
|
const isValue = t === "revenue" || t === "duration";
|
|
@@ -1670,9 +1904,17 @@ export const toolDefinitions = {
|
|
|
1670
1904
|
.number()
|
|
1671
1905
|
.optional()
|
|
1672
1906
|
.describe("Months of waterfall history to include (2-36, default 13)."),
|
|
1907
|
+
start: z
|
|
1908
|
+
.string()
|
|
1909
|
+
.optional()
|
|
1910
|
+
.describe("Explicit window start, YYYY-MM-DD (mapped to calendar months; use with end — overrides months)."),
|
|
1911
|
+
end: z
|
|
1912
|
+
.string()
|
|
1913
|
+
.optional()
|
|
1914
|
+
.describe("Explicit window end, YYYY-MM-DD (use with start)."),
|
|
1673
1915
|
}),
|
|
1674
|
-
handler: async ({ months }) => {
|
|
1675
|
-
const q = months
|
|
1916
|
+
handler: async ({ months, start, end }) => {
|
|
1917
|
+
const q = windowQuery({ months, start, end });
|
|
1676
1918
|
const res = await apiGet(`/api/saas/recurring-revenue${q}`);
|
|
1677
1919
|
const d = res.data;
|
|
1678
1920
|
if (!d?.hasData) {
|
|
@@ -1708,9 +1950,17 @@ export const toolDefinitions = {
|
|
|
1708
1950
|
.number()
|
|
1709
1951
|
.optional()
|
|
1710
1952
|
.describe("Months of series history to include (2-36, default 13)."),
|
|
1953
|
+
start: z
|
|
1954
|
+
.string()
|
|
1955
|
+
.optional()
|
|
1956
|
+
.describe("Explicit window start, YYYY-MM-DD (mapped to calendar months; use with end — overrides months)."),
|
|
1957
|
+
end: z
|
|
1958
|
+
.string()
|
|
1959
|
+
.optional()
|
|
1960
|
+
.describe("Explicit window end, YYYY-MM-DD (use with start)."),
|
|
1711
1961
|
}),
|
|
1712
|
-
handler: async ({ months }) => {
|
|
1713
|
-
const q = months
|
|
1962
|
+
handler: async ({ months, start, end }) => {
|
|
1963
|
+
const q = windowQuery({ months, start, end });
|
|
1714
1964
|
const res = await apiGet(`/api/saas/expansion${q}`);
|
|
1715
1965
|
const d = res.data;
|
|
1716
1966
|
if (!d?.hasData) {
|
|
@@ -1743,14 +1993,22 @@ export const toolDefinitions = {
|
|
|
1743
1993
|
};
|
|
1744
1994
|
},
|
|
1745
1995
|
},
|
|
1746
|
-
|
|
1747
|
-
description: `${APEX} —
|
|
1996
|
+
get_ecommerce_product_sales: {
|
|
1997
|
+
description: `${APEX} — E-commerce product sales: top products by revenue with the per-product view → add-to-cart → purchase funnel and refund counts. Products need a product_id on commerce events; never-wired signals are reported as unwired, not zero.`,
|
|
1748
1998
|
schema: z.object({
|
|
1749
1999
|
days: z.number().optional().describe("Trailing window in days (default 90, max 180)."),
|
|
2000
|
+
start: z
|
|
2001
|
+
.string()
|
|
2002
|
+
.optional()
|
|
2003
|
+
.describe("Explicit window start, YYYY-MM-DD (use with end — overrides days)."),
|
|
2004
|
+
end: z
|
|
2005
|
+
.string()
|
|
2006
|
+
.optional()
|
|
2007
|
+
.describe("Explicit window end, YYYY-MM-DD (use with start)."),
|
|
1750
2008
|
}),
|
|
1751
|
-
handler: async ({ days }) => {
|
|
1752
|
-
const q = days
|
|
1753
|
-
const res = await apiGet(`/api/
|
|
2009
|
+
handler: async ({ days, start, end }) => {
|
|
2010
|
+
const q = windowQuery({ days, start, end });
|
|
2011
|
+
const res = await apiGet(`/api/ecommerce/product-sales${q}`);
|
|
1754
2012
|
const d = res.data;
|
|
1755
2013
|
if (!d?.hasData) {
|
|
1756
2014
|
return {
|
|
@@ -1764,7 +2022,7 @@ export const toolDefinitions = {
|
|
|
1764
2022
|
}
|
|
1765
2023
|
const money = (n) => `$${Math.round(n).toLocaleString()}`;
|
|
1766
2024
|
const lines = [
|
|
1767
|
-
`${APEX}
|
|
2025
|
+
`${APEX} Product sales — ${money(d.totals.revenue)} product revenue, ${d.totals.purchases} purchases`,
|
|
1768
2026
|
"═".repeat(40),
|
|
1769
2027
|
...d.items
|
|
1770
2028
|
.slice(0, 10)
|
|
@@ -1775,14 +2033,22 @@ export const toolDefinitions = {
|
|
|
1775
2033
|
return { content: [{ type: "text", text: lines.join("\n") }] };
|
|
1776
2034
|
},
|
|
1777
2035
|
},
|
|
1778
|
-
|
|
1779
|
-
description: `${APEX} —
|
|
2036
|
+
get_ecommerce_returns: {
|
|
2037
|
+
description: `${APEX} — E-commerce returns & refunds: refund rate (money back, order basis), return rate (goods back — separate metric, never summed), reason breakdown, refund cycle time, AOV, and repeat purchase. Refund data lands automatically from Stripe; return_requested / return_completed events power the goods-back story.`,
|
|
1780
2038
|
schema: z.object({
|
|
1781
2039
|
days: z.number().optional().describe("Trailing window in days (default 90, max 180)."),
|
|
2040
|
+
start: z
|
|
2041
|
+
.string()
|
|
2042
|
+
.optional()
|
|
2043
|
+
.describe("Explicit window start, YYYY-MM-DD (use with end — overrides days)."),
|
|
2044
|
+
end: z
|
|
2045
|
+
.string()
|
|
2046
|
+
.optional()
|
|
2047
|
+
.describe("Explicit window end, YYYY-MM-DD (use with start)."),
|
|
1782
2048
|
}),
|
|
1783
|
-
handler: async ({ days }) => {
|
|
1784
|
-
const q = days
|
|
1785
|
-
const res = await apiGet(`/api/
|
|
2049
|
+
handler: async ({ days, start, end }) => {
|
|
2050
|
+
const q = windowQuery({ days, start, end });
|
|
2051
|
+
const res = await apiGet(`/api/ecommerce/returns${q}`);
|
|
1786
2052
|
const d = res.data;
|
|
1787
2053
|
if (!d?.hasData) {
|
|
1788
2054
|
return {
|
|
@@ -1811,14 +2077,26 @@ export const toolDefinitions = {
|
|
|
1811
2077
|
description: `${APEX} — Marketplace health across six pillars: liquidity (search→transaction, match rate, zero-result searches, unfulfilled demand), supply & demand balance, economics (GMV vs the take — fee_amount is the platform's revenue, GMV never inflates LTV), buyer retention, trust rates, and concentration risk. Sliceable by category / geo / price_band (low-volume slices are suppressed for privacy). Metrics whose events aren't wired come back as unwired hints, never fabricated zeros.`,
|
|
1812
2078
|
schema: z.object({
|
|
1813
2079
|
days: z.number().optional().describe("Trailing window in days (default 90, max 180)."),
|
|
2080
|
+
start: z
|
|
2081
|
+
.string()
|
|
2082
|
+
.optional()
|
|
2083
|
+
.describe("Explicit window start, YYYY-MM-DD (use with end — overrides days)."),
|
|
2084
|
+
end: z
|
|
2085
|
+
.string()
|
|
2086
|
+
.optional()
|
|
2087
|
+
.describe("Explicit window end, YYYY-MM-DD (use with start)."),
|
|
1814
2088
|
category: z.string().optional().describe("Slice: listing category."),
|
|
1815
2089
|
geo: z.string().optional().describe("Slice: transaction geo."),
|
|
1816
2090
|
priceBand: z.string().optional().describe("Slice: price band (e.g. '100-250')."),
|
|
1817
2091
|
}),
|
|
1818
|
-
handler: async ({ days, category, geo, priceBand, }) => {
|
|
2092
|
+
handler: async ({ days, start, end, category, geo, priceBand, }) => {
|
|
1819
2093
|
const params = new URLSearchParams();
|
|
1820
2094
|
if (days)
|
|
1821
2095
|
params.set("days", String(days));
|
|
2096
|
+
if (start && end) {
|
|
2097
|
+
params.set("start", start);
|
|
2098
|
+
params.set("end", end);
|
|
2099
|
+
}
|
|
1822
2100
|
if (category)
|
|
1823
2101
|
params.set("category", category);
|
|
1824
2102
|
if (geo)
|
|
@@ -3445,20 +3723,31 @@ Events fired through this tool carry a stable synthetic visitorId (mcp-agent-*,
|
|
|
3445
3723
|
}),
|
|
3446
3724
|
handler: async ({ journeyId, confirmLive }) => {
|
|
3447
3725
|
const path = `/api/journeys/${encodeURIComponent(journeyId)}/publish`;
|
|
3726
|
+
// Non-blocking coverage warnings ("we couldn't verify these until the
|
|
3727
|
+
// journey fires") — surfaced so an agent can preview before publishing.
|
|
3728
|
+
const warnText = (warnings) => {
|
|
3729
|
+
const list = Array.isArray(warnings) ? warnings.filter((w) => w?.reason) : [];
|
|
3730
|
+
if (list.length === 0)
|
|
3731
|
+
return "";
|
|
3732
|
+
return (`\n\n${list.length} thing${list.length === 1 ? "" : "s"} we couldn't verify until it fires (preview with example data to check):\n` +
|
|
3733
|
+
list.map((w) => ` • ${w.reason}`).join("\n"));
|
|
3734
|
+
};
|
|
3448
3735
|
if (!confirmLive) {
|
|
3449
3736
|
try {
|
|
3450
|
-
await apiPost(`${path}?dryRun=true`, {});
|
|
3451
|
-
return { content: [{ type: "text", text: `${APEX} Dry-run passed — journey ${journeyId} is valid and ready
|
|
3737
|
+
const dr = await apiPost(`${path}?dryRun=true`, {});
|
|
3738
|
+
return { content: [{ type: "text", text: `${APEX} Dry-run passed — journey ${journeyId} is valid and ready.${warnText(dr?.warnings)}\nRe-call with confirmLive:true to publish it to live customers.` }] };
|
|
3452
3739
|
}
|
|
3453
3740
|
catch (err) {
|
|
3454
|
-
|
|
3741
|
+
// A variable-coverage block arrives here with a plain message like
|
|
3742
|
+
// "This send uses {{first_name}}, but the trigger doesn't provide it."
|
|
3743
|
+
return { content: [{ type: "text", text: `${APEX} Dry-run found a problem before publishing: ${errMsg(err)}\nIf a variable won't resolve, map it to a field the trigger provides or remove it, then preview with example data.` }], isError: true };
|
|
3455
3744
|
}
|
|
3456
3745
|
}
|
|
3457
3746
|
try {
|
|
3458
3747
|
const res = await apiPost(path, {});
|
|
3459
3748
|
if (!tenantOk(res.workspaceKey))
|
|
3460
3749
|
return tenantMismatch();
|
|
3461
|
-
return { content: [{ type: "text", text: `${APEX} Published journey ${journeyId} — it is now live on trigger events
|
|
3750
|
+
return { content: [{ type: "text", text: `${APEX} Published journey ${journeyId} — it is now live on trigger events.${warnText(res?.warnings)} View it: ${journeyLink(journeyId)}` }] };
|
|
3462
3751
|
}
|
|
3463
3752
|
catch (err) {
|
|
3464
3753
|
return { content: [{ type: "text", text: `${APEX} ${errMsg(err)}` }], isError: true };
|
|
@@ -4344,5 +4633,104 @@ _Suggest the next step the user should tackle based on what's incomplete in the
|
|
|
4344
4633
|
};
|
|
4345
4634
|
},
|
|
4346
4635
|
},
|
|
4636
|
+
// ─── Adoption Engine ──────────────────────────────────────────────────────
|
|
4637
|
+
list_adoption_milestones: {
|
|
4638
|
+
description: `${APEX} — List the workspace's adoption milestones (per-feature "has this user adopted X?" definitions). A milestone is NOT a goal.`,
|
|
4639
|
+
schema: z.object({}),
|
|
4640
|
+
handler: async () => {
|
|
4641
|
+
const data = await apiGet("/api/adoption/milestones");
|
|
4642
|
+
return {
|
|
4643
|
+
content: [
|
|
4644
|
+
{ type: "text", text: JSON.stringify(data?.data?.milestones ?? [], null, 2) },
|
|
4645
|
+
],
|
|
4646
|
+
};
|
|
4647
|
+
},
|
|
4648
|
+
},
|
|
4649
|
+
get_adoption_milestone: {
|
|
4650
|
+
description: `${APEX} — Fetch one adoption milestone by id.`,
|
|
4651
|
+
schema: z.object({ milestoneId: z.string().describe("The milestone id") }),
|
|
4652
|
+
handler: async ({ milestoneId }) => {
|
|
4653
|
+
const data = await apiGet(`/api/adoption/milestones/${encodeURIComponent(milestoneId)}`);
|
|
4654
|
+
return {
|
|
4655
|
+
content: [{ type: "text", text: JSON.stringify(data?.data?.milestone ?? null, null, 2) }],
|
|
4656
|
+
};
|
|
4657
|
+
},
|
|
4658
|
+
},
|
|
4659
|
+
create_adoption_milestone: {
|
|
4660
|
+
description: `${APEX} — Create an adoption milestone. "Adopted" is defined by one of the workspace's own events (adoptedWhenKind="event", eventName) or a trait (adoptedWhenKind="trait", traitPath + traitValue). Starts OFF unless active=true.`,
|
|
4661
|
+
schema: z.object({
|
|
4662
|
+
name: z.string().describe("Human label, e.g. 'Ran first report'"),
|
|
4663
|
+
featureKey: z.string().describe("Stable feature key, e.g. 'reporting'"),
|
|
4664
|
+
adoptedWhenKind: z.enum(["event", "trait"]).describe("How adoption is detected"),
|
|
4665
|
+
eventName: z.string().optional().describe("event kind: the event whose firing means adopted"),
|
|
4666
|
+
traitPath: z.string().optional().describe("trait kind: dotted trait path, e.g. 'plan'"),
|
|
4667
|
+
traitValue: z.string().optional().describe("trait kind: value to match (equals)"),
|
|
4668
|
+
priority: z.number().optional().describe("Lower is nudged first. Default 100."),
|
|
4669
|
+
gapDays: z.number().optional().describe("Nudge this many days after signup if not adopted. Default 3."),
|
|
4670
|
+
active: z.boolean().optional().describe("Turn the milestone on. Default false."),
|
|
4671
|
+
}),
|
|
4672
|
+
handler: async (args) => {
|
|
4673
|
+
const body = {
|
|
4674
|
+
name: args.name,
|
|
4675
|
+
featureKey: args.featureKey,
|
|
4676
|
+
priority: args.priority ?? 100,
|
|
4677
|
+
active: args.active ?? false,
|
|
4678
|
+
gapPolicy: { kind: "days_since_signup", days: args.gapDays ?? 3 },
|
|
4679
|
+
adoptedWhen: args.adoptedWhenKind === "event"
|
|
4680
|
+
? { kind: "event", eventName: args.eventName }
|
|
4681
|
+
: { kind: "trait", traitPath: args.traitPath, traitOp: "equals", traitValue: args.traitValue },
|
|
4682
|
+
};
|
|
4683
|
+
const data = await apiPost("/api/adoption/milestones", body);
|
|
4684
|
+
return {
|
|
4685
|
+
content: [{ type: "text", text: JSON.stringify(data?.data?.milestone ?? data, null, 2) }],
|
|
4686
|
+
};
|
|
4687
|
+
},
|
|
4688
|
+
},
|
|
4689
|
+
update_adoption_milestone: {
|
|
4690
|
+
description: `${APEX} — Update an adoption milestone (name, priority, active, gapDays, nudgeJourneyId).`,
|
|
4691
|
+
schema: z.object({
|
|
4692
|
+
milestoneId: z.string(),
|
|
4693
|
+
name: z.string().optional(),
|
|
4694
|
+
priority: z.number().optional(),
|
|
4695
|
+
active: z.boolean().optional(),
|
|
4696
|
+
gapDays: z.number().optional(),
|
|
4697
|
+
nudgeJourneyId: z.string().optional(),
|
|
4698
|
+
}),
|
|
4699
|
+
handler: async (args) => {
|
|
4700
|
+
const updates = {};
|
|
4701
|
+
if (args.name !== undefined)
|
|
4702
|
+
updates.name = args.name;
|
|
4703
|
+
if (args.priority !== undefined)
|
|
4704
|
+
updates.priority = args.priority;
|
|
4705
|
+
if (args.active !== undefined)
|
|
4706
|
+
updates.active = args.active;
|
|
4707
|
+
if (args.nudgeJourneyId !== undefined)
|
|
4708
|
+
updates.nudgeJourneyId = args.nudgeJourneyId;
|
|
4709
|
+
if (args.gapDays !== undefined)
|
|
4710
|
+
updates.gapPolicy = { days: args.gapDays };
|
|
4711
|
+
const data = await apiPatch(`/api/adoption/milestones/${encodeURIComponent(args.milestoneId)}`, updates);
|
|
4712
|
+
return {
|
|
4713
|
+
content: [{ type: "text", text: JSON.stringify(data?.data?.milestone ?? data, null, 2) }],
|
|
4714
|
+
};
|
|
4715
|
+
},
|
|
4716
|
+
},
|
|
4717
|
+
delete_adoption_milestone: {
|
|
4718
|
+
description: `${APEX} — Delete an adoption milestone.`,
|
|
4719
|
+
schema: z.object({ milestoneId: z.string() }),
|
|
4720
|
+
handler: async ({ milestoneId }) => {
|
|
4721
|
+
await apiDelete(`/api/adoption/milestones/${encodeURIComponent(milestoneId)}`);
|
|
4722
|
+
return { content: [{ type: "text", text: `Deleted milestone ${milestoneId}.` }] };
|
|
4723
|
+
},
|
|
4724
|
+
},
|
|
4725
|
+
get_adoption_metrics: {
|
|
4726
|
+
description: `${APEX} — Aggregated adoption metrics: per-milestone funnel + lift-vs-holdout, plus workspace health. Counts only (no user PII).`,
|
|
4727
|
+
schema: z.object({}),
|
|
4728
|
+
handler: async () => {
|
|
4729
|
+
const data = await apiGet("/api/adoption/metrics");
|
|
4730
|
+
return {
|
|
4731
|
+
content: [{ type: "text", text: JSON.stringify(data?.data ?? {}, null, 2) }],
|
|
4732
|
+
};
|
|
4733
|
+
},
|
|
4734
|
+
},
|
|
4347
4735
|
};
|
|
4348
4736
|
//# sourceMappingURL=tools.js.map
|