Commit f289e48
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
File tree
- packages
- core
- src
- tools
- test/unit/tools
- mcp/test/audit
- promptforge/tool-descriptions
- composite
- granular
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
26 | 26 | | |
27 | 27 | | |
28 | 28 | | |
29 | | - | |
30 | | - | |
31 | | - | |
32 | | - | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
33 | 38 | | |
34 | 39 | | |
35 | 40 | | |
| |||
237 | 242 | | |
238 | 243 | | |
239 | 244 | | |
| 245 | + | |
| 246 | + | |
| 247 | + | |
| 248 | + | |
| 249 | + | |
| 250 | + | |
| 251 | + | |
| 252 | + | |
| 253 | + | |
| 254 | + | |
| 255 | + | |
| 256 | + | |
| 257 | + | |
| 258 | + | |
| 259 | + | |
| 260 | + | |
| 261 | + | |
240 | 262 | | |
241 | 263 | | |
242 | 264 | | |
| |||
256 | 278 | | |
257 | 279 | | |
258 | 280 | | |
| 281 | + | |
| 282 | + | |
| 283 | + | |
| 284 | + | |
| 285 | + | |
| 286 | + | |
| 287 | + | |
| 288 | + | |
| 289 | + | |
| 290 | + | |
| 291 | + | |
| 292 | + | |
| 293 | + | |
| 294 | + | |
| 295 | + | |
| 296 | + | |
| 297 | + | |
| 298 | + | |
| 299 | + | |
| 300 | + | |
| 301 | + | |
| 302 | + | |
| 303 | + | |
| 304 | + | |
| 305 | + | |
| 306 | + | |
| 307 | + | |
| 308 | + | |
| 309 | + | |
| 310 | + | |
| 311 | + | |
| 312 | + | |
| 313 | + | |
| 314 | + | |
| 315 | + | |
| 316 | + | |
| 317 | + | |
| 318 | + | |
| 319 | + | |
| 320 | + | |
| 321 | + | |
| 322 | + | |
| 323 | + | |
| 324 | + | |
| 325 | + | |
| 326 | + | |
| 327 | + | |
| 328 | + | |
| 329 | + | |
| 330 | + | |
| 331 | + | |
| 332 | + | |
| 333 | + | |
| 334 | + | |
| 335 | + | |
| 336 | + | |
| 337 | + | |
| 338 | + | |
| 339 | + | |
| 340 | + | |
| 341 | + | |
| 342 | + | |
| 343 | + | |
| 344 | + | |
| 345 | + | |
| 346 | + | |
| 347 | + | |
259 | 348 | | |
260 | 349 | | |
261 | 350 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
72 | 72 | | |
73 | 73 | | |
74 | 74 | | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
75 | 82 | | |
76 | 83 | | |
77 | 84 | | |
| |||
310 | 317 | | |
311 | 318 | | |
312 | 319 | | |
| 320 | + | |
| 321 | + | |
| 322 | + | |
| 323 | + | |
| 324 | + | |
| 325 | + | |
| 326 | + | |
| 327 | + | |
| 328 | + | |
| 329 | + | |
| 330 | + | |
| 331 | + | |
313 | 332 | | |
314 | 333 | | |
315 | 334 | | |
| |||
0 commit comments