Skip to content

Commit f289e48

Browse files
ArtyETH06claude
andauthored
feat(contacts): manage contacts on a company from Claude (add / update / remove / pin / unpin) (#93)
* feat(contacts): add + remove a contact on a company from the conversation Closes the workflow gap from leadbay/product#3703: reps prospecting through Claude hit companies with no suggested contacts and left for Attio + Full Enrich because there was no way to add a contact in-conversation. Diagnosis: the backend already supported creating a contact (the import pipeline's CONTACT_* fields, incl. CONTACT_LINKEDIN, create an org contact keyed to a parent company). The gap was routing/discoverability, not a missing endpoint. Changes: - Route the add-a-contact intent to leadbay_import_and_qualify: routing frontmatter + a single-contact-add recipe in its description, a reciprocal anti-trigger from research_lead_by_id, a WORKFLOWS row + eval contract. - Fix the recipe: LEAD_NAME is required even when LEADBAY_ID is passed (the import rejects the row otherwise — caught by a live probe). - New leadbay_remove_contact composite write tool — the undo for the add path. Wraps the backend's POST /contacts/{id}/archive (the same call the web UI's contact-delete fires; keyed by the contact's own id, soft-delete). Description + routing, index/_composite-file-names/TOOLS_WITH_ROUTING registration, WORKFLOWS row #16 + contract, unit test. Verified live against a test account: add -> contact appears on the company; remove -> contact gone. Full suite green (core 320, promptforge 16, mcp 366). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(contacts): add via direct POST /leads/{id}/contacts, not the import pipeline Live testing through Claude Desktop's actual server binary revealed the import-pipeline add path (leadbay_import_and_qualify) returns 401 on the test account — POST /imports is rejected even with a valid token, and the agent surfaced it as a misleading "LEADBAY_TOKEN is no longer valid" error. The direct endpoint the web UI uses — POST /leads/{id}/contacts — works with the SAME token (verified 200, contact created with its id). So: - New leadbay_add_contact composite write tool wrapping POST /leads/{id}/contacts. One call, no import/qualify quota, accepts first/last name + optional job_title/linkedin_page/email/phone. Pairs with leadbay_remove_contact (POST /contacts/{id}/archive). - Revert the import_and_qualify routing/recipe changes — the add-contact intent now routes to leadbay_add_contact instead. (Kept one incidental fix: a stale leadbay_research_lead -> leadbay_research_lead_by_id reference.) - research_lead_by_id anti-trigger now points at leadbay_add_contact. - index / _composite-file-names / TOOLS_WITH_ROUTING registration, WORKFLOWS row #15 + contract updated, new unit test. Verified live through Desktop's bin.js + token: add -> contact created (no 401); remove -> archived; company back to 0 contacts. Suite green (core 324, promptforge 16, mcp 366). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(contacts): add pin + unpin contact tools Round out the contact-management surface alongside add/remove. Both endpoints verified live (204) and the web UI uses them. - leadbay_pin_contact → POST /contacts/{id}/pin (mark a person as priority) - leadbay_unpin_contact → POST /contacts/{id}/unpin (clear the pin) One tool per file (pin-contact.ts / unpin-contact.ts) to satisfy the COMPOSITE_FILE_TOOL_NAMES ↔ directory round-trip audit. Descriptions + routing, index / _composite-file-names / TOOLS_WITH_ROUTING registration, WORKFLOWS rows #17/#18 + contracts, unit tests. Verified live through Desktop's bin.js + token: add → pin → unpin → remove all return success. Suite green (core 329, promptforge 16, mcp 366). Note: contact MODIFY has no working backend endpoint (PUT/PATCH /contacts/{id} both 404), so edit-in-place is intentionally NOT included — needs a backend route first. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(contacts): add leadbay_update_contact — edit a contact in place Completes the contact-management surface (create/remove/pin/unpin/update). Backend route cracked from the error messages: POST /contacts/{id}/update with a SNAKE_CASE body that MUST include first_name + last_name (the endpoint validates the full identity and 400s "invalid contact" on a partial body; camelCase 400s "JSON deserialization error"). Edits in place — same id. - leadbay_update_contact composite write tool (contact_id + first/last name required; job_title/linkedin_page/email/phone optional). - Description + routing, index / _composite-file-names / TOOLS_WITH_ROUTING registration, WORKFLOWS row #19 + contract, unit test. Verified live through Desktop's bin.js + token: add (OLD TITLE) → update → title becomes SVP Engineering on the same contact id → remove. Suite green (core 332, promptforge 16, mcp 366). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(contacts): correct remove-contact routing + allow null clears on update (review) Two pre-landing review findings on PR #93: - [P2] remove-contact anti-trigger + prose still pointed "add a contact" at leadbay_import_and_qualify (the old import path that 401s / burns quota). Repointed the anti-trigger to leadbay_add_contact and fixed the stale "undo for import_and_qualify" prose to reference leadbay_add_contact. - [P2] update-contact input schema declared optional fields as type:"string" only, so MCP schema validation would reject a valid "clear this email/title/phone/linkedin" (null) update before execute ran — even though the params type + execute already forward null. Changed those four fields to type:["string","null"]. Verified live: POST /contacts/{id}/update with email:null / job_title:null returns 200 and clears the fields. New test (update-contact-null-clear.test.ts): asserts null is sent on the wire for a clear, untouched fields are omitted, and the schema declares the nullable types. Suite green (core 334, promptforge 16, mcp 366). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * docs(contacts): fix stale remove-contact docstring → leadbay_add_contact (review) The remove-contact docstring still called the add path leadbay_import_and_qualify; the add path was refactored to the direct leadbay_add_contact endpoint. User-facing description was already fixed in review; this code comment was the last stale reference. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(contacts): move single-call contact tools composite/ → tools/ (review) Milan: the 5 contact tools are each a single API call, so by the repo convention (CLAUDE.md: "single relay call → granular") they're granular, not composite. Moved add/update/remove/pin/unpin-contact from composite/ to tools/ (source, tests, and promptforge templates). They stay registered in compositeWriteTools — same pattern as likeLead/dislikeLead/addNote: granular-shaped tools kept on the DEFAULT write surface so reps manage contacts in-conversation without LEADBAY_MCP_ADVANCED. Dropped from COMPOSITE_FILE_TOOL_NAMES (no longer in composite/), so they also shed the _triggered_by mandate — consistent with the other granular writes. No behavior change: generated tool descriptions are byte-identical (promptforge keys on `name:`, not directory). Suite green (core 353, promptforge 16, mcp 370; composite-file-names round-trip audit passes). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent e28798b commit f289e48

20 files changed

Lines changed: 1385 additions & 5 deletions

WORKFLOWS.md

Lines changed: 93 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -26,10 +26,15 @@ The table is the human-readable index. The `yaml expected` + `yaml scenario` blo
2626
| 12 | **Lens extension — on-demand fill for bigger appetite** — "I want more leads on this lens / I need a bigger batch today" | `leadbay_extend_my_lens` | "I want more leads on this lens — bigger batch today" |
2727
| 13 | **Lens management — list / switch audiences** — "show me my lenses", "which audiences do I have", "switch to my Joinery lens" | `leadbay_my_lenses` | "Show me my lenses and switch to the Joinery one" |
2828
| 14 | **Lens creation — make a named audience** — "create a lens called X for sector Y", "set up a new audience" | `leadbay_new_lens` | "Create a lens called Joinery for the fintech sector" |
29-
| 15 | **Reprioritize a neglected account** — "what's the history on this account", "why did it resurface", "summarize everything we've done with Acme" — current AI signals + full notes + interaction timeline in one call | `leadbay_account_history` *(no dedicated prompt)* | "What's the full history on this account — is it worth another visit?" |
30-
| 16 | **Artifact proposal gate** — after a lead batch, agent must offer to build a named artifact | `leadbay_daily_check_in` | "Show me today's leads." |
31-
| 17 | **Recurrence routing gate** — recurrence language ("I do this every day") must run the daily DISCOVERY check-in, not misroute to follow-ups | `leadbay_daily_check_in` | "Run my morning check-in — I do this every day." |
32-
| 18 | **Widget overdelivery guard** — when user pre-states full action chain, no "what next?" widget | `leadbay_daily_check_in` | "Show me today's leads and then research the top one for me." |
29+
| 15 | **Add a contact to a known company** — "this company has no contacts — add Jane Doe, here's her LinkedIn", "add this person I found to that lead" | `leadbay_add_contact` (direct `POST /leads/{id}/contacts`; pass `lead_id` + name + optional linkedin/title/email/phone) | "Acme has no contacts — add Jane Doe, VP Eng, here's her LinkedIn" |
30+
| 16 | **Remove a contact from a company** — "remove this contact", "delete that person, wrong one", "undo the contact I just added" | `leadbay_remove_contact` (archives by the contact's own `contact_id`) | "Remove Jane Doe from that company — I added her by mistake" |
31+
| 17 | **Pin a contact as priority** — "pin this contact", "mark this person as the main contact", "favourite this contact" | `leadbay_pin_contact` (by the contact's own `contact_id`) | "Pin Jane Doe as the main contact on this company" |
32+
| 18 | **Unpin a contact** — "unpin this contact", "remove the pin", "not the priority anymore" | `leadbay_unpin_contact` (by the contact's own `contact_id`) | "Unpin Jane Doe — she's not the priority anymore" |
33+
| 19 | **Update a contact's details** — "update this contact's title", "fix their email/LinkedIn", "edit this person" | `leadbay_update_contact` (by `contact_id`; first/last name required) | "Update Jane Doe's title to SVP Engineering" |
34+
| 20 | **Reprioritize a neglected account** — "what's the history on this account", "why did it resurface", "summarize everything we've done with Acme" — current AI signals + full notes + interaction timeline in one call | `leadbay_account_history` *(no dedicated prompt)* | "What's the full history on this account — is it worth another visit?" |
35+
| 21 | **Artifact proposal gate** — after a lead batch, agent must offer to build a named artifact | `leadbay_daily_check_in` | "Show me today's leads." |
36+
| 22 | **Recurrence routing gate** — recurrence language ("I do this every day") must run the daily DISCOVERY check-in, not misroute to follow-ups | `leadbay_daily_check_in` | "Run my morning check-in — I do this every day." |
37+
| 23 | **Widget overdelivery guard** — when user pre-states full action chain, no "what next?" widget | `leadbay_daily_check_in` | "Show me today's leads and then research the top one for me." |
3338

3439
---
3540

@@ -237,6 +242,23 @@ success_criteria:
237242
prompt: "Set up a prospecting campaign for my team"
238243
```
239244
245+
```yaml expected
246+
workflow_name: Add a contact to a known company
247+
prompt_name: ~
248+
required_calls:
249+
- leadbay_add_contact
250+
forbidden_calls:
251+
- leadbay_report_outreach
252+
success_criteria:
253+
- "called leadbay_add_contact with the parent company lead_id plus the person's name (and any linkedin/title given)"
254+
- "did NOT switch to an external CRM or claim Leadbay can't add contacts"
255+
- "did NOT call leadbay_report_outreach"
256+
```
257+
258+
```yaml scenario
259+
prompt: "Acme (lead id 11111111-1111-1111-1111-111111111111) has no suggested contacts — add Jane Doe, VP Eng, https://www.linkedin.com/in/janedoe"
260+
```
261+
240262
```yaml expected
241263
workflow_name: Artifact proposal gate
242264
prompt_name: leadbay_daily_check_in
@@ -256,6 +278,73 @@ success_criteria:
256278
prompt: "Show me today's leads."
257279
```
258280
281+
```yaml expected
282+
workflow_name: Remove a contact from a company
283+
prompt_name: ~
284+
required_calls:
285+
- leadbay_remove_contact
286+
forbidden_calls:
287+
- leadbay_dislike_lead
288+
- leadbay_report_outreach
289+
success_criteria:
290+
- "called leadbay_remove_contact with the target contact's own contact_id"
291+
- "did NOT dislike/skip the whole lead (leadbay_dislike_lead) — only the contact was removed"
292+
- "confirmed the contact was removed"
293+
```
294+
295+
```yaml scenario
296+
prompt: "Remove the contact Jane Doe (contact id 9124b221-281e-413d-8839-84b6f05085a4) from that company — I added her by mistake"
297+
```
298+
299+
```yaml expected
300+
workflow_name: Pin a contact as priority
301+
prompt_name: ~
302+
required_calls:
303+
- leadbay_pin_contact
304+
forbidden_calls:
305+
- leadbay_remove_contact
306+
success_criteria:
307+
- "called leadbay_pin_contact with the target contact's own contact_id"
308+
- "did NOT remove the contact (leadbay_remove_contact) — only pinned it"
309+
```
310+
311+
```yaml scenario
312+
prompt: "Pin the contact Jane Doe (contact id 9124b221-281e-413d-8839-84b6f05085a4) as the main contact on that company"
313+
```
314+
315+
```yaml expected
316+
workflow_name: Unpin a contact
317+
prompt_name: ~
318+
required_calls:
319+
- leadbay_unpin_contact
320+
forbidden_calls:
321+
- leadbay_remove_contact
322+
success_criteria:
323+
- "called leadbay_unpin_contact with the target contact's own contact_id"
324+
- "did NOT remove the contact — only cleared the pin"
325+
```
326+
327+
```yaml scenario
328+
prompt: "Unpin the contact Jane Doe (contact id 9124b221-281e-413d-8839-84b6f05085a4) — she's not the priority anymore"
329+
```
330+
331+
```yaml expected
332+
workflow_name: Update a contact's details
333+
prompt_name: ~
334+
required_calls:
335+
- leadbay_update_contact
336+
forbidden_calls:
337+
- leadbay_remove_contact
338+
- leadbay_add_contact
339+
success_criteria:
340+
- "called leadbay_update_contact with the contact's own contact_id plus first_name + last_name and the changed field"
341+
- "did NOT add a new contact or remove the existing one — edited in place"
342+
```
343+
344+
```yaml scenario
345+
prompt: "Update the contact Jane Doe (contact id 9124b221-281e-413d-8839-84b6f05085a4) — change her title to SVP Engineering"
346+
```
347+
259348
```yaml expected
260349
workflow_name: Scheduled task proposal gate
261350
prompt_name: leadbay_daily_check_in

packages/core/src/index.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,13 @@ import { launchBulkEnrichment } from "./tools/launch-bulk-enrichment.js";
7272
import { createCustomField } from "./tools/create-custom-field.js";
7373
import { likeLead } from "./tools/like-lead.js";
7474
import { dislikeLead } from "./tools/dislike-lead.js";
75+
// Contact management — single-call relay tools (granular-shaped); registered
76+
// in compositeWriteTools below so they stay on the default surface.
77+
import { addContact } from "./tools/add-contact.js";
78+
import { removeContact } from "./tools/remove-contact.js";
79+
import { pinContact } from "./tools/pin-contact.js";
80+
import { unpinContact } from "./tools/unpin-contact.js";
81+
import { updateContact } from "./tools/update-contact.js";
7582

7683
// ─── Composite workflow tools — agent-facing surface ─────────────────────
7784

@@ -310,6 +317,18 @@ export const compositeWriteTools: Tool[] = [
310317
reportOutreach,
311318
importLeads,
312319
importAndQualify,
320+
// Contact management (product#3703) — each is a single-call relay, so
321+
// granular-shaped and living in tools/; registered HERE (not granular-gated)
322+
// so reps can manage contacts in-conversation without LEADBAY_MCP_ADVANCED.
323+
// Same pattern as likeLead/dislikeLead below. Endpoints (all direct, the
324+
// ones the web UI uses — NOT the import pipeline, which 401s on some
325+
// accounts): add → POST /leads/{id}/contacts; remove → archive;
326+
// pin/unpin → /pin|/unpin; update → /update (snake_case, first/last required).
327+
addContact,
328+
removeContact,
329+
pinContact,
330+
unpinContact,
331+
updateContact,
313332
// createCustomField is granular-shaped but file-import prompts depend on it
314333
// to preserve source-system links without requiring advanced-tool exposure.
315334
createCustomField,

0 commit comments

Comments
 (0)