@sellable/mcp 0.1.0

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.
Files changed (200) hide show
  1. package/.claude-plugin/plugin.json +12 -0
  2. package/.mcp.json +9 -0
  3. package/README.md +355 -0
  4. package/dist/api.d.ts +21 -0
  5. package/dist/api.js +73 -0
  6. package/dist/auth.d.ts +60 -0
  7. package/dist/auth.js +246 -0
  8. package/dist/engage-memory.d.ts +63 -0
  9. package/dist/engage-memory.js +354 -0
  10. package/dist/index-dev.d.ts +2 -0
  11. package/dist/index-dev.js +17 -0
  12. package/dist/index.d.ts +7 -0
  13. package/dist/index.js +8 -0
  14. package/dist/server.d.ts +1 -0
  15. package/dist/server.js +499 -0
  16. package/dist/skills.d.ts +11 -0
  17. package/dist/skills.js +97 -0
  18. package/dist/tools/auth.d.ts +30 -0
  19. package/dist/tools/auth.js +124 -0
  20. package/dist/tools/blueprint-commit.d.ts +174 -0
  21. package/dist/tools/blueprint-commit.js +286 -0
  22. package/dist/tools/bootstrap.d.ts +64 -0
  23. package/dist/tools/bootstrap.js +246 -0
  24. package/dist/tools/campaigns.d.ts +589 -0
  25. package/dist/tools/campaigns.js +892 -0
  26. package/dist/tools/cells.d.ts +58 -0
  27. package/dist/tools/cells.js +48 -0
  28. package/dist/tools/context.d.ts +88 -0
  29. package/dist/tools/context.js +271 -0
  30. package/dist/tools/csv-domains.d.ts +73 -0
  31. package/dist/tools/csv-domains.js +464 -0
  32. package/dist/tools/csv-linkedin.d.ts +102 -0
  33. package/dist/tools/csv-linkedin.js +712 -0
  34. package/dist/tools/direct-campaigns.d.ts +240 -0
  35. package/dist/tools/direct-campaigns.js +250 -0
  36. package/dist/tools/engage-bootstrap.d.ts +94 -0
  37. package/dist/tools/engage-bootstrap.js +205 -0
  38. package/dist/tools/engage-discovery.d.ts +78 -0
  39. package/dist/tools/engage-discovery.js +150 -0
  40. package/dist/tools/engage-memory.d.ts +181 -0
  41. package/dist/tools/engage-memory.js +143 -0
  42. package/dist/tools/engage-state.d.ts +72 -0
  43. package/dist/tools/engage-state.js +62 -0
  44. package/dist/tools/enrichment.d.ts +167 -0
  45. package/dist/tools/enrichment.js +174 -0
  46. package/dist/tools/flow-preflight.d.ts +68 -0
  47. package/dist/tools/flow-preflight.js +138 -0
  48. package/dist/tools/framework.d.ts +44 -0
  49. package/dist/tools/framework.js +153 -0
  50. package/dist/tools/interaction-mode.d.ts +27 -0
  51. package/dist/tools/interaction-mode.js +102 -0
  52. package/dist/tools/leads.d.ts +2417 -0
  53. package/dist/tools/leads.js +2307 -0
  54. package/dist/tools/linkedin.d.ts +210 -0
  55. package/dist/tools/linkedin.js +229 -0
  56. package/dist/tools/navigation.d.ts +91 -0
  57. package/dist/tools/navigation.js +381 -0
  58. package/dist/tools/one-off.d.ts +229 -0
  59. package/dist/tools/one-off.js +273 -0
  60. package/dist/tools/processing.d.ts +70 -0
  61. package/dist/tools/processing.js +56 -0
  62. package/dist/tools/prompts.d.ts +211 -0
  63. package/dist/tools/prompts.js +210 -0
  64. package/dist/tools/provider-preflight.d.ts +21 -0
  65. package/dist/tools/provider-preflight.js +59 -0
  66. package/dist/tools/readiness.d.ts +261 -0
  67. package/dist/tools/readiness.js +510 -0
  68. package/dist/tools/rows.d.ts +126 -0
  69. package/dist/tools/rows.js +105 -0
  70. package/dist/tools/rubrics.d.ts +497 -0
  71. package/dist/tools/rubrics.js +681 -0
  72. package/dist/tools/senders.d.ts +44 -0
  73. package/dist/tools/senders.js +69 -0
  74. package/dist/tools/sequencer.d.ts +127 -0
  75. package/dist/tools/sequencer.js +194 -0
  76. package/dist/tools/tables.d.ts +35 -0
  77. package/dist/tools/tables.js +36 -0
  78. package/dist/tools/verify-row.d.ts +36 -0
  79. package/dist/tools/verify-row.js +38 -0
  80. package/dist/tools/workspaces.d.ts +140 -0
  81. package/dist/tools/workspaces.js +139 -0
  82. package/dist/utils/workspace-root.d.ts +1 -0
  83. package/dist/utils/workspace-root.js +39 -0
  84. package/package.json +46 -0
  85. package/skills/building-gtm-tables/SKILL.md +216 -0
  86. package/skills/building-gtm-tables/core/auto-execute.yaml +19 -0
  87. package/skills/building-gtm-tables/core/blueprint-schema.json +72 -0
  88. package/skills/building-gtm-tables/references/brief-to-blueprint.md +334 -0
  89. package/skills/building-gtm-tables/references/column-type-catalog.md +318 -0
  90. package/skills/building-gtm-tables/references/common-blueprints.fixtures.ts +199 -0
  91. package/skills/building-gtm-tables/references/common-blueprints.md +44 -0
  92. package/skills/building-gtm-tables/references/failure-taxonomy.md +197 -0
  93. package/skills/building-gtm-tables/references/uat-seed-prompts.md +37 -0
  94. package/skills/building-gtm-tables/references/verify-loop.md +74 -0
  95. package/skills/campaign-messages/SKILL.md +173 -0
  96. package/skills/campaign-messages/flow.v1.json +75 -0
  97. package/skills/craft-message/SKILL.md +401 -0
  98. package/skills/create-campaign/ARCHITECTURE.md +232 -0
  99. package/skills/create-campaign/DISCUSS.md +296 -0
  100. package/skills/create-campaign/FLOW_ASCII.md +240 -0
  101. package/skills/create-campaign/HOST-PARITY-CHECKLIST.md +49 -0
  102. package/skills/create-campaign/README.md +142 -0
  103. package/skills/create-campaign/SKILL.md +286 -0
  104. package/skills/create-campaign/context/README.md +67 -0
  105. package/skills/create-campaign/context/_TEMPLATE.md +12 -0
  106. package/skills/create-campaign/context/context.md +35 -0
  107. package/skills/create-campaign/context/learnings.md +16 -0
  108. package/skills/create-campaign/context/registry.json +19 -0
  109. package/skills/create-campaign/core/flow.v1.json +217 -0
  110. package/skills/create-campaign/core/policy.md +191 -0
  111. package/skills/create-campaign/core/providers/apollo.json +35 -0
  112. package/skills/create-campaign/core/providers/prospeo.json +34 -0
  113. package/skills/create-campaign/core/providers/registry.json +31 -0
  114. package/skills/create-campaign/core/providers/sales-nav.json +37 -0
  115. package/skills/create-campaign/core/providers/signal-discovery.json +42 -0
  116. package/skills/create-campaign/references/brief-template.md +64 -0
  117. package/skills/create-campaign/references/campaign-quality.md +84 -0
  118. package/skills/create-campaign/references/copy-calibration-examples.md +120 -0
  119. package/skills/create-campaign/references/offer-patterns.md +108 -0
  120. package/skills/create-campaign/references/provider-selection-strategy.md +212 -0
  121. package/skills/create-campaign/references/question-examples.md +167 -0
  122. package/skills/create-campaign/references/token-fill-examples.md +81 -0
  123. package/skills/create-campaign-brief/ARCHITECTURE.md +72 -0
  124. package/skills/create-campaign-brief/DISCUSS.md +64 -0
  125. package/skills/create-campaign-brief/README.md +176 -0
  126. package/skills/create-campaign-brief/SKILL.md +537 -0
  127. package/skills/create-campaign-brief/references/brief-synthesis-rules.md +100 -0
  128. package/skills/create-campaign-brief/references/brief-template.md +220 -0
  129. package/skills/create-campaign-brief/references/campaign-idea-options.md +30 -0
  130. package/skills/create-campaign-brief/references/copy-appendix-template.md +62 -0
  131. package/skills/create-campaign-brief/references/draft-lifecycle.md +23 -0
  132. package/skills/create-campaign-brief/references/examples/MANIFEST.json +89 -0
  133. package/skills/create-campaign-brief/references/examples/briefs/clover.md +223 -0
  134. package/skills/create-campaign-brief/references/examples/briefs/galley.md +222 -0
  135. package/skills/create-campaign-brief/references/examples/briefs/gelee.md +220 -0
  136. package/skills/create-campaign-brief/references/examples/briefs/hey-digital.md +234 -0
  137. package/skills/create-campaign-brief/references/examples/briefs/persona.md +231 -0
  138. package/skills/create-campaign-brief/references/examples/briefs/revvix.md +220 -0
  139. package/skills/create-campaign-brief/references/examples/briefs/sellable-dev.md +220 -0
  140. package/skills/create-campaign-brief/references/examples/briefs/superposition.md +233 -0
  141. package/skills/create-campaign-brief/references/examples/briefs/superpower.md +219 -0
  142. package/skills/create-campaign-brief/references/examples/briefs/westpark-villas.md +220 -0
  143. package/skills/create-campaign-brief/references/icp-lock-question-bank.md +43 -0
  144. package/skills/create-campaign-brief/references/messaging-inputs.md +58 -0
  145. package/skills/create-campaign-brief/references/output-acceptance-rubric.md +62 -0
  146. package/skills/create-campaign-brief/references/phase75-active-runtime-message-pack.md +248 -0
  147. package/skills/create-campaign-brief/references/phase75-canonical-brief-template.md +319 -0
  148. package/skills/create-campaign-brief/references/phase75-good-brief-and-messaging-examples.md +445 -0
  149. package/skills/create-campaign-brief/references/quick-research-protocol.md +39 -0
  150. package/skills/create-campaign-brief/references/reference-sheet-protocol.md +60 -0
  151. package/skills/create-campaign-brief/references/zero-shot-iteration-rules.md +66 -0
  152. package/skills/create-campaign-v2/SKILL.md +1619 -0
  153. package/skills/create-campaign-v2/core/auto-execute.README.md +219 -0
  154. package/skills/create-campaign-v2/core/auto-execute.yaml +121 -0
  155. package/skills/create-campaign-v2/core/flow.v2.json +1643 -0
  156. package/skills/create-campaign-v2/core/policy.md +82 -0
  157. package/skills/create-campaign-v2/references/ai-tells.md +253 -0
  158. package/skills/create-campaign-v2/references/approval-gate-framing.md +346 -0
  159. package/skills/create-campaign-v2/references/draft-lifecycle.md +110 -0
  160. package/skills/create-campaign-v2/references/escalation-ladder.md +119 -0
  161. package/skills/create-campaign-v2/references/filter-leads.md +495 -0
  162. package/skills/create-campaign-v2/references/final-handoff-contract.md +176 -0
  163. package/skills/create-campaign-v2/references/gold-standard-message-examples.md +394 -0
  164. package/skills/create-campaign-v2/references/gold-standard-message-patterns.md +314 -0
  165. package/skills/create-campaign-v2/references/gold-standard-message-validation-example.md +212 -0
  166. package/skills/create-campaign-v2/references/lead-validation-preview.md +172 -0
  167. package/skills/create-campaign-v2/references/parallel-critique-protocol.md +368 -0
  168. package/skills/create-campaign-v2/references/sample-validation-loop.md +289 -0
  169. package/skills/create-campaign-v2/references/step-13-import-leads.md +151 -0
  170. package/skills/create-campaign-v2/references/step-15-re-cascade.md +90 -0
  171. package/skills/create-campaign-v2/references/thomas-revision-filters.md +521 -0
  172. package/skills/create-campaign-v2/references/thomas-variant-selection.md +202 -0
  173. package/skills/create-campaign-v2/references/tier-routing-matrix.md +66 -0
  174. package/skills/create-campaign-v2/references/validation-criteria.md +367 -0
  175. package/skills/create-campaign-v2/references/watch-link-handoff.md +106 -0
  176. package/skills/create-campaign-v2-validation/SKILL.md +296 -0
  177. package/skills/create-post/SKILL.md +1308 -0
  178. package/skills/create-rubric/SKILL.md +251 -0
  179. package/skills/engage/SKILL.md +549 -0
  180. package/skills/engage/core/README.md +23 -0
  181. package/skills/engage/core/proven-searches.json +11 -0
  182. package/skills/engage/core/style-guide.template.md +47 -0
  183. package/skills/engage/core/tracked-people.json +10 -0
  184. package/skills/enrich-prospects/SKILL.md +97 -0
  185. package/skills/find-leads/SKILL.md +467 -0
  186. package/skills/generate-messages/SKILL.md +2361 -0
  187. package/skills/interview/SKILL.md +132 -0
  188. package/skills/interview/core/ENGAGE_STYLE_GUIDE.template.md +54 -0
  189. package/skills/interview/core/ICP.template.md +54 -0
  190. package/skills/interview/core/VOICE_PROFILE.template.md +101 -0
  191. package/skills/providers/apollo.md +520 -0
  192. package/skills/providers/prospeo.md +398 -0
  193. package/skills/providers/sales-nav.md +372 -0
  194. package/skills/providers/signal-discovery.md +495 -0
  195. package/skills/research/SKILL.md +258 -0
  196. package/skills/research/config.json +9 -0
  197. package/skills/research/override.md +13 -0
  198. package/skills/research-prospect/SKILL.md +99 -0
  199. package/skills/research-sender/SKILL.md +158 -0
  200. package/skills/workflow-sequences/SKILL.md +85 -0
@@ -0,0 +1,681 @@
1
+ import { getApi } from "../api.js";
2
+ import { fetchCampaignRubrics, upsertRubricV3, } from "./processing.js";
3
+ import { getTableRowsMinimal } from "./rows.js";
4
+ const rubricDrafts = new Map();
5
+ const DEFAULT_MAX_PROSPECTS = 5;
6
+ const MAX_PROSPECTS_LIMIT = 25;
7
+ const DEFAULT_TIMEOUT_MS = 90000;
8
+ const DEFAULT_INTERVAL_MS = 2000;
9
+ function sleep(ms) {
10
+ return new Promise((resolve) => setTimeout(resolve, ms));
11
+ }
12
+ function normalizeRubricItemDefaults(item) {
13
+ return {
14
+ ...item,
15
+ checkName: item.checkName?.trim(),
16
+ description: item.description?.trim(),
17
+ reason: item.reason?.trim(),
18
+ criterion: item.criterion?.trim() || item.description?.trim(),
19
+ useCheckForScoring: item.useCheckForScoring !== false,
20
+ isRequiredCheck: item.isRequiredCheck === true,
21
+ allowPartialCredit: item.allowPartialCredit !== false,
22
+ strictMatching: item.strictMatching === true,
23
+ };
24
+ }
25
+ function assertRubricFields(items) {
26
+ const invalid = items.filter((item) => !item.checkName || !item.description || !item.reason || !item.criterion);
27
+ if (invalid.length > 0) {
28
+ throw new Error("Each rubric item must include checkName, description, criterion, and reason.");
29
+ }
30
+ }
31
+ function ensureUniqueCheckNames(items) {
32
+ const seen = new Set();
33
+ const duplicates = new Set();
34
+ items.forEach((item) => {
35
+ if (!item.checkName)
36
+ return;
37
+ if (seen.has(item.checkName)) {
38
+ duplicates.add(item.checkName);
39
+ }
40
+ seen.add(item.checkName);
41
+ });
42
+ if (duplicates.size > 0) {
43
+ throw new Error(`Duplicate checkName values found: ${Array.from(duplicates).join(", ")}`);
44
+ }
45
+ }
46
+ function dedupeRubricsByCheckName(items) {
47
+ const seen = new Set();
48
+ const unique = [];
49
+ items.forEach((item) => {
50
+ const checkName = item.checkName;
51
+ if (!checkName) {
52
+ unique.push(item);
53
+ return;
54
+ }
55
+ if (seen.has(checkName))
56
+ return;
57
+ seen.add(checkName);
58
+ unique.push(item);
59
+ });
60
+ return unique;
61
+ }
62
+ function normalizeRubricItems(items) {
63
+ const normalized = items.map((item) => normalizeRubricItemDefaults(item));
64
+ ensureUniqueCheckNames(normalized);
65
+ return normalized;
66
+ }
67
+ function normalizeCampaignRubrics(items) {
68
+ return normalizeRubricItems(dedupeRubricsByCheckName(items));
69
+ }
70
+ function setDraftRubrics(campaignOfferId, rubrics, source) {
71
+ const normalized = normalizeRubricItems(rubrics);
72
+ rubricDrafts.set(campaignOfferId, {
73
+ campaignOfferId,
74
+ rubrics: normalized,
75
+ updatedAt: new Date().toISOString(),
76
+ source,
77
+ });
78
+ return normalized;
79
+ }
80
+ function getDraftRubrics(campaignOfferId) {
81
+ return rubricDrafts.get(campaignOfferId)?.rubrics ?? null;
82
+ }
83
+ async function fetchCampaignOffer(campaignOfferId) {
84
+ const v3 = await fetchCampaignRubrics(campaignOfferId);
85
+ return {
86
+ workflowTableId: v3.workflowTableId,
87
+ enableICPFilters: v3.enableICPFilters,
88
+ leadScoringRubrics: v3.rubrics,
89
+ };
90
+ }
91
+ async function getRubricsForEdit(campaignOfferId) {
92
+ const draft = getDraftRubrics(campaignOfferId);
93
+ if (draft) {
94
+ return { rubrics: draft, source: "draft" };
95
+ }
96
+ const campaign = await fetchCampaignOffer(campaignOfferId);
97
+ const rubrics = normalizeCampaignRubrics(campaign.leadScoringRubrics || []);
98
+ setDraftRubrics(campaignOfferId, rubrics, "campaign");
99
+ return { rubrics, source: "campaign" };
100
+ }
101
+ function resolveMaxProspects(value) {
102
+ const fallback = DEFAULT_MAX_PROSPECTS;
103
+ const parsed = typeof value === "number" && !Number.isNaN(value) ? value : fallback;
104
+ return Math.min(MAX_PROSPECTS_LIMIT, Math.max(1, Math.floor(parsed)));
105
+ }
106
+ function mergeRubricsWithExisting(draftRubrics, existingRubrics) {
107
+ const existingByCheckName = new Map();
108
+ existingRubrics.forEach((item) => {
109
+ if (item.checkName)
110
+ existingByCheckName.set(item.checkName, item);
111
+ });
112
+ const normalizedDraft = normalizeRubricItems(draftRubrics).map((item) => {
113
+ if (item.id)
114
+ return item;
115
+ const existing = item.checkName
116
+ ? existingByCheckName.get(item.checkName)
117
+ : undefined;
118
+ if (existing?.id) {
119
+ return { ...item, id: existing.id };
120
+ }
121
+ return item;
122
+ });
123
+ const draftIds = new Set(normalizedDraft.map((item) => item.id).filter(Boolean));
124
+ const deletedRubricIds = existingRubrics
125
+ .map((item) => item.id)
126
+ .filter((id) => Boolean(id))
127
+ .filter((id) => !draftIds.has(id));
128
+ return { normalizedDraft, deletedRubricIds };
129
+ }
130
+ function summarizeRubrics(rubrics) {
131
+ return rubrics.map((rubric) => ({
132
+ checkName: rubric.checkName,
133
+ description: rubric.description,
134
+ id: rubric.id,
135
+ }));
136
+ }
137
+ function mergeSavedWithUnsaved(savedRubrics, draftRubrics) {
138
+ if (!savedRubrics)
139
+ return draftRubrics;
140
+ const normalizedSaved = normalizeCampaignRubrics(savedRubrics);
141
+ const unsaved = draftRubrics.filter((rubric) => !rubric.id);
142
+ return normalizeRubricItems([...normalizedSaved, ...unsaved]);
143
+ }
144
+ export const rubricToolDefinitions = [
145
+ {
146
+ name: "draft_rubrics",
147
+ description: "Generate initial lead scoring rubrics and store them for this campaign. Use save_rubrics to persist.",
148
+ inputSchema: {
149
+ type: "object",
150
+ properties: {
151
+ campaignOfferId: {
152
+ type: "string",
153
+ description: "Campaign offer ID",
154
+ },
155
+ leadScoringRubrics: {
156
+ type: "array",
157
+ description: "Array of rubric criteria",
158
+ items: {
159
+ type: "object",
160
+ properties: {
161
+ id: { type: "string" },
162
+ checkName: { type: "string" },
163
+ description: { type: "string" },
164
+ criterion: { type: "string" },
165
+ reason: { type: "string" },
166
+ useCheckForScoring: { type: "boolean" },
167
+ isRequiredCheck: { type: "boolean" },
168
+ allowPartialCredit: { type: "boolean" },
169
+ strictMatching: { type: "boolean" },
170
+ },
171
+ required: ["checkName", "description", "criterion", "reason"],
172
+ additionalProperties: false,
173
+ },
174
+ },
175
+ },
176
+ required: ["campaignOfferId", "leadScoringRubrics"],
177
+ additionalProperties: false,
178
+ },
179
+ },
180
+ {
181
+ name: "select_necessary_rubrics",
182
+ description: "Keep only the required rubric criteria and mark them as required. Use after draft_rubrics.",
183
+ inputSchema: {
184
+ type: "object",
185
+ properties: {
186
+ campaignOfferId: {
187
+ type: "string",
188
+ description: "Campaign offer ID",
189
+ },
190
+ requiredCheckNames: {
191
+ type: "array",
192
+ description: "Array of checkName values to keep and mark as required (1-3 recommended).",
193
+ items: { type: "string" },
194
+ },
195
+ },
196
+ required: ["campaignOfferId", "requiredCheckNames"],
197
+ additionalProperties: false,
198
+ },
199
+ },
200
+ {
201
+ name: "save_rubrics",
202
+ description: "Persist rubric criteria to the campaign. Pass leadScoringRubrics directly to save without drafting. Use update_campaign to enable ICP filtering and change steps.",
203
+ inputSchema: {
204
+ type: "object",
205
+ properties: {
206
+ campaignOfferId: {
207
+ type: "string",
208
+ description: "Campaign offer ID",
209
+ },
210
+ leadScoringRubrics: {
211
+ type: "array",
212
+ description: "Optional: pass rubrics directly to save without drafting first. If omitted, reads from draft state.",
213
+ items: {
214
+ type: "object",
215
+ properties: {
216
+ checkName: { type: "string" },
217
+ description: { type: "string" },
218
+ criterion: { type: "string" },
219
+ reason: { type: "string" },
220
+ isRequiredCheck: { type: "boolean" },
221
+ useCheckForScoring: { type: "boolean" },
222
+ },
223
+ },
224
+ },
225
+ },
226
+ required: ["campaignOfferId"],
227
+ additionalProperties: false,
228
+ },
229
+ },
230
+ {
231
+ name: "add_rubric_item",
232
+ description: "Add a single rubric criterion to the current draft. Call save_rubrics to persist.",
233
+ inputSchema: {
234
+ type: "object",
235
+ properties: {
236
+ campaignOfferId: {
237
+ type: "string",
238
+ description: "Campaign offer ID",
239
+ },
240
+ rubric: {
241
+ type: "object",
242
+ description: "Rubric item to add",
243
+ properties: {
244
+ checkName: { type: "string" },
245
+ description: { type: "string" },
246
+ criterion: { type: "string" },
247
+ reason: { type: "string" },
248
+ useCheckForScoring: { type: "boolean" },
249
+ isRequiredCheck: { type: "boolean" },
250
+ allowPartialCredit: { type: "boolean" },
251
+ strictMatching: { type: "boolean" },
252
+ },
253
+ required: ["checkName", "description", "criterion", "reason"],
254
+ additionalProperties: false,
255
+ },
256
+ },
257
+ required: ["campaignOfferId", "rubric"],
258
+ additionalProperties: false,
259
+ },
260
+ },
261
+ {
262
+ name: "update_rubric_item",
263
+ description: "Update a single rubric criterion by ID and persist immediately.",
264
+ inputSchema: {
265
+ type: "object",
266
+ properties: {
267
+ campaignOfferId: {
268
+ type: "string",
269
+ description: "Campaign offer ID",
270
+ },
271
+ rubricId: {
272
+ type: "string",
273
+ description: "Rubric item ID to update",
274
+ },
275
+ updates: {
276
+ type: "object",
277
+ description: "Fields to update on the rubric item",
278
+ properties: {
279
+ checkName: { type: "string" },
280
+ description: { type: "string" },
281
+ criterion: { type: "string" },
282
+ reason: { type: "string" },
283
+ useCheckForScoring: { type: "boolean" },
284
+ isRequiredCheck: { type: "boolean" },
285
+ allowPartialCredit: { type: "boolean" },
286
+ strictMatching: { type: "boolean" },
287
+ },
288
+ additionalProperties: false,
289
+ },
290
+ },
291
+ required: ["campaignOfferId", "rubricId", "updates"],
292
+ additionalProperties: false,
293
+ },
294
+ },
295
+ {
296
+ name: "delete_rubric_item",
297
+ description: "Delete a single rubric criterion by checkName and persist immediately.",
298
+ inputSchema: {
299
+ type: "object",
300
+ properties: {
301
+ campaignOfferId: {
302
+ type: "string",
303
+ description: "Campaign offer ID",
304
+ },
305
+ checkName: {
306
+ type: "string",
307
+ description: "checkName of the rubric to delete",
308
+ },
309
+ },
310
+ required: ["campaignOfferId", "checkName"],
311
+ additionalProperties: false,
312
+ },
313
+ },
314
+ {
315
+ name: "check_rubric",
316
+ description: "Queue enrichment + scoring for a sample of prospects (default 7). Requires saved rubrics.",
317
+ inputSchema: {
318
+ type: "object",
319
+ properties: {
320
+ campaignOfferId: {
321
+ type: "string",
322
+ description: "Campaign offer ID",
323
+ },
324
+ maxProspects: {
325
+ type: "number",
326
+ description: `Number of prospects to test (default ${DEFAULT_MAX_PROSPECTS}, max ${MAX_PROSPECTS_LIMIT}).`,
327
+ },
328
+ },
329
+ required: ["campaignOfferId"],
330
+ additionalProperties: false,
331
+ },
332
+ },
333
+ {
334
+ name: "wait_for_rubric_results",
335
+ description: "Wait for ICP rubric pass-rate results to complete for queued prospects.",
336
+ inputSchema: {
337
+ type: "object",
338
+ properties: {
339
+ campaignOfferId: {
340
+ type: "string",
341
+ description: "Campaign offer ID (used to resolve workflow table)",
342
+ },
343
+ tableId: {
344
+ type: "string",
345
+ description: "Workflow table ID (optional override)",
346
+ },
347
+ targetCount: {
348
+ type: "number",
349
+ description: `Number of completed rubric results to wait for (default ${DEFAULT_MAX_PROSPECTS}).`,
350
+ },
351
+ timeoutMs: {
352
+ type: "number",
353
+ description: `Max time to wait in ms (default ${DEFAULT_TIMEOUT_MS}).`,
354
+ },
355
+ intervalMs: {
356
+ type: "number",
357
+ description: `Polling interval in ms (default ${DEFAULT_INTERVAL_MS}).`,
358
+ },
359
+ },
360
+ required: [],
361
+ additionalProperties: false,
362
+ },
363
+ },
364
+ ];
365
+ export async function draftRubrics(input) {
366
+ const rubrics = normalizeRubricItems(input.leadScoringRubrics || []);
367
+ assertRubricFields(rubrics);
368
+ const normalized = setDraftRubrics(input.campaignOfferId, rubrics, "draft");
369
+ return {
370
+ success: true,
371
+ message: `Drafted ${normalized.length} rubric criteria. Use save_rubrics to persist to the campaign.`,
372
+ criteriaCount: normalized.length,
373
+ criteria: summarizeRubrics(normalized),
374
+ };
375
+ }
376
+ export async function selectNecessaryRubrics(input) {
377
+ const { rubrics } = await getRubricsForEdit(input.campaignOfferId);
378
+ if (rubrics.length === 0) {
379
+ throw new Error("No rubric criteria found. Use draft_rubrics first to create criteria.");
380
+ }
381
+ const existingCheckNames = rubrics.map((r) => r.checkName);
382
+ const invalidCheckNames = input.requiredCheckNames.filter((name) => !existingCheckNames.includes(name));
383
+ if (invalidCheckNames.length > 0) {
384
+ throw new Error(`Invalid checkName(s): ${invalidCheckNames.join(", ")}. Available: ${existingCheckNames.join(", ")}`);
385
+ }
386
+ const updatedRubrics = rubrics
387
+ .filter((rubric) => input.requiredCheckNames.includes(rubric.checkName))
388
+ .map((rubric) => ({
389
+ ...rubric,
390
+ isRequiredCheck: true,
391
+ useCheckForScoring: true,
392
+ }));
393
+ setDraftRubrics(input.campaignOfferId, updatedRubrics, "draft");
394
+ return {
395
+ success: true,
396
+ message: `Kept ${updatedRubrics.length} required criteria and removed ${rubrics.length - updatedRubrics.length} non-essential ones. Use save_rubrics to persist changes.`,
397
+ requiredCount: updatedRubrics.length,
398
+ deletedCount: rubrics.length - updatedRubrics.length,
399
+ requiredChecks: updatedRubrics.map((r) => r.checkName),
400
+ };
401
+ }
402
+ export async function addRubricItem(input) {
403
+ const { rubrics } = await getRubricsForEdit(input.campaignOfferId);
404
+ const nextRubric = normalizeRubricItemDefaults(input.rubric);
405
+ assertRubricFields([nextRubric]);
406
+ if (rubrics.some((r) => r.checkName === nextRubric.checkName)) {
407
+ throw new Error(`Rubric criterion "${nextRubric.checkName}" already exists. Use update_rubric_item instead.`);
408
+ }
409
+ const updatedRubrics = [...rubrics, nextRubric];
410
+ setDraftRubrics(input.campaignOfferId, updatedRubrics, "draft");
411
+ return {
412
+ success: true,
413
+ message: `Added "${nextRubric.checkName}" criterion. Now ${updatedRubrics.length} total criteria. Use save_rubrics to persist to the campaign.`,
414
+ totalCriteria: updatedRubrics.length,
415
+ criteria: summarizeRubrics(updatedRubrics),
416
+ };
417
+ }
418
+ export async function updateRubricItem(input) {
419
+ const { rubrics } = await getRubricsForEdit(input.campaignOfferId);
420
+ const rubricIndex = rubrics.findIndex((r) => r.id === input.rubricId);
421
+ if (rubricIndex === -1) {
422
+ const availableIds = rubrics
423
+ .map((r) => r.id)
424
+ .filter((id) => Boolean(id));
425
+ throw new Error(`Rubric item with ID ${input.rubricId} not found. Available IDs: ${availableIds.join(", ")}`);
426
+ }
427
+ const updatedRubrics = [...rubrics];
428
+ updatedRubrics[rubricIndex] = normalizeRubricItemDefaults({
429
+ ...updatedRubrics[rubricIndex],
430
+ ...input.updates,
431
+ });
432
+ ensureUniqueCheckNames(updatedRubrics);
433
+ assertRubricFields([updatedRubrics[rubricIndex]]);
434
+ const result = await upsertRubricV3({
435
+ campaignOfferId: input.campaignOfferId,
436
+ upsert: [updatedRubrics[rubricIndex]],
437
+ });
438
+ const mergedDraft = mergeSavedWithUnsaved(result?.rubrics, updatedRubrics);
439
+ setDraftRubrics(input.campaignOfferId, mergedDraft, "draft");
440
+ return {
441
+ success: true,
442
+ message: `Updated rubric criterion "${updatedRubrics[rubricIndex].checkName}"`,
443
+ rubricId: input.rubricId,
444
+ updatedFields: Object.keys(input.updates || {}),
445
+ };
446
+ }
447
+ export async function deleteRubricItem(input) {
448
+ const { rubrics } = await getRubricsForEdit(input.campaignOfferId);
449
+ const rubricToDelete = rubrics.find((r) => r.checkName === input.checkName);
450
+ if (!rubricToDelete) {
451
+ throw new Error(`Rubric criterion "${input.checkName}" not found. Available: ${rubrics
452
+ .map((r) => r.checkName)
453
+ .join(", ")}`);
454
+ }
455
+ const updatedRubrics = rubrics.filter((r) => r.checkName !== input.checkName);
456
+ if (!rubricToDelete.id) {
457
+ setDraftRubrics(input.campaignOfferId, updatedRubrics, "draft");
458
+ return {
459
+ success: true,
460
+ message: `Deleted "${input.checkName}" criterion. Now ${updatedRubrics.length} total criteria. (Not saved yet)`,
461
+ totalCriteria: updatedRubrics.length,
462
+ deletedFromDatabase: false,
463
+ };
464
+ }
465
+ const result = await upsertRubricV3({
466
+ campaignOfferId: input.campaignOfferId,
467
+ upsert: [],
468
+ deleteIds: [rubricToDelete.id],
469
+ });
470
+ const mergedDraft = mergeSavedWithUnsaved(result?.rubrics, updatedRubrics);
471
+ setDraftRubrics(input.campaignOfferId, mergedDraft, "draft");
472
+ return {
473
+ success: true,
474
+ message: `Deleted "${input.checkName}" criterion. Now ${updatedRubrics.length} total criteria.`,
475
+ totalCriteria: updatedRubrics.length,
476
+ deletedFromDatabase: true,
477
+ };
478
+ }
479
+ export async function saveRubrics(input) {
480
+ // If rubrics passed directly, use them (skip draft state)
481
+ let rubrics;
482
+ if (input.leadScoringRubrics && input.leadScoringRubrics.length > 0) {
483
+ rubrics = normalizeRubricItems(input.leadScoringRubrics);
484
+ assertRubricFields(rubrics);
485
+ setDraftRubrics(input.campaignOfferId, rubrics, "draft");
486
+ }
487
+ else {
488
+ ({ rubrics } = await getRubricsForEdit(input.campaignOfferId));
489
+ }
490
+ if (rubrics.length === 0) {
491
+ throw new Error("No rubric criteria to save. Pass leadScoringRubrics or call draft_rubrics first.");
492
+ }
493
+ const activeRubrics = rubrics.filter((r) => r.useCheckForScoring !== false);
494
+ if (activeRubrics.length === 0) {
495
+ throw new Error("No active rubric criteria found.");
496
+ }
497
+ assertRubricFields(activeRubrics);
498
+ const campaign = await fetchCampaignOffer(input.campaignOfferId);
499
+ const existingRubrics = normalizeCampaignRubrics(campaign.leadScoringRubrics || []);
500
+ const { normalizedDraft, deletedRubricIds } = mergeRubricsWithExisting(activeRubrics, existingRubrics);
501
+ const result = await upsertRubricV3({
502
+ campaignOfferId: input.campaignOfferId,
503
+ upsert: normalizedDraft,
504
+ deleteIds: deletedRubricIds,
505
+ });
506
+ if (result?.rubrics) {
507
+ setDraftRubrics(input.campaignOfferId, result.rubrics, "campaign");
508
+ }
509
+ else {
510
+ setDraftRubrics(input.campaignOfferId, normalizedDraft, "draft");
511
+ }
512
+ // Auto-enable ICP filtering on the campaign when rubrics are saved.
513
+ // The PUT triggers syncCampaignTableIcpFiltering on the server, which is
514
+ // the only path that updates the workflow table's Enable ICP Filtering
515
+ // column config + cells to true. Always fire the PUT even if
516
+ // CampaignOffer.enableICPFilters is already true: create_campaign pre-sets
517
+ // that server flag, but the workflow table column is created later with
518
+ // inputMapping.value.value=false by default, so the column never syncs
519
+ // without this call. Observed on Phase 95 live UAT: Skillsync + Patientdesk
520
+ // both had CampaignOffer.enableICPFilters=true but Enable ICP Filtering
521
+ // column cells=false → ICP Score runCondition evaluated false → entire
522
+ // cascade blocked.
523
+ let enableICPFiltersSet = false;
524
+ try {
525
+ const api = getApi();
526
+ await api.put(`/api/v2/campaign-offers/${input.campaignOfferId}`, {
527
+ enableICPFilters: true,
528
+ });
529
+ enableICPFiltersSet = true;
530
+ }
531
+ catch (error) {
532
+ // Non-fatal: the rubric save already succeeded. Log and
533
+ // let the caller fix the flag via update_campaign if needed.
534
+ console.error("[save_rubrics] Could not auto-enable ICP filtering on campaign", input.campaignOfferId, error);
535
+ }
536
+ return {
537
+ success: true,
538
+ message: enableICPFiltersSet || campaign.enableICPFilters === true
539
+ ? `Saved ${normalizedDraft.length} rubric criteria and ICP filtering is ON.`
540
+ : `Saved ${normalizedDraft.length} rubric criteria to the campaign. WARNING: could not auto-enable ICP filtering — call update_campaign({ campaignId, enableICPFilters: true }) to activate rubric-based filtering.`,
541
+ criteriaCount: normalizedDraft.length,
542
+ deletedCount: deletedRubricIds.length,
543
+ enableICPFiltersSet,
544
+ };
545
+ }
546
+ export async function checkRubric(input) {
547
+ const maxProspects = resolveMaxProspects(input.maxProspects);
548
+ const campaign = await fetchCampaignOffer(input.campaignOfferId);
549
+ if (campaign.enableICPFilters !== true) {
550
+ throw new Error("ICP filtering is not enabled. Save and apply rubrics before running check_rubric.");
551
+ }
552
+ const draft = getDraftRubrics(input.campaignOfferId);
553
+ const rubrics = normalizeCampaignRubrics((draft || campaign.leadScoringRubrics || []).filter((r) => r.useCheckForScoring !== false));
554
+ if (rubrics.length === 0) {
555
+ throw new Error("No active rubric criteria found. Use draft_rubrics and save_rubrics first.");
556
+ }
557
+ const unsavedRubrics = rubrics.filter((r) => !r.id);
558
+ if (unsavedRubrics.length > 0) {
559
+ throw new Error(`${unsavedRubrics.length} rubric criteria are not saved to database. Call save_rubrics before check_rubric.`);
560
+ }
561
+ const workflowTableId = campaign.workflowTableId;
562
+ if (!workflowTableId) {
563
+ throw new Error("No workflow table found. Complete the Find Leads step first to create a workflow table with leads.");
564
+ }
565
+ const api = getApi();
566
+ const tableResponse = await api.get(`/api/v3/campaigns/${input.campaignOfferId}/workflow-table`);
567
+ const workflowTable = tableResponse.workflowTable;
568
+ if (!workflowTable) {
569
+ throw new Error("Workflow table not found for this campaign.");
570
+ }
571
+ const enrichColumn = workflowTable.columns?.find((col) => col.columnType === "enrich_person");
572
+ if (!enrichColumn) {
573
+ throw new Error("No Enrich Person column found in workflow table. The table may not be set up correctly.");
574
+ }
575
+ const rows = workflowTable.rows || [];
576
+ const cellsToQueue = [];
577
+ for (const row of rows) {
578
+ if (cellsToQueue.length >= maxProspects)
579
+ break;
580
+ const enrichCell = row.cells?.find((cell) => cell.columnId === enrichColumn.id);
581
+ if (!enrichCell)
582
+ continue;
583
+ const queueableStatuses = ["pending", "error", "dependency_blocked"];
584
+ if (queueableStatuses.includes(enrichCell.status || "")) {
585
+ cellsToQueue.push(enrichCell.id);
586
+ }
587
+ }
588
+ if (cellsToQueue.length === 0) {
589
+ return {
590
+ success: true,
591
+ message: "No rows need processing. All enrich cells are either completed, cached, or currently processing.",
592
+ processedRows: 0,
593
+ tableId: workflowTable.id,
594
+ };
595
+ }
596
+ // Use the same robust batch queue path as Launch "Enrich All":
597
+ // atomically queue + dispatch with lock protection.
598
+ const queueResult = await api.post(`/api/v3/workflow-tables/cells/queue-batch`, {
599
+ tableId: workflowTable.id,
600
+ cellIds: cellsToQueue,
601
+ });
602
+ const queuedCount = queueResult?.queued ?? 0;
603
+ const failedCount = queueResult?.failed ?? 0;
604
+ const skippedCount = queueResult?.skipped ?? 0;
605
+ const dispatchedCount = queueResult?.dispatched ?? queuedCount;
606
+ return {
607
+ success: true,
608
+ message: `Queued ${queuedCount} prospects for enrichment and scoring.${failedCount > 0 ? ` (${failedCount} failed to dispatch)` : ""}`,
609
+ processedRows: queuedCount,
610
+ queuedCount,
611
+ dispatchedCount,
612
+ failedCount,
613
+ skippedCount,
614
+ tableId: workflowTable.id,
615
+ };
616
+ }
617
+ export async function waitForRubricResults(input) {
618
+ const api = getApi();
619
+ const timeoutMs = Math.max(5000, input.timeoutMs ?? DEFAULT_TIMEOUT_MS);
620
+ const intervalMs = Math.max(500, input.intervalMs ?? DEFAULT_INTERVAL_MS);
621
+ const targetCount = resolveMaxProspects(input.targetCount);
622
+ let tableId = input.tableId;
623
+ if (!tableId && input.campaignOfferId) {
624
+ const campaign = await fetchCampaignOffer(input.campaignOfferId);
625
+ tableId = campaign.workflowTableId ?? undefined;
626
+ }
627
+ if (!tableId) {
628
+ throw new Error("Workflow table ID is required. Provide tableId or campaignOfferId.");
629
+ }
630
+ const start = Date.now();
631
+ let attempts = 0;
632
+ let lastStats = null;
633
+ while (Date.now() - start <= timeoutMs) {
634
+ attempts += 1;
635
+ const stats = await api.get(`/api/v3/workflow-tables/${tableId}/stats`);
636
+ lastStats = stats;
637
+ const completed = stats.passRate?.completed ?? 0;
638
+ const totalRows = stats.totalRows ?? 0;
639
+ const effectiveTarget = totalRows > 0 ? Math.min(targetCount, totalRows) : targetCount;
640
+ if (completed >= effectiveTarget) {
641
+ const passed = stats.passRate?.passed ?? 0;
642
+ const percent = completed > 0 ? Math.round((passed / completed) * 100) : 0;
643
+ // Fetch current row states so caller sees per-row enrich/ICP status
644
+ const rowSnapshot = await getTableRowsMinimal(tableId, {
645
+ limit: effectiveTarget,
646
+ });
647
+ return {
648
+ ready: true,
649
+ attempts,
650
+ elapsedMs: Date.now() - start,
651
+ tableId,
652
+ passRate: {
653
+ completed,
654
+ passed,
655
+ percent,
656
+ targetCount: effectiveTarget,
657
+ },
658
+ rows: rowSnapshot.rows,
659
+ stats,
660
+ };
661
+ }
662
+ await sleep(intervalMs);
663
+ }
664
+ const completed = lastStats?.passRate?.completed ?? 0;
665
+ const passed = lastStats?.passRate?.passed ?? 0;
666
+ const percent = completed > 0 ? Math.round((passed / completed) * 100) : 0;
667
+ return {
668
+ ready: false,
669
+ reason: "timeout",
670
+ attempts,
671
+ elapsedMs: Date.now() - start,
672
+ tableId,
673
+ passRate: {
674
+ completed,
675
+ passed,
676
+ percent,
677
+ targetCount,
678
+ },
679
+ stats: lastStats,
680
+ };
681
+ }