@apex-inc/mcp-server 0.28.0 → 0.29.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/README.md +21 -0
- package/dist/adoption-compose.d.ts +1 -1
- package/dist/adoption-compose.d.ts.map +1 -1
- package/dist/adoption-compose.js +8 -3
- package/dist/adoption-compose.js.map +1 -1
- package/dist/index.js +12 -4
- package/dist/index.js.map +1 -1
- package/dist/live-test-guard.d.ts +45 -0
- package/dist/live-test-guard.d.ts.map +1 -0
- package/dist/live-test-guard.js +78 -0
- package/dist/live-test-guard.js.map +1 -0
- package/dist/screenshot-cli.d.ts +35 -3
- package/dist/screenshot-cli.d.ts.map +1 -1
- package/dist/screenshot-cli.js +126 -38
- package/dist/screenshot-cli.js.map +1 -1
- package/dist/tools.d.ts +122 -9
- package/dist/tools.d.ts.map +1 -1
- package/dist/tools.js +227 -14
- package/dist/tools.js.map +1 -1
- package/package.json +1 -1
- package/skills/apex-adoption/SKILL.md +36 -6
- package/skills/apex-communications/SKILL.md +4 -5
- package/skills/apex-experimentation/SKILL.md +4 -1
package/dist/tools.js
CHANGED
|
@@ -10,6 +10,7 @@ import { notStartedExplanation, publishedExperimentLine, } from "./publish-commu
|
|
|
10
10
|
import { activationLiveLine, shouldRefuseActivationWiring, } from "./activation-wiring.js";
|
|
11
11
|
import { milestoneWriteBody, preflightAdoptionMilestone, } from "./adoption-compose.js";
|
|
12
12
|
import { applyAddJourneyStep } from "./add-journey-step.js";
|
|
13
|
+
import { fetchLiveTestHits, formatLiveTestStop, isFileStop, isObjectStop, refuseLiveApexObject, } from "./live-test-guard.js";
|
|
13
14
|
const APEX = "Apex";
|
|
14
15
|
/**
|
|
15
16
|
* MOBX-006 — stable synthetic visitor id for agent-fired events.
|
|
@@ -1868,6 +1869,99 @@ export const toolDefinitions = {
|
|
|
1868
1869
|
}
|
|
1869
1870
|
},
|
|
1870
1871
|
},
|
|
1872
|
+
check_live_tests: {
|
|
1873
|
+
description: `${APEX} — Before you edit a page, commit, or push, call this even if the user did not mention experiments. Also call it before you change a letter, Adaptive Journey, or Milestone. If something live is on those files or ids, stop and ask. Do not edit. Do not commit.`,
|
|
1874
|
+
schema: z.object({
|
|
1875
|
+
file_paths: z
|
|
1876
|
+
.array(z.string())
|
|
1877
|
+
.optional()
|
|
1878
|
+
.describe("Repo-relative paths you are about to edit or commit. Full file is read locally when content is omitted."),
|
|
1879
|
+
file_contents: z
|
|
1880
|
+
.array(z.object({
|
|
1881
|
+
path: z.string(),
|
|
1882
|
+
content: z.string().describe("Full file at HEAD, not the diff hunk."),
|
|
1883
|
+
}))
|
|
1884
|
+
.optional(),
|
|
1885
|
+
urls: z
|
|
1886
|
+
.array(z.string())
|
|
1887
|
+
.optional()
|
|
1888
|
+
.describe("Page URLs you are about to change. URL-only hits are a warning, not a stop."),
|
|
1889
|
+
communication_ids: z
|
|
1890
|
+
.array(z.string())
|
|
1891
|
+
.optional()
|
|
1892
|
+
.describe("Letters you are about to edit or publish."),
|
|
1893
|
+
journey_ids: z
|
|
1894
|
+
.array(z.string())
|
|
1895
|
+
.optional()
|
|
1896
|
+
.describe("Adaptive Journeys you are about to change or publish."),
|
|
1897
|
+
milestone_ids: z
|
|
1898
|
+
.array(z.string())
|
|
1899
|
+
.optional()
|
|
1900
|
+
.describe("Milestones you are about to change or delete."),
|
|
1901
|
+
}),
|
|
1902
|
+
handler: async (args) => {
|
|
1903
|
+
try {
|
|
1904
|
+
const byPath = new Map();
|
|
1905
|
+
for (const row of args.file_contents ?? []) {
|
|
1906
|
+
if (row?.path)
|
|
1907
|
+
byPath.set(row.path, { path: row.path, content: row.content });
|
|
1908
|
+
}
|
|
1909
|
+
for (const raw of args.file_paths ?? []) {
|
|
1910
|
+
if (!raw || byPath.has(raw))
|
|
1911
|
+
continue;
|
|
1912
|
+
const abs = raw.startsWith("/") ? raw : join(process.cwd(), raw);
|
|
1913
|
+
byPath.set(raw, {
|
|
1914
|
+
path: raw,
|
|
1915
|
+
content: existsSync(abs) ? readFileSync(abs, "utf8") : undefined,
|
|
1916
|
+
});
|
|
1917
|
+
}
|
|
1918
|
+
const files = [...byPath.values()];
|
|
1919
|
+
const hits = await fetchLiveTestHits({
|
|
1920
|
+
files,
|
|
1921
|
+
urls: args.urls,
|
|
1922
|
+
communicationIds: args.communication_ids,
|
|
1923
|
+
journeyIds: args.journey_ids,
|
|
1924
|
+
milestoneIds: args.milestone_ids,
|
|
1925
|
+
includePublishedJourneys: (args.journey_ids ?? []).length > 0,
|
|
1926
|
+
});
|
|
1927
|
+
const stopping = hits.filter((h) => isFileStop(h) || isObjectStop(h));
|
|
1928
|
+
if (stopping.length === 0) {
|
|
1929
|
+
if (hits.length === 0) {
|
|
1930
|
+
return {
|
|
1931
|
+
content: [
|
|
1932
|
+
{
|
|
1933
|
+
type: "text",
|
|
1934
|
+
text: `${APEX} Nothing live is on those files or ids. You can edit and commit.`,
|
|
1935
|
+
},
|
|
1936
|
+
],
|
|
1937
|
+
};
|
|
1938
|
+
}
|
|
1939
|
+
const soft = hits
|
|
1940
|
+
.map((h) => `${h.name} (${h.status}, ${h.confidence}: ${h.reason})`)
|
|
1941
|
+
.join("; ");
|
|
1942
|
+
return {
|
|
1943
|
+
content: [
|
|
1944
|
+
{
|
|
1945
|
+
type: "text",
|
|
1946
|
+
text: `${APEX} Soft warning only — not a stop: ${soft}. If you turn a paused test back on, this is the page.`,
|
|
1947
|
+
},
|
|
1948
|
+
],
|
|
1949
|
+
};
|
|
1950
|
+
}
|
|
1951
|
+
return {
|
|
1952
|
+
content: [
|
|
1953
|
+
{
|
|
1954
|
+
type: "text",
|
|
1955
|
+
text: formatLiveTestStop(stopping),
|
|
1956
|
+
},
|
|
1957
|
+
],
|
|
1958
|
+
};
|
|
1959
|
+
}
|
|
1960
|
+
catch (err) {
|
|
1961
|
+
return { content: [{ type: "text", text: `${APEX} ${errMsg(err)}` }], isError: true };
|
|
1962
|
+
}
|
|
1963
|
+
},
|
|
1964
|
+
},
|
|
1871
1965
|
set_experiment_mutex: {
|
|
1872
1966
|
description: `${APEX} — Put two experiments in a mutual-exclusion group: no visitor is ever assigned to both, so they run concurrently with partitioned traffic (the scientifically clean way to run two experiments that touch the same surface). Bidirectional. Pass mode:"remove" to unlink. Safe on running experiments — only affects future assignments.`,
|
|
1873
1967
|
schema: z.object({
|
|
@@ -3368,9 +3462,17 @@ Events fired through this tool carry a stable synthetic visitorId (mcp-agent-*,
|
|
|
3368
3462
|
confirmLiveExperiment: z
|
|
3369
3463
|
.boolean()
|
|
3370
3464
|
.optional()
|
|
3371
|
-
.describe("Edit a communication that is the treatment in a RUNNING experiment.
|
|
3465
|
+
.describe("Edit a communication that is the treatment in a RUNNING experiment. Marks the numbers untrustworthy: we will not call a winner and we will not learn from mixed counts. Only pass true when they picked an out."),
|
|
3372
3466
|
}),
|
|
3373
3467
|
handler: async (args) => {
|
|
3468
|
+
const guard = await refuseLiveApexObject({
|
|
3469
|
+
communicationIds: [args.communicationId],
|
|
3470
|
+
confirmed: args.confirmLiveExperiment === true,
|
|
3471
|
+
purpose: "draft",
|
|
3472
|
+
});
|
|
3473
|
+
if (guard.stop) {
|
|
3474
|
+
return { content: [{ type: "text", text: guard.text }] };
|
|
3475
|
+
}
|
|
3374
3476
|
const body = {};
|
|
3375
3477
|
if (args.status)
|
|
3376
3478
|
body.status = args.status;
|
|
@@ -3427,7 +3529,7 @@ Events fired through this tool carry a stable synthetic visitorId (mcp-agent-*,
|
|
|
3427
3529
|
confirmLiveExperiment: z
|
|
3428
3530
|
.boolean()
|
|
3429
3531
|
.optional()
|
|
3430
|
-
.describe("Publish over a communication that is the treatment in a RUNNING experiment. Marks
|
|
3532
|
+
.describe("Publish over a communication that is the treatment in a RUNNING experiment. Marks the numbers untrustworthy: we will not call a winner and we will not learn from mixed counts. Only pass true when they picked an out."),
|
|
3431
3533
|
hostJourneyId: z
|
|
3432
3534
|
.string()
|
|
3433
3535
|
.optional()
|
|
@@ -3450,6 +3552,14 @@ Events fired through this tool carry a stable synthetic visitorId (mcp-agent-*,
|
|
|
3450
3552
|
.describe("Override the prefilled decision window in days."),
|
|
3451
3553
|
}),
|
|
3452
3554
|
handler: async (args) => {
|
|
3555
|
+
const guard = await refuseLiveApexObject({
|
|
3556
|
+
communicationIds: [args.communicationId],
|
|
3557
|
+
confirmed: args.confirmLiveExperiment === true,
|
|
3558
|
+
purpose: "publish",
|
|
3559
|
+
});
|
|
3560
|
+
if (guard.stop) {
|
|
3561
|
+
return { content: [{ type: "text", text: guard.text }] };
|
|
3562
|
+
}
|
|
3453
3563
|
const body = {};
|
|
3454
3564
|
if (args.intent)
|
|
3455
3565
|
body.intent = args.intent;
|
|
@@ -4220,6 +4330,13 @@ Events fired through this tool carry a stable synthetic visitorId (mcp-agent-*,
|
|
|
4220
4330
|
}),
|
|
4221
4331
|
handler: async (args) => {
|
|
4222
4332
|
try {
|
|
4333
|
+
const guard = await refuseLiveApexObject({
|
|
4334
|
+
journeyIds: [args.journeyId],
|
|
4335
|
+
purpose: "draft",
|
|
4336
|
+
});
|
|
4337
|
+
if (guard.stop) {
|
|
4338
|
+
return { content: [{ type: "text", text: guard.text }] };
|
|
4339
|
+
}
|
|
4223
4340
|
const j = await apiGet(`/api/journeys/${encodeURIComponent(args.journeyId)}`);
|
|
4224
4341
|
const newId = `step_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 6)}`;
|
|
4225
4342
|
const applied = applyAddJourneyStep(j.steps ?? [], args, newId);
|
|
@@ -4246,6 +4363,14 @@ Events fired through this tool carry a stable synthetic visitorId (mcp-agent-*,
|
|
|
4246
4363
|
}),
|
|
4247
4364
|
handler: async (args) => {
|
|
4248
4365
|
try {
|
|
4366
|
+
const guard = await refuseLiveApexObject({
|
|
4367
|
+
journeyIds: [args.journeyId],
|
|
4368
|
+
communicationIds: [args.commId],
|
|
4369
|
+
purpose: "draft",
|
|
4370
|
+
});
|
|
4371
|
+
if (guard.stop) {
|
|
4372
|
+
return { content: [{ type: "text", text: guard.text }] };
|
|
4373
|
+
}
|
|
4249
4374
|
const j = await apiGet(`/api/journeys/${encodeURIComponent(args.journeyId)}`);
|
|
4250
4375
|
const steps = [...(j.steps ?? [])];
|
|
4251
4376
|
const sends = steps.filter((s) => s.type === "send");
|
|
@@ -4306,6 +4431,14 @@ Events fired through this tool carry a stable synthetic visitorId (mcp-agent-*,
|
|
|
4306
4431
|
}),
|
|
4307
4432
|
handler: async (args) => {
|
|
4308
4433
|
try {
|
|
4434
|
+
const guard = await refuseLiveApexObject({
|
|
4435
|
+
journeyIds: [args.journeyId],
|
|
4436
|
+
communicationIds: [args.whenTrueCommId, args.whenFalseCommId].filter((id) => Boolean(id)),
|
|
4437
|
+
purpose: "draft",
|
|
4438
|
+
});
|
|
4439
|
+
if (guard.stop) {
|
|
4440
|
+
return { content: [{ type: "text", text: guard.text }] };
|
|
4441
|
+
}
|
|
4309
4442
|
const c = args.condition;
|
|
4310
4443
|
// Build the predicate leaf (AudiencePredicate is a bare leaf here).
|
|
4311
4444
|
let predicate;
|
|
@@ -4375,12 +4508,16 @@ Events fired through this tool carry a stable synthetic visitorId (mcp-agent-*,
|
|
|
4375
4508
|
},
|
|
4376
4509
|
},
|
|
4377
4510
|
publish_journey: {
|
|
4378
|
-
description: `${APEX} — Publish a draft journey so it runs on live trigger events. SAFETY: defaults to a dry-run (validation only) — pass confirmLive:true to actually publish to real customers. Email sends require a verified sender domain or publish is blocked.`,
|
|
4511
|
+
description: `${APEX} — Publish a draft journey so it runs on live trigger events. SAFETY: defaults to a dry-run (validation only) — pass confirmLive:true to actually publish to real customers. Email sends require a verified sender domain or publish is blocked. If this path is already live or a test is measuring it, stop and ask. Do not pass confirmTreatmentChange unless they picked an out.`,
|
|
4379
4512
|
schema: z.object({
|
|
4380
4513
|
journeyId: z.string(),
|
|
4381
4514
|
confirmLive: z.boolean().optional().describe("Set true to publish for real. Omitted/false = dry-run validation only."),
|
|
4515
|
+
confirmTreatmentChange: z
|
|
4516
|
+
.boolean()
|
|
4517
|
+
.optional()
|
|
4518
|
+
.describe("Publish anyway when this path is live or a test is measuring it. Only pass true after they picked an out."),
|
|
4382
4519
|
}),
|
|
4383
|
-
handler: async ({ journeyId, confirmLive }) => {
|
|
4520
|
+
handler: async ({ journeyId, confirmLive, confirmTreatmentChange, }) => {
|
|
4384
4521
|
const path = `/api/journeys/${encodeURIComponent(journeyId)}/publish`;
|
|
4385
4522
|
// Non-blocking coverage warnings ("we couldn't verify these until the
|
|
4386
4523
|
// journey fires") — surfaced so an agent can preview before publishing.
|
|
@@ -4402,6 +4539,15 @@ Events fired through this tool carry a stable synthetic visitorId (mcp-agent-*,
|
|
|
4402
4539
|
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 };
|
|
4403
4540
|
}
|
|
4404
4541
|
}
|
|
4542
|
+
const guard = await refuseLiveApexObject({
|
|
4543
|
+
journeyIds: [journeyId],
|
|
4544
|
+
includePublishedJourneys: true,
|
|
4545
|
+
confirmed: confirmTreatmentChange === true,
|
|
4546
|
+
purpose: "publish",
|
|
4547
|
+
});
|
|
4548
|
+
if (guard.stop) {
|
|
4549
|
+
return { content: [{ type: "text", text: guard.text }] };
|
|
4550
|
+
}
|
|
4405
4551
|
try {
|
|
4406
4552
|
const res = await apiPost(path, {});
|
|
4407
4553
|
if (!tenantOk(res.workspaceKey))
|
|
@@ -5482,12 +5628,12 @@ _Suggest the next step the user should tackle based on what's incomplete in the
|
|
|
5482
5628
|
},
|
|
5483
5629
|
},
|
|
5484
5630
|
create_adoption_milestone: {
|
|
5485
|
-
description: `${APEX} — Create an adoption milestone. "Adopted" is one event or one trait. Default enroll_mode is "future" (only people who sign up after this is created). Use enroll_mode="all" to include everyone already here. gap_hours wins over gap_days. Starts OFF unless active=true.`,
|
|
5631
|
+
description: `${APEX} — Create an adoption milestone. "Adopted" is one event or one trait. Omit adopted_when_kind to create the milestone before its finishing moment is chosen, then set it later with update_adoption_milestone; a milestone with no moment counts nobody, mails nobody, and cannot be turned on. Default enroll_mode is "future" (only people who sign up after this is created). Use enroll_mode="all" to include everyone already here. gap_hours wins over gap_days. Starts OFF unless active=true.`,
|
|
5486
5632
|
schema: z.object({
|
|
5487
5633
|
name: z.string().describe("Human label, e.g. 'Ran first report'"),
|
|
5488
5634
|
featureKey: z.string().describe("Stable feature key, e.g. 'reporting'"),
|
|
5489
5635
|
actionLabel: z.string().optional().describe("Sentence verb: hasn't {action_label}. Never the raw event key."),
|
|
5490
|
-
adoptedWhenKind: z.enum(["event", "trait"]).describe("How adoption is detected"),
|
|
5636
|
+
adoptedWhenKind: z.enum(["event", "trait"]).optional().describe("How adoption is detected. Omit to decide the finishing moment later; the milestone stays inert until it is set."),
|
|
5491
5637
|
eventName: z.string().optional().describe("event kind: the event whose firing means adopted"),
|
|
5492
5638
|
traitPath: z.string().optional().describe("trait kind: dotted trait path, e.g. 'plan'"),
|
|
5493
5639
|
traitValue: z.string().optional().describe("trait kind: value to match (equals)"),
|
|
@@ -5499,10 +5645,14 @@ _Suggest the next step the user should tackle based on what's incomplete in the
|
|
|
5499
5645
|
gapKind: z.enum(["days_since_signup", "event_not_fired"]).optional().describe("Anchor. Default days_since_signup. event_not_fired needs since_event_name."),
|
|
5500
5646
|
sinceEventName: z.string().optional().describe("event_not_fired: the event the wait starts from."),
|
|
5501
5647
|
enrollMode: z.enum(["all", "future"]).optional().describe("future (default) = only people who sign up from now on. all = historical roster too."),
|
|
5648
|
+
finishWhen: z.enum(["this_person", "anyone_here"]).optional().describe("this_person (default) = only that person finishes. anyone_here = first person on the same Account finishes it for the company. Event finishes only."),
|
|
5502
5649
|
active: z.boolean().optional().describe("Turn the milestone on. Default false."),
|
|
5503
5650
|
}),
|
|
5504
5651
|
handler: async (args) => {
|
|
5505
5652
|
const warnings = await preflightAdoptionMilestone(args);
|
|
5653
|
+
if (!args.adoptedWhenKind) {
|
|
5654
|
+
warnings.push("No finishing moment set. This milestone counts nobody and cannot be turned on until update_adoption_milestone sets adopted_when_kind.");
|
|
5655
|
+
}
|
|
5506
5656
|
const body = milestoneWriteBody({
|
|
5507
5657
|
...args,
|
|
5508
5658
|
enrollMode: args.enrollMode ?? "future",
|
|
@@ -5519,11 +5669,16 @@ _Suggest the next step the user should tackle based on what's incomplete in the
|
|
|
5519
5669
|
},
|
|
5520
5670
|
},
|
|
5521
5671
|
update_adoption_milestone: {
|
|
5522
|
-
description: `${APEX} — Update an adoption milestone (name, priority, active, gap days/hours, enroll mode, nudge or
|
|
5672
|
+
description: `${APEX} — Update an adoption milestone (finishing moment, name, priority, active, gap days/hours, enroll mode, who finishing counts, nudge journey). Pass adopted_when_kind plus event_name or trait_path to set the finishing moment on a milestone created without one. active=true is REFUSED while there is no moment, because nobody could ever finish it and everyone would stay late forever. Changing the moment resets the lift window. If the Milestone is ON, changing what "finished" means stops and asks — do not pass confirmLiveMilestone unless they picked an out. A Milestone keeps BOTH letters on its one Adaptive Journey: the Celebration is a Send on the finish arm, not a second journey. finish_when=anyone_here on a live event Milestone backfills leftover Nudges for Accounts that already have a doer.`,
|
|
5523
5673
|
schema: z.object({
|
|
5524
5674
|
milestoneId: z.string(),
|
|
5525
5675
|
name: z.string().optional(),
|
|
5526
5676
|
actionLabel: z.string().optional().describe("Sentence verb: hasn't {action_label}."),
|
|
5677
|
+
adoptedWhenKind: z.enum(["event", "trait"]).optional().describe("Set the finishing moment. Changing it resets the lift window."),
|
|
5678
|
+
eventName: z.string().optional().describe("event kind: the event whose firing means adopted"),
|
|
5679
|
+
traitPath: z.string().optional().describe("trait kind: dotted trait path, e.g. 'plan'"),
|
|
5680
|
+
traitValue: z.string().optional().describe("trait kind: value to match (equals)"),
|
|
5681
|
+
traitOp: z.enum(["equals", "not_equals", "exists"]).optional().describe("trait kind: comparison. Default equals."),
|
|
5527
5682
|
priority: z.number().optional(),
|
|
5528
5683
|
order: z.number().optional().describe("0-based sequence order. Prefer reorder_adoption_milestones to sequence the whole set."),
|
|
5529
5684
|
active: z.boolean().optional(),
|
|
@@ -5532,10 +5687,34 @@ _Suggest the next step the user should tackle based on what's incomplete in the
|
|
|
5532
5687
|
gapKind: z.enum(["days_since_signup", "event_not_fired"]).optional(),
|
|
5533
5688
|
sinceEventName: z.string().optional(),
|
|
5534
5689
|
enrollMode: z.enum(["all", "future"]).optional(),
|
|
5690
|
+
finishWhen: z.enum(["this_person", "anyone_here"]).optional().describe("this_person = only that person. anyone_here = first person on the Account. Event finishes only."),
|
|
5535
5691
|
nudgeJourneyId: z.string().optional(),
|
|
5536
|
-
|
|
5692
|
+
confirmLiveMilestone: z
|
|
5693
|
+
.boolean()
|
|
5694
|
+
.optional()
|
|
5695
|
+
.describe("Change an ON Milestone anyway. Only pass true after they picked an out."),
|
|
5537
5696
|
}),
|
|
5538
5697
|
handler: async (args) => {
|
|
5698
|
+
const changesWhatCounts = args.adoptedWhenKind !== undefined ||
|
|
5699
|
+
args.eventName !== undefined ||
|
|
5700
|
+
args.traitPath !== undefined ||
|
|
5701
|
+
args.gapDays !== undefined ||
|
|
5702
|
+
args.gapHours !== undefined ||
|
|
5703
|
+
args.gapKind !== undefined ||
|
|
5704
|
+
args.active !== undefined ||
|
|
5705
|
+
args.enrollMode !== undefined ||
|
|
5706
|
+
args.finishWhen !== undefined ||
|
|
5707
|
+
args.nudgeJourneyId !== undefined;
|
|
5708
|
+
if (changesWhatCounts) {
|
|
5709
|
+
const guard = await refuseLiveApexObject({
|
|
5710
|
+
milestoneIds: [args.milestoneId],
|
|
5711
|
+
confirmed: args.confirmLiveMilestone === true,
|
|
5712
|
+
purpose: "milestone",
|
|
5713
|
+
});
|
|
5714
|
+
if (guard.stop) {
|
|
5715
|
+
return { content: [{ type: "text", text: guard.text }] };
|
|
5716
|
+
}
|
|
5717
|
+
}
|
|
5539
5718
|
const updates = milestoneWriteBody(args);
|
|
5540
5719
|
if (args.gapDays === undefined && args.gapHours === undefined && args.gapKind === undefined && args.sinceEventName === undefined) {
|
|
5541
5720
|
delete updates.gapPolicy;
|
|
@@ -5547,9 +5726,23 @@ _Suggest the next step the user should tackle based on what's incomplete in the
|
|
|
5547
5726
|
},
|
|
5548
5727
|
},
|
|
5549
5728
|
delete_adoption_milestone: {
|
|
5550
|
-
description: `${APEX} — Delete an adoption milestone.`,
|
|
5551
|
-
schema: z.object({
|
|
5552
|
-
|
|
5729
|
+
description: `${APEX} — Delete an adoption milestone. If it is on, stop and ask. Do not pass confirmLiveMilestone unless they picked an out.`,
|
|
5730
|
+
schema: z.object({
|
|
5731
|
+
milestoneId: z.string(),
|
|
5732
|
+
confirmLiveMilestone: z
|
|
5733
|
+
.boolean()
|
|
5734
|
+
.optional()
|
|
5735
|
+
.describe("Delete an ON Milestone anyway. Only pass true after they picked an out."),
|
|
5736
|
+
}),
|
|
5737
|
+
handler: async ({ milestoneId, confirmLiveMilestone, }) => {
|
|
5738
|
+
const guard = await refuseLiveApexObject({
|
|
5739
|
+
milestoneIds: [milestoneId],
|
|
5740
|
+
confirmed: confirmLiveMilestone === true,
|
|
5741
|
+
purpose: "milestone",
|
|
5742
|
+
});
|
|
5743
|
+
if (guard.stop) {
|
|
5744
|
+
return { content: [{ type: "text", text: guard.text }] };
|
|
5745
|
+
}
|
|
5553
5746
|
await apiDelete(`/api/adoption/milestones/${encodeURIComponent(milestoneId)}`);
|
|
5554
5747
|
return { content: [{ type: "text", text: `Deleted milestone ${milestoneId}.` }] };
|
|
5555
5748
|
},
|
|
@@ -5579,6 +5772,7 @@ _Suggest the next step the user should tackle based on what's incomplete in the
|
|
|
5579
5772
|
gapKind: z.enum(["days_since_signup", "event_not_fired"]).optional(),
|
|
5580
5773
|
sinceEventName: z.string().optional(),
|
|
5581
5774
|
enrollMode: z.enum(["all", "future"]).optional().describe("future (default) = only people who sign up from now on."),
|
|
5775
|
+
finishWhen: z.enum(["this_person", "anyone_here"]).optional().describe("this_person (default) = only that person finishes. anyone_here = first person on the same Account. Event finishes only."),
|
|
5582
5776
|
active: z.boolean().optional().describe("Turn the milestone on. Default false."),
|
|
5583
5777
|
scaffold: z.enum(["both", "celebration", "nudge", "none"]).optional().describe("Which letters to include. Default both."),
|
|
5584
5778
|
journeyName: z.string().optional().describe("Name for the Adaptive Journey. Default is the Milestone name."),
|
|
@@ -5658,7 +5852,7 @@ _Suggest the next step the user should tackle based on what's incomplete in the
|
|
|
5658
5852
|
},
|
|
5659
5853
|
},
|
|
5660
5854
|
attach_adoption_milestone_still: {
|
|
5661
|
-
description: `${APEX} — Attach a still of the empty screen (not_yet) or the finished screen (done) to a Milestone. Same two pictures the shop uploads. Pass imageBase64 or a public url. A Milestone is NOT a Target.`,
|
|
5855
|
+
description: `${APEX} — Attach a still of the empty screen (not_yet) or the finished screen (done) to a Milestone. Same two pictures the shop uploads. Pass imageBase64 or a public url. For a signed-in product page, capture on their machine with \`npx @apex-inc/mcp-server screenshot --milestone … --state not_yet|done --url … --storage-state <login file>\` — do not use hosted recapture. A Milestone is NOT a Target.`,
|
|
5662
5856
|
schema: z.object({
|
|
5663
5857
|
milestoneId: z.string().describe("The Milestone to attach the still to"),
|
|
5664
5858
|
state: z.enum(["not_yet", "done"]).describe("not_yet = empty screen; done = finished screen"),
|
|
@@ -5702,7 +5896,7 @@ _Suggest the next step the user should tackle based on what's incomplete in the
|
|
|
5702
5896
|
},
|
|
5703
5897
|
},
|
|
5704
5898
|
recapture_adoption_milestone_stills: {
|
|
5705
|
-
description: `${APEX} — Capture the Not yet and Done stills from public pages on a named workspace host. Not localhost. A login wall fails honestly and writes nothing
|
|
5899
|
+
description: `${APEX} — Capture the Not yet and Done stills from public pages on a named workspace host. Not localhost. A login wall fails honestly and writes nothing. For a signed-in product page, sit it on their machine: \`npx @apex-inc/mcp-server screenshot --milestone … --storage-state <login file>\`. Same two pictures the shop uploads.`,
|
|
5706
5900
|
schema: z.object({
|
|
5707
5901
|
milestoneId: z.string().describe("The Milestone to capture stills for"),
|
|
5708
5902
|
notYetUrl: z.string().url().optional().describe("Public https URL of the empty screen."),
|
|
@@ -5788,8 +5982,27 @@ _Suggest the next step the user should tackle based on what's incomplete in the
|
|
|
5788
5982
|
};
|
|
5789
5983
|
},
|
|
5790
5984
|
},
|
|
5985
|
+
get_adoption_account_progress: {
|
|
5986
|
+
description: `${APEX} — One Account's Path: which Milestones this company has finished, who did them, and how many people are Not yet / Started / Done. account_id is the company id from identify({ account: { id } }). Never includes holdout.`,
|
|
5987
|
+
schema: z.object({
|
|
5988
|
+
account_id: z
|
|
5989
|
+
.string()
|
|
5990
|
+
.describe("The Account id (identify account.id or the Apex acct_ id)"),
|
|
5991
|
+
}),
|
|
5992
|
+
handler: async ({ account_id }) => {
|
|
5993
|
+
const data = await apiGet(`/api/adoption/progress?account_id=${encodeURIComponent(account_id)}`);
|
|
5994
|
+
return {
|
|
5995
|
+
content: [
|
|
5996
|
+
{
|
|
5997
|
+
type: "text",
|
|
5998
|
+
text: JSON.stringify(data?.data ?? {}, null, 2),
|
|
5999
|
+
},
|
|
6000
|
+
],
|
|
6001
|
+
};
|
|
6002
|
+
},
|
|
6003
|
+
},
|
|
5791
6004
|
get_adoption_user_progress: {
|
|
5792
|
-
description: `${APEX} — One
|
|
6005
|
+
description: `${APEX} — One person's Path (Not yet / Started / Done) across every active Milestone. Includes finish_when and inherited when Anyone here finished for their company. Never email, name, or holdout. end_user_id must be a valid user id, not an email.`,
|
|
5793
6006
|
schema: z.object({
|
|
5794
6007
|
end_user_id: z
|
|
5795
6008
|
.string()
|