@sellable/mcp 0.1.110 → 0.1.111
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/campaigns.js +5 -4
- package/dist/tools/context.d.ts +1 -1
- package/dist/tools/context.js +1 -1
- package/dist/tools/leads.d.ts +5 -0
- package/dist/tools/leads.js +66 -3
- package/dist/tools/navigation.d.ts +2 -2
- package/dist/tools/navigation.js +27 -14
- package/dist/tools/readiness.d.ts +3 -3
- package/dist/tools/watch-url-security.js +18 -3
- package/package.json +1 -1
- package/skills/create-campaign/SKILL.md +10 -10
- package/skills/create-campaign-v2/core/flow.v2.json +1 -1
- package/skills/create-campaign-v2/references/watch-link-handoff.md +10 -10
package/dist/tools/campaigns.js
CHANGED
|
@@ -21,10 +21,11 @@ function normalizeLeadSourceProvider(input) {
|
|
|
21
21
|
}
|
|
22
22
|
export function buildWatchUrl(config, path) {
|
|
23
23
|
const workspaceId = config.activeWorkspaceId || config.workspaceId;
|
|
24
|
-
const
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
24
|
+
const url = new URL(path, config.apiUrl);
|
|
25
|
+
if (workspaceId && !url.searchParams.has("workspaceId")) {
|
|
26
|
+
url.searchParams.set("workspaceId", workspaceId);
|
|
27
|
+
}
|
|
28
|
+
return url.toString();
|
|
28
29
|
}
|
|
29
30
|
function isLinkedInProfileUrl(input) {
|
|
30
31
|
try {
|
package/dist/tools/context.d.ts
CHANGED
package/dist/tools/context.js
CHANGED
package/dist/tools/leads.d.ts
CHANGED
|
@@ -2460,6 +2460,11 @@ export declare function confirmLeadList(input: ConfirmLeadListInput): Promise<{
|
|
|
2460
2460
|
rowIds?: string[];
|
|
2461
2461
|
requestedTargetLeadCount?: number;
|
|
2462
2462
|
sourceRowLimit?: number;
|
|
2463
|
+
async?: boolean;
|
|
2464
|
+
campaignTableReady?: boolean;
|
|
2465
|
+
importStatus?: string | null;
|
|
2466
|
+
remainingRowCount?: number | null;
|
|
2467
|
+
rowCount?: number | null;
|
|
2463
2468
|
};
|
|
2464
2469
|
boundedReviewBatch: {
|
|
2465
2470
|
requestedTargetLeadCount: number;
|
package/dist/tools/leads.js
CHANGED
|
@@ -2199,7 +2199,30 @@ export async function confirmLeadList(input) {
|
|
|
2199
2199
|
if (!resolvedLeadListId) {
|
|
2200
2200
|
throw new Error("confirm_lead_list requires sourceLeadListId");
|
|
2201
2201
|
}
|
|
2202
|
-
const
|
|
2202
|
+
const safePostConfirmCurrentSteps = new Set([
|
|
2203
|
+
"filter-choice",
|
|
2204
|
+
"create-icp-rubric",
|
|
2205
|
+
"apply-icp-rubric",
|
|
2206
|
+
"validate-sample",
|
|
2207
|
+
"auto-execute-leads",
|
|
2208
|
+
"messages",
|
|
2209
|
+
"auto-execute-messaging",
|
|
2210
|
+
"awaiting-user-greenlight",
|
|
2211
|
+
"settings",
|
|
2212
|
+
"sequence",
|
|
2213
|
+
"send",
|
|
2214
|
+
"claude-greenlight",
|
|
2215
|
+
"running",
|
|
2216
|
+
]);
|
|
2217
|
+
const effectiveCurrentStep = currentStep === undefined
|
|
2218
|
+
? "filter-choice"
|
|
2219
|
+
: typeof currentStep === "string" &&
|
|
2220
|
+
currentStep.length > 0 &&
|
|
2221
|
+
safePostConfirmCurrentSteps.has(currentStep)
|
|
2222
|
+
? currentStep
|
|
2223
|
+
: currentStep === null
|
|
2224
|
+
? null
|
|
2225
|
+
: "filter-choice";
|
|
2203
2226
|
const shouldSetCurrentStep = typeof effectiveCurrentStep === "string" && effectiveCurrentStep.length > 0;
|
|
2204
2227
|
let readiness;
|
|
2205
2228
|
const leadListMeta = await api.get(`/api/v3/workflow-tables/${resolvedLeadListId}?mode=meta`);
|
|
@@ -2257,13 +2280,27 @@ export async function confirmLeadList(input) {
|
|
|
2257
2280
|
}
|
|
2258
2281
|
throw new Error("Import still in progress. Keep the campaign at confirm-lead-list; retry readiness, cancel the import, or re-run-source before launching post-import scouts.");
|
|
2259
2282
|
}
|
|
2260
|
-
const
|
|
2283
|
+
const isTerminalAccessError = (error) => error instanceof SellableApiError && [401, 403, 404].includes(error.status);
|
|
2284
|
+
const formatTerminalAccessError = (error) => {
|
|
2285
|
+
if (error.status === 401 || error.status === 403) {
|
|
2286
|
+
return "Campaign or lead-list access failed. Fix workspace/auth access before retrying confirm_lead_list.";
|
|
2287
|
+
}
|
|
2288
|
+
return "Campaign or source lead list could not be found. Re-open the correct campaign or reselect the source list before retrying confirm_lead_list.";
|
|
2289
|
+
};
|
|
2290
|
+
const importResult = await api
|
|
2291
|
+
.post(`/api/v3/campaign-builder/import-leads`, {
|
|
2261
2292
|
sourceLeadListId: resolvedLeadListId,
|
|
2262
2293
|
campaignOfferId,
|
|
2263
2294
|
campaignName,
|
|
2264
2295
|
keepInSync,
|
|
2265
2296
|
...(typeof targetLeadCount === "number" ? { targetLeadCount } : {}),
|
|
2266
2297
|
...(shouldSetCurrentStep ? { currentStep: effectiveCurrentStep } : {}),
|
|
2298
|
+
})
|
|
2299
|
+
.catch((error) => {
|
|
2300
|
+
if (isTerminalAccessError(error)) {
|
|
2301
|
+
throw new Error(formatTerminalAccessError(error));
|
|
2302
|
+
}
|
|
2303
|
+
throw error;
|
|
2267
2304
|
});
|
|
2268
2305
|
const campaignTableId = importResult.workflowTableId ?? importResult.campaignTableId;
|
|
2269
2306
|
const requestedTargetLeadCount = typeof targetLeadCount === "number" && Number.isFinite(targetLeadCount)
|
|
@@ -2275,6 +2312,29 @@ export async function confirmLeadList(input) {
|
|
|
2275
2312
|
const overflowRowIds = campaignTableId && requestedTargetLeadCount !== null
|
|
2276
2313
|
? importedRowIds.slice(requestedTargetLeadCount)
|
|
2277
2314
|
: [];
|
|
2315
|
+
const importedRowCount = importedRowIds.length > 0
|
|
2316
|
+
? importedRowIds.length
|
|
2317
|
+
: typeof importResult.rowCount === "number"
|
|
2318
|
+
? importResult.rowCount
|
|
2319
|
+
: typeof importResult.leadsImported === "number"
|
|
2320
|
+
? importResult.leadsImported
|
|
2321
|
+
: 0;
|
|
2322
|
+
const remainingRowCount = typeof importResult.remainingRowCount === "number"
|
|
2323
|
+
? importResult.remainingRowCount
|
|
2324
|
+
: 0;
|
|
2325
|
+
const importStatus = String(importResult.importStatus ?? "").toLowerCase();
|
|
2326
|
+
const campaignTableReady = importResult.campaignTableReady === true ||
|
|
2327
|
+
(!importResult.async &&
|
|
2328
|
+
importStatus !== "pending" &&
|
|
2329
|
+
importStatus !== "partial" &&
|
|
2330
|
+
remainingRowCount <= 0 &&
|
|
2331
|
+
importedRowCount > 0);
|
|
2332
|
+
if (!campaignTableReady) {
|
|
2333
|
+
throw new Error("Campaign review rows are still importing. Stay on lead import until the bounded campaign table is ready, then retry confirm_lead_list or wait_for_campaign_table_ready.");
|
|
2334
|
+
}
|
|
2335
|
+
if (importedRowCount <= 0) {
|
|
2336
|
+
throw new Error("No usable review rows were kept for the campaign table. Tighten or change the source before continuing.");
|
|
2337
|
+
}
|
|
2278
2338
|
if (campaignTableId && overflowRowIds.length > 0) {
|
|
2279
2339
|
const deleteBatchSize = 25;
|
|
2280
2340
|
for (let index = 0; index < overflowRowIds.length; index += deleteBatchSize) {
|
|
@@ -2310,7 +2370,10 @@ export async function confirmLeadList(input) {
|
|
|
2310
2370
|
trimmedOverflowRowCount: overflowRowIds.length,
|
|
2311
2371
|
}
|
|
2312
2372
|
: undefined,
|
|
2313
|
-
message:
|
|
2373
|
+
message: requestedTargetLeadCount !== null &&
|
|
2374
|
+
leadListRowCount > requestedTargetLeadCount
|
|
2375
|
+
? `I found ${leadListRowCount} source candidates and imported the first ${Math.min(importedRowCount, requestedTargetLeadCount)} into the campaign review table.`
|
|
2376
|
+
: "Lead list imported into the campaign review table. Next: sample rows with get_rows_minimal or continue into filter work.",
|
|
2314
2377
|
};
|
|
2315
2378
|
}
|
|
2316
2379
|
export function getProviderPrompt(input) {
|
|
@@ -81,7 +81,7 @@ export declare function computeCampaignNavigationStateFromCampaign(campaign: Cam
|
|
|
81
81
|
};
|
|
82
82
|
watchUrl: {
|
|
83
83
|
urlPresent: boolean;
|
|
84
|
-
|
|
84
|
+
direct: boolean | undefined;
|
|
85
85
|
redirectPath: string | null;
|
|
86
86
|
};
|
|
87
87
|
table: {
|
|
@@ -125,7 +125,7 @@ export declare function getCampaignNavigationState(input: GetCampaignNavigationS
|
|
|
125
125
|
};
|
|
126
126
|
watchUrl: {
|
|
127
127
|
urlPresent: boolean;
|
|
128
|
-
|
|
128
|
+
direct: boolean | undefined;
|
|
129
129
|
redirectPath: string | null;
|
|
130
130
|
};
|
|
131
131
|
table: {
|
package/dist/tools/navigation.js
CHANGED
|
@@ -281,9 +281,9 @@ function mapHeadlessToUiStep(step) {
|
|
|
281
281
|
return "signal-discovery";
|
|
282
282
|
if (step === "filter-choice")
|
|
283
283
|
return "filter-choice";
|
|
284
|
-
if (step === "create-icp-rubric"
|
|
285
|
-
|
|
286
|
-
|
|
284
|
+
if (step === "create-icp-rubric" ||
|
|
285
|
+
step === "apply-icp-rubric" ||
|
|
286
|
+
step === "validate-sample") {
|
|
287
287
|
return "filter-leads";
|
|
288
288
|
}
|
|
289
289
|
if (step === "messages" ||
|
|
@@ -381,7 +381,7 @@ function describeVisibleStep(step) {
|
|
|
381
381
|
};
|
|
382
382
|
}
|
|
383
383
|
}
|
|
384
|
-
function
|
|
384
|
+
function parseWatchUrl(watchUrl, campaignId) {
|
|
385
385
|
if (!watchUrl) {
|
|
386
386
|
return {
|
|
387
387
|
urlPresent: false,
|
|
@@ -392,26 +392,39 @@ function parseSignedWatchUrl(watchUrl, campaignId) {
|
|
|
392
392
|
}
|
|
393
393
|
try {
|
|
394
394
|
const url = new URL(watchUrl);
|
|
395
|
+
const directPath = `/campaign-builder/${campaignId}`;
|
|
396
|
+
const isDirectCampaignUrl = url.pathname === directPath &&
|
|
397
|
+
(url.searchParams.get("mode") === "claude" ||
|
|
398
|
+
url.searchParams.get("mode") === "codex" ||
|
|
399
|
+
url.searchParams.get("mode") === "watch");
|
|
400
|
+
if (isDirectCampaignUrl) {
|
|
401
|
+
return {
|
|
402
|
+
urlPresent: true,
|
|
403
|
+
direct: true,
|
|
404
|
+
redirectPath: `${url.pathname}?${url.searchParams.toString()}`.replace(/\?$/, ""),
|
|
405
|
+
warning: null,
|
|
406
|
+
};
|
|
407
|
+
}
|
|
395
408
|
const redirect = url.searchParams.get("redirect");
|
|
396
409
|
const decodedRedirect = redirect ? decodeURIComponent(redirect) : null;
|
|
397
|
-
const
|
|
410
|
+
const isLegacySigned = url.pathname === "/auth/continue" &&
|
|
398
411
|
Boolean(url.searchParams.get("token")) &&
|
|
399
|
-
decodedRedirect
|
|
412
|
+
decodedRedirect?.startsWith(`${directPath}?mode=`) === true;
|
|
400
413
|
return {
|
|
401
414
|
urlPresent: true,
|
|
402
|
-
|
|
415
|
+
direct: false,
|
|
403
416
|
redirectPath: decodedRedirect,
|
|
404
|
-
warning:
|
|
405
|
-
?
|
|
406
|
-
: "Watch URL is not a
|
|
417
|
+
warning: isLegacySigned
|
|
418
|
+
? "Watch URL still uses a tokenized /auth/continue handoff. Return the safe direct /campaign-builder/{campaignId}?mode=claude watch URL instead."
|
|
419
|
+
: "Watch URL is not a safe direct /campaign-builder/{campaignId}?mode=claude watch link.",
|
|
407
420
|
};
|
|
408
421
|
}
|
|
409
422
|
catch {
|
|
410
423
|
return {
|
|
411
424
|
urlPresent: true,
|
|
412
|
-
|
|
425
|
+
direct: false,
|
|
413
426
|
redirectPath: null,
|
|
414
|
-
warning: "Watch URL is malformed; recover a fresh
|
|
427
|
+
warning: "Watch URL is malformed; recover a fresh safe direct campaign-builder watch link before browser handoff.",
|
|
415
428
|
};
|
|
416
429
|
}
|
|
417
430
|
}
|
|
@@ -554,7 +567,7 @@ export function computeCampaignNavigationStateFromCampaign(campaign, options) {
|
|
|
554
567
|
checkpoint: "send",
|
|
555
568
|
}
|
|
556
569
|
: describeVisibleStep(orientationStep);
|
|
557
|
-
const watchUrlState =
|
|
570
|
+
const watchUrlState = parseWatchUrl(campaign.watchUrl, campaign.id);
|
|
558
571
|
if (watchUrlState.warning) {
|
|
559
572
|
warnings.push(watchUrlState.warning);
|
|
560
573
|
}
|
|
@@ -579,7 +592,7 @@ export function computeCampaignNavigationStateFromCampaign(campaign, options) {
|
|
|
579
592
|
},
|
|
580
593
|
watchUrl: {
|
|
581
594
|
urlPresent: watchUrlState.urlPresent,
|
|
582
|
-
|
|
595
|
+
direct: watchUrlState.direct,
|
|
583
596
|
redirectPath: watchUrlState.redirectPath,
|
|
584
597
|
},
|
|
585
598
|
table: {
|
|
@@ -134,7 +134,7 @@ export declare function waitForCampaignTableReady(input: WaitForCampaignTableRea
|
|
|
134
134
|
};
|
|
135
135
|
watchUrl: {
|
|
136
136
|
urlPresent: boolean;
|
|
137
|
-
|
|
137
|
+
direct: boolean | undefined;
|
|
138
138
|
redirectPath: string | null;
|
|
139
139
|
};
|
|
140
140
|
table: {
|
|
@@ -183,7 +183,7 @@ export declare function waitForCampaignTableReady(input: WaitForCampaignTableRea
|
|
|
183
183
|
};
|
|
184
184
|
watchUrl: {
|
|
185
185
|
urlPresent: boolean;
|
|
186
|
-
|
|
186
|
+
direct: boolean | undefined;
|
|
187
187
|
redirectPath: string | null;
|
|
188
188
|
};
|
|
189
189
|
table: {
|
|
@@ -234,7 +234,7 @@ export declare function waitForCampaignTableReady(input: WaitForCampaignTableRea
|
|
|
234
234
|
};
|
|
235
235
|
watchUrl: {
|
|
236
236
|
urlPresent: boolean;
|
|
237
|
-
|
|
237
|
+
direct: boolean | undefined;
|
|
238
238
|
redirectPath: string | null;
|
|
239
239
|
};
|
|
240
240
|
table: {
|
|
@@ -1,7 +1,22 @@
|
|
|
1
1
|
function sanitizeWatchUrlValue(value) {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
2
|
+
try {
|
|
3
|
+
const url = new URL(value);
|
|
4
|
+
if (url.pathname === "/auth/continue") {
|
|
5
|
+
const redirect = url.searchParams.get("redirect");
|
|
6
|
+
if (redirect) {
|
|
7
|
+
const direct = new URL(decodeURIComponent(redirect), url.origin);
|
|
8
|
+
const workspaceId = url.searchParams.get("workspaceId");
|
|
9
|
+
if (workspaceId && !direct.searchParams.has("workspaceId")) {
|
|
10
|
+
direct.searchParams.set("workspaceId", workspaceId);
|
|
11
|
+
}
|
|
12
|
+
return direct.toString();
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
return value;
|
|
16
|
+
}
|
|
17
|
+
catch {
|
|
18
|
+
return value;
|
|
19
|
+
}
|
|
5
20
|
}
|
|
6
21
|
export function sanitizeWatchUrlsForMcpResult(value) {
|
|
7
22
|
if (!value || typeof value !== "object")
|
package/package.json
CHANGED
|
@@ -162,11 +162,11 @@ copy.
|
|
|
162
162
|
## Codex Watch Browser Handoff
|
|
163
163
|
|
|
164
164
|
When a campaign tool returns `watchUrl`, treat it as an active browser handoff,
|
|
165
|
-
not only a URL to print. A valid handoff link must be a
|
|
166
|
-
`/
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
165
|
+
not only a URL to print. A valid handoff link must be a safe direct
|
|
166
|
+
`/campaign-builder/{campaignId}?mode=claude` URL. `create_campaign.watchUrl`,
|
|
167
|
+
`create_campaign({ campaignId }).watchUrl`, and `get_campaign.watchUrl` are all
|
|
168
|
+
acceptable only when they return that direct campaign-builder shape, with
|
|
169
|
+
`workspaceId` used only as a safe routing hint when needed.
|
|
170
170
|
|
|
171
171
|
In Codex Desktop, when in-app browser control is available, open the returned watch link on the user's behalf. After opening it, inspect the browser-visible campaign state, then explain what the user is seeing before continuing with more campaign tools. Use customer language such as:
|
|
172
172
|
|
|
@@ -175,8 +175,8 @@ I’ll open the campaign view and keep it in sync as I build.
|
|
|
175
175
|
```
|
|
176
176
|
|
|
177
177
|
If browser control is unavailable, provide the watch link without discussing the
|
|
178
|
-
runtime limitation. In off-desktop Codex runs, make the
|
|
179
|
-
copy/open, then continue with explicit customer-facing campaign progress and
|
|
178
|
+
runtime limitation. In off-desktop Codex runs, make the direct watch link easy
|
|
179
|
+
to copy/open, then continue with explicit customer-facing campaign progress and
|
|
180
180
|
`get_campaign_navigation_state` orientation checks.
|
|
181
181
|
Use this fallback shape:
|
|
182
182
|
|
|
@@ -188,9 +188,9 @@ here as I build.
|
|
|
188
188
|
```
|
|
189
189
|
|
|
190
190
|
If opening the watch link lands on an auth, 404, permission, blank, or visible
|
|
191
|
-
error state, report only what is visible, recover a fresh
|
|
192
|
-
|
|
193
|
-
|
|
191
|
+
error state, report only what is visible, recover a fresh watch link once with
|
|
192
|
+
`create_campaign({ campaignId })` or `get_campaign`, and try that link. Do not
|
|
193
|
+
claim the browser was opened, inspected, or synchronized unless the visible
|
|
194
194
|
browser state was actually observed. Do not claim the browser was opened unless
|
|
195
195
|
it was opened and observed.
|
|
196
196
|
|
|
@@ -330,7 +330,7 @@
|
|
|
330
330
|
"campaignBrief"
|
|
331
331
|
],
|
|
332
332
|
"watchUrlSource": "create_campaign.watchUrl",
|
|
333
|
-
"requiredWatchUrlShape": "
|
|
333
|
+
"requiredWatchUrlShape": "direct /campaign-builder/{campaignId}?mode=claude watch URL",
|
|
334
334
|
"codexBrowserHandoff": {
|
|
335
335
|
"openWhenAvailable": true,
|
|
336
336
|
"inspectBeforeContinuing": true,
|
|
@@ -11,12 +11,12 @@ gating, and handoff read campaign state first.
|
|
|
11
11
|
|
|
12
12
|
## Plumbing Reuse
|
|
13
13
|
|
|
14
|
-
`create_campaign` already returns a
|
|
15
|
-
(`CampaignDetail.watchUrl` in
|
|
16
|
-
NOT mint a new token, does NOT call
|
|
17
|
-
|
|
18
|
-
surface it verbatim. The link must
|
|
19
|
-
land on the campaign builder without
|
|
14
|
+
`create_campaign` already returns a safe direct campaign-builder `watchUrl` on
|
|
15
|
+
the response (`CampaignDetail.watchUrl` in
|
|
16
|
+
`mcp/sellable/src/tools/campaigns.ts`). V2 does NOT mint a new token, does NOT call
|
|
17
|
+
a different route, and does NOT construct the URL locally. Capture whatever
|
|
18
|
+
`watchUrl` the existing tool returns and surface it verbatim. The link must
|
|
19
|
+
land on the campaign builder without exposing token secrets in the MCP result.
|
|
20
20
|
|
|
21
21
|
## Shell-First Link
|
|
22
22
|
|
|
@@ -59,7 +59,7 @@ If shell creation fails or the response is missing `watchUrl`, stop and surface
|
|
|
59
59
|
the error. Do not continue into campaignless source scouting in the active
|
|
60
60
|
shell-first flow.
|
|
61
61
|
If a legacy atomic-mint response is missing `watchUrl`, it is not a silent skip.
|
|
62
|
-
Stop before `save_rubrics` and recover the missing
|
|
62
|
+
Stop before `save_rubrics` and recover the missing watch URL first.
|
|
63
63
|
|
|
64
64
|
## Step Orientation
|
|
65
65
|
|
|
@@ -81,19 +81,19 @@ Do not spam the link between internal tool calls.
|
|
|
81
81
|
On resume:
|
|
82
82
|
|
|
83
83
|
1. Load the `CampaignOffer` for the campaign being resumed.
|
|
84
|
-
2. Recover the
|
|
84
|
+
2. Recover the direct watch link through the existing resume path
|
|
85
85
|
(`create_campaign({ campaignId })` when available).
|
|
86
86
|
3. Inspect `currentStep` and print a short orientation for where the user will
|
|
87
87
|
land now.
|
|
88
88
|
4. Print the recovered `watchUrl`.
|
|
89
89
|
|
|
90
|
-
The re-surface must use the
|
|
90
|
+
The re-surface must use the `watchUrl` from the tool response, not a
|
|
91
91
|
cached or reconstructed URL.
|
|
92
92
|
|
|
93
93
|
## Hard Rules
|
|
94
94
|
|
|
95
95
|
- Never fabricate or reconstruct the URL locally.
|
|
96
|
-
- The watch link must
|
|
96
|
+
- The watch link must be a safe direct campaign-builder URL.
|
|
97
97
|
- Missing `watchUrl` is an error, not a silent skip.
|
|
98
98
|
- The first watch link must be shown only after the v1 brief is on the campaign.
|
|
99
99
|
- A watch link is not approval to import, attach sequence, or start.
|