@sellable/mcp 0.1.467 → 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.
@@ -1,8 +1,234 @@
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",
@@ -68,8 +294,8 @@ export const refillTargetPlanToolDefinitions = [
68
294
  },
69
295
  },
70
296
  ];
71
- export function getRefillTargetPlan(input = {}) {
72
- return postRefillTargetPlan({
297
+ export async function getRefillTargetPlan(input = {}) {
298
+ const result = await postRefillTargetPlan({
73
299
  intent: input.intent,
74
300
  horizonSendDays: input.horizonSendDays,
75
301
  untilDate: input.untilDate,
@@ -81,4 +307,5 @@ export function getRefillTargetPlan(input = {}) {
81
307
  actionTypes: input.actionTypes,
82
308
  approvalMode: input.approvalMode,
83
309
  });
310
+ return sanitizeRefillTargetPlanResult(result);
84
311
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sellable/mcp",
3
- "version": "0.1.467",
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",