@apex-inc/mcp-server 0.40.0 → 0.41.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/README.md +1 -1
- package/dist/api-client.d.ts +21 -1
- package/dist/api-client.d.ts.map +1 -1
- package/dist/api-client.js +55 -2
- package/dist/api-client.js.map +1 -1
- package/dist/auth.js +2 -1
- package/dist/auth.js.map +1 -1
- package/dist/base-url.d.ts +8 -0
- package/dist/base-url.d.ts.map +1 -0
- package/dist/base-url.js +10 -0
- package/dist/base-url.js.map +1 -0
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -1
- package/dist/prompts.d.ts +14 -0
- package/dist/prompts.d.ts.map +1 -1
- package/dist/prompts.js +31 -6
- package/dist/prompts.js.map +1 -1
- package/dist/repo-binding.d.ts +8 -0
- package/dist/repo-binding.d.ts.map +1 -0
- package/dist/repo-binding.js +13 -0
- package/dist/repo-binding.js.map +1 -0
- package/dist/resources.d.ts +13 -0
- package/dist/resources.d.ts.map +1 -1
- package/dist/resources.js +16 -0
- package/dist/resources.js.map +1 -1
- package/dist/setup-catchup-tools.d.ts +279 -1
- package/dist/setup-catchup-tools.d.ts.map +1 -1
- package/dist/setup-catchup-tools.js +350 -24
- package/dist/setup-catchup-tools.js.map +1 -1
- package/dist/setup-checklist.d.ts +68 -0
- package/dist/setup-checklist.d.ts.map +1 -0
- package/dist/setup-checklist.js +128 -0
- package/dist/setup-checklist.js.map +1 -0
- package/dist/setup-flow.d.ts +14 -0
- package/dist/setup-flow.d.ts.map +1 -0
- package/dist/setup-flow.js +52 -0
- package/dist/setup-flow.js.map +1 -0
- package/dist/tool-groups.d.ts.map +1 -1
- package/dist/tool-groups.js +10 -0
- package/dist/tool-groups.js.map +1 -1
- package/dist/tool-shared.d.ts.map +1 -1
- package/dist/tool-shared.js +11 -4
- package/dist/tool-shared.js.map +1 -1
- package/dist/tools/ad-platforms.d.ts +1 -1
- package/dist/tools/ad-platforms.js +23 -23
- package/dist/tools/ad-platforms.js.map +1 -1
- package/dist/tools/adoption.js +2 -2
- package/dist/tools/adoption.js.map +1 -1
- package/dist/tools/context.d.ts.map +1 -1
- package/dist/tools/context.js +15 -33
- package/dist/tools/context.js.map +1 -1
- package/dist/tools/experiments.d.ts +2 -2
- package/dist/tools/experiments.d.ts.map +1 -1
- package/dist/tools/experiments.js +21 -21
- package/dist/tools/experiments.js.map +1 -1
- package/dist/tools/letters.d.ts +6 -6
- package/dist/tools/letters.js +1 -1
- package/dist/tools/letters.js.map +1 -1
- package/dist/tools/measurement.d.ts +2 -2
- package/dist/tools/measurement.d.ts.map +1 -1
- package/dist/tools/measurement.js +4 -3
- package/dist/tools/measurement.js.map +1 -1
- package/dist/tools.d.ts +307 -11
- package/dist/tools.d.ts.map +1 -1
- package/package.json +3 -2
- package/skills/apex-adoption/SKILL.md +1 -1
- package/skills/apex-communications/SKILL.md +2 -2
- package/skills/apex-experimentation/SKILL.md +6 -6
- package/skills/apex-growth-intelligence/SKILL.md +6 -6
- package/skills/apex-growth-tracking/SKILL.md +4 -4
- package/skills/apex-integration-cookbook/SKILL.md +11 -15
- package/skills/apex-journeys/SKILL.md +3 -3
- package/skills/apex-partner-network/SKILL.md +1 -1
- package/skills/apex-setup/SKILL.md +69 -0
|
@@ -4,18 +4,55 @@
|
|
|
4
4
|
* stamp the rail complete.
|
|
5
5
|
*/
|
|
6
6
|
import { z } from "zod";
|
|
7
|
-
import { apiGet, apiPatch, apiPost, apiPut, getActiveOrg, getActiveWorkspace, } from "./api-client.js";
|
|
7
|
+
import { apiGet, apiPatch, apiPost, apiPut, apiDeleteJson, getActiveOrg, getActiveWorkspaceLabel, getActiveWorkspace, refreshUserContext, setActiveWorkspace, } from "./api-client.js";
|
|
8
|
+
import { REPO_BINDING_PATH } from "./repo-binding.js";
|
|
9
|
+
import { apexBaseUrl } from "./base-url.js";
|
|
8
10
|
const APEX = "Apex";
|
|
11
|
+
/** `workspace` names where the call landed, so a wrong-workspace write shows at once. */
|
|
9
12
|
function jsonResult(data, error = null) {
|
|
13
|
+
const label = getActiveWorkspaceLabel();
|
|
10
14
|
return {
|
|
11
15
|
content: [
|
|
12
16
|
{
|
|
13
17
|
type: "text",
|
|
14
|
-
text: JSON.stringify({ data, error }, null, 2),
|
|
18
|
+
text: JSON.stringify({ data, error, ...(label ? { workspace: label } : {}) }, null, 2),
|
|
15
19
|
},
|
|
16
20
|
],
|
|
17
21
|
};
|
|
18
22
|
}
|
|
23
|
+
/** The areas Set up Apex lets an owner decline (app `PILLAR_DECLINE_LABEL`). */
|
|
24
|
+
export const DECLINABLE_AREAS = [
|
|
25
|
+
"mobile-measurement",
|
|
26
|
+
"server-events",
|
|
27
|
+
"communications",
|
|
28
|
+
"attribution",
|
|
29
|
+
"partner-network",
|
|
30
|
+
];
|
|
31
|
+
const ASK_FIRST = "Only after the owner says so in this conversation, in their words. Never call this to make setup look finished.";
|
|
32
|
+
/**
|
|
33
|
+
* After creating a workspace: re-read the org graph so the new one has names,
|
|
34
|
+
* switch to it, and hand back the binding file for this repo.
|
|
35
|
+
*/
|
|
36
|
+
async function landOnNewShop(workspaceKey, created) {
|
|
37
|
+
await refreshUserContext().catch(() => undefined);
|
|
38
|
+
setActiveWorkspace(workspaceKey);
|
|
39
|
+
const label = getActiveWorkspaceLabel();
|
|
40
|
+
return jsonResult({
|
|
41
|
+
...created,
|
|
42
|
+
repo_binding: {
|
|
43
|
+
path: REPO_BINDING_PATH,
|
|
44
|
+
contents: {
|
|
45
|
+
workspace_key: workspaceKey,
|
|
46
|
+
workspace_name: label?.workspace_name,
|
|
47
|
+
org_name: label?.org_name,
|
|
48
|
+
},
|
|
49
|
+
next: `Write this file in the repo root and commit it, then call get_setup_state.`,
|
|
50
|
+
},
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
function setupStatePath(workspaceKey, id) {
|
|
54
|
+
return `/api/workspaces/${encodeURIComponent(workspaceKey)}/setup-state/${encodeURIComponent(id)}`;
|
|
55
|
+
}
|
|
19
56
|
function resolveOrgId(orgId) {
|
|
20
57
|
return orgId?.trim() || getActiveOrg();
|
|
21
58
|
}
|
|
@@ -53,13 +90,13 @@ function scoringWeightsToApi(weights) {
|
|
|
53
90
|
}
|
|
54
91
|
export const setupCatchupTools = {
|
|
55
92
|
update_organization: {
|
|
56
|
-
description: `${APEX} — Rename the organization. Same save as Set up Apex → name your organization. Admin only.
|
|
93
|
+
description: `${APEX} — Rename the organization. Same save as Set up Apex → name your organization. Admin only. To start a separate company, use create_organization instead. The rail finishes when the name is saved — do not mark the step complete.`,
|
|
57
94
|
schema: z.object({
|
|
58
95
|
orgId: z
|
|
59
96
|
.string()
|
|
60
97
|
.optional()
|
|
61
98
|
.describe("Organization id. Defaults to the active org."),
|
|
62
|
-
name: z.string().min(1).describe("What the
|
|
99
|
+
name: z.string().min(1).describe("What the workspace should be called."),
|
|
63
100
|
}),
|
|
64
101
|
handler: async ({ orgId, name }) => {
|
|
65
102
|
const id = resolveOrgId(orgId);
|
|
@@ -71,7 +108,7 @@ export const setupCatchupTools = {
|
|
|
71
108
|
},
|
|
72
109
|
},
|
|
73
110
|
update_workspace: {
|
|
74
|
-
description: `${APEX} — Name the workspace and set vertical, conversion model, website, and app URL. One save, same as Set up Apex first-run. Omit a field to leave it unchanged. Admin only. The rail finishes from the live name / vertical / conversion model — do not mark the step complete. app_url is where the
|
|
111
|
+
description: `${APEX} — Name the workspace and set vertical, conversion model, website, how Apex installs on it, and app URL. One save, same as Set up Apex first-run. Omit a field to leave it unchanged. Admin only. The rail finishes from the live name / vertical / conversion model / website + install path — do not mark the step complete. app_url is where the workspace's customers sign in or use the product (Brand settings → App URL); letter buttons that use {{app_url}} go there.`,
|
|
75
112
|
schema: z.object({
|
|
76
113
|
workspaceName: z
|
|
77
114
|
.string()
|
|
@@ -80,7 +117,7 @@ export const setupCatchupTools = {
|
|
|
80
117
|
vertical: z
|
|
81
118
|
.enum(VERTICALS)
|
|
82
119
|
.optional()
|
|
83
|
-
.describe("What the
|
|
120
|
+
.describe("What the organization does (b2b-saas, ecommerce, marketplace, agency, other)."),
|
|
84
121
|
conversionModel: z
|
|
85
122
|
.enum(CONVERSION_MODELS)
|
|
86
123
|
.optional()
|
|
@@ -88,14 +125,18 @@ export const setupCatchupTools = {
|
|
|
88
125
|
websiteUrl: z
|
|
89
126
|
.string()
|
|
90
127
|
.optional()
|
|
91
|
-
.describe("The
|
|
128
|
+
.describe("The workspace's website URL."),
|
|
129
|
+
siteInstall: z
|
|
130
|
+
.enum(["codebase", "snippet"])
|
|
131
|
+
.optional()
|
|
132
|
+
.describe("How Apex goes on the site: codebase (packages in this repo) or snippet (pasted tag, for sites you cannot reach in code). Pick codebase when you are installing from the repo."),
|
|
92
133
|
appUrl: z
|
|
93
134
|
.string()
|
|
94
135
|
.url()
|
|
95
136
|
.optional()
|
|
96
|
-
.describe("Where the
|
|
137
|
+
.describe("Where the workspace's customers sign in or use the product, e.g. https://app.example.com. Letter buttons using {{app_url}} go here."),
|
|
97
138
|
}),
|
|
98
|
-
handler: async ({ workspaceName, vertical, conversionModel, websiteUrl, appUrl, }) => {
|
|
139
|
+
handler: async ({ workspaceName, vertical, conversionModel, websiteUrl, siteInstall, appUrl, }) => {
|
|
99
140
|
const body = {};
|
|
100
141
|
if (workspaceName?.trim())
|
|
101
142
|
body.name = workspaceName.trim();
|
|
@@ -105,8 +146,10 @@ export const setupCatchupTools = {
|
|
|
105
146
|
body.conversionModel = conversionModel;
|
|
106
147
|
if (websiteUrl !== undefined)
|
|
107
148
|
body.url = websiteUrl.trim();
|
|
149
|
+
if (siteInstall)
|
|
150
|
+
body.siteDeploymentModel = siteInstall;
|
|
108
151
|
if (Object.keys(body).length === 0 && !appUrl?.trim()) {
|
|
109
|
-
return jsonResult(null, "Provide at least one of workspace_name, vertical, conversion_model, website_url, app_url.");
|
|
152
|
+
return jsonResult(null, "Provide at least one of workspace_name, vertical, conversion_model, website_url, site_install, app_url.");
|
|
110
153
|
}
|
|
111
154
|
const workspaceKey = getActiveWorkspace();
|
|
112
155
|
if (workspaceName?.trim() && workspaceKey) {
|
|
@@ -129,8 +172,136 @@ export const setupCatchupTools = {
|
|
|
129
172
|
return jsonResult(data);
|
|
130
173
|
},
|
|
131
174
|
},
|
|
175
|
+
create_workspace: {
|
|
176
|
+
description: `${APEX} — A new workspace inside one of the owner's organizations: another site, app, or product of the same business. For a separate company use create_organization instead; ask "separate company, or another site of the same business?" first. Switches to the new workspace and returns the ${REPO_BINDING_PATH} contents to commit in this repo. Org owner or admin only.`,
|
|
177
|
+
schema: z.object({
|
|
178
|
+
orgId: z
|
|
179
|
+
.string()
|
|
180
|
+
.optional()
|
|
181
|
+
.describe("Organization it joins (from list_orgs). Defaults to the active org."),
|
|
182
|
+
name: z.string().min(1).max(100).describe("What the owner calls this site or product."),
|
|
183
|
+
websiteUrl: z.string().optional().describe("Its website, if it has one."),
|
|
184
|
+
vertical: z.enum(VERTICALS).optional(),
|
|
185
|
+
conversionModel: z.enum(CONVERSION_MODELS).optional(),
|
|
186
|
+
}),
|
|
187
|
+
handler: async ({ orgId, name, websiteUrl, vertical, conversionModel, }) => {
|
|
188
|
+
const id = resolveOrgId(orgId);
|
|
189
|
+
if (!id) {
|
|
190
|
+
return jsonResult(null, "No active organization. Pass org_id from list_orgs.");
|
|
191
|
+
}
|
|
192
|
+
const res = await apiPost("/api/workspaces", {
|
|
193
|
+
orgId: id,
|
|
194
|
+
name: name.trim(),
|
|
195
|
+
...(websiteUrl?.trim() ? { url: websiteUrl.trim() } : {}),
|
|
196
|
+
...(vertical ? { vertical } : {}),
|
|
197
|
+
...(conversionModel ? { conversionModel } : {}),
|
|
198
|
+
});
|
|
199
|
+
return landOnNewShop(res.workspace.workspaceKey, { workspace: res.workspace });
|
|
200
|
+
},
|
|
201
|
+
},
|
|
202
|
+
create_organization: {
|
|
203
|
+
description: `${APEX} — A new organization for a separate company, with its own team and billing and one production workspace. The owner of this sign-in becomes its owner. Only when the owner says this repo is a separate company; for another site of the same business use create_workspace. Needs the person's own sign-in (browser approval), not a workspace key. Switches to the new workspace and returns the ${REPO_BINDING_PATH} contents to commit in this repo.`,
|
|
204
|
+
schema: z.object({
|
|
205
|
+
name: z.string().min(1).max(100).describe("The company's name."),
|
|
206
|
+
workspaceName: z
|
|
207
|
+
.string()
|
|
208
|
+
.max(100)
|
|
209
|
+
.optional()
|
|
210
|
+
.describe("Name for its first workspace. Defaults to '<company> Workspace'."),
|
|
211
|
+
}),
|
|
212
|
+
handler: async ({ name, workspaceName }) => {
|
|
213
|
+
const res = await apiPost("/api/orgs", {
|
|
214
|
+
name: name.trim(),
|
|
215
|
+
...(workspaceName?.trim() ? { workspaceName: workspaceName.trim() } : {}),
|
|
216
|
+
});
|
|
217
|
+
const { org, workspace } = res.data;
|
|
218
|
+
return landOnNewShop(workspace.workspaceKey, { org, workspace });
|
|
219
|
+
},
|
|
220
|
+
},
|
|
221
|
+
skip_setup_step: {
|
|
222
|
+
description: `${APEX} — Record the owner's "not now" or "not for us" on one Set up Apex step. ${ASK_FIRST} snooze: true puts it off and it comes back on its own; otherwise it is skipped until someone undoes it. This never marks the step done, and the checklist shows it as skipped, not finished. Admin only. Use the step id from get_setup_state.`,
|
|
223
|
+
schema: z.object({
|
|
224
|
+
stepId: z
|
|
225
|
+
.string()
|
|
226
|
+
.min(1)
|
|
227
|
+
.describe("Step id from get_setup_state, e.g. mobile-measurement:push."),
|
|
228
|
+
reason: z
|
|
229
|
+
.string()
|
|
230
|
+
.min(1)
|
|
231
|
+
.max(500)
|
|
232
|
+
.describe("Why, in the owner's words."),
|
|
233
|
+
snooze: z
|
|
234
|
+
.boolean()
|
|
235
|
+
.optional()
|
|
236
|
+
.describe("true = remind them later (Apex picks the date). Omit to skip."),
|
|
237
|
+
}),
|
|
238
|
+
handler: async ({ stepId, reason, snooze, }) => {
|
|
239
|
+
const workspaceKey = getActiveWorkspace();
|
|
240
|
+
if (!workspaceKey) {
|
|
241
|
+
return jsonResult(null, "No active workspace. Call switch_workspace first.");
|
|
242
|
+
}
|
|
243
|
+
const data = await apiPost(setupStatePath(workspaceKey, stepId.trim()), {
|
|
244
|
+
action: snooze ? "snooze" : "skip",
|
|
245
|
+
reason: reason.trim(),
|
|
246
|
+
});
|
|
247
|
+
return jsonResult(data);
|
|
248
|
+
},
|
|
249
|
+
},
|
|
250
|
+
decline_setup_area: {
|
|
251
|
+
description: `${APEX} — Record that a whole area of Set up Apex does not apply to this workspace: no app, no server events, no messages from Apex, no ad platforms, or no partners. ${ASK_FIRST} The area's steps leave the checklist; nothing in it is marked done. Admin only.`,
|
|
252
|
+
schema: z.object({
|
|
253
|
+
area: z
|
|
254
|
+
.enum(DECLINABLE_AREAS)
|
|
255
|
+
.describe("mobile-measurement (no app), server-events, communications, attribution (no ad platforms), partner-network."),
|
|
256
|
+
reason: z
|
|
257
|
+
.string()
|
|
258
|
+
.min(1)
|
|
259
|
+
.max(500)
|
|
260
|
+
.describe("Why, in the owner's words."),
|
|
261
|
+
}),
|
|
262
|
+
handler: async ({ area, reason, }) => {
|
|
263
|
+
const workspaceKey = getActiveWorkspace();
|
|
264
|
+
if (!workspaceKey) {
|
|
265
|
+
return jsonResult(null, "No active workspace. Call switch_workspace first.");
|
|
266
|
+
}
|
|
267
|
+
const data = await apiPost(setupStatePath(workspaceKey, area), {
|
|
268
|
+
action: "decline",
|
|
269
|
+
reason: reason.trim(),
|
|
270
|
+
});
|
|
271
|
+
return jsonResult(data);
|
|
272
|
+
},
|
|
273
|
+
},
|
|
274
|
+
undo_setup_answer: {
|
|
275
|
+
description: `${APEX} — Put a step or area back on the checklist: undo a skip, snooze, decline, or a step a person ticked done. Only when the owner asks for that step by name; never to tidy up. A step Apex can see is done (events arriving, domain verified) stays done anyway. Admin only.`,
|
|
276
|
+
schema: z.object({
|
|
277
|
+
stepId: z
|
|
278
|
+
.string()
|
|
279
|
+
.min(1)
|
|
280
|
+
.describe("The step id or area id that was skipped, snoozed, declined, or ticked done."),
|
|
281
|
+
}),
|
|
282
|
+
handler: async ({ stepId }) => {
|
|
283
|
+
const workspaceKey = getActiveWorkspace();
|
|
284
|
+
if (!workspaceKey) {
|
|
285
|
+
return jsonResult(null, "No active workspace. Call switch_workspace first.");
|
|
286
|
+
}
|
|
287
|
+
const path = setupStatePath(workspaceKey, stepId.trim());
|
|
288
|
+
const current = await apiGet(path);
|
|
289
|
+
const action = current?.data?.override?.action;
|
|
290
|
+
if (!action) {
|
|
291
|
+
return jsonResult(null, "Nothing to undo: no skip, snooze, decline, or tick is recorded for that id.");
|
|
292
|
+
}
|
|
293
|
+
const data = await apiDeleteJson(path);
|
|
294
|
+
return jsonResult({
|
|
295
|
+
undone: action,
|
|
296
|
+
...(action === "complete" && {
|
|
297
|
+
note: "Unticked. If Apex can see this step is done on its own, it will still show done. Re-read get_setup_state.",
|
|
298
|
+
}),
|
|
299
|
+
...data,
|
|
300
|
+
});
|
|
301
|
+
},
|
|
302
|
+
},
|
|
132
303
|
claim_links_subdomain: {
|
|
133
|
-
description: `${APEX} — Claim the
|
|
304
|
+
description: `${APEX} — Claim the workspace's links subdomain (name.links.apex.inc). Same save as Set up Apex → claim links subdomain. Admin only. Fails if another workspace already has that name.`,
|
|
134
305
|
schema: z.object({
|
|
135
306
|
subdomain: z
|
|
136
307
|
.string()
|
|
@@ -145,11 +316,11 @@ export const setupCatchupTools = {
|
|
|
145
316
|
},
|
|
146
317
|
},
|
|
147
318
|
set_consent_posture: {
|
|
148
|
-
description: `${APEX} — Save the
|
|
319
|
+
description: `${APEX} — Save the workspace's consent-form answers (do they have a box, which kind). Does NOT attest outbound sending and does NOT fake a live-site tap. The merchant still has to tap Allow on their site. Admin only.`,
|
|
149
320
|
schema: z.object({
|
|
150
321
|
hasForm: z
|
|
151
322
|
.enum(["yes", "no"])
|
|
152
|
-
.describe("Whether the
|
|
323
|
+
.describe("Whether the website already shows a consent box."),
|
|
153
324
|
banner: z
|
|
154
325
|
.enum(CONSENT_BANNERS)
|
|
155
326
|
.optional()
|
|
@@ -251,7 +422,7 @@ export const setupCatchupTools = {
|
|
|
251
422
|
},
|
|
252
423
|
},
|
|
253
424
|
get_scoring_model: {
|
|
254
|
-
description: `${APEX} — Read the active scoring weights. If the
|
|
425
|
+
description: `${APEX} — Read the active scoring weights. If the workspace has not saved a model yet, this returns that the vertical preset is still in use — not an error.`,
|
|
255
426
|
schema: z.object({}),
|
|
256
427
|
handler: async () => {
|
|
257
428
|
try {
|
|
@@ -525,6 +696,164 @@ export const setupCatchupTools = {
|
|
|
525
696
|
return jsonResult(data);
|
|
526
697
|
},
|
|
527
698
|
},
|
|
699
|
+
set_app_links: {
|
|
700
|
+
description: `${APEX} — Save the iOS and/or Android app ids Apex needs to publish the Universal Links and App Links files on the workspace's links subdomain, then check the published files. Same save as Universal Links and App Links in Set up Apex. Read the ids from the repo (Xcode project, build.gradle) when you can. Returns the Associated Domains line to add to the iOS entitlements. Admin only.`,
|
|
701
|
+
schema: z.object({
|
|
702
|
+
ios: z
|
|
703
|
+
.object({
|
|
704
|
+
bundle_id: z.string().min(1).describe("e.g. com.acme.app"),
|
|
705
|
+
team_id: z.string().min(1).describe("10-character Apple Team ID."),
|
|
706
|
+
})
|
|
707
|
+
.optional(),
|
|
708
|
+
android: z
|
|
709
|
+
.object({
|
|
710
|
+
package_name: z.string().min(1).describe("applicationId, e.g. com.acme.app"),
|
|
711
|
+
sha256_cert_fingerprint: z
|
|
712
|
+
.string()
|
|
713
|
+
.min(1)
|
|
714
|
+
.describe("Play app signing SHA-256 fingerprint (colons optional)."),
|
|
715
|
+
})
|
|
716
|
+
.optional(),
|
|
717
|
+
}),
|
|
718
|
+
handler: async (args) => {
|
|
719
|
+
if (!args.ios && !args.android) {
|
|
720
|
+
return jsonResult(null, "Pass ios, android, or both.");
|
|
721
|
+
}
|
|
722
|
+
const current = await apiGet("/api/config");
|
|
723
|
+
const existing = current.mobileAppConfig ?? {};
|
|
724
|
+
const ios = args.ios
|
|
725
|
+
? { bundleId: args.ios.bundle_id.trim(), teamId: args.ios.team_id.trim().toUpperCase() }
|
|
726
|
+
: null;
|
|
727
|
+
const android = args.android
|
|
728
|
+
? {
|
|
729
|
+
packageName: args.android.package_name.trim(),
|
|
730
|
+
sha256CertFingerprint: args.android.sha256_cert_fingerprint.trim().toUpperCase(),
|
|
731
|
+
}
|
|
732
|
+
: null;
|
|
733
|
+
await apiPatch("/api/config", {
|
|
734
|
+
mobileAppConfig: { ...existing, ...(ios ?? {}), ...(android ?? {}) },
|
|
735
|
+
});
|
|
736
|
+
const subdomain = current.linkSubdomain ?? existing.linkSubdomain;
|
|
737
|
+
if (!subdomain) {
|
|
738
|
+
return jsonResult({
|
|
739
|
+
saved: { ios: Boolean(ios), android: Boolean(android) },
|
|
740
|
+
next: "Claim a links subdomain with claim_links_subdomain. Apex publishes the files on that host.",
|
|
741
|
+
});
|
|
742
|
+
}
|
|
743
|
+
const check = async (path, params) => {
|
|
744
|
+
try {
|
|
745
|
+
return await apiGet(`${path}?${new URLSearchParams(params)}`);
|
|
746
|
+
}
|
|
747
|
+
catch {
|
|
748
|
+
return { ok: false, propagating: true, message: "Could not reach the check. Try again in a minute." };
|
|
749
|
+
}
|
|
750
|
+
};
|
|
751
|
+
const [iosCheck, androidCheck] = await Promise.all([
|
|
752
|
+
ios
|
|
753
|
+
? check("/api/onboarding/verify-aasa", { subdomain, bundleId: ios.bundleId })
|
|
754
|
+
: null,
|
|
755
|
+
android
|
|
756
|
+
? check("/api/onboarding/verify-assetlinks", {
|
|
757
|
+
subdomain,
|
|
758
|
+
packageName: android.packageName,
|
|
759
|
+
fingerprint: android.sha256CertFingerprint,
|
|
760
|
+
})
|
|
761
|
+
: null,
|
|
762
|
+
]);
|
|
763
|
+
const host = iosCheck?.host ?? androidCheck?.host;
|
|
764
|
+
return jsonResult({
|
|
765
|
+
saved: { ios: Boolean(ios), android: Boolean(android) },
|
|
766
|
+
links_host: host ?? null,
|
|
767
|
+
ios: iosCheck
|
|
768
|
+
? {
|
|
769
|
+
live: Boolean(iosCheck.ok),
|
|
770
|
+
still_publishing: Boolean(iosCheck.propagating),
|
|
771
|
+
message: iosCheck.message ?? null,
|
|
772
|
+
associated_domain: host ? `applinks:${host}` : null,
|
|
773
|
+
}
|
|
774
|
+
: null,
|
|
775
|
+
android: androidCheck
|
|
776
|
+
? {
|
|
777
|
+
live: Boolean(androidCheck.ok),
|
|
778
|
+
still_publishing: Boolean(androidCheck.propagating),
|
|
779
|
+
message: androidCheck.message ?? null,
|
|
780
|
+
intent_filter_host: host ?? null,
|
|
781
|
+
}
|
|
782
|
+
: null,
|
|
783
|
+
});
|
|
784
|
+
},
|
|
785
|
+
},
|
|
786
|
+
set_smart_banner: {
|
|
787
|
+
description: `${APEX} — Turn on or edit the Smart Banner, the "get the app" bar Apex shows phone visitors on the workspace's website. Same save as Smart Banner in Set up Apex. Turning it on shows it to real visitors right away, so ask the owner first. Only the fields you pass change. Admin only.`,
|
|
788
|
+
schema: z.object({
|
|
789
|
+
enabled: z.boolean().optional(),
|
|
790
|
+
title: z.string().optional().describe('Short headline, e.g. "Acme is better in the app".'),
|
|
791
|
+
subtitle: z.string().optional(),
|
|
792
|
+
ctaLabel: z.string().optional().describe('Button label. Default "Get the app".'),
|
|
793
|
+
ctaColor: z.string().optional().describe("Button color as #rgb or #rrggbb."),
|
|
794
|
+
destinationSlug: z
|
|
795
|
+
.string()
|
|
796
|
+
.optional()
|
|
797
|
+
.describe("Apex Link slug the button opens, e.g. get-app."),
|
|
798
|
+
dismissDays: z
|
|
799
|
+
.number()
|
|
800
|
+
.int()
|
|
801
|
+
.min(0)
|
|
802
|
+
.optional()
|
|
803
|
+
.describe("Days a dismissal lasts. 0 = show on every page."),
|
|
804
|
+
position: z.enum(["top", "bottom"]).optional(),
|
|
805
|
+
}),
|
|
806
|
+
handler: async (args) => {
|
|
807
|
+
if (args.ctaColor && !/^#([0-9a-f]{3}|[0-9a-f]{6})$/i.test(args.ctaColor)) {
|
|
808
|
+
return jsonResult(null, "cta_color must be a hex color like #16a34a.");
|
|
809
|
+
}
|
|
810
|
+
const current = await apiGet("/api/config");
|
|
811
|
+
const existing = current.smartBanner ?? { enabled: false };
|
|
812
|
+
const changes = Object.fromEntries(Object.entries(args).filter(([, v]) => v !== undefined));
|
|
813
|
+
const data = await apiPatch("/api/config", {
|
|
814
|
+
smartBanner: { ...existing, ...changes },
|
|
815
|
+
});
|
|
816
|
+
return jsonResult(data);
|
|
817
|
+
},
|
|
818
|
+
},
|
|
819
|
+
list_transactional_api_keys: {
|
|
820
|
+
description: `${APEX} — List the workspace's receipt keys (the keys its server uses to send receipts and other transactional messages). Names and ids only, never the secret.`,
|
|
821
|
+
schema: z.object({}),
|
|
822
|
+
handler: async () => {
|
|
823
|
+
const data = await apiGet("/api/transactional-api-keys");
|
|
824
|
+
return jsonResult(data);
|
|
825
|
+
},
|
|
826
|
+
},
|
|
827
|
+
create_transactional_api_key: {
|
|
828
|
+
description: `${APEX} — Mint a receipt key so the workspace's server can send receipts and other transactional messages. Same as the receipt-key step in Set up Apex. Returns the secret ONCE: put it in the server's secret store or env (never in a committed file) and do not repeat it in chat. Call list_transactional_api_keys first so you do not mint a duplicate. Admin only.`,
|
|
829
|
+
schema: z.object({
|
|
830
|
+
name: z.string().optional().describe('A label, e.g. "Production server".'),
|
|
831
|
+
}),
|
|
832
|
+
handler: async ({ name }) => {
|
|
833
|
+
const res = await apiPost("/api/transactional-api-keys", { name: name?.trim() || "Transactional" });
|
|
834
|
+
if (!res.rawKey)
|
|
835
|
+
return jsonResult(null, "Apex did not return a key.");
|
|
836
|
+
return jsonResult({
|
|
837
|
+
id: res.id ?? null,
|
|
838
|
+
name: res.name ?? null,
|
|
839
|
+
raw_key: res.rawKey,
|
|
840
|
+
shown_once: true,
|
|
841
|
+
});
|
|
842
|
+
},
|
|
843
|
+
},
|
|
844
|
+
connect_microsoft_clarity: {
|
|
845
|
+
description: `${APEX} — Connect Microsoft Clarity with a Data Export token. The owner makes the token in Clarity (Settings, then Data Export) and pastes it to you; never invent one. Same save as Connect Microsoft Clarity in Set up Apex. The first sync can take a while because Clarity allows ten exports a day. Admin only.`,
|
|
846
|
+
schema: z.object({
|
|
847
|
+
apiToken: z.string().min(11).describe("The Clarity Data Export token."),
|
|
848
|
+
}),
|
|
849
|
+
handler: async ({ apiToken }) => {
|
|
850
|
+
const data = await apiPost("/api/integrations", {
|
|
851
|
+
type: "microsoft-clarity",
|
|
852
|
+
credentials: { api_key: apiToken.trim() },
|
|
853
|
+
});
|
|
854
|
+
return jsonResult(data);
|
|
855
|
+
},
|
|
856
|
+
},
|
|
528
857
|
set_protect_auto_pause: {
|
|
529
858
|
description: `${APEX} — Turn on or off auto-pause when a protected metric is hurt. Same switch as Protect your experiments in Set up Apex. Admin only.`,
|
|
530
859
|
schema: z.object({
|
|
@@ -540,7 +869,7 @@ export const setupCatchupTools = {
|
|
|
540
869
|
},
|
|
541
870
|
},
|
|
542
871
|
list_apex_links: {
|
|
543
|
-
description: `${APEX} — List this
|
|
872
|
+
description: `${APEX} — List this workspace's Apex Links. After ads are connected, make a paid Apex Link before inventing UTMs.`,
|
|
544
873
|
schema: z.object({}),
|
|
545
874
|
handler: async () => {
|
|
546
875
|
const data = await apiGet("/api/mobile/links");
|
|
@@ -558,7 +887,7 @@ export const setupCatchupTools = {
|
|
|
558
887
|
.string()
|
|
559
888
|
.optional()
|
|
560
889
|
.describe("URL slug. Derived from name or the destination if omitted."),
|
|
561
|
-
name: z.string().optional().describe("What the
|
|
890
|
+
name: z.string().optional().describe("What the owner calls this link."),
|
|
562
891
|
campaignName: z.string().optional().describe("Campaign name shared by links in this spend."),
|
|
563
892
|
variant: z.string().optional().describe("Creative or ad variant."),
|
|
564
893
|
}),
|
|
@@ -587,12 +916,12 @@ export const setupCatchupTools = {
|
|
|
587
916
|
},
|
|
588
917
|
},
|
|
589
918
|
get_skan_schema: {
|
|
590
|
-
description: `${APEX} — Read what Apple's SKAdNetwork conversion values (0–63) mean for this
|
|
919
|
+
description: `${APEX} — Read what Apple's SKAdNetwork conversion values (0–63) mean for this workspace's iOS app: which event is "worth it", which is "showed interest", and what Meta and Google read back as low / medium / high. \`schema\` is null until the workspace has named its values (Set up Apex → Mobile → "Say what Apple's numbers mean"). Omit mobile_app_id for the workspace's first app.`,
|
|
591
920
|
schema: z.object({
|
|
592
921
|
mobileAppId: z
|
|
593
922
|
.string()
|
|
594
923
|
.optional()
|
|
595
|
-
.describe("Which iOS app. Omit for the
|
|
924
|
+
.describe("Which iOS app. Omit for the workspace's first app."),
|
|
596
925
|
}),
|
|
597
926
|
handler: async (args) => {
|
|
598
927
|
const q = args.mobileAppId
|
|
@@ -603,12 +932,12 @@ export const setupCatchupTools = {
|
|
|
603
932
|
},
|
|
604
933
|
},
|
|
605
934
|
set_skan_schema: {
|
|
606
|
-
description: `${APEX} — Replace the SKAdNetwork conversion value schema for one iOS app (same save as the "Say what Apple's numbers mean" step; the Capacitor plugin picks it up on the device within a day). Prefer the setup step's two picks unless the
|
|
935
|
+
description: `${APEX} — Replace the SKAdNetwork conversion value schema for one iOS app (same save as the "Say what Apple's numbers mean" step; the Capacitor plugin picks it up on the device within a day). Prefer the setup step's two picks unless the owner asked for specific values. Fine values are 0–63, one per event per window; coarse values are low / medium / high; fine 0 is reserved for install and 1 for app_open. Apex validates and returns \`problems\` with a path per fault. Admin only. The setup step finishes when the schema maps the worth-it event — do not mark it complete.`,
|
|
607
936
|
schema: z.object({
|
|
608
937
|
mobileAppId: z
|
|
609
938
|
.string()
|
|
610
939
|
.optional()
|
|
611
|
-
.describe("Which iOS app. Omit for the
|
|
940
|
+
.describe("Which iOS app. Omit for the workspace's first app."),
|
|
612
941
|
windows: z
|
|
613
942
|
.array(z.object({
|
|
614
943
|
window: z
|
|
@@ -814,9 +1143,6 @@ function hostHint(url) {
|
|
|
814
1143
|
}
|
|
815
1144
|
}
|
|
816
1145
|
export function activateConversionsUrl() {
|
|
817
|
-
|
|
818
|
-
process.env.APEX_API_URL ||
|
|
819
|
-
"http://localhost:3001").replace(/\/$/, "");
|
|
820
|
-
return `${base}/dashboard?setup=attribution&step=attribution:activate-conversions&mode=fullscreen`;
|
|
1146
|
+
return `${apexBaseUrl()}/dashboard?setup=attribution&step=attribution:activate-conversions&mode=fullscreen`;
|
|
821
1147
|
}
|
|
822
1148
|
//# sourceMappingURL=setup-catchup-tools.js.map
|