@apex-inc/mcp-server 0.27.5 → 0.28.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/tools.js CHANGED
@@ -9,6 +9,7 @@ import { getPersonLabel, lowerPerson } from "./person-label.js";
9
9
  import { notStartedExplanation, publishedExperimentLine, } from "./publish-communication-copy.js";
10
10
  import { activationLiveLine, shouldRefuseActivationWiring, } from "./activation-wiring.js";
11
11
  import { milestoneWriteBody, preflightAdoptionMilestone, } from "./adoption-compose.js";
12
+ import { applyAddJourneyStep } from "./add-journey-step.js";
12
13
  const APEX = "Apex";
13
14
  /**
14
15
  * MOBX-006 — stable synthetic visitor id for agent-fired events.
@@ -4204,40 +4205,31 @@ Events fired through this tool carry a stable synthetic visitorId (mcp-agent-*,
4204
4205
  },
4205
4206
  },
4206
4207
  add_journey_step: {
4207
- description: `${APEX} — Append a wait or send step to a draft journey (inserted just before the exit; linear flows). 'wait' delays (ISO-8601 duration, e.g. P1D = 24h); 'send' fires a communication on the given channels. Call repeatedly to build trigger wait send exit.`,
4208
+ description: `${APEX} — Append a Wait or Send to a draft Adaptive Journey. Prefer compose_adoption_milestone for a Milestone (one Wait until they finish, letters already on the arms). Wait mode duration (ISO, e.g. P1D) or until_event (trigger_contract_id + deadline_iso). Send arm event|deadline puts the letter on that Wait arm. Do not add_journey_step send on a compose-built path use set_journey_send to point the existing Send.`,
4208
4209
  schema: z.object({
4209
4210
  journeyId: z.string(),
4210
4211
  kind: z.enum(["wait", "send"]),
4211
- durationIso: z.string().optional().describe("wait only: ISO-8601 duration, e.g. 'P1D' (1 day), 'PT2H' (2 hours)."),
4212
+ mode: z.enum(["duration", "until_event"]).optional().describe("wait only. Default duration."),
4213
+ durationIso: z.string().optional().describe("wait duration: ISO-8601, e.g. 'P1D' (1 day), 'PT2H' (2 hours)."),
4214
+ triggerContractId: z.string().optional().describe("wait until_event: the done-event trigger contract."),
4215
+ deadlineIso: z.string().optional().describe("wait until_event: hours or days as ISO-8601, e.g. 'PT1H', 'P3D'."),
4212
4216
  commId: z.string().optional().describe("send only: the communication id to fire (create_communication or an existing comm)."),
4213
4217
  commVersion: z.number().optional().describe("send only: pinned comm version (default 1)."),
4214
4218
  channels: z.array(z.enum(["email", "in_app_push", "mobile_push", "web_push"])).optional().describe("send only: channels to fire (default ['email']). Use the SAME tokens as create_communication (email, in_app_push, mobile_push) so a comm's channels map cleanly onto the send step; web_push is send-only."),
4219
+ arm: z.enum(["event", "deadline"]).optional().describe("send only: put this Send on the until_event Wait's event or deadline arm."),
4215
4220
  }),
4216
4221
  handler: async (args) => {
4217
4222
  try {
4218
- if (args.kind === "send" && !args.commId) {
4219
- return { content: [{ type: "text", text: `${APEX} A send step needs commId.` }], isError: true };
4220
- }
4221
4223
  const j = await apiGet(`/api/journeys/${encodeURIComponent(args.journeyId)}`);
4222
- const steps = [...(j.steps ?? [])];
4223
- const exit = steps.find((s) => s.type === "exit");
4224
- if (!exit) {
4225
- return { content: [{ type: "text", text: `${APEX} Journey has no exit step; cannot insert.` }], isError: true };
4226
- }
4227
- const exitId = exit.id;
4228
4224
  const newId = `step_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 6)}`;
4229
- const newStep = args.kind === "wait"
4230
- ? { id: newId, type: "wait", label: "Wait", mode: "duration", duration: args.durationIso ?? "P1D", next: exitId }
4231
- : { id: newId, type: "send", label: "Send", commId: args.commId, commVersion: args.commVersion ?? 1, channels: args.channels ?? ["email"], next: exitId };
4232
- // Insert before the exit: rewire whatever currently points to exit.
4233
- const pre = steps.find((s) => s.next === exitId);
4234
- if (pre)
4235
- pre.next = newId;
4236
- steps.push(newStep);
4237
- const updated = await apiPatch(`/api/journeys/${encodeURIComponent(args.journeyId)}`, { steps });
4225
+ const applied = applyAddJourneyStep(j.steps ?? [], args, newId);
4226
+ if (!applied.ok) {
4227
+ return { content: [{ type: "text", text: `${APEX} ${applied.error}` }], isError: true };
4228
+ }
4229
+ const updated = await apiPatch(`/api/journeys/${encodeURIComponent(args.journeyId)}`, { steps: applied.steps });
4238
4230
  if (!tenantOk(updated.workspaceKey))
4239
4231
  return tenantMismatch();
4240
- return { content: [{ type: "text", text: `${APEX} Added ${args.kind} step to journey ${args.journeyId}. Watch it update in the browser: ${journeyLink(args.journeyId)}` }] };
4232
+ return { content: [{ type: "text", text: `${APEX} Added ${args.kind} step ${applied.stepId} to journey ${args.journeyId}. Watch it update in the browser: ${journeyLink(args.journeyId)}` }] };
4241
4233
  }
4242
4234
  catch (err) {
4243
4235
  return { content: [{ type: "text", text: `${APEX} ${errMsg(err)}` }], isError: true };
@@ -5494,6 +5486,7 @@ _Suggest the next step the user should tackle based on what's incomplete in the
5494
5486
  schema: z.object({
5495
5487
  name: z.string().describe("Human label, e.g. 'Ran first report'"),
5496
5488
  featureKey: z.string().describe("Stable feature key, e.g. 'reporting'"),
5489
+ actionLabel: z.string().optional().describe("Sentence verb: hasn't {action_label}. Never the raw event key."),
5497
5490
  adoptedWhenKind: z.enum(["event", "trait"]).describe("How adoption is detected"),
5498
5491
  eventName: z.string().optional().describe("event kind: the event whose firing means adopted"),
5499
5492
  traitPath: z.string().optional().describe("trait kind: dotted trait path, e.g. 'plan'"),
@@ -5530,6 +5523,7 @@ _Suggest the next step the user should tackle based on what's incomplete in the
5530
5523
  schema: z.object({
5531
5524
  milestoneId: z.string(),
5532
5525
  name: z.string().optional(),
5526
+ actionLabel: z.string().optional().describe("Sentence verb: hasn't {action_label}."),
5533
5527
  priority: z.number().optional(),
5534
5528
  order: z.number().optional().describe("0-based sequence order. Prefer reorder_adoption_milestones to sequence the whole set."),
5535
5529
  active: z.boolean().optional(),
@@ -5561,10 +5555,11 @@ _Suggest the next step the user should tackle based on what's incomplete in the
5561
5555
  },
5562
5556
  },
5563
5557
  compose_adoption_milestone: {
5564
- description: `${APEX} — Create an adoption milestone AND scaffold its journey legs in ONE call. scaffold="both" (default, recommended) creates the nudge (hasn't adopted within the gap) AND the celebration. New milestones default to enroll_mode="future" so a short clock does not mail the historical roster. gap_hours wins over gap_days. Journeys are drafts — preview the letter, then publish_journey. Returns the milestone, journey ids, and honesty warnings. A milestone is NOT a goal.`,
5558
+ description: `${APEX} — Create a Milestone and stamp ONE Adaptive Journey (Trigger Wait until they finish → Celebration and/or Nudge). scaffold="both" (default) includes both letters; "nudge" / "celebration" one letter; "none" is just-track. Unique Communication per Send — never reuse the shared catalog letters. New milestones default to enroll_mode="future". Journeys are drafts — preview, then publish_journey. A Milestone is NOT a Target.`,
5565
5559
  schema: z.object({
5566
5560
  name: z.string().describe("Human label, e.g. 'Started first experiment'"),
5567
5561
  featureKey: z.string().describe("Stable feature key, e.g. 'experiments'"),
5562
+ actionLabel: z.string().optional().describe("Sentence verb: hasn't {action_label}."),
5568
5563
  adoptedWhenKind: z.enum(["event", "trait"]).describe("How adoption is detected"),
5569
5564
  eventName: z.string().optional().describe("event kind: the event whose firing means adopted"),
5570
5565
  traitPath: z.string().optional().describe("trait kind: dotted trait path, e.g. 'plan'"),
@@ -5572,19 +5567,33 @@ _Suggest the next step the user should tackle based on what's incomplete in the
5572
5567
  traitOp: z.enum(["equals", "not_equals", "exists"]).optional(),
5573
5568
  priority: z.number().optional().describe("Lower is nudged first. Default 100."),
5574
5569
  order: z.number().optional().describe("0-based sequence order."),
5575
- gapDays: z.number().optional().describe("Nudge window in days. Ignored when gap_hours is set. Default 3."),
5576
- gapHours: z.number().optional().describe("Nudge window in hours. Use for same-day QA. 0 = right away."),
5570
+ gapDays: z.number().optional().describe("First Nudge window in days. Ignored when gap_hours or nudge_clocks is set. Default 3."),
5571
+ gapHours: z.number().optional().describe("First Nudge window in hours. 0 = right away."),
5572
+ nudgeClocks: z
5573
+ .array(z.object({
5574
+ hours: z.number().optional(),
5575
+ days: z.number().optional(),
5576
+ }))
5577
+ .optional()
5578
+ .describe("Each clock is N hours or N days. First is the Wait deadline; later are extra Nudges."),
5577
5579
  gapKind: z.enum(["days_since_signup", "event_not_fired"]).optional(),
5578
5580
  sinceEventName: z.string().optional(),
5579
5581
  enrollMode: z.enum(["all", "future"]).optional().describe("future (default) = only people who sign up from now on."),
5580
5582
  active: z.boolean().optional().describe("Turn the milestone on. Default false."),
5581
- scaffold: z.enum(["both", "celebration", "nudge", "none"]).optional().describe("Which legs to scaffold. Default both."),
5582
- journeyName: z.string().optional().describe("Name for a single scaffolded journey. Ignored for scaffold='both'."),
5583
+ scaffold: z.enum(["both", "celebration", "nudge", "none"]).optional().describe("Which letters to include. Default both."),
5584
+ journeyName: z.string().optional().describe("Name for the Adaptive Journey. Default is the Milestone name."),
5585
+ saveNotYetSegment: z.boolean().optional().describe("Save the live not-yet rule as a Segment."),
5586
+ saveDoneSegment: z.boolean().optional().describe("Save the live done rule as a Segment."),
5583
5587
  }),
5584
5588
  handler: async (args) => {
5585
5589
  const warnings = await preflightAdoptionMilestone(args);
5590
+ const firstClock = args.nudgeClocks?.[0];
5586
5591
  const created = await apiPost("/api/adoption/milestones", milestoneWriteBody({
5587
5592
  ...args,
5593
+ ...(firstClock?.hours !== undefined ? { gapHours: firstClock.hours } : {}),
5594
+ ...(firstClock?.days !== undefined && firstClock.hours === undefined
5595
+ ? { gapDays: firstClock.days }
5596
+ : {}),
5588
5597
  enrollMode: args.enrollMode ?? "future",
5589
5598
  active: args.active ?? false,
5590
5599
  priority: args.priority ?? 100,
@@ -5593,37 +5602,54 @@ _Suggest the next step the user should tackle based on what's incomplete in the
5593
5602
  if (!milestoneId) {
5594
5603
  return { content: [{ type: "text", text: JSON.stringify(created, null, 2) }] };
5595
5604
  }
5605
+ const persistSegments = async () => {
5606
+ if (!args.saveNotYetSegment && !args.saveDoneSegment) {
5607
+ return created.data?.milestone;
5608
+ }
5609
+ const patched = await apiPatch(`/api/adoption/milestones/${encodeURIComponent(milestoneId)}`, {
5610
+ saveNotYetSegment: args.saveNotYetSegment === true,
5611
+ saveDoneSegment: args.saveDoneSegment === true,
5612
+ });
5613
+ return patched.data?.milestone ?? created.data?.milestone;
5614
+ };
5596
5615
  const scaffold = args.scaffold ?? "both";
5597
5616
  if (scaffold === "none") {
5617
+ const milestone = await persistSegments();
5598
5618
  return {
5599
- content: [{ type: "text", text: JSON.stringify({ milestone: created.data?.milestone, journeyId: null, nudgeJourneyId: null, celebrationJourneyId: null, warnings }, null, 2) }],
5619
+ content: [{ type: "text", text: JSON.stringify({
5620
+ milestone,
5621
+ journey_id: null,
5622
+ nudge_journey_id: null,
5623
+ celebration_journey_id: null,
5624
+ sends: [],
5625
+ warnings,
5626
+ }, null, 2) }],
5600
5627
  };
5601
5628
  }
5602
- const scaffoldLeg = async (templateId, name) => {
5603
- const journey = await apiPost("/api/journeys/from-template", {
5604
- templateId,
5605
- milestoneId,
5606
- ...(name ? { name } : {}),
5607
- });
5608
- return journey?.id ?? null;
5609
- };
5610
- const wantsNudge = scaffold === "nudge" || scaffold === "both";
5611
- const wantsCelebration = scaffold === "celebration" || scaffold === "both";
5612
- // Single-leg scaffolds honor journeyName; "both" lets each leg default.
5613
- const legName = scaffold === "both" ? undefined : args.journeyName;
5614
- const nudgeJourneyId = wantsNudge ? await scaffoldLeg("adoption-nudge", legName) : null;
5615
- const celebrationJourneyId = wantsCelebration
5616
- ? await scaffoldLeg("adoption-celebration", legName)
5617
- : null;
5629
+ const journey = await apiPost("/api/journeys/from-template", {
5630
+ templateId: "adoption-nudge",
5631
+ milestoneId,
5632
+ scaffold,
5633
+ name: args.journeyName ?? args.name,
5634
+ ...(args.nudgeClocks ? { nudgeClocks: args.nudgeClocks } : {}),
5635
+ });
5636
+ const journeyId = journey?.id ?? null;
5637
+ const sends = (journey?.sends ?? []).map((s) => ({
5638
+ role: s.role,
5639
+ step_id: s.stepId,
5640
+ comm_id: s.commId,
5641
+ }));
5642
+ const milestone = await persistSegments();
5618
5643
  return {
5619
5644
  content: [
5620
5645
  {
5621
5646
  type: "text",
5622
5647
  text: JSON.stringify({
5623
- milestone: created.data?.milestone,
5624
- journeyId: nudgeJourneyId ?? celebrationJourneyId,
5625
- nudgeJourneyId,
5626
- celebrationJourneyId,
5648
+ milestone,
5649
+ journey_id: journeyId,
5650
+ nudge_journey_id: journeyId,
5651
+ celebration_journey_id: null,
5652
+ sends,
5627
5653
  warnings,
5628
5654
  }, null, 2),
5629
5655
  },
@@ -5631,6 +5657,115 @@ _Suggest the next step the user should tackle based on what's incomplete in the
5631
5657
  };
5632
5658
  },
5633
5659
  },
5660
+ 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. 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
+ schema: z.object({
5663
+ milestoneId: z.string().describe("The Milestone to attach the still to"),
5664
+ state: z.enum(["not_yet", "done"]).describe("not_yet = empty screen; done = finished screen"),
5665
+ imageBase64: z.string().optional().describe("Base64-encoded PNG/JPEG."),
5666
+ url: z.string().url().optional().describe("Already-hosted public image URL."),
5667
+ viewport: z.enum(["desktop", "tablet", "mobile"]).optional(),
5668
+ format: z.enum(["png", "jpeg"]).optional(),
5669
+ label: z.string().optional(),
5670
+ }),
5671
+ handler: async (args) => {
5672
+ if (!args.imageBase64 && !args.url) {
5673
+ return {
5674
+ content: [{
5675
+ type: "text",
5676
+ text: "Provide either imageBase64 or url.",
5677
+ }],
5678
+ isError: true,
5679
+ };
5680
+ }
5681
+ const res = await apiPost(`/api/adoption/milestones/${encodeURIComponent(args.milestoneId)}/assets`, {
5682
+ state: args.state,
5683
+ source: args.imageBase64 ? "agent" : "upload",
5684
+ viewport: args.viewport ?? "desktop",
5685
+ imageBase64: args.imageBase64,
5686
+ url: args.url,
5687
+ format: args.format ?? "png",
5688
+ label: args.label,
5689
+ });
5690
+ const asset = res.data;
5691
+ return {
5692
+ content: [{
5693
+ type: "text",
5694
+ text: JSON.stringify({
5695
+ id: asset.id,
5696
+ state: asset.state,
5697
+ url: asset.url,
5698
+ milestone_id: args.milestoneId,
5699
+ }, null, 2),
5700
+ }],
5701
+ };
5702
+ },
5703
+ },
5704
+ 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. 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
+ schema: z.object({
5707
+ milestoneId: z.string().describe("The Milestone to capture stills for"),
5708
+ notYetUrl: z.string().url().optional().describe("Public https URL of the empty screen."),
5709
+ doneUrl: z.string().url().optional().describe("Public https URL of the finished screen."),
5710
+ }),
5711
+ handler: async (args) => {
5712
+ if (!args.notYetUrl && !args.doneUrl) {
5713
+ return {
5714
+ content: [{ type: "text", text: "Name a public page for Not yet or Done." }],
5715
+ isError: true,
5716
+ };
5717
+ }
5718
+ try {
5719
+ const res = await apiPost(`/api/adoption/milestones/${encodeURIComponent(args.milestoneId)}/recapture`, {
5720
+ notYetUrl: args.notYetUrl,
5721
+ doneUrl: args.doneUrl,
5722
+ });
5723
+ if (res.error) {
5724
+ return {
5725
+ content: [{
5726
+ type: "text",
5727
+ text: res.error,
5728
+ }],
5729
+ isError: true,
5730
+ };
5731
+ }
5732
+ return {
5733
+ content: [{
5734
+ type: "text",
5735
+ text: JSON.stringify({
5736
+ captured: res.data?.captured ?? [],
5737
+ failed: res.data?.failed ?? [],
5738
+ milestone_id: args.milestoneId,
5739
+ }, null, 2),
5740
+ }],
5741
+ };
5742
+ }
5743
+ catch {
5744
+ return {
5745
+ content: [{
5746
+ type: "text",
5747
+ text: "We couldn’t open that page from Apex. It may need a sign-in. Attach a still yourself.",
5748
+ }],
5749
+ isError: true,
5750
+ };
5751
+ }
5752
+ },
5753
+ },
5754
+ list_adoption_milestone_stills: {
5755
+ description: `${APEX} — List the Not yet and Done stills attached to a Milestone. Empty means they have not attached one yet.`,
5756
+ schema: z.object({
5757
+ milestoneId: z.string().describe("The Milestone to list stills for"),
5758
+ }),
5759
+ handler: async ({ milestoneId }) => {
5760
+ const res = await apiGet(`/api/adoption/milestones/${encodeURIComponent(milestoneId)}/assets`);
5761
+ return {
5762
+ content: [{
5763
+ type: "text",
5764
+ text: JSON.stringify(res.data ?? [], null, 2),
5765
+ }],
5766
+ };
5767
+ },
5768
+ },
5634
5769
  reorder_adoption_milestones: {
5635
5770
  description: `${APEX} — Set the sequence order of adoption milestones in one call. Pass the milestone ids in the desired order; each gets order = its index. Ordering the full set unlocks the "Path" funnel in the account view (a step-by-step drop-off chart that is only honest over an explicitly-ordered set).`,
5636
5771
  schema: z.object({