@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.
- package/dist/tools/refill-target-plan.js +229 -2
- package/package.json +1 -1
|
@@ -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
|
-
|
|
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
|
}
|