@sellable/mcp 0.1.475 → 0.1.477

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.
@@ -1,5 +1,7 @@
1
1
  import { getRefillTargetPlan } from "./refill-target-plan.js";
2
2
  import { refreshPaidInmailCredits } from "./senders.js";
3
+ import { startPrepareCampaignMessages } from "./campaign-message-preparation.js";
4
+ import { confirmLeadList } from "./leads.js";
3
5
  const DEFAULT_SCHEDULER_FORWARD_HOURS = 48;
4
6
  function normalizeStrings(values) {
5
7
  if (!Array.isArray(values))
@@ -58,7 +60,7 @@ function normalizeTargetDate(value) {
58
60
  export const refillSendsToolDefinitions = [
59
61
  {
60
62
  name: "refill_sends",
61
- description: "Typed command entrypoint for Sellable refill sends. Accepts --yolo semantics and optional sender selectors, then returns the bounded execution contract for the skill-led refill workflow. In --yolo, it may refresh stale paid InMail credit cache facts for exact selected senders once, then rerun and return the post-refresh target plan. This tool does not mutate campaigns, import leads, approve messages, schedule sends, launch, archive, delete, or write scheduler rows.",
63
+ description: "Typed command entrypoint for Sellable refill sends. Accepts --yolo semantics and optional sender selectors, then returns the bounded execution contract for the skill-led refill workflow. In --yolo, it may refresh stale paid InMail credit cache facts once, or execute exactly one safe primitive from target.globalActionQueue[0] (bounded existing-row preparation, bounded same-source copy, or read-only wait) and reread. It does not approve messages without the bounded approval gate, lower paid InMail thresholds, switch source families, create campaigns, schedule sends, launch, archive, delete, or write scheduler rows.",
62
64
  inputSchema: {
63
65
  type: "object",
64
66
  properties: {
@@ -197,7 +199,7 @@ export function refillSendsCommand(input = {}) {
197
199
  "Render target.eligibleSenderLedger and target.senderRefillPlans before mutation. Preserve the coverage labels Need to prepare, Goal, Already sent, Scheduled, Ready and waiting to be scheduled, and Still need.",
198
200
  "Use target.globalActionQueue as the only cross-sender yolo queue: execute only target.globalActionQueue[0], one globally ranked primitive, then rerun get_refill_target_plan before choosing another action.",
199
201
  "If get_refill_target_plan returns status complete, report eligible sender ledger, target.senderRefillPlans, gross target, selected days, sent count, scheduled count, projected count, campaign ids, targetShapeRevision, and no-op proof without asking for approval.",
200
- "Refill target lanes are only connection invites (send_invite) or paid InMails (send_inmail_closed). Do not count send_dm or send_inmail_open as horizon target capacity; they may exist in sequences, but the planner must pick the refill lane from the most recent active campaign-backed send evidence for each sender.",
202
+ "Refill target lanes are connection invites (send_invite), standalone paid InMails (send_inmail_closed), or unified Sales Nav cascades represented publicly as send_inmail_closed with campaign classification sales_nav_cascade. For a Sales Nav cascade, refill the selected campaign first; its sequence can route prospects to Open InMail, paid InMail while fresh credits are >= 5, or same-campaign connection fallback without asking for separate open/paid/connection campaigns. Do not count send_dm as horizon target capacity.",
201
203
  "If remainingReadyOrProjectedGap is 0 but remainingProjectedGap is positive, run only a persistent read-only scheduler wait/reread loop; do not ask for prep/import/approval and do not close out while scheduler pickup is the only remaining state.",
202
204
  `Resolve route with resolve_campaign_fill_route({ intent: "${intent}"${input.campaignId ? `, campaignId: "${input.campaignId}"` : ""}${input.tableId ? `, tableId: "${input.tableId}"` : ""} }).`,
203
205
  'If the plain route shows stale managed waterfall evidence, archived/completed shared slots, or targets that do not cover the selected sender set, immediately call resolve_campaign_fill_route({ intent: "active" }) and inspect current dashboard-active ACTIVE/PAUSED campaign-backed sequence campaigns before declaring a sender blocked.',
@@ -213,14 +215,14 @@ export function refillSendsCommand(input = {}) {
213
215
  "Treat current dashboard-active PAUSED campaign-backed sequence campaigns as start-eligible candidates: read refill state before deciding whether to prep, approve, start, or skip.",
214
216
  "Fresh reread get_campaign_refill_state immediately before any import, prep, approval, or horizon-fill mutation.",
215
217
  "Maintain a target-window saturation ledger per selected sender from get_refill_target_plan: selected days, gross capacity, actual sent counts, future scheduler-owned scheduled counts, projected counts, ready-to-schedule buffer, remaining projected gap, paid InMail threshold feasibility, targetShapeRevision, stateRevision, and next MCP primitive.",
216
- "In --yolo, this tool automatically maintains a run-local refreshedPaidInmailSenderIds set: when the first target plan returns refresh_paid_inmail_credits candidates for selected paid-InMail lanes, it refreshes each exact sender at most once, reruns get_refill_target_plan, and returns the post-refresh targetPlan before any prep/import/approval/start action is chosen.",
218
+ "In --yolo, this tool automatically maintains a run-local refreshedPaidInmailSenderIds set: when the first target plan returns refresh_paid_inmail_credits candidates for selected paid-InMail lanes, it refreshes each exact sender at most once, reruns get_refill_target_plan, and returns the post-refresh targetPlan before any prep/source-copy/read-only wait action is chosen.",
217
219
  "Do not present paid-credit refresh as the next operator action after refill_sends returns autoPaidInmailRefresh and the post-refresh targetPlan.",
218
- "If paid InMail feasibility remains below threshold after that automatic refresh, report the exact sender/campaign/table/column threshold action or connection fallback; --yolo must not lower paid InMail thresholds or create campaigns.",
220
+ "If paid InMail feasibility remains below threshold after that automatic refresh, report the exact sender/campaign/table/column threshold action or same-campaign connection fallback; --yolo must not lower paid InMail thresholds or create campaigns.",
219
221
  "In --yolo or after one Accept, continue through every safe selected sender/campaign action covered by the rendered target packet; sent/scheduled-count progress changes stateRevision and should continue while targetShapeRevision is stable.",
220
222
  "Do not complete a fill/schedule request until a final get_refill_target_plan or refill-state reread proves projected coverage (sent + scheduled) fills the scheduler-forward target window. Treat awaiting_scheduler_after_ready_buffer as loaded, awaiting scheduler when ready buffer covers the gap; do not import/prep more rows in that state, and keep polling unless Christian stops/statuses the run or a concrete non-scheduler blocker such as paid_inmail_below_threshold appears.",
221
223
  ],
222
224
  approvalContract: yolo
223
- ? "Auto-accept only the rendered bounded refill target packet after get_refill_target_plan and fresh reread. Execute one globally ranked primitive from target.globalActionQueue at a time, then rerun get_refill_target_plan before taking another action. Continue through every safe apply/prep/start action inside that packet, including start_campaign only for exact selected PAUSED campaign-backed sequence refill targets named in the packet. After each action, rerun get_refill_target_plan; keep going while targetShapeRevision is stable and projected coverage (sent + scheduled) progresses toward the target, even though stateRevision changes. Ready-to-schedule rows are buffer, not completion. If ready buffer covers the projected gap, use only persistent read-only scheduler wait/reread and keep the run open until projected coverage fills or Christian stops/statuses it. Stop immediately if targetShapeRevision changes because sender set, route, ids, caps, dates, blockers, action class, paid InMail threshold feasibility, or side-effect class drifts. --yolo does not authorize lowering paid InMail thresholds or creating connection campaigns."
225
+ ? "Auto-accept only the rendered bounded refill target packet after get_refill_target_plan and fresh reread. Execute one globally ranked primitive from target.globalActionQueue at a time, then rerun get_refill_target_plan before taking another action. Continue through every safe prep/source-copy/read-only wait action inside that packet. Approval, start_campaign, source-family switches, threshold changes, and campaign creation require their own explicit gates. After each safe action, rerun get_refill_target_plan; keep going while targetShapeRevision is stable and projected coverage (sent + scheduled) progresses toward the target, even though stateRevision changes. Ready-to-schedule rows are buffer, not completion. If ready buffer covers the projected gap, use only persistent read-only scheduler wait/reread and keep the run open until projected coverage fills or Christian stops/statuses it. Stop immediately if targetShapeRevision changes because sender set, route, ids, caps, dates, blockers, action class, paid InMail threshold feasibility, or side-effect class drifts. --yolo does not authorize lowering paid InMail thresholds or creating connection campaigns."
224
226
  : hasSenderSelectors
225
227
  ? "Before mutation, post the full bounded refill packet in normal chat as Markdown, including workspace, sender scope, campaign table, exact ids, caps/dates, gross target, sent count, scheduled count, projected count, ready buffer, remaining projected gap, paid InMail threshold feasibility, targetShapeRevision/stateRevision, side effects, forbidden actions, and stop condition. Then ask the final Accept/Decline structured approval question with a compact body that refers back to the posted packet instead of duplicating it. Only ask when get_refill_target_plan reports a positive remaining projected/ready gap. If target is complete by projected coverage, no-op without approval. If ready buffer covers the projected gap, run only persistent read-only scheduler wait/reread and keep the run open until projected coverage fills or Christian stops/statuses it. Threshold changes and campaign creation need separate explicit approval."
226
228
  : "Ask which eligible enrolled senders to refill first, then, before mutation, post the full bounded refill packet in normal chat as Markdown and ask the final Accept/Decline structured approval question with a compact body that refers back to the posted packet.",
@@ -302,6 +304,14 @@ function recordValue(value) {
302
304
  function stringValue(value) {
303
305
  return typeof value === "string" && value.trim() ? value.trim() : null;
304
306
  }
307
+ function numberValue(value) {
308
+ return typeof value === "number" && Number.isFinite(value) ? value : null;
309
+ }
310
+ function stringArray(value) {
311
+ if (!Array.isArray(value))
312
+ return [];
313
+ return value.filter((item) => typeof item === "string");
314
+ }
305
315
  function paidRefreshActionFrom(value) {
306
316
  const action = recordValue(value);
307
317
  if (!action || action.type !== "refresh_paid_inmail_credits")
@@ -349,6 +359,121 @@ function collectPaidInmailRefreshActions(plan) {
349
359
  add(action);
350
360
  return [...bySender.values()];
351
361
  }
362
+ function firstGlobalAction(plan) {
363
+ const root = recordValue(plan);
364
+ const target = recordValue(root?.target);
365
+ const globalActionQueue = Array.isArray(target?.globalActionQueue)
366
+ ? target.globalActionQueue
367
+ : [];
368
+ const [first] = globalActionQueue;
369
+ return recordValue(first);
370
+ }
371
+ function actionIds(action) {
372
+ return recordValue(action.ids) ?? {};
373
+ }
374
+ function actionToolInput(action) {
375
+ return recordValue(action.toolInput) ?? {};
376
+ }
377
+ function actionCampaignId(action) {
378
+ const ids = actionIds(action);
379
+ const toolInput = actionToolInput(action);
380
+ return (stringValue(toolInput.campaignId) ??
381
+ stringValue(toolInput.campaignOfferId) ??
382
+ stringValue(ids.campaignId) ??
383
+ stringValue(action.campaignId));
384
+ }
385
+ function actionTableId(action) {
386
+ const ids = actionIds(action);
387
+ const toolInput = actionToolInput(action);
388
+ return (stringValue(toolInput.tableId) ??
389
+ stringValue(ids.tableId) ??
390
+ stringValue(action.tableId));
391
+ }
392
+ function actionSourceLeadListId(action) {
393
+ const ids = actionIds(action);
394
+ const toolInput = actionToolInput(action);
395
+ return (stringValue(toolInput.sourceLeadListId) ??
396
+ stringValue(ids.sourceLeadListId) ??
397
+ stringValue(action.sourceLeadListId));
398
+ }
399
+ async function executeOneYoloPrimitive(action) {
400
+ if (!action)
401
+ return { status: "no_action" };
402
+ if (action.yoloEligible === false) {
403
+ return {
404
+ status: "refused",
405
+ refusalReason: "first global action is not yolo eligible",
406
+ };
407
+ }
408
+ switch (action.type) {
409
+ case "wait_for_scheduler":
410
+ case "wait_for_active_work":
411
+ case "wait_for_source_import":
412
+ return { status: "read_only_reread", result: { waited: true } };
413
+ case "prepare_messages": {
414
+ const campaignId = actionCampaignId(action);
415
+ const tableId = actionTableId(action) ?? undefined;
416
+ const toolInput = actionToolInput(action);
417
+ const targetPreparedMessages = numberValue(toolInput.targetPreparedMessages);
418
+ const approvalMode = toolInput.approvalMode === "approve" ? "approve" : "mark_ready";
419
+ if (!campaignId || !targetPreparedMessages || targetPreparedMessages <= 0) {
420
+ return {
421
+ status: "refused",
422
+ refusalReason: "prepare_messages action is missing campaignId or bounded targetPreparedMessages",
423
+ };
424
+ }
425
+ const result = await startPrepareCampaignMessages({
426
+ campaignId,
427
+ tableId,
428
+ targetPreparedMessages,
429
+ approvalMode,
430
+ autoContinue: true,
431
+ });
432
+ return { status: "executed_and_reread", result };
433
+ }
434
+ case "copy_selected_source_rows": {
435
+ const campaignOfferId = actionCampaignId(action);
436
+ const sourceLeadListId = actionSourceLeadListId(action);
437
+ const toolInput = actionToolInput(action);
438
+ const sourceRowIds = stringArray(toolInput.sourceRowIds);
439
+ const sourceRowLimit = numberValue(toolInput.sourceRowLimit);
440
+ const sourceFingerprint = stringValue(toolInput.sourceFingerprint) ??
441
+ stringValue(actionIds(action).sourceFingerprint);
442
+ if (!campaignOfferId || !sourceLeadListId || !sourceFingerprint) {
443
+ return {
444
+ status: "refused",
445
+ refusalReason: "copy_selected_source_rows action is missing campaignOfferId, sourceLeadListId, or sourceFingerprint",
446
+ };
447
+ }
448
+ if (sourceRowIds.length === 0 && (!sourceRowLimit || sourceRowLimit <= 0)) {
449
+ return {
450
+ status: "refused",
451
+ refusalReason: "copy_selected_source_rows action is missing exact sourceRowIds or a bounded sourceRowLimit",
452
+ };
453
+ }
454
+ const result = await confirmLeadList({
455
+ campaignOfferId,
456
+ sourceLeadListId,
457
+ currentStep: null,
458
+ confirmed: true,
459
+ sourceRowIds: sourceRowIds.length > 0 ? sourceRowIds : undefined,
460
+ sourceRowLimit: sourceRowIds.length > 0 ? undefined : sourceRowLimit ?? undefined,
461
+ reviewBatchLimit: sourceRowIds.length > 0 ? sourceRowIds.length : sourceRowLimit ?? undefined,
462
+ });
463
+ return { status: "executed_and_reread", result };
464
+ }
465
+ case "approve_messages":
466
+ return {
467
+ status: "refused",
468
+ refusalReason: "approve_messages requires the bounded approval mutation gate; refill_sends yolo will not treat select_campaign_cells as approval",
469
+ };
470
+ default:
471
+ return {
472
+ status: "refused",
473
+ refusalReason: `action ${String(action.type)} is not a safe yolo primitive`,
474
+ };
475
+ }
476
+ }
352
477
  export async function executeRefillSendsCommand(input = {}) {
353
478
  const command = refillSendsCommand(input);
354
479
  if (input.yolo !== true) {
@@ -361,6 +486,12 @@ export async function executeRefillSendsCommand(input = {}) {
361
486
  failedPaidInmailRefreshes: [],
362
487
  note: "Automatic paid InMail credit refresh only runs for --yolo refill_sends calls.",
363
488
  },
489
+ yoloExecution: {
490
+ enabled: false,
491
+ status: "not_run_without_yolo",
492
+ selectedAction: null,
493
+ targetPlanReread: false,
494
+ },
364
495
  };
365
496
  }
366
497
  const targetPlanInput = targetPlanInputFor(input);
@@ -391,10 +522,21 @@ export async function executeRefillSendsCommand(input = {}) {
391
522
  const targetPlan = refreshActions.length > 0
392
523
  ? await getRefillTargetPlan(targetPlanInput)
393
524
  : targetPlanBeforePaidRefresh;
525
+ const selectedAction = refreshActions.length > 0 ? null : firstGlobalAction(targetPlan);
526
+ const primitiveAttempt = refreshActions.length > 0
527
+ ? null
528
+ : await executeOneYoloPrimitive(selectedAction);
529
+ const shouldRereadAfterPrimitive = primitiveAttempt?.status === "executed_and_reread" ||
530
+ primitiveAttempt?.status === "read_only_reread";
531
+ const postActionTargetPlan = shouldRereadAfterPrimitive
532
+ ? await getRefillTargetPlan(targetPlanInput)
533
+ : null;
534
+ const finalTargetPlan = postActionTargetPlan ?? targetPlan;
394
535
  return {
395
536
  ...command,
396
- targetPlan,
537
+ targetPlan: finalTargetPlan,
397
538
  targetPlanBeforePaidRefresh: refreshActions.length > 0 ? targetPlanBeforePaidRefresh : null,
539
+ targetPlanBeforeYoloPrimitive: refreshActions.length === 0 && postActionTargetPlan ? targetPlan : null,
398
540
  autoPaidInmailRefresh: {
399
541
  enabled: true,
400
542
  status: refreshActions.length === 0
@@ -411,5 +553,23 @@ export async function executeRefillSendsCommand(input = {}) {
411
553
  ? "refill_sends refreshed stale paid InMail credit facts internally, once per sender, then returned the post-refresh targetPlan."
412
554
  : "No stale or missing paid InMail credit refresh candidates were present in the initial target plan.",
413
555
  },
556
+ yoloExecution: refreshActions.length > 0
557
+ ? {
558
+ enabled: true,
559
+ status: "skipped_after_paid_refresh",
560
+ selectedAction: null,
561
+ targetPlanReread: true,
562
+ }
563
+ : {
564
+ enabled: true,
565
+ status: primitiveAttempt?.status ?? "no_action",
566
+ selectedAction,
567
+ result: primitiveAttempt?.result,
568
+ targetPlanReread: shouldRereadAfterPrimitive,
569
+ refusalReason: primitiveAttempt?.refusalReason,
570
+ postActionFirstAction: postActionTargetPlan
571
+ ? firstGlobalAction(postActionTargetPlan)
572
+ : null,
573
+ },
414
574
  };
415
575
  }
@@ -9,6 +9,13 @@ const SAFE_PACKET_ACTION_TYPES = new Set([
9
9
  "approve_messages",
10
10
  "start_paused_campaign",
11
11
  "wait_for_scheduler",
12
+ "wait_for_active_work",
13
+ "wait_for_source_import",
14
+ "copy_selected_source_rows",
15
+ "continue_signal_discovery_source",
16
+ "continue_sales_nav_source",
17
+ "continue_prospeo_source",
18
+ "manual_source_replenishment",
12
19
  "refresh_paid_inmail_credits",
13
20
  "lower_paid_inmail_threshold",
14
21
  "switch_to_connection_lane",
@@ -18,6 +25,10 @@ const MANUAL_ONLY_PACKET_ACTION_TYPES = new Set([
18
25
  "lower_paid_inmail_threshold",
19
26
  "switch_to_connection_lane",
20
27
  "create_connection_campaign",
28
+ "continue_signal_discovery_source",
29
+ "continue_sales_nav_source",
30
+ "continue_prospeo_source",
31
+ "manual_source_replenishment",
21
32
  ]);
22
33
  const SOURCE_PLAN_STATES = new Set([
23
34
  "not_needed",
@@ -26,6 +37,8 @@ const SOURCE_PLAN_STATES = new Set([
26
37
  "needs_signal_import",
27
38
  "waiting_for_import",
28
39
  "needs_confirm",
40
+ "needs_same_source_copy",
41
+ "needs_provider_continuation",
29
42
  "source_shortfall",
30
43
  "provider_exhausted",
31
44
  "provider_missing",
@@ -57,6 +70,9 @@ function stringValue(value) {
57
70
  function booleanValue(value) {
58
71
  return typeof value === "boolean" ? value : undefined;
59
72
  }
73
+ function campaignClassificationValue(value) {
74
+ return value === "sales_nav_cascade" ? "sales_nav_cascade" : undefined;
75
+ }
60
76
  function stringArray(value) {
61
77
  if (!Array.isArray(value))
62
78
  return [];
@@ -149,6 +165,16 @@ function sanitizeActionCandidate(candidate) {
149
165
  tableId: stringValue(candidate.tableId),
150
166
  tableName: stringValue(candidate.tableName) ?? null,
151
167
  columnId: stringValue(candidate.columnId) ?? null,
168
+ sourceLeadListId: stringValue(candidate.sourceLeadListId) ?? null,
169
+ leadSourceProvider: stringValue(candidate.leadSourceProvider) ?? null,
170
+ sourceFingerprint: stringValue(candidate.sourceFingerprint) ?? null,
171
+ sourceInventoryStatus: stringValue(candidate.sourceInventoryStatus) ?? null,
172
+ sourceRowIds: stringArray(candidate.sourceRowIds),
173
+ sourceRowLimit: typeof candidate.sourceRowLimit === "number"
174
+ ? candidate.sourceRowLimit
175
+ : undefined,
176
+ activeJobId: stringValue(candidate.activeJobId) ?? null,
177
+ activeImportStatus: stringValue(candidate.activeImportStatus) ?? null,
152
178
  targetRows: typeof candidate.targetRows === "number" ? candidate.targetRows : undefined,
153
179
  columnRole: stringValue(candidate.columnRole),
154
180
  rowSelector: sanitizeRowSelector(candidate.rowSelector),
@@ -186,6 +212,21 @@ function sanitizeToolInput(value) {
186
212
  campaignId: stringValue(value.campaignId),
187
213
  tableId: stringValue(value.tableId),
188
214
  columnId: stringValue(value.columnId) ?? null,
215
+ sourceLeadListId: stringValue(value.sourceLeadListId) ?? null,
216
+ leadSourceProvider: stringValue(value.leadSourceProvider) ?? null,
217
+ sourceFingerprint: stringValue(value.sourceFingerprint) ?? null,
218
+ activeJobId: stringValue(value.activeJobId) ?? null,
219
+ activeImportStatus: stringValue(value.activeImportStatus) ?? null,
220
+ sourceInventoryStatus: stringValue(value.sourceInventoryStatus) ?? null,
221
+ sourceRowIds: stringArray(value.sourceRowIds),
222
+ sourceRowLimit: typeof value.sourceRowLimit === "number"
223
+ ? value.sourceRowLimit
224
+ : undefined,
225
+ campaignOfferId: stringValue(value.campaignOfferId),
226
+ provider: stringValue(value.provider),
227
+ allowDifferentSourceLeadList: typeof value.allowDifferentSourceLeadList === "boolean"
228
+ ? value.allowDifferentSourceLeadList
229
+ : undefined,
189
230
  targetPreparedMessages: typeof value.targetPreparedMessages === "number"
190
231
  ? value.targetPreparedMessages
191
232
  : undefined,
@@ -212,6 +253,10 @@ function sanitizePacketIds(value) {
212
253
  campaignId: stringValue(value.campaignId),
213
254
  tableId: stringValue(value.tableId),
214
255
  columnId: stringValue(value.columnId) ?? null,
256
+ sourceLeadListId: stringValue(value.sourceLeadListId) ?? null,
257
+ leadSourceProvider: stringValue(value.leadSourceProvider) ?? null,
258
+ sourceFingerprint: stringValue(value.sourceFingerprint) ?? null,
259
+ activeJobId: stringValue(value.activeJobId) ?? null,
215
260
  };
216
261
  }
217
262
  function sanitizeStructuredAction(action, selectedBySender, mode, rank) {
@@ -307,6 +352,7 @@ function sanitizeCampaignRanking(value) {
307
352
  tableId: stringValue(option.tableId) ?? "",
308
353
  tableName: stringValue(option.tableName) ?? null,
309
354
  campaignStatus: stringValue(option.campaignStatus) ?? null,
355
+ classification: campaignClassificationValue(option.classification),
310
356
  supportsSelectedLane: option.supportsSelectedLane === true,
311
357
  rankSignals: stringArray(option.rankSignals),
312
358
  rankReason: stringValue(option.rankReason) ?? "",
@@ -330,6 +376,9 @@ function sanitizeSourcePlan(value) {
330
376
  reason: stringValue(value.reason),
331
377
  campaignId: stringValue(value.campaignId),
332
378
  tableId: stringValue(value.tableId),
379
+ sourceLeadListId: stringValue(value.sourceLeadListId) ?? null,
380
+ leadSourceProvider: stringValue(value.leadSourceProvider) ?? null,
381
+ sourceFingerprint: stringValue(value.sourceFingerprint) ?? null,
333
382
  remainingRowsNeeded: typeof value.remainingRowsNeeded === "number"
334
383
  ? value.remainingRowsNeeded
335
384
  : undefined,
@@ -351,6 +400,11 @@ function sanitizePaidInmail(value) {
351
400
  tableId: stringValue(value.tableId) ?? null,
352
401
  tableName: stringValue(value.tableName) ?? null,
353
402
  columnId: stringValue(value.columnId) ?? null,
403
+ maxStalenessSeconds: numberValue(value.maxStalenessSeconds),
404
+ campaignClassification: campaignClassificationValue(value.campaignClassification) ?? null,
405
+ connectionFallbackAvailable: booleanValue(value.connectionFallbackAvailable) ?? false,
406
+ connectionFallbackCampaignId: stringValue(value.connectionFallbackCampaignId) ?? null,
407
+ connectionFallbackCampaignName: stringValue(value.connectionFallbackCampaignName) ?? null,
354
408
  suggestedThreshold: typeof value.suggestedThreshold === "number"
355
409
  ? value.suggestedThreshold
356
410
  : null,
@@ -622,7 +676,7 @@ function sanitizeRefillTargetPlanResult(result) {
622
676
  export const refillTargetPlanToolDefinitions = [
623
677
  {
624
678
  name: "get_refill_target_plan",
625
- description: "read-only refill target planner to call before any refill mutation. It identifies eligible senders and infers one implicit refill lane per sender: connection invites or paid InMails, chosen from future scheduled, recent scheduled, and ready-to-schedule evidence in active campaign-backed sequence campaigns before falling back to campaign sequences. DMs and open-profile InMails are follow-up sequence actions, not refill target lanes. By default the planner computes the scheduler-forward 48-hour target window, selected sender-local days whose sending windows overlap that window, gross target, actual sent coverage, scheduler-owned scheduled coverage, projected coverage (sent + scheduled), ready buffer, remaining projected gap, paid InMail credit/threshold feasibility, bounded action candidates, and targetRevision drift proof. This tool does not create rows, import leads, prepare messages, approve messages, does not schedule sends, start campaigns, lower paid InMail thresholds, create campaigns, launch, spend InMail credits, or write scheduler fields. Complete projected targets no-op without approval; loaded, awaiting scheduler targets use read-only wait/reread until projected coverage fills.",
679
+ description: "read-only refill target planner to call before any refill mutation. It identifies eligible senders and infers one implicit refill lane per sender. For unified Sales Nav cascade campaigns, the public refill lane is the paid-InMail cascade target (send_inmail_closed) with campaign classification sales_nav_cascade; the same campaign can still route prospects to Open InMail or connection-request fallback based on row eligibility and fresh paid credits >= 5. Non-cascade lanes remain connection invites or paid InMails, chosen from future scheduled, recent scheduled, and ready-to-schedule evidence in active campaign-backed sequence campaigns before falling back to campaign sequences. DMs remain follow-up sequence actions, not refill target lanes. By default the planner computes the scheduler-forward 48-hour target window, selected sender-local days whose sending windows overlap that window, gross target, actual sent coverage, scheduler-owned scheduled coverage, projected coverage (sent + scheduled), ready buffer, remaining projected gap, paid InMail credit/threshold/freshness feasibility, same-campaign connection fallback availability, bounded action candidates, and targetRevision drift proof. This tool does not create rows, import leads, prepare messages, approve messages, does not schedule sends, start campaigns, lower paid InMail thresholds, create campaigns, launch, spend InMail credits, or write scheduler fields. Complete projected targets no-op without approval; loaded, awaiting scheduler targets use read-only wait/reread until projected coverage fills.",
626
680
  inputSchema: {
627
681
  type: "object",
628
682
  properties: {
@@ -676,7 +730,7 @@ export const refillTargetPlanToolDefinitions = [
676
730
  type: "string",
677
731
  enum: ["send_invite", "send_inmail_closed"],
678
732
  },
679
- description: "Optional refill target lane override. Supported refill lanes are connection invites (send_invite) and paid InMails (send_inmail_closed) only. If omitted, the planner infers one per-sender lane from active campaign future scheduled, recent scheduled, and ready-to-schedule evidence before falling back to selected active campaign sequences.",
733
+ description: "Optional refill target lane override. Supported public refill lanes are connection invites (send_invite) and paid InMails / unified Sales Nav cascades (send_inmail_closed) only. If omitted, the planner infers one per-sender lane from active campaign future scheduled, recent scheduled, and ready-to-schedule evidence before falling back to selected active campaign sequences.",
680
734
  },
681
735
  approvalMode: {
682
736
  type: "string",
@@ -2952,6 +2952,8 @@ export declare const allTools: ({
2952
2952
  keepInSync?: undefined;
2953
2953
  jobId?: undefined;
2954
2954
  reviewBatchLimit?: undefined;
2955
+ sourceRowIds?: undefined;
2956
+ sourceRowLimit?: undefined;
2955
2957
  allowPartialSourceList?: undefined;
2956
2958
  includeRawImportResult?: undefined;
2957
2959
  selections?: undefined;
@@ -3146,6 +3148,8 @@ export declare const allTools: ({
3146
3148
  keepInSync?: undefined;
3147
3149
  jobId?: undefined;
3148
3150
  reviewBatchLimit?: undefined;
3151
+ sourceRowIds?: undefined;
3152
+ sourceRowLimit?: undefined;
3149
3153
  allowPartialSourceList?: undefined;
3150
3154
  includeRawImportResult?: undefined;
3151
3155
  selections?: undefined;
@@ -3239,6 +3243,8 @@ export declare const allTools: ({
3239
3243
  keepInSync?: undefined;
3240
3244
  jobId?: undefined;
3241
3245
  reviewBatchLimit?: undefined;
3246
+ sourceRowIds?: undefined;
3247
+ sourceRowLimit?: undefined;
3242
3248
  allowPartialSourceList?: undefined;
3243
3249
  includeRawImportResult?: undefined;
3244
3250
  selections?: undefined;
@@ -3404,6 +3410,8 @@ export declare const allTools: ({
3404
3410
  keepInSync?: undefined;
3405
3411
  jobId?: undefined;
3406
3412
  reviewBatchLimit?: undefined;
3413
+ sourceRowIds?: undefined;
3414
+ sourceRowLimit?: undefined;
3407
3415
  allowPartialSourceList?: undefined;
3408
3416
  includeRawImportResult?: undefined;
3409
3417
  selections?: undefined;
@@ -3511,6 +3519,8 @@ export declare const allTools: ({
3511
3519
  keepInSync?: undefined;
3512
3520
  jobId?: undefined;
3513
3521
  reviewBatchLimit?: undefined;
3522
+ sourceRowIds?: undefined;
3523
+ sourceRowLimit?: undefined;
3514
3524
  allowPartialSourceList?: undefined;
3515
3525
  includeRawImportResult?: undefined;
3516
3526
  selections?: undefined;
@@ -3627,6 +3637,8 @@ export declare const allTools: ({
3627
3637
  keepInSync?: undefined;
3628
3638
  jobId?: undefined;
3629
3639
  reviewBatchLimit?: undefined;
3640
+ sourceRowIds?: undefined;
3641
+ sourceRowLimit?: undefined;
3630
3642
  allowPartialSourceList?: undefined;
3631
3643
  includeRawImportResult?: undefined;
3632
3644
  selections?: undefined;
@@ -3732,6 +3744,8 @@ export declare const allTools: ({
3732
3744
  keepInSync?: undefined;
3733
3745
  jobId?: undefined;
3734
3746
  reviewBatchLimit?: undefined;
3747
+ sourceRowIds?: undefined;
3748
+ sourceRowLimit?: undefined;
3735
3749
  allowPartialSourceList?: undefined;
3736
3750
  includeRawImportResult?: undefined;
3737
3751
  selections?: undefined;
@@ -3842,6 +3856,8 @@ export declare const allTools: ({
3842
3856
  keepInSync?: undefined;
3843
3857
  jobId?: undefined;
3844
3858
  reviewBatchLimit?: undefined;
3859
+ sourceRowIds?: undefined;
3860
+ sourceRowLimit?: undefined;
3845
3861
  allowPartialSourceList?: undefined;
3846
3862
  includeRawImportResult?: undefined;
3847
3863
  selections?: undefined;
@@ -3940,6 +3956,8 @@ export declare const allTools: ({
3940
3956
  keepInSync?: undefined;
3941
3957
  jobId?: undefined;
3942
3958
  reviewBatchLimit?: undefined;
3959
+ sourceRowIds?: undefined;
3960
+ sourceRowLimit?: undefined;
3943
3961
  allowPartialSourceList?: undefined;
3944
3962
  includeRawImportResult?: undefined;
3945
3963
  selections?: undefined;
@@ -4830,6 +4848,8 @@ export declare const allTools: ({
4830
4848
  keepInSync?: undefined;
4831
4849
  jobId?: undefined;
4832
4850
  reviewBatchLimit?: undefined;
4851
+ sourceRowIds?: undefined;
4852
+ sourceRowLimit?: undefined;
4833
4853
  allowPartialSourceList?: undefined;
4834
4854
  includeRawImportResult?: undefined;
4835
4855
  selections?: undefined;
@@ -4937,6 +4957,8 @@ export declare const allTools: ({
4937
4957
  keepInSync?: undefined;
4938
4958
  jobId?: undefined;
4939
4959
  reviewBatchLimit?: undefined;
4960
+ sourceRowIds?: undefined;
4961
+ sourceRowLimit?: undefined;
4940
4962
  allowPartialSourceList?: undefined;
4941
4963
  includeRawImportResult?: undefined;
4942
4964
  selections?: undefined;
@@ -5982,6 +6004,8 @@ export declare const allTools: ({
5982
6004
  keepInSync?: undefined;
5983
6005
  jobId?: undefined;
5984
6006
  reviewBatchLimit?: undefined;
6007
+ sourceRowIds?: undefined;
6008
+ sourceRowLimit?: undefined;
5985
6009
  allowPartialSourceList?: undefined;
5986
6010
  includeRawImportResult?: undefined;
5987
6011
  selections?: undefined;
@@ -6136,6 +6160,8 @@ export declare const allTools: ({
6136
6160
  keepInSync?: undefined;
6137
6161
  jobId?: undefined;
6138
6162
  reviewBatchLimit?: undefined;
6163
+ sourceRowIds?: undefined;
6164
+ sourceRowLimit?: undefined;
6139
6165
  allowPartialSourceList?: undefined;
6140
6166
  includeRawImportResult?: undefined;
6141
6167
  selections?: undefined;
@@ -6277,6 +6303,8 @@ export declare const allTools: ({
6277
6303
  keepInSync?: undefined;
6278
6304
  jobId?: undefined;
6279
6305
  reviewBatchLimit?: undefined;
6306
+ sourceRowIds?: undefined;
6307
+ sourceRowLimit?: undefined;
6280
6308
  allowPartialSourceList?: undefined;
6281
6309
  includeRawImportResult?: undefined;
6282
6310
  selections?: undefined;
@@ -6373,6 +6401,8 @@ export declare const allTools: ({
6373
6401
  keepInSync?: undefined;
6374
6402
  jobId?: undefined;
6375
6403
  reviewBatchLimit?: undefined;
6404
+ sourceRowIds?: undefined;
6405
+ sourceRowLimit?: undefined;
6376
6406
  allowPartialSourceList?: undefined;
6377
6407
  includeRawImportResult?: undefined;
6378
6408
  selections?: undefined;
@@ -6419,6 +6449,17 @@ export declare const allTools: ({
6419
6449
  type: string;
6420
6450
  description: string;
6421
6451
  };
6452
+ sourceRowIds: {
6453
+ type: string;
6454
+ items: {
6455
+ type: string;
6456
+ };
6457
+ description: string;
6458
+ };
6459
+ sourceRowLimit: {
6460
+ type: string;
6461
+ description: string;
6462
+ };
6422
6463
  allowPartialSourceList: {
6423
6464
  type: string;
6424
6465
  description: string;
@@ -6628,6 +6669,8 @@ export declare const allTools: ({
6628
6669
  keepInSync?: undefined;
6629
6670
  jobId?: undefined;
6630
6671
  reviewBatchLimit?: undefined;
6672
+ sourceRowIds?: undefined;
6673
+ sourceRowLimit?: undefined;
6631
6674
  allowPartialSourceList?: undefined;
6632
6675
  includeRawImportResult?: undefined;
6633
6676
  };
@@ -6725,6 +6768,8 @@ export declare const allTools: ({
6725
6768
  keepInSync?: undefined;
6726
6769
  jobId?: undefined;
6727
6770
  reviewBatchLimit?: undefined;
6771
+ sourceRowIds?: undefined;
6772
+ sourceRowLimit?: undefined;
6728
6773
  allowPartialSourceList?: undefined;
6729
6774
  includeRawImportResult?: undefined;
6730
6775
  selections?: undefined;
@@ -65,13 +65,12 @@ export const sequencerToolDefinitions = [
65
65
  name: "attach_recommended_sequence",
66
66
  description: "Attach the tier-recommended sequence template to a campaign. " +
67
67
  "Delegates template selection to the backend's existing tier-aware selector " +
68
- "(selectTemplateForTiers), which picks `If Open Profile -> INMAIL_OPEN, else INVITE` " +
69
- "for Sales Nav/Recruiter senders and `INVITE -> accepted -> DM` for Basic, Premium, or mixed sender sets. " +
70
- "Paid InMail Campaign is not auto-selected because it can spend InMail credits; " +
71
- "use attach_sequence with an explicit paid-InMail template only after the user opts in. " +
68
+ "(selectTemplateForTiers), which picks the unified Sales Nav cascade for Sales Nav/Recruiter senders: " +
69
+ "Open Profile check -> Open InMail, otherwise fresh paid credits >= 5 -> paid InMail, otherwise same-campaign connection request -> accepted wait -> DM. " +
70
+ "Basic, Premium, or mixed sender sets still get `INVITE -> accepted -> DM`. " +
71
+ "This is the default Sales Nav product path; use attach_sequence directly only when the caller needs a custom non-recommended cadence. " +
72
72
  "Prefer this over attach_sequence during the create-campaign-v2 autonomous tail — " +
73
- "it removes the risk of hand-authoring an invalid template mid-long-context. " +
74
- "Use attach_sequence directly only when the caller needs a custom non-recommended cadence.",
73
+ "it removes the risk of hand-authoring an invalid template mid-long-context.",
75
74
  inputSchema: {
76
75
  type: "object",
77
76
  properties: {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sellable/mcp",
3
- "version": "0.1.475",
3
+ "version": "0.1.477",
4
4
  "type": "module",
5
5
  "description": "Sellable MCP server for Claude Code and Codex campaign workflows",
6
6
  "main": "dist/index.js",
@@ -58,10 +58,12 @@ currentStep: "sequence" })` to attach the sender via the v3 senders route and
58
58
  move the watched app to Sequence.
59
59
  8. Call `attach_recommended_sequence({ campaignId, currentStep: "send" })` to
60
60
  bind the tier-aware recommended sequence. Explain the sequence choice in the
61
- handoff: Sales Nav/Recruiter senders get the Sales Nav Open Profile
62
- strategy, Basic/Premium/mixed senders get the Premium invite-to-DM strategy,
63
- and Paid InMail Campaign is only an explicit paid-InMail opt-in because it
64
- can spend InMail credits. If that response does not persist `currentStep:
61
+ handoff: Sales Nav/Recruiter senders get the unified Sales Nav cascade
62
+ (Open Profile -> Open InMail, otherwise fresh paid credits >= 5 -> paid
63
+ InMail, otherwise connection request -> accepted wait -> DM), while
64
+ Basic/Premium/mixed senders get the Premium invite-to-DM strategy. The manual
65
+ Paid InMail Campaign remains an explicit custom template, not the normal
66
+ recommended path. If that response does not persist `currentStep:
65
67
  "send"`, call `update_campaign({ campaignId, currentStep: "send" })`.
66
68
  Then reread `get_campaign({ campaignId })` and `list_tables()` and verify the
67
69
  current `workflowTableId` has `hasSequence:true`. A campaign-level
@@ -69,9 +71,9 @@ currentStep: "sequence" })` to attach the sender via the v3 senders route and
69
71
  copy step has moved the campaign to a new current campaign table while an old
70
72
  stale shell table still owns the sequence columns. In that stale-shell case,
71
73
  either repair only the current workflowTableId with `attach_sequence` using
72
- the same non-paid product template the recommended selector would have chosen,
73
- or stop and report the stale-shell sequence blocker if replacement was not
74
- explicitly approved. Never choose a paid-InMail template for this repair.
74
+ the same product template the recommended selector would have chosen, or stop
75
+ and report the stale-shell sequence blocker if replacement was not explicitly
76
+ approved. Do not substitute the manual Paid InMail Campaign for this repair.
75
77
  9. Surface the `handoff.orientation` string from `auto-execute.yaml` and
76
78
  summarize the visible Settings/Sequence/Send state without repeating the
77
79
  watch URL.