@sellable/mcp 0.1.94 → 0.1.96

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.
@@ -68,6 +68,22 @@ export declare function getCampaignContext(input: GetCampaignContextInput): Prom
68
68
  expectedHeadlessStep: string | null;
69
69
  headlessCurrentStep: string | null;
70
70
  uiStep: string | null;
71
+ orientation: {
72
+ visibleStep: string | null;
73
+ browserStepLabel: string;
74
+ customerSummary: string;
75
+ nextVisibleMilestone: string;
76
+ blockedReason: string | null;
77
+ agentGuidance: string;
78
+ browserCheckpoint: string;
79
+ senderSelectionAllowed: boolean;
80
+ safeToAskSender: boolean;
81
+ };
82
+ watchUrl: {
83
+ urlPresent: boolean;
84
+ signed: boolean;
85
+ redirectPath: string | null;
86
+ };
71
87
  table: {
72
88
  workflowTableId: string | null;
73
89
  checked: boolean;
@@ -165,6 +165,22 @@ export function markCampaignContextDirty(campaignId, reason) {
165
165
  expectedHeadlessStep: "create-offer",
166
166
  headlessCurrentStep: null,
167
167
  uiStep: "plan",
168
+ orientation: {
169
+ visibleStep: "plan",
170
+ browserStepLabel: "Campaign brief",
171
+ customerSummary: "The campaign view is waiting for saved campaign context.",
172
+ nextVisibleMilestone: "Lead source search",
173
+ blockedReason: "Missing campaignContextNotLoaded",
174
+ agentGuidance: "Refresh campaign navigation state before describing the browser.",
175
+ browserCheckpoint: "brief",
176
+ senderSelectionAllowed: false,
177
+ safeToAskSender: false,
178
+ },
179
+ watchUrl: {
180
+ urlPresent: false,
181
+ signed: false,
182
+ redirectPath: null,
183
+ },
168
184
  table: {
169
185
  workflowTableId: null,
170
186
  checked: false,
@@ -1035,7 +1035,7 @@ export const leadToolDefinitions = [
1035
1035
  },
1036
1036
  targetLeadCount: {
1037
1037
  type: "number",
1038
- description: "Target lead count requested (used to validate completion when status is unavailable).",
1038
+ description: "Target lead count requested. Also caps the campaign-table clone for bounded review batches.",
1039
1039
  },
1040
1040
  confirmed: {
1041
1041
  type: "boolean",
@@ -2262,6 +2262,7 @@ export async function confirmLeadList(input) {
2262
2262
  campaignOfferId,
2263
2263
  campaignName,
2264
2264
  keepInSync,
2265
+ ...(typeof targetLeadCount === "number" ? { targetLeadCount } : {}),
2265
2266
  ...(shouldSetCurrentStep ? { currentStep: effectiveCurrentStep } : {}),
2266
2267
  });
2267
2268
  const campaignTableId = importResult.workflowTableId ?? importResult.campaignTableId;
@@ -25,6 +25,7 @@ export type CampaignOfferNavigation = {
25
25
  campaignStatus?: string | null;
26
26
  status?: string | null;
27
27
  currentStep?: string | null;
28
+ watchUrl?: string | null;
28
29
  };
29
30
  type NavigationDebugPayload = Record<string, unknown>;
30
31
  export declare function logNavigationDebug(event: string, payload?: NavigationDebugPayload, campaignId?: string | null): void;
@@ -67,6 +68,22 @@ export declare function computeCampaignNavigationStateFromCampaign(campaign: Cam
67
68
  expectedHeadlessStep: string | null;
68
69
  headlessCurrentStep: string | null;
69
70
  uiStep: string | null;
71
+ orientation: {
72
+ visibleStep: string | null;
73
+ browserStepLabel: string;
74
+ customerSummary: string;
75
+ nextVisibleMilestone: string;
76
+ blockedReason: string | null;
77
+ agentGuidance: string;
78
+ browserCheckpoint: string;
79
+ senderSelectionAllowed: boolean;
80
+ safeToAskSender: boolean;
81
+ };
82
+ watchUrl: {
83
+ urlPresent: boolean;
84
+ signed: boolean;
85
+ redirectPath: string | null;
86
+ };
70
87
  table: {
71
88
  workflowTableId: string | null;
72
89
  checked: boolean;
@@ -95,6 +112,22 @@ export declare function getCampaignNavigationState(input: GetCampaignNavigationS
95
112
  expectedHeadlessStep: string | null;
96
113
  headlessCurrentStep: string | null;
97
114
  uiStep: string | null;
115
+ orientation: {
116
+ visibleStep: string | null;
117
+ browserStepLabel: string;
118
+ customerSummary: string;
119
+ nextVisibleMilestone: string;
120
+ blockedReason: string | null;
121
+ agentGuidance: string;
122
+ browserCheckpoint: string;
123
+ senderSelectionAllowed: boolean;
124
+ safeToAskSender: boolean;
125
+ };
126
+ watchUrl: {
127
+ urlPresent: boolean;
128
+ signed: boolean;
129
+ redirectPath: string | null;
130
+ };
98
131
  table: {
99
132
  workflowTableId: string | null;
100
133
  checked: boolean;
@@ -301,6 +301,120 @@ function mapHeadlessToUiStep(step) {
301
301
  }
302
302
  return null;
303
303
  }
304
+ function describeVisibleStep(step) {
305
+ switch (step) {
306
+ case "plan":
307
+ return {
308
+ label: "Campaign brief",
309
+ summary: "The campaign brief is visible in the campaign view.",
310
+ nextMilestone: "Lead source search",
311
+ guidance: "Confirm the browser-visible campaign state before moving to lead sourcing.",
312
+ checkpoint: "brief",
313
+ };
314
+ case "pick-provider":
315
+ case "contact-search":
316
+ case "signal-discovery":
317
+ return {
318
+ label: "Lead sourcing",
319
+ summary: "Lead source selection or search is visible.",
320
+ nextMilestone: "Lead preview",
321
+ guidance: "Explain the selected lead-source lane and visible search state before continuing.",
322
+ checkpoint: "lead-source",
323
+ };
324
+ case "leads":
325
+ return {
326
+ label: "Lead import",
327
+ summary: "The lead preview or imported review batch is visible.",
328
+ nextMilestone: "Fit filtering and message review",
329
+ guidance: "Confirm the browser-visible campaign state and row batch before filter/message work.",
330
+ checkpoint: "import",
331
+ };
332
+ case "filter-choice":
333
+ case "filter-rules":
334
+ case "filter-leads":
335
+ return {
336
+ label: "Fit filtering",
337
+ summary: "Fit filtering or sample validation is visible.",
338
+ nextMilestone: "Message review or validation",
339
+ guidance: "Explain the visible filter or validation state and wait for the next approved step.",
340
+ checkpoint: "filters",
341
+ };
342
+ case "messages":
343
+ return {
344
+ label: "Message review",
345
+ summary: "Message review or generation is visible.",
346
+ nextMilestone: "10-row validation",
347
+ guidance: "Explain the visible message state before asking for approval or validation.",
348
+ checkpoint: "messages",
349
+ };
350
+ case "settings":
351
+ return {
352
+ label: "Settings",
353
+ summary: "Settings is visible. Sender selection belongs here.",
354
+ nextMilestone: "Safe sender selection",
355
+ guidance: "Select a safe connected sender here only; stop before sequence/start/live send in UAT.",
356
+ checkpoint: "settings",
357
+ };
358
+ case "sequence":
359
+ return {
360
+ label: "Sequence setup",
361
+ summary: "Sequence setup is visible.",
362
+ nextMilestone: "Explicit launch approval",
363
+ guidance: "Do not attach or change sequence state during watch UAT unless explicitly approved.",
364
+ checkpoint: "sequence",
365
+ };
366
+ case "send":
367
+ return {
368
+ label: "Send review",
369
+ summary: "Send review or running status is visible.",
370
+ nextMilestone: "Explicit launch approval",
371
+ guidance: "Do not start the campaign or trigger live send without explicit user confirmation.",
372
+ checkpoint: "send",
373
+ };
374
+ default:
375
+ return {
376
+ label: "Campaign view",
377
+ summary: "The campaign view is loading or waiting for the next saved step.",
378
+ nextMilestone: "Next saved campaign step",
379
+ guidance: "Inspect the browser-visible campaign state before continuing with more tools.",
380
+ checkpoint: "unknown",
381
+ };
382
+ }
383
+ }
384
+ function parseSignedWatchUrl(watchUrl, campaignId) {
385
+ if (!watchUrl) {
386
+ return {
387
+ urlPresent: false,
388
+ signed: false,
389
+ redirectPath: null,
390
+ warning: null,
391
+ };
392
+ }
393
+ try {
394
+ const url = new URL(watchUrl);
395
+ const redirect = url.searchParams.get("redirect");
396
+ const decodedRedirect = redirect ? decodeURIComponent(redirect) : null;
397
+ const signed = url.pathname === "/auth/continue" &&
398
+ Boolean(url.searchParams.get("token")) &&
399
+ decodedRedirect === `/campaign-builder/${campaignId}?mode=claude`;
400
+ return {
401
+ urlPresent: true,
402
+ signed,
403
+ redirectPath: decodedRedirect,
404
+ warning: signed
405
+ ? null
406
+ : "Watch URL is not a signed /auth/continue watch link for /campaign-builder/{campaignId}?mode=claude.",
407
+ };
408
+ }
409
+ catch {
410
+ return {
411
+ urlPresent: true,
412
+ signed: false,
413
+ redirectPath: null,
414
+ warning: "Watch URL is malformed; recover a fresh signed /auth/continue watch link before browser handoff.",
415
+ };
416
+ }
417
+ }
304
418
  export const navigationToolDefinitions = [
305
419
  {
306
420
  name: "get_campaign_navigation_state",
@@ -401,30 +515,49 @@ export function computeCampaignNavigationStateFromCampaign(campaign, options) {
401
515
  if (!blockedAt && evaluateTailState) {
402
516
  computedStep = isRunningCampaign(campaign) ? "running" : "send";
403
517
  }
404
- const expectedHeadlessStep = computedStep === "campaign-created"
518
+ const stepForHeadless = blockedAt ?? computedStep;
519
+ const expectedHeadlessStep = stepForHeadless === "campaign-created"
405
520
  ? "create-offer"
406
- : computedStep === "provider-search"
521
+ : stepForHeadless === "provider-search"
407
522
  ? providerConfig?.currentStep || campaign.leadSourceProvider || null
408
- : computedStep === "filter-rules"
523
+ : stepForHeadless === "filter-rules"
409
524
  ? "create-icp-rubric"
410
- : computedStep === "messages"
525
+ : stepForHeadless === "messages"
411
526
  ? "messages"
412
- : computedStep === "settings"
527
+ : stepForHeadless === "settings"
413
528
  ? "settings"
414
- : computedStep === "sequence"
529
+ : stepForHeadless === "sequence"
415
530
  ? "sequence"
416
- : computedStep === "running"
531
+ : stepForHeadless === "running"
417
532
  ? "running"
418
- : computedStep;
533
+ : stepForHeadless;
534
+ const currentUiStep = mapHeadlessToUiStep(campaign.currentStep);
535
+ const expectedUiStep = mapHeadlessToUiStep(expectedHeadlessStep);
419
536
  const stepAligned = !campaign.currentStep || !expectedHeadlessStep
420
537
  ? null
421
- : campaign.currentStep === expectedHeadlessStep;
538
+ : campaign.currentStep === expectedHeadlessStep ||
539
+ (currentUiStep !== null && currentUiStep === expectedUiStep);
422
540
  if (stepAligned === false && campaign.currentStep && expectedHeadlessStep) {
423
541
  warnings.push(`Headless step mismatch: expected "${expectedHeadlessStep}" but campaign.currentStep is "${campaign.currentStep}".`);
424
542
  }
425
543
  if (!providerConfig && campaign.leadSourceProvider) {
426
544
  warnings.push(`Provider config not found for "${campaign.leadSourceProvider}".`);
427
545
  }
546
+ const uiStep = currentUiStep;
547
+ const orientationStep = uiStep || mapHeadlessToUiStep(expectedHeadlessStep);
548
+ const orientationSource = isRunningCampaign(campaign)
549
+ ? {
550
+ label: "Running campaign",
551
+ summary: "The campaign is running and the send review is visible.",
552
+ nextMilestone: "Explicit launch approval",
553
+ guidance: "Confirm the browser-visible campaign state before describing the running status.",
554
+ checkpoint: "send",
555
+ }
556
+ : describeVisibleStep(orientationStep);
557
+ const watchUrlState = parseSignedWatchUrl(campaign.watchUrl, campaign.id);
558
+ if (watchUrlState.warning) {
559
+ warnings.push(watchUrlState.warning);
560
+ }
428
561
  const result = {
429
562
  campaignId: campaign.id,
430
563
  computedStep,
@@ -432,7 +565,23 @@ export function computeCampaignNavigationStateFromCampaign(campaign, options) {
432
565
  missing,
433
566
  expectedHeadlessStep,
434
567
  headlessCurrentStep: campaign.currentStep ?? null,
435
- uiStep: mapHeadlessToUiStep(campaign.currentStep),
568
+ uiStep,
569
+ orientation: {
570
+ visibleStep: orientationStep,
571
+ browserStepLabel: orientationSource.label,
572
+ customerSummary: orientationSource.summary,
573
+ nextVisibleMilestone: orientationSource.nextMilestone,
574
+ blockedReason: missing.length > 0 ? `Missing ${missing.join(", ")}` : null,
575
+ agentGuidance: orientationSource.guidance,
576
+ browserCheckpoint: orientationSource.checkpoint,
577
+ senderSelectionAllowed: orientationStep === "settings",
578
+ safeToAskSender: orientationStep === "settings",
579
+ },
580
+ watchUrl: {
581
+ urlPresent: watchUrlState.urlPresent,
582
+ signed: watchUrlState.signed,
583
+ redirectPath: watchUrlState.redirectPath,
584
+ },
436
585
  table: {
437
586
  workflowTableId: campaign.workflowTableId ?? null,
438
587
  checked: options.tableChecked,
@@ -461,6 +610,8 @@ export function computeCampaignNavigationStateFromCampaign(campaign, options) {
461
610
  expectedHeadlessStep: result.expectedHeadlessStep,
462
611
  headlessCurrentStep: result.headlessCurrentStep,
463
612
  uiStep: result.uiStep,
613
+ orientation: result.orientation,
614
+ watchUrl: result.watchUrl,
464
615
  table: result.table,
465
616
  context: result.context,
466
617
  warnings: result.warnings,
@@ -121,6 +121,22 @@ export declare function waitForCampaignTableReady(input: WaitForCampaignTableRea
121
121
  expectedHeadlessStep: string | null;
122
122
  headlessCurrentStep: string | null;
123
123
  uiStep: string | null;
124
+ orientation: {
125
+ visibleStep: string | null;
126
+ browserStepLabel: string;
127
+ customerSummary: string;
128
+ nextVisibleMilestone: string;
129
+ blockedReason: string | null;
130
+ agentGuidance: string;
131
+ browserCheckpoint: string;
132
+ senderSelectionAllowed: boolean;
133
+ safeToAskSender: boolean;
134
+ };
135
+ watchUrl: {
136
+ urlPresent: boolean;
137
+ signed: boolean;
138
+ redirectPath: string | null;
139
+ };
124
140
  table: {
125
141
  workflowTableId: string | null;
126
142
  checked: boolean;
@@ -154,6 +170,22 @@ export declare function waitForCampaignTableReady(input: WaitForCampaignTableRea
154
170
  expectedHeadlessStep: string | null;
155
171
  headlessCurrentStep: string | null;
156
172
  uiStep: string | null;
173
+ orientation: {
174
+ visibleStep: string | null;
175
+ browserStepLabel: string;
176
+ customerSummary: string;
177
+ nextVisibleMilestone: string;
178
+ blockedReason: string | null;
179
+ agentGuidance: string;
180
+ browserCheckpoint: string;
181
+ senderSelectionAllowed: boolean;
182
+ safeToAskSender: boolean;
183
+ };
184
+ watchUrl: {
185
+ urlPresent: boolean;
186
+ signed: boolean;
187
+ redirectPath: string | null;
188
+ };
157
189
  table: {
158
190
  workflowTableId: string | null;
159
191
  checked: boolean;
@@ -189,6 +221,22 @@ export declare function waitForCampaignTableReady(input: WaitForCampaignTableRea
189
221
  expectedHeadlessStep: string | null;
190
222
  headlessCurrentStep: string | null;
191
223
  uiStep: string | null;
224
+ orientation: {
225
+ visibleStep: string | null;
226
+ browserStepLabel: string;
227
+ customerSummary: string;
228
+ nextVisibleMilestone: string;
229
+ blockedReason: string | null;
230
+ agentGuidance: string;
231
+ browserCheckpoint: string;
232
+ senderSelectionAllowed: boolean;
233
+ safeToAskSender: boolean;
234
+ };
235
+ watchUrl: {
236
+ urlPresent: boolean;
237
+ signed: boolean;
238
+ redirectPath: string | null;
239
+ };
192
240
  table: {
193
241
  workflowTableId: string | null;
194
242
  checked: boolean;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sellable/mcp",
3
- "version": "0.1.94",
3
+ "version": "0.1.96",
4
4
  "type": "module",
5
5
  "description": "Sellable MCP server for Claude Code and Codex campaign workflows",
6
6
  "main": "dist/index.js",
@@ -158,6 +158,45 @@ Never mention MCP namespaces, prompt chunking, plugin cache paths, missing
158
158
  linked skill versions, runbooks, or local skill files in normal customer-facing
159
159
  copy.
160
160
 
161
+ ## Codex Watch Browser Handoff
162
+
163
+ When a campaign tool returns `watchUrl`, treat it as an active browser handoff,
164
+ not only a URL to print. A valid handoff link must be a signed
165
+ `/auth/continue?token=...&redirect=/campaign-builder/{campaignId}?mode=claude`
166
+ URL. `create_campaign.watchUrl`, `create_campaign({ campaignId }).watchUrl`,
167
+ and `get_campaign.watchUrl` are all acceptable only when they return that signed
168
+ shape.
169
+
170
+ 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:
171
+
172
+ ```text
173
+ I’ll open the campaign view and keep it in sync as I build.
174
+ ```
175
+
176
+ If browser control is unavailable, say so briefly and provide the watch link.
177
+ In VPS or other off-desktop Codex runs, this fallback is the expected handoff for
178
+ the local headed watch observer: make the signed link easy to copy/open, then
179
+ continue with explicit chat progress and `get_campaign_navigation_state`
180
+ orientation checks.
181
+ Use this fallback shape:
182
+
183
+ ```text
184
+ I can’t inspect the campaign browser from this Codex session, so I’ll keep the
185
+ chat steps explicit here. Watch link: {watchUrl}
186
+ ```
187
+
188
+ If opening the watch link lands on an auth, 404, permission, blank, or visible
189
+ error state, report only what is visible, recover a fresh signed watch link once
190
+ with `create_campaign({ campaignId })` or `get_campaign`, and try that link. Do
191
+ not claim the browser was opened, inspected, or synchronized unless the visible
192
+ browser state was actually observed. Do not claim the browser was opened unless
193
+ it was opened and observed.
194
+
195
+ After every `update_campaign({ campaignId, currentStep })`, use
196
+ `get_campaign_navigation_state` when available as a compact orientation check:
197
+ match the browser-visible step to the saved campaign state, explain the current
198
+ state in one sentence, and only then continue. Sender selection belongs at Settings after message approval and 10-row validation. Do not attach a sequence, start the campaign, or trigger a live send unless the user explicitly confirms that launch action outside UAT.
199
+
161
200
  ## Names To Use
162
201
 
163
202
  Use these exact public names so Claude Code and Codex do not drift:
@@ -100,7 +100,7 @@ Optional debug/UAT draft directory, disabled in normal customer runs:
100
100
  `awaiting-user-greenlight` for Settings. Do not advance `currentStep` backward.
101
101
  - After the shell is minted, normal resume paths read the CampaignOffer first.
102
102
  Use debug files to explain or reconstruct work, not to decide whether a
103
- post-mint gate is allowed. Validation/debug artifacts are not durable campaign
103
+ post-mint gate is allowed. validation/debug artifacts are not durable campaign
104
104
  state.
105
105
  - The campaign shell may receive setup state before import: approved brief text,
106
106
  campaign-attached provider searches/selections, the bounded review-batch
@@ -307,6 +307,14 @@
307
307
  "campaignBrief"
308
308
  ],
309
309
  "watchUrlSource": "create_campaign.watchUrl",
310
+ "requiredWatchUrlShape": "signed /auth/continue?token=...&redirect=/campaign-builder/{campaignId}?mode=claude",
311
+ "codexBrowserHandoff": {
312
+ "openWhenAvailable": true,
313
+ "inspectBeforeContinuing": true,
314
+ "explainVisibleStateBeforeContinuing": true,
315
+ "fallbackMustNotClaimInspection": true,
316
+ "recoverFreshSignedLinkOnceOnOpenFailure": true
317
+ },
310
318
  "immediateNextMainChatLine": "Cool, let's find leads."
311
319
  }
312
320
  ],
@@ -1415,13 +1423,26 @@
1415
1423
  "when": "after_message_approved_before_import",
1416
1424
  "fields": [
1417
1425
  "campaignBrief with ## Approved Message Template, Token Fill Rules, Token Fill Examples",
1418
- "currentStep:validate-sample",
1419
1426
  "useMessagingTemplate:true"
1420
1427
  ],
1421
1428
  "requiredBeforeImport": false,
1422
1429
  "onFailure": "stop_before_import_or_enrichment",
1423
1430
  "requiredBeforeCascade": true,
1424
1431
  "writesCampaignState": "campaignBrief.Approved Message Template"
1432
+ },
1433
+ {
1434
+ "action": "advance_watch_to_validation_after_message_approval",
1435
+ "tool": "update_campaign",
1436
+ "requires": [
1437
+ "campaignId",
1438
+ "message-review-decision.md"
1439
+ ],
1440
+ "requiredValues": {
1441
+ "currentStep": "validate-sample"
1442
+ },
1443
+ "when": "after_update_campaign_brief_succeeds",
1444
+ "requiredBeforeCascade": true,
1445
+ "writesCampaignState": "currentStep:validate-sample"
1425
1446
  }
1426
1447
  ],
1427
1448
  "requiredArtifacts": [
@@ -1439,7 +1460,8 @@
1439
1460
  "AskUserQuestion",
1440
1461
  "request_user_input",
1441
1462
  "get_subskill_prompt",
1442
- "update_campaign_brief"
1463
+ "update_campaign_brief",
1464
+ "update_campaign"
1443
1465
  ],
1444
1466
  "doNotAllow": [
1445
1467
  "create_campaign",