@agifyai/leadify-mcp 5.0.1 → 6.0.1

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/server.js CHANGED
@@ -11,12 +11,12 @@ import { registerDataSourceTools } from "./tools/datasources.js";
11
11
  import { registerPersonaTools } from "./tools/personas.js";
12
12
  import { registerLeadGroupTools } from "./tools/lead_groups.js";
13
13
  import { registerOrganizationTools } from "./tools/organizations.js";
14
- import { registerOutreachSettingsTools } from "./tools/outreach_settings.js";
15
14
  import { registerPipelineTools } from "./tools/pipeline.js";
15
+ import { registerFineTuningTools } from "./tools/fine_tuning.js";
16
16
  export function createServer() {
17
17
  const server = new McpServer({
18
18
  name: "leadify",
19
- version: "3.6.2",
19
+ version: "6.0.0",
20
20
  });
21
21
  registerAuthTools(server);
22
22
  registerOrganizationTools(server);
@@ -30,7 +30,7 @@ export function createServer() {
30
30
  registerDataRoomTools(server);
31
31
  registerDataSourceTools(server);
32
32
  registerPersonaTools(server);
33
- registerOutreachSettingsTools(server);
34
33
  registerPipelineTools(server);
34
+ registerFineTuningTools(server);
35
35
  return server;
36
36
  }
@@ -276,16 +276,17 @@ export function registerDataRoomTools(server) {
276
276
  }
277
277
  });
278
278
  // ── get_outreach_context ──────────────────────────────────────────────
279
- server.tool("get_outreach_context", "Get a curated outreach context for a lead group: the outreach-relevant slice " +
280
- "of companyInfo only (identity, pitch, constraints, curated products, economics " +
281
- "triggers/baseline, product ICP roles, proof wording). " +
279
+ server.tool("get_outreach_context", "Get a lean outreach context for a lead group. Returns the shape " +
280
+ "`{ leadGroupId, leadGroupName, organizationId, company }` where `company` is the " +
281
+ "outreach-relevant slice of companyInfo (identity, pitch, constraints, curated " +
282
+ "products, economics triggers/baseline, product ICP roles, proof wording). " +
282
283
  "Takes a lead_group_id (NOT organization_id) — the org is resolved server-side, " +
283
284
  "eliminating the cross-org footgun of get_data_room. " +
284
- "Returns a HARD error if no persona is linked to the group. " +
285
- "NOTE: the `outreach` settings block is NOT returned by this tool " +
286
- "outreach configuration (positioning, tone, rules, templates, etc.) is " +
287
- "managed exclusively via the Fine Tuning tools (append_fine_tuning / " +
288
- "set_fine_tuning / get_fine_tuning). " +
285
+ "Does NOT require a persona: the group needs no linked persona and an empty data " +
286
+ "room returns success (not an error). No persona or outreach block is returned. " +
287
+ "Outreach configuration (positioning, tone, rules, templates, etc.) is managed " +
288
+ "exclusively via the Fine Tuning tools (append_fine_tuning / set_fine_tuning / " +
289
+ "get_fine_tuning). " +
289
290
  "Use this instead of get_data_room + get_lead_group_persona in the outreach " +
290
291
  "pipeline Phase 0.", {
291
292
  lead_group_id: z.string().describe("ID of the lead group to resolve context for."),
@@ -24,14 +24,14 @@ export function registerFineTuningTools(server) {
24
24
  "Read STRICTLY from the FineTuning model — zero OutreachSettings mixed in. " +
25
25
  "Use this when editing the Fine Tuning page or when an agent needs the copywriting " +
26
26
  "guardrails (rules + pitfalls + structure + examples + languages). " +
27
- "DO NOT use get_outreach_settings for this: that endpoint returns 100× more payload " +
28
- "and is for the Outreach agent preflight, not for fine tuning. " +
29
27
  "Upsert-on-read: if no FineTuning row exists for the lead group, one is created " +
30
28
  "with defaults, so the response shape is always stable.", {
31
29
  lead_group_id: z
32
30
  .string()
33
31
  .describe("ID of the lead group whose fine tuning configuration to fetch. " +
34
- "Resolve via list_organizations + get_leads if needed."),
32
+ "Pass the lead_group_id directly; if you need to resolve it, use " +
33
+ "pipeline_next_lead (do NOT use get_leads — its payload is huge and is " +
34
+ "banned in the outreach pipeline)."),
35
35
  }, async ({ lead_group_id }) => {
36
36
  try {
37
37
  const data = await getClient().get(`/api/lead-group/${encodeURIComponent(lead_group_id)}/fine-tuning`);
@@ -159,21 +159,19 @@ export function registerLeadGroupTools(server) {
159
159
  server.tool("get_lead_group_persona", "Retrieve the persona assigned to a specific lead group, with group context. " +
160
160
  "Returns null persona if none is assigned. Use this to check what targeting " +
161
161
  "rules apply to a group before generating messages. " +
162
- "By default, `persona` is SLIM ({id, name, organizationId}) — about ~150 chars " +
163
- "instead of 63k just enough to resolve the org / link the persona to a lead group. " +
164
- "Pass `view=outreach` to get the outreach-specific projection (only when you actually " +
165
- "need the outreach trio — prefer the dedicated get_outreach_templates tool for that). " +
162
+ "By default, `persona` uses the OUTREACH projection (messaging + targeting), " +
163
+ "the lean supported view. Pass `view=full` only when you need the complete " +
164
+ "persona payload prefer the dedicated get_outreach_templates tool for the outreach trio. " +
166
165
  "SILO: outreach data is its own concern. It does not leak into the default persona payload.", {
167
166
  lead_group_id: z.string().describe("Lead group ID."),
168
167
  view: z
169
- .enum(["slim", "outreach"])
168
+ .enum(["full", "outreach"])
170
169
  .optional()
171
- .default("slim")
170
+ .default("outreach")
172
171
  .describe("Persona projection. " +
173
- "'slim' (default) = {id, name, organizationId} fast, context-friendly. " +
174
- "'outreach' = projection used by the outreach agent (messaging + targeting). " +
175
- "The legacy 'full' view is gone (SILO enforcement) use get_persona + " +
176
- "get_outreach_templates for a complete picture, scoped per concern."),
172
+ "'outreach' (default) = projection used by the outreach agent (messaging + targeting). " +
173
+ "'full' = the complete persona payload. " +
174
+ "Prefer get_persona + get_outreach_templates for a complete picture, scoped per concern."),
177
175
  }, async ({ lead_group_id, view }) => {
178
176
  try {
179
177
  const params = new URLSearchParams();
@@ -21,7 +21,13 @@ export function registerPipelineTools(server) {
21
21
  "`get_lead` call); the caller only needs `lead_id`. " +
22
22
  "The response is decorated with a Trigger.dev `traceUrl` deep-link when missing " +
23
23
  "(https://cloud.trigger.dev/runs/{runId}), so you can always jump to the trace. " +
24
- "Waits up to 300s for the remote agent to complete.", {
24
+ "WAIT BEHAVIOR: this is a single blocking HTTP call to the backend with no " +
25
+ "client-side timeout — the wait is entirely server-side. The backend may return " +
26
+ "BEFORE the agent finishes (observed ~120s) with `status: EXECUTING` and a runId; " +
27
+ "this is NOT a failure — the run keeps going server-side, follow it via the " +
28
+ "traceUrl. In dry-run (dryrun=true) the call is safe to retry, since nothing is " +
29
+ "persisted; for production writes (dryrun=false) prefer following the trace over " +
30
+ "blindly retrying to avoid duplicate generation.", {
25
31
  lead_id: z.string().describe("ID of the lead to generate a message for."),
26
32
  dryrun: z
27
33
  .boolean()
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agifyai/leadify-mcp",
3
- "version": "5.0.1",
3
+ "version": "6.0.1",
4
4
  "description": "MCP server for Leadify lead management API",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -12,7 +12,7 @@
12
12
  "README.md"
13
13
  ],
14
14
  "scripts": {
15
- "build": "tsc && chmod +x dist/index.js",
15
+ "build": "rm -rf dist && tsc && chmod +x dist/index.js",
16
16
  "start": "node dist/index.js",
17
17
  "dev": "tsc --watch",
18
18
  "prepublishOnly": "npm run build"
@@ -1,2 +0,0 @@
1
- import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
- export declare function registerOutreachSettingsTools(server: McpServer): void;
@@ -1,1004 +0,0 @@
1
- import { z } from "zod";
2
- import { getClient } from "../client.js";
3
- import { toolResult, toolError, handleToolError } from "../types.js";
4
- // ── Schemas mirror apps/server/src/lib/outreach-settings-schemas.ts ────────
5
- const sequenceBlock = z
6
- .object({
7
- goal: z.string().optional(),
8
- coreElements: z.array(z.string()).optional(),
9
- })
10
- .passthrough();
11
- const positioning = z
12
- .object({
13
- dream: z.string().max(200).optional(),
14
- fear: z.string().max(200).optional(),
15
- whyNow: z.string().max(200).optional(),
16
- })
17
- .passthrough();
18
- const sequenceDesign = z
19
- .object({
20
- connexion: sequenceBlock.optional(),
21
- linkedin: z
22
- .object({
23
- one: sequenceBlock.optional(),
24
- two: sequenceBlock.optional(),
25
- three: sequenceBlock.optional(),
26
- })
27
- .partial()
28
- .passthrough()
29
- .optional(),
30
- email: z
31
- .object({
32
- one: sequenceBlock.optional(),
33
- two: sequenceBlock.optional(),
34
- three: sequenceBlock.optional(),
35
- })
36
- .partial()
37
- .passthrough()
38
- .optional(),
39
- })
40
- .passthrough();
41
- const rules = z
42
- .object({
43
- forbidden: z.array(z.string()).optional(),
44
- priority: z.array(z.string()).optional(),
45
- format: z
46
- .object({
47
- signatureEmail: z.string().optional(),
48
- signatureLinkedin: z.string().optional(),
49
- vouvoiement: z.boolean().optional(),
50
- bannedChars: z.array(z.string()).optional(),
51
- bannedStyles: z.array(z.string()).optional(),
52
- maxWordsPerSentence: z.number().int().positive().optional(),
53
- })
54
- .passthrough()
55
- .optional(),
56
- })
57
- .passthrough();
58
- const caseStudy = z
59
- .object({
60
- title: z.string().min(1),
61
- summary: z.string().max(200),
62
- idealFor: z.string().optional(),
63
- proofPoints: z.array(z.string()).optional(),
64
- })
65
- .passthrough();
66
- const urlValue = z
67
- .union([z.literal(""), z.string().url()])
68
- .nullable()
69
- .optional();
70
- // ── Schemas for persona-migrated fields (PRD-737) ────────────────────────
71
- const outreachTemplate = z
72
- .object({
73
- id: z.string().optional(),
74
- signal: z.string().optional(),
75
- channel: z.string().nullable().optional(),
76
- fieldName: z.string().optional(),
77
- signalContext: z.string().optional(),
78
- purpose: z.string().optional(),
79
- example: z.string().optional(),
80
- maxLength: z.number().optional(),
81
- subject: z.string().optional(),
82
- body: z.string().optional(),
83
- })
84
- .passthrough();
85
- const outreachSignalRouting = z
86
- .object({
87
- signalSourceField: z.string().optional(),
88
- defaultSignal: z.string().optional(),
89
- signals: z.array(z.string()).optional(),
90
- })
91
- .passthrough()
92
- .nullable();
93
- const extraPromptPhase = z
94
- .object({
95
- scope: z.enum(["qualify", "outreach"]),
96
- insertionPoint: z.enum(["prefix", "phase_1_5", "phase_2_extra", "suffix"]),
97
- content: z.string(),
98
- })
99
- .passthrough();
100
- const senderOverride = z
101
- .object({ name: z.string().min(1), title: z.string().optional() })
102
- .nullable()
103
- .optional();
104
- const icpStrategy = z
105
- .object({
106
- dealType: z.string().optional(),
107
- dreamOutcome: z.string().optional(),
108
- mainPains: z.array(z.string()).optional(),
109
- keyObjections: z.array(z.string()).optional(),
110
- decisionDrivers: z.array(z.string()).optional(),
111
- })
112
- .passthrough()
113
- .nullable()
114
- .optional();
115
- async function fetchCurrentSettings(leadGroupId) {
116
- const data = await getClient().get(`/api/lead-group/${encodeURIComponent(leadGroupId)}/outreach-settings`);
117
- return data;
118
- }
119
- async function putPartial(leadGroupId, body) {
120
- return getClient().put(`/api/lead-group/${encodeURIComponent(leadGroupId)}/outreach-settings`, body);
121
- }
122
- export function registerOutreachSettingsTools(server) {
123
- // ── get_outreach_settings ──────────────────────────────────────────────
124
- server.tool("get_outreach_settings", "Get the outreach settings (positioning, sequence design, rules, URLs, case studies) " +
125
- "for a specific lead group / campaign. The settings drive the [Leadify] Outreach agent " +
126
- "preflight. GET auto-creates a row with defaults on first read. " +
127
- "Pass `sections` (CSV) to slice the response and avoid saturating the LLM context " +
128
- "when you only need one part (e.g. 'positioning,urls'). Allowed: " +
129
- "positioning, sequenceDesign, rules, caseStudies, urls, connection.", {
130
- lead_group_id: z.string().describe("Lead group ID."),
131
- sections: z
132
- .string()
133
- .optional()
134
- .describe("Optional CSV of section names to return. " +
135
- "Allowed values: positioning, sequenceDesign, rules, caseStudies, urls, connection. " +
136
- "If omitted, the full row is returned (legacy behaviour). " +
137
- "Use this to keep the LLM context small when you only need one section."),
138
- }, async ({ lead_group_id, sections }) => {
139
- try {
140
- const params = new URLSearchParams();
141
- if (sections)
142
- params.set("sections", sections);
143
- const data = await getClient().get(`/api/lead-group/${encodeURIComponent(lead_group_id)}/outreach-settings`, params);
144
- return toolResult(data);
145
- }
146
- catch (error) {
147
- return handleToolError(error);
148
- }
149
- });
150
- // ── update_outreach_settings ───────────────────────────────────────────
151
- // Full-form escape hatch: set the whole block at once. Prefer the
152
- // granular tools below when you only want to touch a single slot / field.
153
- server.tool("update_outreach_settings", "Full-form update (escape hatch). Replaces JSON blocks wholesale when " +
154
- "provided — if you only want to tweak a single slot or sub-field, prefer " +
155
- "update_outreach_positioning / update_outreach_sequence_slot / " +
156
- "update_outreach_rules / update_outreach_case_study / update_outreach_urls / " +
157
- "set_outreach_connection_request / update_outreach_agent_identity / " +
158
- "update_outreach_icp_strategy / update_outreach_pain_point / " +
159
- "update_outreach_lookalike_client / update_outreach_outreach_template / " +
160
- "update_outreach_signal_routing, which do safe read-modify-write merges.", {
161
- lead_group_id: z.string().describe("Lead group ID."),
162
- connection_request_enabled: z
163
- .boolean()
164
- .optional()
165
- .describe("If true, first touchpoint is a LinkedIn connection request; if false, " +
166
- "card_linkedin_one is written as a cold message."),
167
- positioning: positioning.optional().describe("Value Equation — 3 short phrases (200 chars max each): dream (what this lead " +
168
- "group wants), fear (what they dread), whyNow (why act now rather than later)."),
169
- sequence_design: sequenceDesign
170
- .optional()
171
- .describe("Per-message blueprints keyed by channel/slot (connexion, linkedin.one/two/three, " +
172
- "email.one/two/three). Each block = {goal, coreElements[]}."),
173
- rules: rules
174
- .optional()
175
- .describe("Formatting/content rules: forbidden phrases, priority points, and format " +
176
- "(signatures, vouvoiement, bannedChars, bannedStyles, maxWordsPerSentence)."),
177
- booking_url: urlValue.describe("Booking URL (e.g. Cal.com). Empty string or null clears it."),
178
- website_url: urlValue.describe("Website URL. Empty string or null clears it."),
179
- case_studies: z
180
- .array(caseStudy)
181
- .max(5)
182
- .optional()
183
- .describe("Up to 5 case studies used as proof (title, summary ≤200 chars, idealFor, proofPoints[])."),
184
- // ── Fields migrated from Persona (PRD-737) ───────────────────────────
185
- description: z
186
- .string()
187
- .optional()
188
- .describe("Description / context for this lead group's outreach."),
189
- output_language: z
190
- .string()
191
- .optional()
192
- .describe("Output language code (fr, en, de, ...)."),
193
- agent_domain: z
194
- .string()
195
- .optional()
196
- .describe("Agent identity string (e.g. 'for medtech companies')."),
197
- icp_strategy: icpStrategy
198
- .optional()
199
- .describe("ICP strategy mental map: {dealType, dreamOutcome, mainPains[], keyObjections[], decisionDrivers[]}."),
200
- pain_points: z
201
- .array(z.object({ title: z.string().optional(), description: z.string().optional() }).passthrough())
202
- .optional()
203
- .describe("Structured business pain points ({title, description})."),
204
- lookalike_clients: z
205
- .array(z
206
- .object({
207
- name: z.string(),
208
- sector: z.string().optional(),
209
- size: z.string().optional(),
210
- segment: z.string().optional(),
211
- comparison_criteria: z.string().optional(),
212
- })
213
- .passthrough())
214
- .optional()
215
- .describe("Lookalike client references ({name, sector?, size?, segment?, comparison_criteria?})."),
216
- outreach_templates: z
217
- .array(outreachTemplate)
218
- .optional()
219
- .describe("Outreach message templates by channel/signal. Each: {channel, signal, fieldName, purpose, example, subject, body}."),
220
- tone_instructions: z
221
- .string()
222
- .optional()
223
- .describe("Free-form markdown voice / tone guidance."),
224
- messaging_guidelines: z
225
- .any()
226
- .optional()
227
- .describe("Messaging approach: {tone, language, can_mention, must_not_mention, value_propositions}."),
228
- outreach_signal_routing: outreachSignalRouting
229
- .optional()
230
- .describe("Signal routing config: {signalSourceField, defaultSignal, signals[]}. defaultSignal must exist in signals[]."),
231
- sender_override: senderOverride
232
- .describe("Override SDR identity: {name, title?}. Pass null to clear."),
233
- outreach_fields: z
234
- .any()
235
- .optional()
236
- .describe("Variables referenceable in templates: [{key, label, source}]."),
237
- extra_prompt_phases: z
238
- .array(extraPromptPhase)
239
- .optional()
240
- .describe("Custom prompt snippets: [{scope, insertionPoint, content}]. scope: qualify|outreach. insertionPoint: prefix|phase_1_5|phase_2_extra|suffix."),
241
- }, async ({ lead_group_id, connection_request_enabled, positioning: pos, sequence_design, rules: rls, booking_url, website_url, case_studies,
242
- // ── PRD-737 fields ──────────────────────────────────────────────────
243
- description, output_language, agent_domain, icp_strategy: icp, pain_points, lookalike_clients, outreach_templates, tone_instructions, messaging_guidelines, outreach_signal_routing: signal_routing, sender_override: sdr_override, outreach_fields, extra_prompt_phases, }) => {
244
- try {
245
- const body = {};
246
- // Legacy fields
247
- if (connection_request_enabled !== undefined)
248
- body.connectionRequestEnabled = connection_request_enabled;
249
- if (pos !== undefined)
250
- body.positioning = pos;
251
- if (sequence_design !== undefined)
252
- body.sequenceDesign = sequence_design;
253
- if (rls !== undefined)
254
- body.rules = rls;
255
- if (booking_url !== undefined)
256
- body.bookingUrl = booking_url;
257
- if (website_url !== undefined)
258
- body.websiteUrl = website_url;
259
- if (case_studies !== undefined)
260
- body.caseStudies = case_studies;
261
- // PRD-737 fields
262
- if (description !== undefined)
263
- body.description = description;
264
- if (output_language !== undefined)
265
- body.outputLanguage = output_language;
266
- if (agent_domain !== undefined)
267
- body.agentDomain = agent_domain;
268
- if (icp !== undefined)
269
- body.icpStrategy = icp;
270
- if (pain_points !== undefined)
271
- body.painPoints = pain_points;
272
- if (lookalike_clients !== undefined)
273
- body.lookalikeClients = lookalike_clients;
274
- if (outreach_templates !== undefined)
275
- body.outreachTemplates = outreach_templates;
276
- if (tone_instructions !== undefined)
277
- body.toneInstructions = tone_instructions;
278
- if (messaging_guidelines !== undefined)
279
- body.messagingGuidelines = messaging_guidelines;
280
- if (signal_routing !== undefined)
281
- body.outreachSignalRouting = signal_routing;
282
- if (sdr_override !== undefined)
283
- body.senderOverride = sdr_override;
284
- if (outreach_fields !== undefined)
285
- body.outreachFields = outreach_fields;
286
- if (extra_prompt_phases !== undefined)
287
- body.extraPromptPhases = extra_prompt_phases;
288
- const data = await putPartial(lead_group_id, body);
289
- return toolResult(data);
290
- }
291
- catch (error) {
292
- return handleToolError(error);
293
- }
294
- });
295
- // ── update_outreach_positioning ────────────────────────────────────────
296
- server.tool("update_outreach_positioning", "Patch only the positioning block (dream/fear/whyNow). Reads the current " +
297
- "positioning, merges the fields you pass, and writes it back. Pass any " +
298
- "subset of dream/fear/whyNow — unspecified fields stay intact.", {
299
- lead_group_id: z.string().describe("Lead group ID."),
300
- dream: z
301
- .string()
302
- .max(200)
303
- .optional()
304
- .describe("What this lead group wants (≤200 chars)."),
305
- fear: z
306
- .string()
307
- .max(200)
308
- .optional()
309
- .describe("What they dread (≤200 chars)."),
310
- whyNow: z
311
- .string()
312
- .max(200)
313
- .optional()
314
- .describe("Why act now rather than later (≤200 chars)."),
315
- }, async ({ lead_group_id, dream, fear, whyNow }) => {
316
- try {
317
- if (dream === undefined && fear === undefined && whyNow === undefined) {
318
- return toolError("Provide at least one of: dream, fear, whyNow.");
319
- }
320
- const current = await fetchCurrentSettings(lead_group_id);
321
- const merged = {
322
- ...(current.positioning ?? {}),
323
- ...(dream !== undefined && { dream }),
324
- ...(fear !== undefined && { fear }),
325
- ...(whyNow !== undefined && { whyNow }),
326
- };
327
- const data = await putPartial(lead_group_id, { positioning: merged });
328
- return toolResult(data);
329
- }
330
- catch (error) {
331
- return handleToolError(error);
332
- }
333
- });
334
- // ── update_outreach_sequence_slot ──────────────────────────────────────
335
- server.tool("update_outreach_sequence_slot", "Patch a single sequence slot (connexion, linkedin.one/two/three, or " +
336
- "email.one/two/three). Reads the current sequenceDesign, mutates just the " +
337
- "target slot, and writes the whole block back so other slots are untouched. " +
338
- "goal and coreElements within the slot are each replaced wholesale when passed.", {
339
- lead_group_id: z.string().describe("Lead group ID."),
340
- channel: z
341
- .enum(["connexion", "linkedin", "email"])
342
- .describe("Channel: 'connexion' (single slot, LinkedIn invite note), 'linkedin' " +
343
- "(slots one/two/three), or 'email' (slots one/two/three)."),
344
- slot: z
345
- .enum(["one", "two", "three"])
346
- .optional()
347
- .describe("Slot index. Required when channel is 'linkedin' or 'email'. Ignored for 'connexion'."),
348
- goal: z
349
- .string()
350
- .optional()
351
- .describe("The message goal (one sentence)."),
352
- core_elements: z
353
- .array(z.string())
354
- .optional()
355
- .describe("List of elements the message must include (short bullet points). Replaces the array wholesale."),
356
- }, async ({ lead_group_id, channel, slot, goal, core_elements }) => {
357
- try {
358
- if (goal === undefined && core_elements === undefined) {
359
- return toolError("Provide at least one of: goal, core_elements.");
360
- }
361
- if (channel !== "connexion" && !slot) {
362
- return toolError(`slot is required when channel is '${channel}'.`);
363
- }
364
- const current = await fetchCurrentSettings(lead_group_id);
365
- const next = { ...(current.sequenceDesign ?? {}) };
366
- if (channel === "connexion") {
367
- const existing = (next.connexion ?? {});
368
- next.connexion = {
369
- ...existing,
370
- ...(goal !== undefined && { goal }),
371
- ...(core_elements !== undefined && { coreElements: core_elements }),
372
- };
373
- }
374
- else {
375
- const channelExisting = {
376
- ...(next[channel] ?? {}),
377
- };
378
- const slotExisting = (channelExisting[slot] ??
379
- {});
380
- channelExisting[slot] = {
381
- ...slotExisting,
382
- ...(goal !== undefined && { goal }),
383
- ...(core_elements !== undefined && { coreElements: core_elements }),
384
- };
385
- next[channel] = channelExisting;
386
- }
387
- const data = await putPartial(lead_group_id, { sequenceDesign: next });
388
- return toolResult(data);
389
- }
390
- catch (error) {
391
- return handleToolError(error);
392
- }
393
- });
394
- // ── update_outreach_rules ──────────────────────────────────────────────
395
- server.tool("update_outreach_rules", "Patch only the rules block. forbidden and priority arrays are replaced " +
396
- "wholesale when passed. format.* sub-fields are deep-merged individually " +
397
- "(signatureEmail/signatureLinkedin/vouvoiement/bannedChars/bannedStyles/" +
398
- "maxWordsPerSentence) — pass only what you want to change.", {
399
- lead_group_id: z.string().describe("Lead group ID."),
400
- forbidden: z
401
- .array(z.string())
402
- .optional()
403
- .describe("Forbidden phrases (replaces the array when passed)."),
404
- priority: z
405
- .array(z.string())
406
- .optional()
407
- .describe("Priority points (replaces the array when passed)."),
408
- format: z
409
- .object({
410
- signatureEmail: z.string().optional(),
411
- signatureLinkedin: z.string().optional(),
412
- vouvoiement: z.boolean().optional(),
413
- bannedChars: z.array(z.string()).optional(),
414
- bannedStyles: z.array(z.string()).optional(),
415
- maxWordsPerSentence: z.number().int().positive().optional(),
416
- })
417
- .passthrough()
418
- .optional()
419
- .describe("Format sub-fields: only fields passed are updated. Arrays (bannedChars/bannedStyles) are replaced wholesale."),
420
- }, async ({ lead_group_id, forbidden, priority, format }) => {
421
- try {
422
- if (forbidden === undefined &&
423
- priority === undefined &&
424
- format === undefined) {
425
- return toolError("Provide at least one of: forbidden, priority, format.");
426
- }
427
- const current = await fetchCurrentSettings(lead_group_id);
428
- const currentRules = (current.rules ?? {});
429
- const next = { ...currentRules };
430
- if (forbidden !== undefined)
431
- next.forbidden = forbidden;
432
- if (priority !== undefined)
433
- next.priority = priority;
434
- if (format !== undefined) {
435
- next.format = {
436
- ...(currentRules.format ?? {}),
437
- ...format,
438
- };
439
- }
440
- const data = await putPartial(lead_group_id, { rules: next });
441
- return toolResult(data);
442
- }
443
- catch (error) {
444
- return handleToolError(error);
445
- }
446
- });
447
- // ── update_outreach_case_study ─────────────────────────────────────────
448
- server.tool("update_outreach_case_study", "Add, replace, or remove a single case study without re-sending the whole " +
449
- "list. Reads the current caseStudies array, mutates the target entry, " +
450
- "and writes the full list back. Cap is 5 entries.", {
451
- lead_group_id: z.string().describe("Lead group ID."),
452
- action: z
453
- .enum(["add", "replace", "remove"])
454
- .describe("add = append (fails if list is at cap 5); replace = overwrite at index; remove = delete at index."),
455
- index: z
456
- .number()
457
- .int()
458
- .nonnegative()
459
- .optional()
460
- .describe("Zero-based index. Required for 'replace' and 'remove'. Ignored for 'add'."),
461
- case_study: caseStudy
462
- .optional()
463
- .describe("Case study payload (title, summary ≤200 chars, idealFor, proofPoints[]). Required for 'add' and 'replace'."),
464
- }, async ({ lead_group_id, action, index, case_study }) => {
465
- try {
466
- if ((action === "add" || action === "replace") && !case_study) {
467
- return toolError(`case_study is required for action '${action}'.`);
468
- }
469
- if ((action === "replace" || action === "remove") &&
470
- index === undefined) {
471
- return toolError(`index is required for action '${action}'.`);
472
- }
473
- const current = await fetchCurrentSettings(lead_group_id);
474
- const list = Array.isArray(current.caseStudies)
475
- ? [...current.caseStudies]
476
- : [];
477
- if (action === "add") {
478
- if (list.length >= 5) {
479
- return toolError("Cannot add — case studies list is at cap (5). Use 'replace' or 'remove' first.");
480
- }
481
- list.push(case_study);
482
- }
483
- else if (action === "replace") {
484
- if (index < 0 || index >= list.length) {
485
- return toolError(`Index ${index} out of bounds (list has ${list.length} entries).`);
486
- }
487
- list[index] = case_study;
488
- }
489
- else {
490
- if (index < 0 || index >= list.length) {
491
- return toolError(`Index ${index} out of bounds (list has ${list.length} entries).`);
492
- }
493
- list.splice(index, 1);
494
- }
495
- const data = await putPartial(lead_group_id, { caseStudies: list });
496
- return toolResult(data);
497
- }
498
- catch (error) {
499
- return handleToolError(error);
500
- }
501
- });
502
- // ── update_outreach_urls ───────────────────────────────────────────────
503
- server.tool("update_outreach_urls", "Patch bookingUrl and/or websiteUrl only. Empty string or null clears a " +
504
- "field. Other outreach settings are untouched.", {
505
- lead_group_id: z.string().describe("Lead group ID."),
506
- booking_url: urlValue.describe("Booking URL (e.g. Cal.com). Empty string or null clears it."),
507
- website_url: urlValue.describe("Website URL. Empty string or null clears it."),
508
- }, async ({ lead_group_id, booking_url, website_url }) => {
509
- try {
510
- if (booking_url === undefined && website_url === undefined) {
511
- return toolError("Provide at least one of: booking_url, website_url.");
512
- }
513
- const body = {};
514
- if (booking_url !== undefined)
515
- body.bookingUrl = booking_url;
516
- if (website_url !== undefined)
517
- body.websiteUrl = website_url;
518
- const data = await putPartial(lead_group_id, body);
519
- return toolResult(data);
520
- }
521
- catch (error) {
522
- return handleToolError(error);
523
- }
524
- });
525
- // ── set_outreach_connection_request ────────────────────────────────────
526
- server.tool("set_outreach_connection_request", "Toggle whether the first LinkedIn touchpoint is a connection request " +
527
- "(true) or a cold direct message (false). When false, card_linkedin_one " +
528
- "is written as a cold first-contact message and the update_lead handler " +
529
- "rejects any write to card_connexion.", {
530
- lead_group_id: z.string().describe("Lead group ID."),
531
- enabled: z
532
- .boolean()
533
- .describe("true = connection request first; false = cold DM."),
534
- }, async ({ lead_group_id, enabled }) => {
535
- try {
536
- const data = await putPartial(lead_group_id, {
537
- connectionRequestEnabled: enabled,
538
- });
539
- return toolResult(data);
540
- }
541
- catch (error) {
542
- return handleToolError(error);
543
- }
544
- });
545
- // ═══════════════════════════════════════════════════════════════════════
546
- // Granular tools for persona-migrated fields (PRD-737)
547
- // ═══════════════════════════════════════════════════════════════════════
548
- // ── update_outreach_agent_identity ─────────────────────────────────────
549
- server.tool("update_outreach_agent_identity", "Patch the agent identity fields (description, outputLanguage, " +
550
- "agentDomain). Reads current settings, merges your patch, writes back. " +
551
- "Pass only the fields you want to change.", {
552
- lead_group_id: z.string().describe("Lead group ID."),
553
- description: z
554
- .string()
555
- .optional()
556
- .describe("Description / context for this lead group's outreach."),
557
- output_language: z
558
- .string()
559
- .optional()
560
- .describe("Output language code (fr, en, de, ...)."),
561
- agent_domain: z
562
- .string()
563
- .optional()
564
- .describe("Agent identity string (e.g. 'for medtech companies')."),
565
- }, async ({ lead_group_id, description, output_language, agent_domain }) => {
566
- try {
567
- if (description === undefined &&
568
- output_language === undefined &&
569
- agent_domain === undefined) {
570
- return toolError("Provide at least one of: description, output_language, agent_domain.");
571
- }
572
- const current = await fetchCurrentSettings(lead_group_id);
573
- const body = {};
574
- if (description !== undefined)
575
- body.description = description;
576
- if (output_language !== undefined)
577
- body.outputLanguage = output_language;
578
- if (agent_domain !== undefined)
579
- body.agentDomain = agent_domain;
580
- const data = await putPartial(lead_group_id, body);
581
- return toolResult(data);
582
- }
583
- catch (error) {
584
- return handleToolError(error);
585
- }
586
- });
587
- // ── update_outreach_icp_strategy ──────────────────────────────────────
588
- server.tool("update_outreach_icp_strategy", "Patch the ICP strategy mental map for the outreach agent. Reads " +
589
- "current icpStrategy, shallow-merges the keys you pass, writes back. " +
590
- "Arrays (mainPains/keyObjections/decisionDrivers) are replaced " +
591
- "wholesale when passed.", {
592
- lead_group_id: z.string().describe("Lead group ID."),
593
- deal_type: z
594
- .string()
595
- .optional()
596
- .describe("Type of deal (e.g. 'midmarket', 'enterprise', 'self-serve')."),
597
- dream_outcome: z
598
- .string()
599
- .optional()
600
- .describe("The aspirational result the lead wants."),
601
- main_pains: z
602
- .array(z.string())
603
- .optional()
604
- .describe("Replaces mainPains[] wholesale."),
605
- key_objections: z
606
- .array(z.string())
607
- .optional()
608
- .describe("Replaces keyObjections[] wholesale."),
609
- decision_drivers: z
610
- .array(z.string())
611
- .optional()
612
- .describe("Replaces decisionDrivers[] wholesale."),
613
- }, async ({ lead_group_id, deal_type, dream_outcome, main_pains, key_objections, decision_drivers, }) => {
614
- try {
615
- if (deal_type === undefined &&
616
- dream_outcome === undefined &&
617
- main_pains === undefined &&
618
- key_objections === undefined &&
619
- decision_drivers === undefined) {
620
- return toolError("Provide at least one field to update.");
621
- }
622
- const current = await fetchCurrentSettings(lead_group_id);
623
- const merged = {
624
- ...(current.icpStrategy ?? {}),
625
- ...(deal_type !== undefined && { dealType: deal_type }),
626
- ...(dream_outcome !== undefined && { dreamOutcome: dream_outcome }),
627
- ...(main_pains !== undefined && { mainPains: main_pains }),
628
- ...(key_objections !== undefined && { keyObjections: key_objections }),
629
- ...(decision_drivers !== undefined && { decisionDrivers: decision_drivers }),
630
- };
631
- const data = await putPartial(lead_group_id, { icpStrategy: merged });
632
- return toolResult(data);
633
- }
634
- catch (error) {
635
- return handleToolError(error);
636
- }
637
- });
638
- // ── update_outreach_pain_point ────────────────────────────────────────
639
- server.tool("update_outreach_pain_point", "Add, replace, or remove a single pain point without re-sending the " +
640
- "whole list. Pain points have no stable id — entries are addressed by " +
641
- "zero-based index. Beware: indices shift after a remove.", {
642
- lead_group_id: z.string().describe("Lead group ID."),
643
- action: z
644
- .enum(["add", "replace", "remove"])
645
- .describe("add = append; replace = overwrite at index; remove = delete at index."),
646
- index: z
647
- .number()
648
- .int()
649
- .nonnegative()
650
- .optional()
651
- .describe("Zero-based index. Required for 'replace' and 'remove'."),
652
- pain: z
653
- .object({
654
- title: z.string().optional(),
655
- description: z.string().optional(),
656
- })
657
- .optional()
658
- .describe("Pain point payload {title?, description?}. Required for 'add' and 'replace'."),
659
- }, async ({ lead_group_id, action, index, pain }) => {
660
- try {
661
- if ((action === "add" || action === "replace") && !pain) {
662
- return toolError(`pain is required for action '${action}'.`);
663
- }
664
- if ((action === "replace" || action === "remove") && index === undefined) {
665
- return toolError(`index is required for action '${action}'.`);
666
- }
667
- const current = await fetchCurrentSettings(lead_group_id);
668
- const list = Array.isArray(current.painPoints)
669
- ? [...current.painPoints]
670
- : [];
671
- if (action === "add") {
672
- list.push(pain);
673
- }
674
- else if (action === "replace") {
675
- if (index < 0 || index >= list.length) {
676
- return toolError(`Index ${index} out of bounds (list has ${list.length} entries).`);
677
- }
678
- list[index] = pain;
679
- }
680
- else {
681
- if (index < 0 || index >= list.length) {
682
- return toolError(`Index ${index} out of bounds (list has ${list.length} entries).`);
683
- }
684
- list.splice(index, 1);
685
- }
686
- const data = await putPartial(lead_group_id, { painPoints: list });
687
- return toolResult(data);
688
- }
689
- catch (error) {
690
- return handleToolError(error);
691
- }
692
- });
693
- // ── update_outreach_lookalike_client ──────────────────────────────────
694
- server.tool("update_outreach_lookalike_client", "Add, replace, or remove a single lookalike client without re-sending " +
695
- "the whole list. Lookups are by client name (case-sensitive exact match).", {
696
- lead_group_id: z.string().describe("Lead group ID."),
697
- action: z
698
- .enum(["add", "replace", "remove"])
699
- .describe("add = append (fails if name exists); replace = overwrite by name; remove = delete by name."),
700
- name: z
701
- .string()
702
- .optional()
703
- .describe("Lookup key for replace/remove. Required for those actions."),
704
- client: z
705
- .object({
706
- name: z.string(),
707
- sector: z.string().optional(),
708
- size: z.string().optional(),
709
- segment: z.string().optional(),
710
- comparison_criteria: z.string().optional(),
711
- })
712
- .passthrough()
713
- .optional()
714
- .describe("Client payload {name, sector?, size?, segment?, comparison_criteria?}. Required for 'add' and 'replace'."),
715
- }, async ({ lead_group_id, action, name, client }) => {
716
- try {
717
- if ((action === "add" || action === "replace") && !client) {
718
- return toolError(`client is required for action '${action}'.`);
719
- }
720
- if (action !== "add" && !name) {
721
- return toolError(`name is required for action '${action}'.`);
722
- }
723
- const current = await fetchCurrentSettings(lead_group_id);
724
- const list = Array.isArray(current.lookalikeClients)
725
- ? [...current.lookalikeClients]
726
- : [];
727
- if (action === "add") {
728
- const exists = list.some((c) => c.name === client.name);
729
- if (exists) {
730
- return toolError(`Lookalike client '${client.name}' already exists. Use 'replace' to update.`);
731
- }
732
- list.push(client);
733
- }
734
- else if (action === "replace") {
735
- const idx = list.findIndex((c) => c.name === name);
736
- if (idx === -1) {
737
- return toolError(`Lookalike client '${name}' not found.`);
738
- }
739
- list[idx] = client;
740
- }
741
- else {
742
- const idx = list.findIndex((c) => c.name === name);
743
- if (idx === -1) {
744
- return toolError(`Lookalike client '${name}' not found.`);
745
- }
746
- list.splice(idx, 1);
747
- }
748
- const data = await putPartial(lead_group_id, { lookalikeClients: list });
749
- return toolResult(data);
750
- }
751
- catch (error) {
752
- return handleToolError(error);
753
- }
754
- });
755
- // ── update_outreach_outreach_template ─────────────────────────────────
756
- server.tool("update_outreach_outreach_template", "Add, replace, or remove a single outreach template without re-sending " +
757
- "the whole list. Lookups are by template id (preferred) or by " +
758
- "composite key (channel + signal + fieldName).", {
759
- lead_group_id: z.string().describe("Lead group ID."),
760
- action: z
761
- .enum(["add", "replace", "remove"])
762
- .describe("add = append; replace = overwrite by lookup; remove = delete by lookup."),
763
- template_id: z
764
- .string()
765
- .optional()
766
- .describe("Template id for replace/remove. Preferred lookup key."),
767
- lookup_channel: z
768
- .string()
769
- .optional()
770
- .describe("Fallback lookup: channel (combined with lookup_signal + lookup_field_name)."),
771
- lookup_signal: z
772
- .string()
773
- .optional()
774
- .describe("Fallback lookup: signal."),
775
- lookup_field_name: z
776
- .string()
777
- .optional()
778
- .describe("Fallback lookup: field_name."),
779
- template: outreachTemplate
780
- .optional()
781
- .describe("Template payload. Required for 'add' and 'replace'. " +
782
- "{channel, signal, fieldName, purpose, example, subject, body}."),
783
- }, async ({ lead_group_id, action, template_id, lookup_channel, lookup_signal, lookup_field_name, template: tmpl, }) => {
784
- try {
785
- if ((action === "add" || action === "replace") && !tmpl) {
786
- return toolError(`template is required for action '${action}'.`);
787
- }
788
- const current = await fetchCurrentSettings(lead_group_id);
789
- const list = Array.isArray(current.outreachTemplates)
790
- ? [...current.outreachTemplates]
791
- : [];
792
- if (action === "add") {
793
- list.push(tmpl);
794
- }
795
- else {
796
- // Resolve lookup key
797
- let idx = -1;
798
- if (template_id) {
799
- idx = list.findIndex((t) => t.id === template_id);
800
- }
801
- else if (lookup_channel && lookup_signal && lookup_field_name) {
802
- idx = list.findIndex((t) => t.channel === lookup_channel &&
803
- t.signal === lookup_signal &&
804
- t.fieldName === lookup_field_name);
805
- }
806
- else {
807
- return toolError("Provide template_id or (lookup_channel + lookup_signal + lookup_field_name) for replace/remove.");
808
- }
809
- if (idx === -1) {
810
- return toolError("Template not found with the given lookup key.");
811
- }
812
- if (action === "replace") {
813
- list[idx] = tmpl;
814
- }
815
- else {
816
- list.splice(idx, 1);
817
- }
818
- }
819
- const data = await putPartial(lead_group_id, { outreachTemplates: list });
820
- return toolResult(data);
821
- }
822
- catch (error) {
823
- return handleToolError(error);
824
- }
825
- });
826
- // ── update_outreach_tone_instructions ──────────────────────────────────
827
- server.tool("update_outreach_tone_instructions", "Replace the tone instructions (free-form markdown for voice / tone " +
828
- "guidance). Pass null to clear.", {
829
- lead_group_id: z.string().describe("Lead group ID."),
830
- tone_instructions: z
831
- .union([z.string(), z.null()])
832
- .describe("Markdown tone guidance. Pass null to clear."),
833
- }, async ({ lead_group_id, tone_instructions }) => {
834
- try {
835
- const data = await putPartial(lead_group_id, { toneInstructions: tone_instructions });
836
- return toolResult(data);
837
- }
838
- catch (error) {
839
- return handleToolError(error);
840
- }
841
- });
842
- // ── update_outreach_messaging_guidelines ──────────────────────────────
843
- server.tool("update_outreach_messaging_guidelines", "Patch the messaging guidelines. Reads current messagingGuidelines, " +
844
- "shallow-merges the keys you pass, writes back. Common keys: tone, " +
845
- "language, can_mention, must_not_mention, value_propositions.", {
846
- lead_group_id: z.string().describe("Lead group ID."),
847
- patch: z
848
- .any()
849
- .describe("Object whose keys are merged into existing messagingGuidelines (shallow). " +
850
- "E.g. {\"tone\": \"professional\", \"can_mention\": [\"AI\", \"ROI\"]}."),
851
- remove_keys: z
852
- .array(z.string())
853
- .optional()
854
- .describe("Keys to delete from messagingGuidelines."),
855
- }, async ({ lead_group_id, patch, remove_keys }) => {
856
- try {
857
- const current = await fetchCurrentSettings(lead_group_id);
858
- const existing = (current.messagingGuidelines ?? {});
859
- const merged = { ...existing, ...patch };
860
- if (remove_keys) {
861
- for (const k of remove_keys)
862
- delete merged[k];
863
- }
864
- const data = await putPartial(lead_group_id, { messagingGuidelines: merged });
865
- return toolResult(data);
866
- }
867
- catch (error) {
868
- return handleToolError(error);
869
- }
870
- });
871
- // ── update_outreach_signal_routing ────────────────────────────────────
872
- server.tool("update_outreach_signal_routing", "Patch the outreach signal routing config. signalSourceField and " +
873
- "defaultSignal are scalar — passing one replaces it. signals[] is " +
874
- "replaced wholesale when passed; use add_signals / remove_signals to " +
875
- "splice instead. WARNING: defaultSignal must be present in signals[] " +
876
- "(API rejects otherwise).", {
877
- lead_group_id: z.string().describe("Lead group ID."),
878
- signal_source_field: z
879
- .string()
880
- .optional()
881
- .describe("Card field to read (e.g. card_signals)."),
882
- default_signal: z
883
- .string()
884
- .optional()
885
- .describe("Default signal — must exist in signals[]."),
886
- signals: z
887
- .array(z.string())
888
- .optional()
889
- .describe("Full replacement of the signals list. Mutually exclusive with add_signals / remove_signals."),
890
- add_signals: z
891
- .array(z.string())
892
- .optional()
893
- .describe("Signals to add (deduped). Mutually exclusive with signals."),
894
- remove_signals: z
895
- .array(z.string())
896
- .optional()
897
- .describe("Signals to remove. Mutually exclusive with signals."),
898
- }, async ({ lead_group_id, signal_source_field, default_signal, signals, add_signals, remove_signals, }) => {
899
- try {
900
- const current = await fetchCurrentSettings(lead_group_id);
901
- const existing = (current.outreachSignalRouting ?? {});
902
- const next = { ...existing };
903
- if (signal_source_field !== undefined)
904
- next.signalSourceField = signal_source_field;
905
- if (default_signal !== undefined)
906
- next.defaultSignal = default_signal;
907
- if (signals !== undefined) {
908
- next.signals = signals;
909
- }
910
- else {
911
- const currentSignals = existing.signals ?? [];
912
- let updatedSignals = [...currentSignals];
913
- if (add_signals) {
914
- for (const s of add_signals) {
915
- if (!updatedSignals.includes(s))
916
- updatedSignals.push(s);
917
- }
918
- }
919
- if (remove_signals) {
920
- updatedSignals = updatedSignals.filter((s) => !remove_signals.includes(s));
921
- }
922
- if (add_signals || remove_signals)
923
- next.signals = updatedSignals;
924
- }
925
- const data = await putPartial(lead_group_id, { outreachSignalRouting: next });
926
- return toolResult(data);
927
- }
928
- catch (error) {
929
- return handleToolError(error);
930
- }
931
- });
932
- // ── update_outreach_sender_override ───────────────────────────────────
933
- server.tool("update_outreach_sender_override", "Set or clear the SDR identity override for this lead group. " +
934
- "Pass {name, title?} to set. Pass null to clear.", {
935
- lead_group_id: z.string().describe("Lead group ID."),
936
- sender_override: z
937
- .union([
938
- z.object({ name: z.string().min(1), title: z.string().optional() }),
939
- z.null(),
940
- ])
941
- .describe("SDR identity: {name, title?}. Pass null to clear."),
942
- }, async ({ lead_group_id, sender_override }) => {
943
- try {
944
- const data = await putPartial(lead_group_id, { senderOverride: sender_override });
945
- return toolResult(data);
946
- }
947
- catch (error) {
948
- return handleToolError(error);
949
- }
950
- });
951
- // ── update_outreach_extra_prompt_phase ────────────────────────────────
952
- server.tool("update_outreach_extra_prompt_phase", "Add, replace, or remove a single extra prompt phase snippet. " +
953
- "Entries have no stable id — addressed by zero-based index. " +
954
- "Beware: indices shift after a remove.", {
955
- lead_group_id: z.string().describe("Lead group ID."),
956
- action: z
957
- .enum(["add", "replace", "remove"])
958
- .describe("add = append; replace = overwrite at index; remove = delete at index."),
959
- index: z
960
- .number()
961
- .int()
962
- .nonnegative()
963
- .optional()
964
- .describe("Zero-based index. Required for 'replace' and 'remove'."),
965
- phase: extraPromptPhase
966
- .optional()
967
- .describe("Phase payload {scope, insertionPoint, content}. " +
968
- "scope: qualify|outreach. insertionPoint: prefix|phase_1_5|phase_2_extra|suffix. " +
969
- "Required for 'add' and 'replace'."),
970
- }, async ({ lead_group_id, action, index, phase }) => {
971
- try {
972
- if ((action === "add" || action === "replace") && !phase) {
973
- return toolError(`phase is required for action '${action}'.`);
974
- }
975
- if ((action === "replace" || action === "remove") && index === undefined) {
976
- return toolError(`index is required for action '${action}'.`);
977
- }
978
- const current = await fetchCurrentSettings(lead_group_id);
979
- const list = Array.isArray(current.extraPromptPhases)
980
- ? [...current.extraPromptPhases]
981
- : [];
982
- if (action === "add") {
983
- list.push(phase);
984
- }
985
- else if (action === "replace") {
986
- if (index < 0 || index >= list.length) {
987
- return toolError(`Index ${index} out of bounds (list has ${list.length} entries).`);
988
- }
989
- list[index] = phase;
990
- }
991
- else {
992
- if (index < 0 || index >= list.length) {
993
- return toolError(`Index ${index} out of bounds (list has ${list.length} entries).`);
994
- }
995
- list.splice(index, 1);
996
- }
997
- const data = await putPartial(lead_group_id, { extraPromptPhases: list });
998
- return toolResult(data);
999
- }
1000
- catch (error) {
1001
- return handleToolError(error);
1002
- }
1003
- });
1004
- }