@sellable/mcp 0.1.492 → 0.1.494

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.
@@ -3641,11 +3641,11 @@ export async function searchSignals(input) {
3641
3641
  const effectiveHeadlineICPCriteria = headlineICPCriteria && headlineICPCriteria.length > 0
3642
3642
  ? headlineICPCriteria
3643
3643
  : rubricGuidelines;
3644
- if (campaignOfferId) {
3644
+ if (campaignOfferId && searchCurrentStep) {
3645
3645
  await api.put(`/api/v2/campaign-offers/${campaignOfferId}`, {
3646
3646
  leadSourceType: "new",
3647
3647
  leadSourceProvider: "signal-discovery",
3648
- ...(searchCurrentStep ? { currentStep: searchCurrentStep } : {}),
3648
+ currentStep: searchCurrentStep,
3649
3649
  ...(currentStepTransition ? { currentStepTransition } : {}),
3650
3650
  watchNarration: buildSignalDiscoverySearchWatchNarration(),
3651
3651
  });
@@ -3661,9 +3661,9 @@ export async function searchSignals(input) {
3661
3661
  }
3662
3662
  const response = await api.post(`/api/v1/signal-discovery/search-signals`, searchRequest);
3663
3663
  const summary = summarizeSignalSearchResponse(response);
3664
- if (campaignOfferId) {
3664
+ if (campaignOfferId && searchCurrentStep) {
3665
3665
  await api.put(`/api/v2/campaign-offers/${campaignOfferId}`, {
3666
- ...(searchCurrentStep ? { currentStep: searchCurrentStep } : {}),
3666
+ currentStep: searchCurrentStep,
3667
3667
  ...(currentStepTransition ? { currentStepTransition } : {}),
3668
3668
  watchNarration: buildSignalDiscoveryResultsWatchNarration(summary.postsReturned),
3669
3669
  });
@@ -4572,7 +4572,7 @@ export function getProviderPrompt(input) {
4572
4572
  }
4573
4573
  export async function selectPromisingPosts(input) {
4574
4574
  const api = getApi();
4575
- const { campaignOfferId, selections, headlineICPCriteria, selectionMode, mode, scrapePlanMode, targetEngagerCount, maxPostsToScrape, } = input;
4575
+ const { campaignOfferId, selections, headlineICPCriteria, currentStep, selectionMode, mode, scrapePlanMode, targetEngagerCount, maxPostsToScrape, } = input;
4576
4576
  const effectiveMode = selectionMode ?? mode ?? "replace";
4577
4577
  const effectiveScrapePlanMode = scrapePlanMode ??
4578
4578
  (targetEngagerCount || maxPostsToScrape
@@ -4672,10 +4672,13 @@ Approval card should say:
4672
4672
 
4673
4673
  **Approve scraping ${selectionResult.selectedCount} recommended LinkedIn post${selectionResult.selectedCount === 1 ? "" : "s"}?**`;
4674
4674
  }
4675
- await api.put(`/api/v2/campaign-offers/${campaignOfferId}`, {
4676
- currentStep: "signal-discovery",
4677
- watchNarration: buildSelectedPostApprovalWatchNarration(selectionResult.selectedCount, recommendedPostCount, recommendationTargetEngagerCount),
4678
- });
4675
+ const selectionCurrentStep = currentStep === null ? undefined : (currentStep ?? "signal-discovery");
4676
+ if (selectionCurrentStep) {
4677
+ await api.put(`/api/v2/campaign-offers/${campaignOfferId}`, {
4678
+ currentStep: selectionCurrentStep,
4679
+ watchNarration: buildSelectedPostApprovalWatchNarration(selectionResult.selectedCount, recommendedPostCount, recommendationTargetEngagerCount),
4680
+ });
4681
+ }
4679
4682
  return {
4680
4683
  success: true,
4681
4684
  selectedCount: selectionResult.selectedCount,
@@ -1,7 +1,8 @@
1
1
  import { getRefillTargetPlan } from "./refill-target-plan.js";
2
2
  import { refreshPaidInmailCredits } from "./senders.js";
3
3
  import { startPrepareCampaignMessages } from "./campaign-message-preparation.js";
4
- import { confirmLeadList } from "./leads.js";
4
+ import { confirmLeadList, importLeads, searchSignals, selectPromisingPosts, } from "./leads.js";
5
+ import { markProviderPromptLoaded } from "./provider-preflight.js";
5
6
  import { getApi } from "../api.js";
6
7
  const DEFAULT_SCHEDULER_FORWARD_HOURS = 48;
7
8
  const PAID_INMAIL_REFRESH_MAX_ATTEMPTS = 3;
@@ -320,6 +321,21 @@ function stringArray(value) {
320
321
  return [];
321
322
  return value.filter((item) => typeof item === "string");
322
323
  }
324
+ function uniqueStrings(values) {
325
+ const seen = new Set();
326
+ const result = [];
327
+ for (const value of values) {
328
+ const normalized = typeof value === "string" ? value.trim() : "";
329
+ if (!normalized)
330
+ continue;
331
+ const key = normalized.toLowerCase();
332
+ if (seen.has(key))
333
+ continue;
334
+ seen.add(key);
335
+ result.push(normalized);
336
+ }
337
+ return result;
338
+ }
323
339
  function paidRefreshActionFrom(value) {
324
340
  const action = recordValue(value);
325
341
  if (!action || action.type !== "refresh_paid_inmail_credits")
@@ -427,6 +443,53 @@ function actionSourceFingerprint(action) {
427
443
  stringValue(ids.sourceFingerprint) ??
428
444
  stringValue(action.sourceFingerprint));
429
445
  }
446
+ function actionLeadSourceProvider(action) {
447
+ const ids = actionIds(action);
448
+ const toolInput = actionToolInput(action);
449
+ return (stringValue(toolInput.leadSourceProvider) ??
450
+ stringValue(toolInput.provider) ??
451
+ stringValue(ids.leadSourceProvider) ??
452
+ stringValue(action.leadSourceProvider));
453
+ }
454
+ function normalizedSignalKeyword(value) {
455
+ if (typeof value !== "string")
456
+ return null;
457
+ const keyword = value.trim();
458
+ if (!keyword)
459
+ return null;
460
+ if (/^(https?:\/\/|www\.|linkedin\.com\/|\/?in\/)/i.test(keyword)) {
461
+ return null;
462
+ }
463
+ return keyword;
464
+ }
465
+ function keywordsFromSignalTabs(tabs) {
466
+ const selectedKeywords = [];
467
+ const fallbackKeywords = [];
468
+ for (const tab of tabs) {
469
+ const keyword = normalizedSignalKeyword(tab.keyword);
470
+ if (!keyword)
471
+ continue;
472
+ const selected = (tab.posts ?? []).some((post) => post.isSelected === true);
473
+ if (selected) {
474
+ selectedKeywords.push(keyword);
475
+ }
476
+ else {
477
+ fallbackKeywords.push(keyword);
478
+ }
479
+ }
480
+ return uniqueStrings([...selectedKeywords, ...fallbackKeywords]).slice(0, 5);
481
+ }
482
+ function postIdsFromSignalSearch(summary, maxPosts) {
483
+ const recommendedPostIds = stringArray(summary?.recommendedPostIds);
484
+ const topPostIds = Array.isArray(summary?.topPosts)
485
+ ? summary.topPosts
486
+ .map((post) => post && typeof post === "object"
487
+ ? stringValue(post.id)
488
+ : null)
489
+ .filter((id) => Boolean(id))
490
+ : [];
491
+ return uniqueStrings([...recommendedPostIds, ...topPostIds]).slice(0, Math.max(1, maxPosts));
492
+ }
430
493
  function refillPrepareRequestHash(params) {
431
494
  return [
432
495
  "refill_sends",
@@ -470,6 +533,141 @@ async function approveGeneratedMessagesBatch(action) {
470
533
  });
471
534
  return { status: "executed_and_reread", result };
472
535
  }
536
+ async function continueSignalDiscoverySource(action) {
537
+ const campaignOfferId = actionCampaignId(action);
538
+ const sourceLeadListId = actionSourceLeadListId(action);
539
+ const sourceFingerprint = actionSourceFingerprint(action);
540
+ const toolInput = actionToolInput(action);
541
+ const sourceRowLimit = Math.min(1500, Math.max(100, Math.floor(numberValue(toolInput.sourceRowLimit) ??
542
+ numberValue(toolInput.targetRows) ??
543
+ numberValue(action.targetRows) ??
544
+ 100)));
545
+ const maxPostsToScrape = Math.min(5, Math.max(3, Math.ceil(sourceRowLimit / 500)));
546
+ if (!campaignOfferId || !sourceLeadListId || !sourceFingerprint) {
547
+ return {
548
+ status: "refused",
549
+ refusalReason: "continue_signal_discovery_source action is missing campaignOfferId, sourceLeadListId, or sourceFingerprint",
550
+ };
551
+ }
552
+ const requestedProvider = actionLeadSourceProvider(action);
553
+ if (requestedProvider &&
554
+ requestedProvider !== "signal-discovery" &&
555
+ requestedProvider !== "campaign-tracked-post") {
556
+ return {
557
+ status: "refused",
558
+ refusalReason: "continue_signal_discovery_source can only run for Signal Discovery source families",
559
+ };
560
+ }
561
+ const api = getApi();
562
+ const campaign = await api.get(`/api/v2/campaign-offers/${campaignOfferId}`);
563
+ if (campaign.leadSourceProvider !== "signal-discovery" &&
564
+ campaign.leadSourceProvider !== "campaign-tracked-post") {
565
+ return {
566
+ status: "refused",
567
+ refusalReason: "campaign leadSourceProvider is not Signal Discovery; refusing same-source continuation",
568
+ };
569
+ }
570
+ if (campaign.selectedLeadListId &&
571
+ campaign.selectedLeadListId !== sourceLeadListId) {
572
+ return {
573
+ status: "refused",
574
+ refusalReason: "campaign selectedLeadListId changed since the plan packet; rerun get_refill_target_plan before source continuation",
575
+ };
576
+ }
577
+ const sourceMeta = await api.get(`/api/v3/workflow-tables/${sourceLeadListId}?mode=meta`);
578
+ const sourceConfig = sourceMeta.table?.config ?? null;
579
+ const headlineICPCriteria = stringArray(sourceConfig?.headlineICPCriteria).length > 0
580
+ ? stringArray(sourceConfig?.headlineICPCriteria)
581
+ : stringArray(sourceConfig?.rubricGuidelines);
582
+ const tabsResponse = await api.get(`/api/v3/campaigns/${campaignOfferId}/signal-discovery/tabs`);
583
+ const keywords = keywordsFromSignalTabs(tabsResponse.tabs ?? []);
584
+ if (keywords.length === 0) {
585
+ return {
586
+ status: "refused",
587
+ refusalReason: "no reusable Signal Discovery keywords were found on the campaign tabs",
588
+ };
589
+ }
590
+ markProviderPromptLoaded({
591
+ provider: "signal-discovery",
592
+ campaignOfferId,
593
+ });
594
+ const searchSummary = await searchSignals({
595
+ type: "keywords",
596
+ keywords: keywords.map((keyword) => ({
597
+ keyword,
598
+ source: "refill-sends-source-continuation",
599
+ })),
600
+ campaignOfferId,
601
+ currentStep: null,
602
+ headlineICPCriteria,
603
+ rubricGuidelines: headlineICPCriteria,
604
+ confirmed: true,
605
+ limit: 50,
606
+ });
607
+ const selectedPostIds = postIdsFromSignalSearch(searchSummary, maxPostsToScrape);
608
+ if (selectedPostIds.length === 0) {
609
+ return {
610
+ status: "refused",
611
+ refusalReason: "Signal Discovery search returned no recommended posts to continue the source",
612
+ result: { keywords, searchSummary },
613
+ };
614
+ }
615
+ const selectionResult = await selectPromisingPosts({
616
+ campaignOfferId,
617
+ selections: selectedPostIds.map((postId) => ({
618
+ postId,
619
+ reason: "refill_sends same-source continuation: recent post from the campaign's existing Signal Discovery keyword family",
620
+ })),
621
+ headlineICPCriteria,
622
+ currentStep: null,
623
+ selectionMode: "replace",
624
+ scrapePlanMode: "all-selected",
625
+ });
626
+ if (selectionResult.success === false) {
627
+ return {
628
+ status: "refused",
629
+ refusalReason: stringValue(selectionResult.message) ??
630
+ "select_promising_posts did not select any posts",
631
+ result: { keywords, selectedPostIds, selectionResult },
632
+ };
633
+ }
634
+ const importResult = await importLeads({
635
+ campaignOfferId,
636
+ provider: "signal-discovery",
637
+ sourceLeadListId,
638
+ currentStep: null,
639
+ headlineICPCriteria,
640
+ rubricGuidelines: headlineICPCriteria,
641
+ confirmed: true,
642
+ maxPostsToScrape: selectedPostIds.length,
643
+ allowInvalidSignalPosts: true,
644
+ });
645
+ const importRecord = recordValue(importResult);
646
+ if (importRecord?.error) {
647
+ return {
648
+ status: "refused",
649
+ refusalReason: stringValue(importRecord.message) ??
650
+ `Signal Discovery import returned ${String(importRecord.error)}`,
651
+ result: { keywords, selectedPostIds, selectionResult, importResult },
652
+ };
653
+ }
654
+ return {
655
+ status: "executed_and_reread",
656
+ result: {
657
+ provider: "signal-discovery",
658
+ campaignOfferId,
659
+ previousSourceLeadListId: sourceLeadListId,
660
+ sourceFingerprint,
661
+ keywords,
662
+ selectedPostIds,
663
+ sourceRowLimit,
664
+ maxPostsToScrape: selectedPostIds.length,
665
+ searchSummary,
666
+ selectionResult,
667
+ importResult,
668
+ },
669
+ };
670
+ }
473
671
  async function refreshPaidInmailCreditsWithRetry(senderId) {
474
672
  const errors = [];
475
673
  for (let attempt = 1; attempt <= PAID_INMAIL_REFRESH_MAX_ATTEMPTS; attempt += 1) {
@@ -567,6 +765,8 @@ async function executeOneYoloPrimitive(action) {
567
765
  });
568
766
  return { status: "executed_and_reread", result };
569
767
  }
768
+ case "continue_signal_discovery_source":
769
+ return continueSignalDiscoverySource(action);
570
770
  case "approve_messages":
571
771
  return approveGeneratedMessagesBatch(action);
572
772
  default:
@@ -25,7 +25,6 @@ const MANUAL_ONLY_PACKET_ACTION_TYPES = new Set([
25
25
  "lower_paid_inmail_threshold",
26
26
  "switch_to_connection_lane",
27
27
  "create_connection_campaign",
28
- "continue_signal_discovery_source",
29
28
  "continue_sales_nav_source",
30
29
  "continue_prospeo_source",
31
30
  "manual_source_replenishment",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sellable/mcp",
3
- "version": "0.1.492",
3
+ "version": "0.1.494",
4
4
  "type": "module",
5
5
  "description": "Sellable MCP server for Claude Code and Codex campaign workflows",
6
6
  "main": "dist/index.js",