@sellable/mcp 0.1.113 → 0.1.115
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/agents/source-scout-linkedin-engagement.md +11 -3
- package/dist/index-dev.js +0 -0
- package/dist/index.js +0 -0
- package/dist/tools/campaigns.js +3 -0
- package/dist/tools/context.d.ts +2 -1
- package/dist/tools/context.js +1 -0
- package/dist/tools/leads.js +1 -1
- package/dist/tools/linkedin.js +1 -1
- package/dist/tools/navigation.d.ts +4 -2
- package/dist/tools/navigation.js +13 -4
- package/dist/tools/readiness.d.ts +6 -3
- package/dist/tools/rubrics.js +1 -1
- package/dist/tools/watch-url-security.js +4 -0
- package/package.json +1 -1
- package/skills/create-campaign/SKILL.md +50 -31
- package/skills/create-campaign-v2/SKILL.md +6 -2
- package/skills/create-campaign-v2/core/flow.v2.json +6 -5
- package/skills/create-campaign-v2/references/watch-guide-narration.md +3 -3
- package/skills/create-campaign-v2/references/watch-link-handoff.md +36 -8
- package/skills/providers/signal-discovery.md +7 -0
|
@@ -17,14 +17,22 @@ Use the inherited Sellable MCP tools when available:
|
|
|
17
17
|
- `search_signals` to find recent post lanes. Include `campaignOfferId` whenever
|
|
18
18
|
the parent provides one so selected searches/lists stay attached to the
|
|
19
19
|
campaign.
|
|
20
|
-
- `
|
|
20
|
+
- `select_promising_posts` to promote the exact posts you will sample into the
|
|
21
|
+
campaign UI before fetching engagers. In campaign-attached runs, do this
|
|
22
|
+
before the first `fetch_post_engagers` call so the user can see which posts
|
|
23
|
+
are being sampled.
|
|
24
|
+
- `fetch_post_engagers` to sample engagers from promoted/selected posts.
|
|
21
25
|
|
|
22
26
|
Process:
|
|
23
27
|
|
|
24
28
|
1. Read the campaign brief, kickoff doc, or lane prompt supplied by the parent.
|
|
25
29
|
2. Search 3-5 keyword/topic lanes, favoring fresh posts from the last 7-14 days.
|
|
26
|
-
3. Select 3-5 promising posts when available.
|
|
27
|
-
|
|
30
|
+
3. Select 3-5 promising posts when available. If a `campaignOfferId` was
|
|
31
|
+
supplied, call `select_promising_posts({ campaignOfferId, selectionMode:
|
|
32
|
+
"replace", selections, headlineICPCriteria, currentStep:
|
|
33
|
+
"signal-discovery" })` before sampling so the watched Signal Discovery table
|
|
34
|
+
shows the promoted posts.
|
|
35
|
+
4. Fetch or sample engagers for promoted posts and score rough ICP fit from visible headline/display-name cues only. Do not enrich people during viability estimation.
|
|
28
36
|
5. Estimate usable prospects per selected post from sampled pass rate. If the sample is good but volume is low, say how many more similar posts should be added or scraped.
|
|
29
37
|
6. Return false positives and dead ends explicitly.
|
|
30
38
|
|
package/dist/index-dev.js
CHANGED
|
File without changes
|
package/dist/index.js
CHANGED
|
File without changes
|
package/dist/tools/campaigns.js
CHANGED
|
@@ -25,6 +25,9 @@ export function buildWatchUrl(config, path) {
|
|
|
25
25
|
if (workspaceId && !url.searchParams.has("workspaceId")) {
|
|
26
26
|
url.searchParams.set("workspaceId", workspaceId);
|
|
27
27
|
}
|
|
28
|
+
if (config.token && !url.searchParams.has("token")) {
|
|
29
|
+
url.searchParams.set("token", config.token);
|
|
30
|
+
}
|
|
28
31
|
return url.toString();
|
|
29
32
|
}
|
|
30
33
|
export function getCampaignBuilderWatchModeParam() {
|
package/dist/tools/context.d.ts
CHANGED
package/dist/tools/context.js
CHANGED
package/dist/tools/leads.js
CHANGED
|
@@ -1047,7 +1047,7 @@ export const leadToolDefinitions = [
|
|
|
1047
1047
|
},
|
|
1048
1048
|
{
|
|
1049
1049
|
name: "select_promising_posts",
|
|
1050
|
-
description: "Select the most promising LinkedIn posts for lead scraping AND provide headline ICP criteria. Use the selectionTarget returned by search_signals (default 3).",
|
|
1050
|
+
description: "Select the most promising LinkedIn posts for lead scraping AND provide headline ICP criteria. Use the selectionTarget returned by search_signals (default 3). In campaign-attached sampling, call select_promising_posts first to promote the sampled posts into the watched UI before fetch_post_engagers.",
|
|
1051
1051
|
inputSchema: {
|
|
1052
1052
|
type: "object",
|
|
1053
1053
|
properties: {
|
package/dist/tools/linkedin.js
CHANGED
|
@@ -21,7 +21,7 @@ export const linkedinToolDefinitions = [
|
|
|
21
21
|
},
|
|
22
22
|
{
|
|
23
23
|
name: "fetch_post_engagers",
|
|
24
|
-
description: "Fetch people who engaged with a LinkedIn post (reactions and/or comments). Returns deduplicated engagers with name, headline, profileUrl, and source. Use 'comments' for lead magnets to get people who actively engaged. Uses smart rate limiting and parallel fetching.",
|
|
24
|
+
description: "Fetch people who engaged with a LinkedIn post (reactions and/or comments). Returns deduplicated engagers with name, headline, profileUrl, and source. Use 'comments' for lead magnets to get people who actively engaged. Uses smart rate limiting and parallel fetching. In campaign-attached Signal Discovery sampling, call select_promising_posts first to promote the sampled posts into the watched UI.",
|
|
25
25
|
inputSchema: {
|
|
26
26
|
type: "object",
|
|
27
27
|
properties: {
|
|
@@ -81,7 +81,8 @@ export declare function computeCampaignNavigationStateFromCampaign(campaign: Cam
|
|
|
81
81
|
};
|
|
82
82
|
watchUrl: {
|
|
83
83
|
urlPresent: boolean;
|
|
84
|
-
direct: boolean
|
|
84
|
+
direct: boolean;
|
|
85
|
+
autoLogin: boolean;
|
|
85
86
|
redirectPath: string | null;
|
|
86
87
|
};
|
|
87
88
|
table: {
|
|
@@ -125,7 +126,8 @@ export declare function getCampaignNavigationState(input: GetCampaignNavigationS
|
|
|
125
126
|
};
|
|
126
127
|
watchUrl: {
|
|
127
128
|
urlPresent: boolean;
|
|
128
|
-
direct: boolean
|
|
129
|
+
direct: boolean;
|
|
130
|
+
autoLogin: boolean;
|
|
129
131
|
redirectPath: string | null;
|
|
130
132
|
};
|
|
131
133
|
table: {
|
package/dist/tools/navigation.js
CHANGED
|
@@ -385,7 +385,9 @@ function parseWatchUrl(watchUrl, campaignId) {
|
|
|
385
385
|
if (!watchUrl) {
|
|
386
386
|
return {
|
|
387
387
|
urlPresent: false,
|
|
388
|
+
direct: false,
|
|
388
389
|
signed: false,
|
|
390
|
+
autoLogin: false,
|
|
389
391
|
redirectPath: null,
|
|
390
392
|
warning: null,
|
|
391
393
|
};
|
|
@@ -398,11 +400,15 @@ function parseWatchUrl(watchUrl, campaignId) {
|
|
|
398
400
|
url.searchParams.get("mode") === "codex" ||
|
|
399
401
|
url.searchParams.get("mode") === "watch");
|
|
400
402
|
if (isDirectCampaignUrl) {
|
|
403
|
+
const autoLogin = Boolean(url.searchParams.get("token"));
|
|
401
404
|
return {
|
|
402
405
|
urlPresent: true,
|
|
403
406
|
direct: true,
|
|
407
|
+
autoLogin,
|
|
404
408
|
redirectPath: `${url.pathname}?${url.searchParams.toString()}`.replace(/\?$/, ""),
|
|
405
|
-
warning:
|
|
409
|
+
warning: autoLogin
|
|
410
|
+
? null
|
|
411
|
+
: "Watch URL is direct but missing token auto-login. Recover a fresh /campaign-builder/{campaignId}?mode={claude|codex}&token=... watch link before asking the user to open it.",
|
|
406
412
|
};
|
|
407
413
|
}
|
|
408
414
|
const redirect = url.searchParams.get("redirect");
|
|
@@ -413,18 +419,20 @@ function parseWatchUrl(watchUrl, campaignId) {
|
|
|
413
419
|
return {
|
|
414
420
|
urlPresent: true,
|
|
415
421
|
direct: false,
|
|
422
|
+
autoLogin: false,
|
|
416
423
|
redirectPath: decodedRedirect,
|
|
417
424
|
warning: isLegacySigned
|
|
418
|
-
? "Watch URL still uses a tokenized /auth/continue handoff. Return the
|
|
419
|
-
: "Watch URL is not a
|
|
425
|
+
? "Watch URL still uses a tokenized /auth/continue handoff. Return the direct /campaign-builder/{campaignId}?mode={claude|codex}&token=... watch URL instead."
|
|
426
|
+
: "Watch URL is not a direct /campaign-builder/{campaignId}?mode={claude|codex}&token=... watch link.",
|
|
420
427
|
};
|
|
421
428
|
}
|
|
422
429
|
catch {
|
|
423
430
|
return {
|
|
424
431
|
urlPresent: true,
|
|
425
432
|
direct: false,
|
|
433
|
+
autoLogin: false,
|
|
426
434
|
redirectPath: null,
|
|
427
|
-
warning: "Watch URL is malformed; recover a fresh
|
|
435
|
+
warning: "Watch URL is malformed; recover a fresh direct campaign-builder auto-login watch link before handing it to the user.",
|
|
428
436
|
};
|
|
429
437
|
}
|
|
430
438
|
}
|
|
@@ -593,6 +601,7 @@ export function computeCampaignNavigationStateFromCampaign(campaign, options) {
|
|
|
593
601
|
watchUrl: {
|
|
594
602
|
urlPresent: watchUrlState.urlPresent,
|
|
595
603
|
direct: watchUrlState.direct,
|
|
604
|
+
autoLogin: watchUrlState.autoLogin,
|
|
596
605
|
redirectPath: watchUrlState.redirectPath,
|
|
597
606
|
},
|
|
598
607
|
table: {
|
|
@@ -134,7 +134,8 @@ export declare function waitForCampaignTableReady(input: WaitForCampaignTableRea
|
|
|
134
134
|
};
|
|
135
135
|
watchUrl: {
|
|
136
136
|
urlPresent: boolean;
|
|
137
|
-
direct: boolean
|
|
137
|
+
direct: boolean;
|
|
138
|
+
autoLogin: boolean;
|
|
138
139
|
redirectPath: string | null;
|
|
139
140
|
};
|
|
140
141
|
table: {
|
|
@@ -183,7 +184,8 @@ export declare function waitForCampaignTableReady(input: WaitForCampaignTableRea
|
|
|
183
184
|
};
|
|
184
185
|
watchUrl: {
|
|
185
186
|
urlPresent: boolean;
|
|
186
|
-
direct: boolean
|
|
187
|
+
direct: boolean;
|
|
188
|
+
autoLogin: boolean;
|
|
187
189
|
redirectPath: string | null;
|
|
188
190
|
};
|
|
189
191
|
table: {
|
|
@@ -234,7 +236,8 @@ export declare function waitForCampaignTableReady(input: WaitForCampaignTableRea
|
|
|
234
236
|
};
|
|
235
237
|
watchUrl: {
|
|
236
238
|
urlPresent: boolean;
|
|
237
|
-
direct: boolean
|
|
239
|
+
direct: boolean;
|
|
240
|
+
autoLogin: boolean;
|
|
238
241
|
redirectPath: string | null;
|
|
239
242
|
};
|
|
240
243
|
table: {
|
package/dist/tools/rubrics.js
CHANGED
|
@@ -204,7 +204,7 @@ export const rubricToolDefinitions = [
|
|
|
204
204
|
},
|
|
205
205
|
{
|
|
206
206
|
name: "save_rubrics",
|
|
207
|
-
description: "Persist rubric criteria to the campaign. Pass leadScoringRubrics directly to save without drafting.
|
|
207
|
+
description: "Persist rubric criteria to the campaign. Pass leadScoringRubrics directly to save without drafting. Saving active rubrics enables ICP filtering on the campaign so the client can continue to Filter Leads.",
|
|
208
208
|
inputSchema: {
|
|
209
209
|
type: "object",
|
|
210
210
|
properties: {
|
|
@@ -6,9 +6,13 @@ function sanitizeWatchUrlValue(value) {
|
|
|
6
6
|
if (redirect) {
|
|
7
7
|
const direct = new URL(decodeURIComponent(redirect), url.origin);
|
|
8
8
|
const workspaceId = url.searchParams.get("workspaceId");
|
|
9
|
+
const token = url.searchParams.get("token");
|
|
9
10
|
if (workspaceId && !direct.searchParams.has("workspaceId")) {
|
|
10
11
|
direct.searchParams.set("workspaceId", workspaceId);
|
|
11
12
|
}
|
|
13
|
+
if (token && !direct.searchParams.has("token")) {
|
|
14
|
+
direct.searchParams.set("token", token);
|
|
15
|
+
}
|
|
12
16
|
return direct.toString();
|
|
13
17
|
}
|
|
14
18
|
}
|
package/package.json
CHANGED
|
@@ -134,6 +134,12 @@ claim they ran in parallel and do not surface install status to the customer. In
|
|
|
134
134
|
chat, call the downstream copy stage `message generation`;
|
|
135
135
|
`message-validation.md` is only an internal proof artifact.
|
|
136
136
|
|
|
137
|
+
For campaign-attached Signal Discovery sampling, promote/select the exact posts
|
|
138
|
+
with `select_promising_posts` before `fetch_post_engagers` so the user can see
|
|
139
|
+
which posts are being sampled in the watched app. The watch guide should say
|
|
140
|
+
that we are pulling sample engagers from these posts to confirm the ICP is
|
|
141
|
+
actually engaging and the source is viable.
|
|
142
|
+
|
|
137
143
|
After find-leads returns a lead source and the user approves it, use the same
|
|
138
144
|
registry pattern for the two post-lead branches. The create-campaign-v2 subskill
|
|
139
145
|
calls `get_post_find_leads_scout_registry`, then launches the returned
|
|
@@ -159,45 +165,58 @@ linked skill versions, runbooks, npm/package details, repo-local files, VPS or
|
|
|
159
165
|
browser automation limitations, or local skill files in normal customer-facing
|
|
160
166
|
copy.
|
|
161
167
|
|
|
162
|
-
##
|
|
163
|
-
|
|
164
|
-
When a campaign tool returns `watchUrl`, treat it as
|
|
165
|
-
|
|
166
|
-
`/campaign-builder/{campaignId}?mode=claude` URL.
|
|
167
|
-
`create_campaign({ campaignId }).watchUrl`, and
|
|
168
|
-
acceptable only when they return that direct
|
|
169
|
-
`workspaceId` used only as a safe routing hint when
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
168
|
+
## Watch Link Handoff
|
|
169
|
+
|
|
170
|
+
When a campaign tool returns `watchUrl`, treat it as a user-opened watch link.
|
|
171
|
+
A valid handoff link must be a safe direct
|
|
172
|
+
`/campaign-builder/{campaignId}?mode=claude|codex|watch` URL.
|
|
173
|
+
`create_campaign.watchUrl`, `create_campaign({ campaignId }).watchUrl`, and
|
|
174
|
+
`get_campaign.watchUrl` are all acceptable only when they return that direct
|
|
175
|
+
campaign-builder shape, with `workspaceId` used only as a safe routing hint when
|
|
176
|
+
needed.
|
|
177
|
+
|
|
178
|
+
Do not claim Sellable can force every host to open links in a specific browser.
|
|
179
|
+
The host controls link-click behavior. Do not call Chrome, browser automation,
|
|
180
|
+
shell `open`, Computer Use, or in-app browser-control tools just because a
|
|
181
|
+
watch link exists.
|
|
182
|
+
|
|
183
|
+
- In Codex Desktop, clicking a URL can open the Codex in-app browser, but that
|
|
184
|
+
browser is only reliable for unauthenticated/public pages. Sellable campaign
|
|
185
|
+
watch pages may need a signed-in session. Tell the user to click the link
|
|
186
|
+
first; if it lands on sign-up/auth, they should open the same link in their
|
|
187
|
+
regular browser to watch real-time updates.
|
|
188
|
+
- In Claude Code, provide the link and direct the user to open it in their
|
|
189
|
+
browser. If their Claude Code setup opens links in an in-app/preview browser,
|
|
190
|
+
that is fine, but do not attempt to open Chrome for them.
|
|
191
|
+
|
|
192
|
+
Make the direct watch link easy to copy/open, then continue with explicit
|
|
193
|
+
customer-facing campaign progress and `get_campaign_navigation_state`
|
|
194
|
+
orientation checks. Use this fallback shape:
|
|
182
195
|
|
|
183
196
|
```text
|
|
184
|
-
Watch link: {watchUrl}
|
|
197
|
+
Watch link: [Open campaign]({watchUrl})
|
|
185
198
|
|
|
186
|
-
|
|
187
|
-
|
|
199
|
+
Click that link to watch the campaign. If it opens in an in-app browser and asks
|
|
200
|
+
you to sign in, open the same link in your regular browser so you can watch
|
|
201
|
+
real-time updates from your signed-in session. I’ll keep the brief, lead source,
|
|
202
|
+
and message-review steps explicit here as I build.
|
|
188
203
|
```
|
|
189
204
|
|
|
190
|
-
If
|
|
191
|
-
error state,
|
|
192
|
-
`create_campaign({ campaignId })` or `get_campaign`,
|
|
193
|
-
claim the browser was opened, inspected, or synchronized
|
|
194
|
-
browser state was actually observed. Do not claim the browser was opened unless
|
|
195
|
-
it was opened and observed.
|
|
205
|
+
If the user reports that the watch link lands on auth, 404, permission, blank,
|
|
206
|
+
or an error state, recover a fresh watch link once with
|
|
207
|
+
`create_campaign({ campaignId })` or `get_campaign`, then print the recovered
|
|
208
|
+
link. Do not claim the browser was opened, inspected, or synchronized.
|
|
196
209
|
|
|
197
210
|
After every `update_campaign({ campaignId, currentStep })`, use
|
|
198
211
|
`get_campaign_navigation_state` when available as a compact orientation check:
|
|
199
|
-
match the
|
|
200
|
-
state in one sentence, and only then continue. Sender selection belongs
|
|
212
|
+
match the expected watch-link step to the saved campaign state, explain the
|
|
213
|
+
current state in one sentence, and only then continue. Sender selection belongs
|
|
214
|
+
at Settings after message approval and 10-row validation. After message
|
|
215
|
+
validation, use Settings to help the user connect or select a LinkedIn sender.
|
|
216
|
+
Explain Slack reply review before launch. After sender selection, attach the
|
|
217
|
+
recommended sequence and move the watched UI to Send. Do not start the campaign
|
|
218
|
+
or trigger a live send unless the user explicitly confirms that launch action
|
|
219
|
+
outside UAT.
|
|
201
220
|
|
|
202
221
|
## Names To Use
|
|
203
222
|
|
|
@@ -805,7 +805,9 @@ Required behavior:
|
|
|
805
805
|
`lead-review.md` focused on source evidence, not host-agent availability.
|
|
806
806
|
- Branch A: LinkedIn Engagement / active LinkedIn posts (internal provider:
|
|
807
807
|
Signals / `signal-discovery`). Search relevant keyword lanes, review
|
|
808
|
-
finalist posts,
|
|
808
|
+
finalist posts, promote the sampled posts with `select_promising_posts`
|
|
809
|
+
before fetching engagers when a campaign shell exists, fetch top-post
|
|
810
|
+
engagers, and estimate warm-fit volume.
|
|
809
811
|
- Branch B: Sales Nav / title + company filters. Run preview filters, inspect
|
|
810
812
|
preview rows, and estimate scalable-fit volume.
|
|
811
813
|
- Branch C: Prospeo Contact / domains only when the campaign has a
|
|
@@ -1019,7 +1021,9 @@ lower confidence instead of presenting a precise estimate.
|
|
|
1019
1021
|
|
|
1020
1022
|
For Signals, default to sampling a few promising posts first rather than trying
|
|
1021
1023
|
to prove the entire source can scale before the user sees evidence. Pick 3-5
|
|
1022
|
-
fresh, high-density posts when available
|
|
1024
|
+
fresh, high-density posts when available. In campaign-attached watch runs,
|
|
1025
|
+
promote those posts with `select_promising_posts` before sampling so the user
|
|
1026
|
+
sees which posts are being tested. Then sample engagers, show fit rate, and
|
|
1023
1027
|
state how many additional posts could be added/scraped if that first sample is
|
|
1024
1028
|
good but volume is low.
|
|
1025
1029
|
|
|
@@ -330,11 +330,12 @@
|
|
|
330
330
|
"campaignBrief"
|
|
331
331
|
],
|
|
332
332
|
"watchUrlSource": "create_campaign.watchUrl",
|
|
333
|
-
"requiredWatchUrlShape": "direct /campaign-builder/{campaignId}?mode={claude|codex} watch URL",
|
|
333
|
+
"requiredWatchUrlShape": "direct /campaign-builder/{campaignId}?mode={claude|codex} watch URL with token auto-login and workspace routing",
|
|
334
334
|
"codexBrowserHandoff": {
|
|
335
|
-
"openWhenAvailable":
|
|
336
|
-
"
|
|
337
|
-
"
|
|
335
|
+
"openWhenAvailable": false,
|
|
336
|
+
"printWatchLinkOnly": true,
|
|
337
|
+
"tellUserCommandEnterOrClick": true,
|
|
338
|
+
"mustNotUseBrowserAutomation": true,
|
|
338
339
|
"fallbackMustNotClaimInspection": true,
|
|
339
340
|
"recoverFreshSignedLinkOnceOnOpenFailure": true
|
|
340
341
|
},
|
|
@@ -540,7 +541,7 @@
|
|
|
540
541
|
"uploadedDomains": "prospeo"
|
|
541
542
|
},
|
|
542
543
|
"when": "before_background_source_scouts",
|
|
543
|
-
"rule": "Choose the likely primary visible source lane from source intake, brief preference, or the best first lane the main thread will actually search. Send watchNarration with stage find-leads that says what search was tried or is being tried next, what sample is being checked, and why it helps this campaign. Do this before waiting on background scouts so watch mode never sits on only Pick Provider while source work happens elsewhere."
|
|
544
|
+
"rule": "Choose the likely primary visible source lane from source intake, brief preference, or the best first lane the main thread will actually search. Send watchNarration with stage find-leads that says what search was tried or is being tried next, what sample is being checked, and why it helps this campaign. For Signal Discovery sampling, promote/select the posts with select_promising_posts before fetch_post_engagers so the watched table shows the exact posts being sampled; the guide copy should say Codex is pulling sample engagers from these posts to confirm the ICP is actually engaging and the source is viable. Do this before waiting on background scouts so watch mode never sits on only Pick Provider while source work happens elsewhere."
|
|
544
545
|
},
|
|
545
546
|
{
|
|
546
547
|
"action": "run_first_campaign_attached_source_search",
|
|
@@ -67,9 +67,9 @@ Signal Discovery:
|
|
|
67
67
|
```json
|
|
68
68
|
{
|
|
69
69
|
"stage": "find-leads",
|
|
70
|
-
"headline": "Testing
|
|
71
|
-
"visibleState": "
|
|
72
|
-
"agentIntent": "Codex is
|
|
70
|
+
"headline": "Testing warm LinkedIn activity",
|
|
71
|
+
"visibleState": "The promoted posts are the ones being sampled now.",
|
|
72
|
+
"agentIntent": "Codex is pulling sample engagers from these posts to confirm the ICP is actually engaging and that this source is viable.",
|
|
73
73
|
"nextAction": "Review batch"
|
|
74
74
|
}
|
|
75
75
|
```
|
|
@@ -11,12 +11,13 @@ gating, and handoff read campaign state first.
|
|
|
11
11
|
|
|
12
12
|
## Plumbing Reuse
|
|
13
13
|
|
|
14
|
-
`create_campaign` already returns a
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
14
|
+
`create_campaign` already returns a direct campaign-builder `watchUrl` on the
|
|
15
|
+
response (`CampaignDetail.watchUrl` in `mcp/sellable/src/tools/campaigns.ts`).
|
|
16
|
+
The URL includes the auth token and workspace routing needed for browser
|
|
17
|
+
auto-login, then the app exchanges the token through `/auth/continue` and
|
|
18
|
+
cleans it from the browser URL. V2 does NOT mint a new token, does NOT call a
|
|
19
|
+
different route, and does NOT construct the URL locally. Capture whatever
|
|
20
|
+
`watchUrl` the existing tool returns and surface it verbatim.
|
|
20
21
|
|
|
21
22
|
## Shell-First Link
|
|
22
23
|
|
|
@@ -37,7 +38,13 @@ You can watch the lead source, filters, and messages fill in from here.
|
|
|
37
38
|
|
|
38
39
|
No leads import and nothing sends yet.
|
|
39
40
|
|
|
40
|
-
Watch link: {watchUrl}
|
|
41
|
+
Watch link: [Open campaign]({watchUrl})
|
|
42
|
+
|
|
43
|
+
Click that link to watch the campaign. If it opens in an in-app browser and asks
|
|
44
|
+
you to sign in, open the same link in your regular browser so you can watch
|
|
45
|
+
real-time updates from your signed-in session.
|
|
46
|
+
|
|
47
|
+
Command-Enter or click that link to watch it in Sellable.
|
|
41
48
|
|
|
42
49
|
Cool, let's find leads.
|
|
43
50
|
```
|
|
@@ -55,6 +62,25 @@ clear:
|
|
|
55
62
|
- the brief is already visible in the campaign
|
|
56
63
|
- import, sequence, and start are still blocked
|
|
57
64
|
|
|
65
|
+
## Browser Choice
|
|
66
|
+
|
|
67
|
+
Do not claim Sellable can force the user's host to open links in a specific
|
|
68
|
+
browser. The host controls link-click behavior.
|
|
69
|
+
Do not call Chrome, browser automation, shell `open`, Computer Use, or in-app
|
|
70
|
+
browser-control tools just because a watch link exists.
|
|
71
|
+
|
|
72
|
+
- Codex Desktop URL clicks can open the Codex in-app browser, but that browser
|
|
73
|
+
is only reliable for public or unauthenticated pages. Sellable campaign watch
|
|
74
|
+
pages depend on a signed-in session, so if the in-app browser lands on
|
|
75
|
+
sign-up/auth, tell the user to open the same link in their regular browser to
|
|
76
|
+
watch real-time updates.
|
|
77
|
+
- Claude Code should provide the link and direct the user to open it in their
|
|
78
|
+
browser. If the user's Claude Code setup opens links in an in-app/preview
|
|
79
|
+
browser, that is fine, but do not attempt to open Chrome for them.
|
|
80
|
+
|
|
81
|
+
Surface the link as a normal Markdown link and continue with explicit progress
|
|
82
|
+
updates in chat.
|
|
83
|
+
|
|
58
84
|
If shell creation fails or the response is missing `watchUrl`, stop and surface
|
|
59
85
|
the error. Do not continue into campaignless source scouting in the active
|
|
60
86
|
shell-first flow.
|
|
@@ -93,7 +119,9 @@ cached or reconstructed URL.
|
|
|
93
119
|
## Hard Rules
|
|
94
120
|
|
|
95
121
|
- Never fabricate or reconstruct the URL locally.
|
|
96
|
-
- The watch link must be a
|
|
122
|
+
- The watch link must be a direct campaign-builder URL with token auto-login.
|
|
123
|
+
- Do not call browser-opening tools, shell `open`, Computer Use, or in-app
|
|
124
|
+
browser automation; the user opens the link when they want to watch.
|
|
97
125
|
- Missing `watchUrl` is an error, not a silent skip.
|
|
98
126
|
- The first watch link must be shown only after the v1 brief is on the campaign.
|
|
99
127
|
- A watch link is not approval to import, attach sequence, or start.
|
|
@@ -94,6 +94,8 @@ Use conservative logic:
|
|
|
94
94
|
1. Start from recent strong-post inventory and sample quality, not raw total post count.
|
|
95
95
|
2. If the lane looks promising, sample real engagers before claiming a reachable-lead estimate:
|
|
96
96
|
- choose 1-2 promising posts
|
|
97
|
+
- in campaign-attached runs, call `select_promising_posts` first to promote
|
|
98
|
+
those posts in the watched Signal Discovery UI before sampling engagers
|
|
97
99
|
- call `fetch_post_engagers`
|
|
98
100
|
- fetch only a representative first sample, not a full scrape
|
|
99
101
|
- default to `limit: 25` for one post or `limit: 20` per post when checking two posts
|
|
@@ -325,6 +327,11 @@ Select the best posts to scrape AND provide headline ICP criteria.
|
|
|
325
327
|
Use `selectionTarget` and `recommendedPostIds` returned by `search_signals`
|
|
326
328
|
(default selectionTarget is 3).
|
|
327
329
|
|
|
330
|
+
For campaign-attached viability sampling, this is also the promote step: call it
|
|
331
|
+
before `fetch_post_engagers` so the user sees the exact posts being sampled in
|
|
332
|
+
the watched Signal Discovery table. Use `selectionMode: "replace"` for the first
|
|
333
|
+
sample set unless the user explicitly wants to add to existing promoted posts.
|
|
334
|
+
|
|
328
335
|
```json
|
|
329
336
|
select_promising_posts({
|
|
330
337
|
"campaignOfferId": "cmp_xxx",
|