@sellable/mcp 0.1.466 → 0.1.468

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.
@@ -150,6 +150,7 @@ export function refillSendsCommand(input = {}) {
150
150
  ? `, untilDate: "${untilDate}"`
151
151
  : `, horizonSendDays: ${horizonSendDays}`}, approvalMode: "${approvalMode}" }) before any import, prep, approval, start, or schedule-affecting action.`,
152
152
  "If get_refill_target_plan returns status complete, report eligible sender ledger, gross target, selected days, sent count, scheduled count, projected count, campaign ids, targetShapeRevision, and no-op proof without asking for approval.",
153
+ "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.",
153
154
  "If remainingReadyOrProjectedGap is 0 but remainingProjectedGap is positive, run only a bounded read-only scheduler settle/reread loop; do not ask for prep/import/approval.",
154
155
  `Resolve route with resolve_campaign_fill_route({ intent: "${intent}"${input.campaignId ? `, campaignId: "${input.campaignId}"` : ""}${input.tableId ? `, tableId: "${input.tableId}"` : ""} }).`,
155
156
  '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.',
@@ -1,12 +1,238 @@
1
1
  import { getApi } from "../api.js";
2
+ const REFILL_TARGET_ACTION_TYPES = [
3
+ "send_invite",
4
+ "send_inmail_closed",
5
+ ];
6
+ const REFILL_TARGET_ACTION_SET = new Set(REFILL_TARGET_ACTION_TYPES);
2
7
  async function postRefillTargetPlan(body) {
3
8
  const api = getApi();
4
9
  return api.post("/api/v3/mcp/refill-target-plan", body);
5
10
  }
11
+ function isRecord(value) {
12
+ return Boolean(value && typeof value === "object" && !Array.isArray(value));
13
+ }
14
+ function allowedActionType(value) {
15
+ return typeof value === "string" && REFILL_TARGET_ACTION_SET.has(value)
16
+ ? value
17
+ : null;
18
+ }
19
+ function actionTypesFrom(value) {
20
+ if (!Array.isArray(value))
21
+ return [];
22
+ return [...new Set(value.map(allowedActionType).filter(Boolean))];
23
+ }
24
+ function numberValue(value) {
25
+ return typeof value === "number" && Number.isFinite(value) ? value : 0;
26
+ }
27
+ function evidenceLatestMs(evidence) {
28
+ const latestAt = evidence.latestAt;
29
+ if (typeof latestAt !== "string")
30
+ return 0;
31
+ const timestamp = new Date(latestAt).getTime();
32
+ return Number.isFinite(timestamp) ? timestamp : 0;
33
+ }
34
+ function chooseEvidenceAction(evidence, fallback) {
35
+ const byAction = new Map();
36
+ for (const item of evidence) {
37
+ if (!isRecord(item))
38
+ continue;
39
+ const actionType = allowedActionType(item.actionType);
40
+ if (!actionType)
41
+ continue;
42
+ const current = byAction.get(actionType) ?? { count: 0, latestMs: 0 };
43
+ current.count += numberValue(item.count);
44
+ current.latestMs = Math.max(current.latestMs, evidenceLatestMs(item));
45
+ byAction.set(actionType, current);
46
+ }
47
+ const [best] = [...byAction.entries()].sort((a, b) => {
48
+ if (a[1].latestMs !== b[1].latestMs)
49
+ return b[1].latestMs - a[1].latestMs;
50
+ if (a[1].count !== b[1].count)
51
+ return b[1].count - a[1].count;
52
+ return (REFILL_TARGET_ACTION_TYPES.indexOf(a[0]) -
53
+ REFILL_TARGET_ACTION_TYPES.indexOf(b[0]));
54
+ });
55
+ return best?.[0] ?? fallback[0] ?? null;
56
+ }
57
+ function senderActionKey(senderId, actionType) {
58
+ return typeof senderId === "string" && typeof actionType === "string"
59
+ ? `${senderId}:${actionType}`
60
+ : null;
61
+ }
62
+ function sanitizeCounts(counts, selectedKeys) {
63
+ if (!Array.isArray(counts))
64
+ return [];
65
+ return counts.filter((item) => {
66
+ if (!isRecord(item))
67
+ return false;
68
+ const actionType = allowedActionType(item.actionType);
69
+ const key = senderActionKey(item.senderId, actionType);
70
+ return Boolean(key && selectedKeys.has(key));
71
+ });
72
+ }
73
+ function sanitizeActionCandidates(candidates, selectedKeys, status, remainingReadyOrProjectedGap, remainingProjectedGap) {
74
+ const filtered = Array.isArray(candidates)
75
+ ? candidates.filter((item) => {
76
+ if (!isRecord(item))
77
+ return false;
78
+ const actionType = item.actionType;
79
+ if (actionType === undefined)
80
+ return item.type === "wait_for_scheduler";
81
+ const allowed = allowedActionType(actionType);
82
+ const key = senderActionKey(item.senderId, allowed);
83
+ return Boolean(key && selectedKeys.has(key));
84
+ })
85
+ : [];
86
+ if (status === "awaiting_scheduler_after_ready_buffer" &&
87
+ remainingProjectedGap > 0 &&
88
+ remainingReadyOrProjectedGap === 0 &&
89
+ !filtered.some((item) => item.type === "wait_for_scheduler")) {
90
+ filtered.push({
91
+ type: "wait_for_scheduler",
92
+ reason: "ready buffer covers the projected target gap but scheduledFor coverage is still short",
93
+ });
94
+ }
95
+ return filtered;
96
+ }
97
+ function sanitizeBlockers(blockers, selectedKeys) {
98
+ if (!Array.isArray(blockers))
99
+ return [];
100
+ return blockers.filter((item) => {
101
+ if (!isRecord(item))
102
+ return false;
103
+ const detail = item.detail;
104
+ const code = String(item.code ?? "");
105
+ if (typeof detail !== "string")
106
+ return true;
107
+ const detailSenderId = detail.split(":")[0];
108
+ if (code.startsWith("paid_inmail_")) {
109
+ return selectedKeys.has(`${detailSenderId}:send_inmail_closed`);
110
+ }
111
+ if (detail.includes(":send_dm") || detail.includes(":send_inmail_open")) {
112
+ return false;
113
+ }
114
+ const parts = detail.split(":");
115
+ if (parts.length < 2)
116
+ return true;
117
+ const actionType = allowedActionType(parts[parts.length - 1]);
118
+ if (!actionType)
119
+ return true;
120
+ const senderId = parts[0];
121
+ return selectedKeys.size === 0 || selectedKeys.has(`${senderId}:${actionType}`);
122
+ });
123
+ }
124
+ function sanitizeRefillTargetPlanResult(result) {
125
+ if (!isRecord(result) || !isRecord(result.target))
126
+ return result;
127
+ const target = result.target;
128
+ const rawSelections = Array.isArray(target.actionSelections)
129
+ ? target.actionSelections
130
+ : [];
131
+ const rawSenderPlans = Array.isArray(target.senderPlans)
132
+ ? target.senderPlans
133
+ : [];
134
+ const selectedBySender = new Map();
135
+ for (const rawSelection of rawSelections) {
136
+ if (!isRecord(rawSelection) || typeof rawSelection.senderId !== "string") {
137
+ continue;
138
+ }
139
+ const fallback = actionTypesFrom(rawSelection.actionTypes);
140
+ const evidence = Array.isArray(rawSelection.evidence)
141
+ ? rawSelection.evidence
142
+ : [];
143
+ const chosen = chooseEvidenceAction(evidence, fallback);
144
+ if (chosen)
145
+ selectedBySender.set(rawSelection.senderId, chosen);
146
+ }
147
+ for (const rawPlan of rawSenderPlans) {
148
+ if (!isRecord(rawPlan) ||
149
+ typeof rawPlan.senderId !== "string" ||
150
+ selectedBySender.has(rawPlan.senderId)) {
151
+ continue;
152
+ }
153
+ const actionType = allowedActionType(rawPlan.actionType);
154
+ if (actionType)
155
+ selectedBySender.set(rawPlan.senderId, actionType);
156
+ }
157
+ const selectedKeys = new Set([...selectedBySender.entries()].map(([senderId, actionType]) => `${senderId}:${actionType}`));
158
+ const actionTypes = [...new Set(selectedBySender.values())];
159
+ if (selectedKeys.size === 0)
160
+ return result;
161
+ const senderPlans = rawSenderPlans.filter((plan) => isRecord(plan) &&
162
+ selectedKeys.has(`${String(plan.senderId)}:${String(plan.actionType)}`));
163
+ const selectedDays = Array.isArray(target.selectedDays)
164
+ ? target.selectedDays.filter((day) => isRecord(day) &&
165
+ selectedKeys.has(`${String(day.senderId)}:${String(day.actionType)}`))
166
+ : [];
167
+ const actionSelections = rawSelections
168
+ .filter((selection) => isRecord(selection) &&
169
+ typeof selection.senderId === "string" &&
170
+ selectedBySender.has(selection.senderId))
171
+ .map((selection) => {
172
+ const actionType = selectedBySender.get(selection.senderId);
173
+ const evidence = Array.isArray(selection.evidence)
174
+ ? selection.evidence.filter((item) => isRecord(item) && item.actionType === actionType)
175
+ : [];
176
+ return {
177
+ ...selection,
178
+ actionTypes: actionType ? [actionType] : [],
179
+ evidence,
180
+ };
181
+ });
182
+ const grossTarget = senderPlans.reduce((sum, plan) => sum + numberValue(plan.grossTarget), 0);
183
+ const sent = senderPlans.reduce((sum, plan) => sum + numberValue(plan.sent), 0);
184
+ const scheduled = senderPlans.reduce((sum, plan) => sum + numberValue(plan.scheduled), 0);
185
+ const projected = sent + scheduled;
186
+ const readyBuffer = senderPlans.reduce((sum, plan) => sum + numberValue(plan.readyBuffer), 0);
187
+ const remainingScheduledGap = senderPlans.reduce((sum, plan) => sum + numberValue(plan.remainingScheduledGap), 0);
188
+ const remainingProjectedGap = senderPlans.reduce((sum, plan) => sum + numberValue(plan.remainingProjectedGap), 0);
189
+ const remainingReadyOrProjectedGap = senderPlans.reduce((sum, plan) => sum + numberValue(plan.remainingReadyOrProjectedGap), 0);
190
+ const blockers = sanitizeBlockers(result.blockers, selectedKeys);
191
+ const status = blockers.some((blocker) => ["sender_disconnected", "sender_sales_nav_disconnected"].includes(String(blocker.code))) || grossTarget <= 0
192
+ ? "blocked"
193
+ : remainingProjectedGap === 0
194
+ ? "complete"
195
+ : remainingReadyOrProjectedGap === 0
196
+ ? "awaiting_scheduler_after_ready_buffer"
197
+ : "needs_refill";
198
+ return {
199
+ ...result,
200
+ status,
201
+ request: isRecord(result.request)
202
+ ? { ...result.request, actionTypes }
203
+ : result.request,
204
+ target: {
205
+ ...target,
206
+ actionSelections,
207
+ selectedDays,
208
+ senderPlans,
209
+ grossTarget,
210
+ sent,
211
+ scheduled,
212
+ projected,
213
+ readyBuffer,
214
+ remainingScheduledGap,
215
+ remainingProjectedGap,
216
+ remainingReadyOrProjectedGap,
217
+ remainingReadyOrScheduledGap: remainingReadyOrProjectedGap,
218
+ },
219
+ coverage: isRecord(result.coverage)
220
+ ? {
221
+ ...result.coverage,
222
+ sentCounts: sanitizeCounts(result.coverage.sentCounts, selectedKeys),
223
+ scheduledCounts: sanitizeCounts(result.coverage.scheduledCounts, selectedKeys),
224
+ readyCounts: sanitizeCounts(result.coverage.readyCounts, selectedKeys),
225
+ }
226
+ : result.coverage,
227
+ actionCandidates: sanitizeActionCandidates(result.actionCandidates, selectedKeys, status, remainingReadyOrProjectedGap, remainingProjectedGap),
228
+ blockers,
229
+ mcpSanitizedRefillLanes: true,
230
+ };
231
+ }
6
232
  export const refillTargetPlanToolDefinitions = [
7
233
  {
8
234
  name: "get_refill_target_plan",
9
- description: "read-only refill target planner to call before any refill mutation. It identifies eligible senders, infers implicit per-sender send lanes from future scheduled, ready-to-schedule, and recent scheduled evidence before falling back to campaign sequences, then computes selected sender-local days, 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; incomplete targets must be followed by a bounded approval packet.",
235
+ 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. The planner computes selected sender-local days, 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; incomplete targets must be followed by a bounded approval packet.",
10
236
  inputSchema: {
11
237
  type: "object",
12
238
  properties: {
@@ -53,15 +279,9 @@ export const refillTargetPlanToolDefinitions = [
53
279
  type: "array",
54
280
  items: {
55
281
  type: "string",
56
- enum: [
57
- "send_invite",
58
- "send_dm",
59
- "send_inmail_open",
60
- "send_inmail_closed",
61
- "react_and_comment",
62
- ],
282
+ enum: ["send_invite", "send_inmail_closed"],
63
283
  },
64
- description: "Optional workflow send column/action types. If omitted, the planner infers per-sender lanes from future scheduled, ready-to-schedule, and recent scheduled evidence before falling back to selected active campaign sequences.",
284
+ 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.",
65
285
  },
66
286
  approvalMode: {
67
287
  type: "string",
@@ -74,8 +294,8 @@ export const refillTargetPlanToolDefinitions = [
74
294
  },
75
295
  },
76
296
  ];
77
- export function getRefillTargetPlan(input = {}) {
78
- return postRefillTargetPlan({
297
+ export async function getRefillTargetPlan(input = {}) {
298
+ const result = await postRefillTargetPlan({
79
299
  intent: input.intent,
80
300
  horizonSendDays: input.horizonSendDays,
81
301
  untilDate: input.untilDate,
@@ -87,4 +307,5 @@ export function getRefillTargetPlan(input = {}) {
87
307
  actionTypes: input.actionTypes,
88
308
  approvalMode: input.approvalMode,
89
309
  });
310
+ return sanitizeRefillTargetPlanResult(result);
90
311
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sellable/mcp",
3
- "version": "0.1.466",
3
+ "version": "0.1.468",
4
4
  "type": "module",
5
5
  "description": "Sellable MCP server for Claude Code and Codex campaign workflows",
6
6
  "main": "dist/index.js",
@@ -117,21 +117,30 @@ eligible senders, selected sender-local days, gross target, actual sent
117
117
  coverage, scheduler-owned scheduled coverage across active enrolled campaigns,
118
118
  projected coverage (`sent + scheduled`), inferred per-sender send lane/action
119
119
  selections, ready buffer, remaining projected gap, paid-InMail credit/threshold
120
- feasibility, `targetShapeRevision`, and `stateRevision`. When `actionTypes` are
121
- omitted, trust the target plan's inferred lane rather than asking whether to
122
- fill connections, DMs, or InMails. If the target plan is complete by projected
123
- coverage, report that the selected target is already filled and no-op without
124
- asking for approval. If the ready buffer covers the projected gap but scheduled
125
- coverage is still short, run only a bounded read-only scheduler settle/reread
126
- loop and report `awaiting_scheduler_after_ready_buffer` if it does not settle.
120
+ feasibility, `targetShapeRevision`, and `stateRevision`. Refill target lanes
121
+ are only connection invites (`send_invite`) or paid InMails
122
+ (`send_inmail_closed`), chosen per sender from the most recent current
123
+ dashboard-active campaign-backed send evidence. DMs (`send_dm`) and
124
+ open-profile InMails (`send_inmail_open`) may exist in sequences, but they are
125
+ not refill horizon target capacity and must not be counted as refill sent,
126
+ scheduled, or ready coverage. When `actionTypes` are omitted, trust the target
127
+ plan's inferred connection-or-paid-InMail lane rather than asking which of those
128
+ two lanes to fill. If a stale target plan selects `send_dm` or
129
+ `send_inmail_open`, stop and re-plan with the current planner before mutation.
130
+ Short form: trust the target plan's inferred lane only when it is a
131
+ connection-invite or paid-InMail refill lane.
132
+ If the target plan is complete by projected coverage, report that the selected
133
+ target is already filled and no-op without asking for approval. If the ready
134
+ buffer covers the projected gap but scheduled coverage is still short, run only
135
+ a bounded read-only scheduler settle/reread loop and report
136
+ `awaiting_scheduler_after_ready_buffer` if it does not settle.
127
137
  If paid InMail credit facts are stale or missing and the target plan returns a
128
138
  `refresh_paid_inmail_credits` action candidate, include the exact sender id,
129
139
  campaign/table/column proof, expected sender-credit-cache writes, and rerun plan
130
140
  step in the bounded packet. After `refresh_paid_inmail_credits`, rerun
131
141
  `get_refill_target_plan` before any prep/import/approval/start action. If paid
132
142
  InMail is below threshold after a fresh credit read, report the exact
133
- campaign/table/column threshold action or connection fallback; `--yolo` does not
134
- lower paid-InMail thresholds or create campaigns.
143
+ campaign/table/column threshold action or connection fallback; `--yolo` does not lower paid-InMail thresholds or create campaigns.
135
144
 
136
145
  If the plain route's managed waterfall targets are stale, for example skipped
137
146
  targets show archived/completed shared slots or the returned targets do not cover
@@ -67,10 +67,20 @@ campaign.
67
67
  campaigns, projected coverage (`sent + scheduled`), ready buffer, remaining
68
68
  projected gap, paid-InMail credit/threshold feasibility, bounded action
69
69
  candidates, `targetShapeRevision`, and `stateRevision`.
70
- When `actionTypes` are omitted, trust the target plan's inferred lane; do not
71
- ask the operator whether to fill connections, DMs, or InMails after the plan
72
- has already inferred that from future scheduled, ready, and recent scheduled
73
- evidence.
70
+ Refill target lanes are binary per sender: connection invites
71
+ (`send_invite`) or paid InMails (`send_inmail_closed`). DMs
72
+ (`send_dm`) and open-profile InMails (`send_inmail_open`) can remain in the
73
+ sequence, but they are follow-up actions, not refill horizon capacity. Do not
74
+ count their sent, scheduled, or ready cells when deciding whether a sender
75
+ needs refill. When `actionTypes` are omitted, trust the target plan's
76
+ inferred connection-or-paid-InMail lane; do not ask the operator which of
77
+ those two lanes to fill after the plan has inferred that from active
78
+ campaign future scheduled, recent scheduled, and ready evidence. If a stale
79
+ planner/tool response selects `send_dm` or `send_inmail_open`, treat that as
80
+ unsupported stale refill state and re-plan with a current planner before any
81
+ mutation.
82
+ Short form: trust the target plan's inferred lane only when it is a
83
+ connection-invite or paid-InMail refill lane.
74
84
  If `status:"complete"`, report the target, selected dates, sent count,
75
85
  scheduled count, projected count, campaign ids, and no-op proof without
76
86
  asking for approval or mutating.
@@ -84,8 +94,7 @@ campaign.
84
94
  step in the bounded packet. After `refresh_paid_inmail_credits`, rerun
85
95
  `get_refill_target_plan` before any prep/import/approval/start action. If paid
86
96
  InMail is below threshold after a fresh credit read, report the exact
87
- campaign/table/column threshold action or connection fallback; `--yolo` does not
88
- lower paid-InMail thresholds or create campaigns.
97
+ campaign/table/column threshold action or connection fallback; `--yolo` does not lower paid-InMail thresholds or create campaigns.
89
98
  2. Call `resolve_campaign_fill_route`. Use `intent:"plain"` for generic
90
99
  fill/load language, `intent:"active"` only when the user explicitly narrowed
91
100
  to active regular campaigns or when the plain route has stale managed
@@ -161,7 +170,9 @@ Use the refill-state response as the current facts receipt:
161
170
  approved, ready to schedule, scheduled;
162
171
  - active message prep job;
163
172
  - scheduler-owned scheduled counts by date/action from cells with non-null
164
- `scheduledFor`; treat these as the last sends/continuation signal;
173
+ `scheduledFor`; treat connection-invite and paid-InMail cells as the refill
174
+ lane continuation signal and the last sends/continuation signal, and ignore
175
+ DM/open-InMail cells for refill target capacity;
165
176
  - latest scheduled send date, latest scheduled-cell update when present,
166
177
  sender overlap with the requested or inferred sender set, and whether future
167
178
  scheduled sends already exist;
@@ -210,6 +221,19 @@ in the best same-sender campaign and keep all source-copy/prep caps tied to that
210
221
  two-day packet.
211
222
  Short form: default `--yolo refill senders` target is each eligible sender's two-send-day gap.
212
223
 
224
+ Lane selection must choose one refill lane per sender from current
225
+ dashboard-active campaign-backed sequence evidence:
226
+
227
+ 1. Prefer the sender's most recent future scheduled refill action, then the most
228
+ recent recent scheduled refill action, then ready refill rows.
229
+ 2. Refill actions are only `send_invite` and `send_inmail_closed`.
230
+ 3. If the most recent active campaign evidence is DM or open InMail, ignore it
231
+ for refill lane selection and use the nearest connection-invite or paid-InMail
232
+ evidence instead.
233
+ 4. If no connection/paid-InMail evidence exists, fall back to the selected
234
+ active campaign sequence, still choosing only one of `send_invite` or
235
+ `send_inmail_closed` for that sender.
236
+
213
237
  For `--yolo` fill/schedule requests, do not treat "ready" as completion. Build
214
238
  and maintain a horizon saturation ledger for every selected sender from
215
239
  `get_refill_target_plan`. A sender is complete only when a final target-plan or