@leadbay/mcp 0.17.2 → 0.18.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.
- package/CHANGELOG.md +24 -0
- package/README.md +38 -1
- package/dist/bin.js +1524 -99
- package/dist/http-server.js +1185 -113
- package/dist/installer-electron.js +145 -200
- package/dist/installer-gui.js +77 -109
- package/package.json +3 -4
package/dist/http-server.js
CHANGED
|
@@ -1756,6 +1756,23 @@ var LeadbayClient = class {
|
|
|
1756
1756
|
await this.resolveOrgId();
|
|
1757
1757
|
await this.resolveTasteProfile();
|
|
1758
1758
|
}
|
|
1759
|
+
// ─── Notifications helpers ────────────────────────────────────────────
|
|
1760
|
+
// Backend exposes `GET /notifications`, `POST /notifications/{id}/seen`,
|
|
1761
|
+
// `POST /notifications/{id}/archive`, plus `GET /ws/ticket?v=1.0` to mint
|
|
1762
|
+
// a one-shot WS URL. See backend/docs/adr/notifications.md for shape.
|
|
1763
|
+
async listNotifications(args = {}) {
|
|
1764
|
+
const params = new URLSearchParams();
|
|
1765
|
+
params.set("archived", String(args.archived ?? false));
|
|
1766
|
+
params.set("page", String(args.page ?? 0));
|
|
1767
|
+
params.set("count", String(args.count ?? 50));
|
|
1768
|
+
return this.request("GET", `/notifications?${params.toString()}`);
|
|
1769
|
+
}
|
|
1770
|
+
async acknowledgeNotification(notificationId, action = "seen") {
|
|
1771
|
+
await this.requestVoid("POST", `/notifications/${notificationId}/${action}`);
|
|
1772
|
+
}
|
|
1773
|
+
async getWsTicket() {
|
|
1774
|
+
return this.request("GET", "/auth/ws?v=1.0");
|
|
1775
|
+
}
|
|
1759
1776
|
makeError(code, message, hint, endpoint, retry_after, http_status) {
|
|
1760
1777
|
const out = { error: true, code, message, hint };
|
|
1761
1778
|
if (endpoint || this._region) {
|
|
@@ -6187,6 +6204,8 @@ var COMPOSITE_FILE_TOOL_NAMES = /* @__PURE__ */ new Set([
|
|
|
6187
6204
|
"leadbay_import_leads",
|
|
6188
6205
|
"leadbay_import_status",
|
|
6189
6206
|
"leadbay_list_campaigns",
|
|
6207
|
+
"leadbay_my_lenses",
|
|
6208
|
+
"leadbay_new_lens",
|
|
6190
6209
|
"leadbay_prepare_outreach",
|
|
6191
6210
|
"leadbay_pull_followups",
|
|
6192
6211
|
"leadbay_pull_leads",
|
|
@@ -6203,6 +6222,9 @@ var COMPOSITE_FILE_TOOL_NAMES = /* @__PURE__ */ new Set([
|
|
|
6203
6222
|
"leadbay_tour_plan"
|
|
6204
6223
|
]);
|
|
6205
6224
|
|
|
6225
|
+
// ../core/dist/notifications/inbox.js
|
|
6226
|
+
var DEFAULT_TTL_MS = 24 * 60 * 60 * 1e3;
|
|
6227
|
+
|
|
6206
6228
|
// ../core/dist/tool-descriptions.generated.js
|
|
6207
6229
|
var leadbay_account_status = `## WHEN TO USE
|
|
6208
6230
|
|
|
@@ -6239,10 +6261,53 @@ Show the user's account state \u2014 admin rights, language, last-active lens, c
|
|
|
6239
6261
|
|
|
6240
6262
|
**After a user tops up, do NOT keep refusing \u2014 RETRY.** If the user signals they topped up / bought credits / added credits, the previous QUOTA_EXCEEDED is invalidated the moment the Stripe webhook lands. RE-CALL \`leadbay_account_status\` to pick up the new state AND retry the originally failed call. The retry itself does not require a successful account_status check first \u2014 a topped-up user has cleared the throttle whether or not your cached snapshot reflects it yet. If the retry hits the wall again, only then re-offer top-up / wait. **A stale quota snapshot is never a reason to gate-keep a topped-up user.**
|
|
6241
6263
|
|
|
6264
|
+
**\`notifications\` block.** The response now includes a top-level \`notifications\` array listing background work the user (or agent) initiated that has since completed (\`bulk_enrich\`, \`bulk_qualify\`, \`import\`). These are signals to revise prior agent outputs the just-finished work might have made stale \u2014 they're NOT a pending-task list for the user. After revising (or confirming nothing is affected), call \`leadbay_acknowledge_notification(notification_id)\`. Full handling protocol below.
|
|
6265
|
+
|
|
6266
|
+
## GATE \u2014 INSPECT \`_meta.notifications\` ON EVERY RESPONSE
|
|
6267
|
+
|
|
6268
|
+
Some Leadbay tool responses include a \`_meta.notifications\` array listing **background work the user (or you) initiated that has since completed**. The three relevant kinds:
|
|
6269
|
+
|
|
6270
|
+
| Kind | What just finished |
|
|
6271
|
+
|---|---|
|
|
6272
|
+
| \`bulk_enrich\` | A bulk contact-enrichment job (emails / phone numbers attached to leads' contacts). |
|
|
6273
|
+
| \`bulk_qualify\` | A bulk lead-qualification job (web-fetch + AI rescore, producing \`ai_agent_lead_score\` and qualification answers). |
|
|
6274
|
+
| \`import\` | A CSV / CRM file import (rows resolved to leads in the user's pipeline). |
|
|
6275
|
+
|
|
6276
|
+
**Your job when you see an entry: revise prior outputs the just-finished work might have made stale.** This is NOT a pending-task list. It's a "your earlier answer used data that has now changed" signal.
|
|
6277
|
+
|
|
6278
|
+
| Kind | Outputs you've produced that may now be stale \u2014 refresh them |
|
|
6279
|
+
|---|---|
|
|
6280
|
+
| \`bulk_enrich\` | Outreach drafts mentioning these leads' contacts; contact lists; recommended-lead lists citing \`contact_count\`; NEXT STEPS that asked the user to wait for emails / phones. Re-fetch via \`leadbay_get_contacts(leadId)\` for the affected leads. |
|
|
6281
|
+
| \`bulk_qualify\` | Lead rankings / shortlists you produced without \`ai_agent_lead_score\`; "today's leads"; followup maps; prepare-outreach picks. Re-pull via \`leadbay_pull_leads\` / \`leadbay_research_lead_by_id\`. |
|
|
6282
|
+
| \`import\` | "Available leads" claims; pulls from the affected lens that ran before the import landed; followup planning that needed the imported set. Re-pull via \`leadbay_pull_leads\` / \`leadbay_pull_followups\`. |
|
|
6283
|
+
|
|
6284
|
+
**After revising (or after confirming no prior output is affected):** call \`leadbay_acknowledge_notification(notification_id)\` so the entry stops resurfacing on every tool response. Ack-and-move-on is correct even when nothing was stale \u2014 that's how the inbox stays focused on what's actually pending.
|
|
6285
|
+
|
|
6286
|
+
**Do NOT** interpret these entries as "things waiting for the user." The user expects you to handle them silently. They are signals to YOU \u2014 agent \u2014 that prior outputs need a refresh.
|
|
6287
|
+
|
|
6288
|
+
**Don't poll.** When you launch a long operation (enrichment / qualification / import), simply continue the conversation \u2014 the next time you call any tool, the completed-work entry will appear in \`_meta.notifications\`. Status tools (\`leadbay_bulk_enrich_status\`, \`leadbay_qualify_status\`, \`leadbay_import_status\`) exist for the case where you want the answer NOW (mid-conversation), not for ambient polling.
|
|
6289
|
+
|
|
6290
|
+
Also surfaced as a top-level \`notifications\` array on \`leadbay_account_status\` \u2014 same shape, same handling.
|
|
6291
|
+
|
|
6292
|
+
|
|
6242
6293
|
WHEN TO USE: at the start of a session to know what the agent can/can't do, after a 429 to explain to the user which resource window was exhausted and when it resets (and to offer the top-up alternative), and after the user signals a top-up so the agent can resume the interrupted workflow.
|
|
6243
6294
|
|
|
6244
6295
|
WHEN NOT TO USE: as a pre-flight gate before bulk ops \u2014 operations themselves return 429; this tool is for context, not gating. And: a recent quota snapshot showing "exhausted" is NOT a reason to refuse a write call when the user has just topped up \u2014 re-call this tool first, then proceed.
|
|
6245
6296
|
`;
|
|
6297
|
+
var leadbay_acknowledge_notification = `Acknowledge a Leadbay notification \u2014 i.e. tell the MCP and the backend "I've seen this and acted on it." Wraps \`POST /1.5/notifications/{id}/seen\` (default) or \`/archive\` (when \`archive:true\`) and drops the entry from the local inbox so subsequent \`_meta.notifications\` payloads stop carrying it.
|
|
6298
|
+
|
|
6299
|
+
**When to call.** After you read an entry from \`_meta.notifications\` or \`account_status.notifications\` and have revised whatever prior output the just-finished background work might have made stale (outreach drafts, lead lists, "available leads" claims, followup plans). Mark-seen tells the human team's pipeline you handled this and prevents the notification from re-surfacing on every subsequent tool response.
|
|
6300
|
+
|
|
6301
|
+
If nothing you produced for the user is affected, ack anyway with \`archive:false\` \u2014 the entry should still clear so the inbox stays focused on what's actually pending.
|
|
6302
|
+
|
|
6303
|
+
Use \`archive:true\` only when you want the row gone from the FE notification dropdown too (e.g. a non-actionable system notification that's already handled). Default behaviour is \`seen\` \u2014 same as the FE dropdown's "click to read" semantics.
|
|
6304
|
+
|
|
6305
|
+
WHEN TO USE: immediately after you finish reviewing / revising in response to a \`_meta.notifications[]\` entry. Idempotent \u2014 calling twice with the same id is safe.
|
|
6306
|
+
|
|
6307
|
+
WHEN NOT TO USE: before doing the revision work; for general "mark all read" sweeps (call once per notification id you've actually consumed).
|
|
6308
|
+
|
|
6309
|
+
This tool MUTATES state. The caller (agent or human-in-the-loop) is responsible for confirming intent before invocation; the MCP server does not soft-prompt for confirmation. See \`annotations.destructiveHint\`.
|
|
6310
|
+
`;
|
|
6246
6311
|
var leadbay_add_leads_to_campaign = `## WHEN TO USE
|
|
6247
6312
|
|
|
6248
6313
|
Trigger phrases: "add leads to <name> campaign", "attach these to <campaign>", "put these in Q2 Push", "add to existing campaign".
|
|
@@ -6297,7 +6362,37 @@ WHEN NOT TO USE: to log an outreach action \u2014 use leadbay_report_outreach, w
|
|
|
6297
6362
|
|
|
6298
6363
|
This tool MUTATES state. The caller (agent or human-in-the-loop) is responsible for confirming intent before invocation; the MCP server does not soft-prompt for confirmation. See \`annotations.destructiveHint\`.
|
|
6299
6364
|
`;
|
|
6300
|
-
var leadbay_adjust_audience =
|
|
6365
|
+
var leadbay_adjust_audience = `## WHEN TO USE
|
|
6366
|
+
|
|
6367
|
+
Trigger phrases: "narrow the audience to <sector>", "add <sector> to my <name> lens", "remove <sector> from this lens", "only show me companies of <size>", "stop including <sector>", "broaden this lens to also include <sector>".
|
|
6368
|
+
|
|
6369
|
+
**Memory:** recall + capture via \`leadbay_agent_memory_*\` tools.
|
|
6370
|
+
|
|
6371
|
+
Do NOT use for: "create a new lens called X" \u2192 \`leadbay_new_lens\`; "make a new audience for Y" \u2192 \`leadbay_new_lens\`; "show me / list / switch my lenses" \u2192 \`leadbay_my_lenses\`; "focus on a kind of company beyond sector/size (e.g. 'hospitals running their own IT')" \u2192 \`leadbay_refine_prompt\`.
|
|
6372
|
+
|
|
6373
|
+
Prefer when: user wants to change an EXISTING lens's sectors/sizes. If the user NAMES a lens ('my Joinery lens'), you MUST pass lensName with that name \u2014 do NOT edit the active lens. To create a brand-new lens use leadbay_new_lens instead.
|
|
6374
|
+
|
|
6375
|
+
Examples that SHOULD invoke this tool:
|
|
6376
|
+
- "Add fintech to my Joinery lens."
|
|
6377
|
+
- "Narrow my audience to manufacturing companies, 50\u2013500 people."
|
|
6378
|
+
- "Stop including retail in this lens."
|
|
6379
|
+
|
|
6380
|
+
Examples that should NOT invoke this tool (sound similar, route elsewhere):
|
|
6381
|
+
- "Create a lens called Joinery for fintech."
|
|
6382
|
+
- "Show me my lenses."
|
|
6383
|
+
- "Focus on hospitals that run their own IT."
|
|
6384
|
+
|
|
6385
|
+
## RENDER (quick)
|
|
6386
|
+
|
|
6387
|
+
On \`applied\`: confirm the lens edited (name) + the sectors/sizes added as
|
|
6388
|
+
chips. On \`ambiguous_sectors\` / \`ambiguous_lens\` / \`lens_not_found\`: surface
|
|
6389
|
+
the candidates and ask the user to pick, then re-call with the id/exact name.
|
|
6390
|
+
|
|
6391
|
+
---
|
|
6392
|
+
|
|
6393
|
+
Restrict (or expand) the lens audience by sector / size. Free-text sectors are auto-resolved against the sector taxonomy; ambiguous matches are surfaced to the agent rather than guessed silently. Permission routing is hidden: the default lens auto-clones to a new user lens; an org-level lens defaults to a per-user draft (admins can override with \`save_for_org:true\`). Filter MERGES with existing criteria (unrelated criteria are not dropped).
|
|
6394
|
+
|
|
6395
|
+
**Targeting a lens \u2014 READ THIS.** By default this edits the user's ACTIVE lens. **If the user names a lens** ("add fintech to my **Joinery** lens", "in my Nordics lens, exclude retail"), you MUST pass \`lensName\` with that name (\`lensName:"Joinery"\`). Do NOT silently edit the active lens when a different one was named \u2014 that corrupts the wrong audience and is a top friction source. The name resolves against the user's lenses (case-insensitive, exact then unique-substring); it is edit-only and does NOT change which lens is active. An unmatched name returns \`status:"lens_not_found"\` with the lens list, and a name matching several returns \`status:"ambiguous_lens"\` with the candidates \u2014 surface them and re-call with the exact \`lensName\` or a \`lensId\`. Use \`leadbay_my_lenses\` if the user first wants to SEE or SWITCH lenses. To CREATE a brand-new lens, use \`leadbay_new_lens\` \u2014 not this tool.
|
|
6301
6396
|
|
|
6302
6397
|
WHEN TO USE: when the user wants to see different kinds of leads (sector / size / etc.).
|
|
6303
6398
|
|
|
@@ -7511,6 +7606,258 @@ WHEN NOT TO USE: if a token is already preconfigured \u2014 you'll just overwrit
|
|
|
7511
7606
|
|
|
7512
7607
|
This tool MUTATES state. The caller (agent or human-in-the-loop) is responsible for confirming intent before invocation; the MCP server does not soft-prompt for confirmation. See \`annotations.destructiveHint\`.
|
|
7513
7608
|
`;
|
|
7609
|
+
var leadbay_my_lenses = `## WHEN TO USE
|
|
7610
|
+
|
|
7611
|
+
Trigger phrases: "show me my lenses", "list my lenses", "which audiences do I have", "switch to my <name> lens", "change lens", "rename my <name> lens to <X>", "set the description of my <name> lens", "delete my <name> lens", "remove this lens".
|
|
7612
|
+
|
|
7613
|
+
**Memory:** recall + capture via \`leadbay_agent_memory_*\` tools.
|
|
7614
|
+
|
|
7615
|
+
Do NOT use for: "narrow the audience" \u2192 \`leadbay_adjust_audience\`; "stop showing me <sector>" \u2192 \`leadbay_refine_prompt\`; "more leads on this lens" \u2192 \`leadbay_extend_lens\`; "show me today's leads" \u2192 \`leadbay_pull_leads\`.
|
|
7616
|
+
|
|
7617
|
+
Prefer when: user wants to SEE lenses, CHANGE which is active, RENAME one, or DELETE one \u2014 not edit a lens's sector/size criteria
|
|
7618
|
+
|
|
7619
|
+
Examples that SHOULD invoke this tool:
|
|
7620
|
+
- "Show me my lenses."
|
|
7621
|
+
- "Rename my Auto lens to Automotive and add a description."
|
|
7622
|
+
- "Delete my old Auto lens."
|
|
7623
|
+
|
|
7624
|
+
Examples that should NOT invoke this tool (sound similar, route elsewhere):
|
|
7625
|
+
- "Narrow the audience to fintech only."
|
|
7626
|
+
- "I want more leads on this lens."
|
|
7627
|
+
- "Show me today's leads."
|
|
7628
|
+
|
|
7629
|
+
## RENDER (quick)
|
|
7630
|
+
|
|
7631
|
+
Small markdown table, active lens first: col 1 = \u2B50 prefix when active +
|
|
7632
|
+
lens name; col 2 = description (or \`\u2014\`). After a switch lead with
|
|
7633
|
+
"Now showing **<name>**."; after a rename lead with the rename confirmation.
|
|
7634
|
+
Full algorithm below.
|
|
7635
|
+
|
|
7636
|
+
---
|
|
7637
|
+
|
|
7638
|
+
List the user's lenses (saved audiences) and, when asked, switch which one is active. A lens shapes the kind of leads delivered each day; this tool is how the user sees their audiences and moves between them \u2014 it does NOT edit a lens's criteria (that's \`leadbay_adjust_audience\`).
|
|
7639
|
+
|
|
7640
|
+
**Three modes, one tool:**
|
|
7641
|
+
|
|
7642
|
+
- **List (no args)** \u2014 pure read. Returns \`{status:"listed", lenses:[{id, name, description, is_active}], active_lens_id}\`. The active lens is resolved from the user's last-requested lens, so \`is_active\` is authoritative even if a row's flag is stale.
|
|
7643
|
+
- **Switch (\`switchToLensId\`)** \u2014 changes the active lens to that id and returns the REFRESHED list. The id MUST be one of the user's lenses; an unknown id returns \`{status:"not_found"}\` with the current list \u2014 surface it and ask the user to pick, do NOT invent an id. Switching to the already-active lens is a harmless no-op.
|
|
7644
|
+
- **Edit (\`editLensId\` + \`newName\` and/or \`newDescription\`)** \u2014 rename and/or set the description of a lens in one call, returns the REFRESHED list. Provide either or both; pass \`newDescription:""\` to clear a description. Same not_found handling. Use the \`id\` from the list for the lens the user named.
|
|
7645
|
+
- **Delete (\`deleteLensId\`)** \u2014 DESTRUCTIVE and confirm-gated. Without \`confirm:true\` it returns \`status:"delete_preview"\` with \`will_delete\` and removes NOTHING \u2014 show it, get the user's explicit yes, then re-call with \`confirm:true\`. The DEFAULT lens cannot be deleted (\`status:"cannot_delete_default"\`). Deleting the active lens leaves no active lens until the next switch/pull resolves one.
|
|
7646
|
+
|
|
7647
|
+
**Lens ids are strings** (e.g. \`"40005"\`) \u2014 pass the \`id\` value straight from the list when switching/renaming/deleting; it is fine to pass it as the string it came as.
|
|
7648
|
+
|
|
7649
|
+
**When the user is vague** ("switch lens" with no target), list first, then offer the lenses as a quick choice via \`ask_user_input_v0\` rather than guessing.
|
|
7650
|
+
|
|
7651
|
+
WHEN TO USE: when the user wants to see their lenses or switch the active one. Canonical phrasings: "show me my lenses", "which audiences do I have", "switch to my <name> lens".
|
|
7652
|
+
|
|
7653
|
+
WHEN NOT TO USE: to change a lens's audience criteria \u2014 that's \`leadbay_adjust_audience\`. Not for refining beyond firmographics (\`leadbay_refine_prompt\`), not for topping up the same lens (\`leadbay_extend_lens\`), not for the daily pull (\`leadbay_pull_leads\`).
|
|
7654
|
+
|
|
7655
|
+
This tool MUTATES state. The caller (agent or human-in-the-loop) is responsible for confirming intent before invocation; the MCP server does not soft-prompt for confirmation. See \`annotations.destructiveHint\`.
|
|
7656
|
+
|
|
7657
|
+
|
|
7658
|
+
## GATE \u2014 PREFER BUILT-IN HOST WIDGETS
|
|
7659
|
+
|
|
7660
|
+
Modern chat hosts (Claude, ChatGPT) expose first-party widgets the agent can route into. These ALWAYS produce a better UX than markdown tables / inline prose for the data shapes they support \u2014 they're tappable on mobile, persistent across turns, and integrate with the host's quick-actions.
|
|
7661
|
+
|
|
7662
|
+
**The Big Three** \u2014 when a tool result fits, route there:
|
|
7663
|
+
|
|
7664
|
+
| Host widget | Use when | Field map (from Leadbay payload) |
|
|
7665
|
+
|---|---|---|
|
|
7666
|
+
| \`places_map_display_v0\` (Claude) | Result has \u22652 leads with \`location.city\` set, and the user's intent is geographic / "in person" / travel | \`{name: lead.company_name, address: "<city>, <country>", place_id: lead.location.place_id ?? omit, notes: <one-sentence pitch>}\` per location |
|
|
7667
|
+
| \`message_compose_v1\` (Claude) | You're about to draft outreach (email / message / call opener) | \`{kind: "email", summary_title, variants: [{label, body, subject}]}\` \u2014 2\u20133 variants, labels describe STRATEGY ("Push for alignment", "Reference the M&A signal"), not tone ("Friendly", "Formal") |
|
|
7668
|
+
| \`ask_user_input_v0\` (Claude) | The tool's NEXT STEPS block has 2\u20134 mutually-exclusive next moves and the user hasn't already chosen | \`{questions: [{question: "What next?", type: "single_select", options: [<2-4 short button labels>]}]}\`; max 3 questions per call |
|
|
7669
|
+
|
|
7670
|
+
ChatGPT exposes the same routing pattern via \`_meta.openai/outputTemplate\`. We don't ship any custom widgets ourselves \u2014 this gate is exclusively about routing into the host's first-party widgets when the data shape fits.
|
|
7671
|
+
|
|
7672
|
+
**Rules:**
|
|
7673
|
+
- The widget IS the visual. Do NOT emit a markdown table or prose list of the same data alongside \u2014 that produces two competing UIs.
|
|
7674
|
+
- Pass identifiers (place_id, lead.id, contact_id) verbatim. Don't rewrite.
|
|
7675
|
+
- When the host doesn't expose the named widget, the agent falls back to the prose/table rendering the per-tool description already specifies. The directive is host-conditional; the fallback is automatic.
|
|
7676
|
+
- One short intro sentence in chat is enough \u2014 "Here are your 5 NYC follow-ups." Then route into the widget.
|
|
7677
|
+
|
|
7678
|
+
|
|
7679
|
+
---
|
|
7680
|
+
|
|
7681
|
+
## RENDERING \u2014 lenses table, active-first
|
|
7682
|
+
|
|
7683
|
+
Markdown table with TWO columns. Sort **active lens first**, then by \`name\`
|
|
7684
|
+
ascending. **No score bar** \u2014 the \`\u25B0\u2756\u25B1\` glyph identity belongs to lead
|
|
7685
|
+
discovery, not lenses.
|
|
7686
|
+
|
|
7687
|
+
**Column 1 \u2014 Lens**
|
|
7688
|
+
- Prefix \`\u2B50 \` when \`is_active\` is true; otherwise no prefix.
|
|
7689
|
+
- The lens name in **bold**. (Lenses have no public URL \u2014 do not fabricate a link.)
|
|
7690
|
+
|
|
7691
|
+
**Column 2 \u2014 Description**
|
|
7692
|
+
- \`description\` verbatim, clipped to \u2264 18 words.
|
|
7693
|
+
- When null/empty: render \`\u2014\`.
|
|
7694
|
+
|
|
7695
|
+
**After a \`switched: true\` response**, open with a single confirmation line
|
|
7696
|
+
ABOVE the table: \`Now showing **<name>**.\` For \`status: "not_found"\`, lead with
|
|
7697
|
+
the \`message\` (the bad id) and render the list so the user can pick a real one.
|
|
7698
|
+
|
|
7699
|
+
**Empty list** (\`lenses: []\`): render \`*You don't have any lenses yet.*\` \u2014 do not
|
|
7700
|
+
render an empty table.
|
|
7701
|
+
|
|
7702
|
+
**Legend:** \u2B50 active lens.
|
|
7703
|
+
|
|
7704
|
+
|
|
7705
|
+
## NEXT STEPS \u2014 after \`leadbay_my_lenses\`
|
|
7706
|
+
|
|
7707
|
+
**RENDER NEXT STEPS via \`ask_user_input_v0\` when the host exposes it.**
|
|
7708
|
+
|
|
7709
|
+
The (Observation, Suggest, Calls) table below is the source of truth for which moves are valid. Pick the 2\u20134 most relevant rows based on what the response actually contains, then surface them as a \`single_select\` quick-select widget:
|
|
7710
|
+
|
|
7711
|
+
\`\`\`
|
|
7712
|
+
ask_user_input_v0({
|
|
7713
|
+
questions: [{
|
|
7714
|
+
question: "What next?",
|
|
7715
|
+
type: "single_select",
|
|
7716
|
+
options: [
|
|
7717
|
+
"<Suggest column from row 1>",
|
|
7718
|
+
"<Suggest column from row 2>",
|
|
7719
|
+
"<Suggest column from row 3>"
|
|
7720
|
+
]
|
|
7721
|
+
}]
|
|
7722
|
+
})
|
|
7723
|
+
\`\`\`
|
|
7724
|
+
|
|
7725
|
+
When the user picks an option, you call the matching tool from the \`Calls\` column. Constraints carried over from the widget contract: 2\u20134 mutually-exclusive options per question, button-sized labels (\u22646 words), max 3 questions per call.
|
|
7726
|
+
|
|
7727
|
+
**Fallback prose mode** \u2014 when the host doesn't expose \`ask_user_input_v0\` (or it returned an error): surface the same 2\u20133 picks as a short bulleted list of "Suggest" phrasings. The table itself stays internal; never recite the whole table to the user.
|
|
7728
|
+
|
|
7729
|
+
---
|
|
7730
|
+
|
|
7731
|
+
|
|
7732
|
+
|
|
7733
|
+
Pick the 2\u20133 rows that fit what the user is likely to want next. When the user
|
|
7734
|
+
named no target but wants to switch, offer the lenses themselves as the
|
|
7735
|
+
quick-select options (each option = a lens name \u2192 \`leadbay_my_lenses(switchToLensId=<id>)\`).
|
|
7736
|
+
|
|
7737
|
+
| Observation | Suggest | Calls |
|
|
7738
|
+
|--------------------------------------|------------------------------------------|------------------------------------------------------|
|
|
7739
|
+
| User wants a different lens | "Switch to <lens name>" | \`leadbay_my_lenses(switchToLensId=<id>)\` |
|
|
7740
|
+
| User wants to rename / describe a lens| "Rename or describe <lens>" | \`leadbay_my_lenses(editLensId=<id>, newName?=<X>, newDescription?=<Y>)\` |
|
|
7741
|
+
| User wants to delete a lens | "Delete <lens>" | \`leadbay_my_lenses(deleteLensId=<id>)\` \u2192 confirm \u2192 \`confirm=true\` |
|
|
7742
|
+
| \`delete_preview\` (not yet deleted) | "Yes, delete it" | \`leadbay_my_lenses(deleteLensId=<id>, confirm=true)\` |
|
|
7743
|
+
| User wants leads on the active lens | "Pull today's leads" | \`leadbay_pull_leads()\` |
|
|
7744
|
+
| User wants to change the audience | "Adjust this lens's audience" | \`leadbay_adjust_audience(...)\` |
|
|
7745
|
+
| User wants more of the same | "Get a bigger batch on this lens" | \`leadbay_extend_lens(...)\` |
|
|
7746
|
+
|
|
7747
|
+
If nothing fits, default to "pull today's leads on the active lens" \u2014 never
|
|
7748
|
+
invent a tool that doesn't exist.
|
|
7749
|
+
`;
|
|
7750
|
+
var leadbay_new_lens = `## WHEN TO USE
|
|
7751
|
+
|
|
7752
|
+
Trigger phrases: "create a lens", "create a new lens called <name>", "create a lens specialized in/into <X>", "make me a new audience for <X>", "set up a lens for <sector>", "new lens named <name>", "I want a lens just for <X>".
|
|
7753
|
+
|
|
7754
|
+
**Memory:** recall + capture via \`leadbay_agent_memory_*\` tools.
|
|
7755
|
+
|
|
7756
|
+
Do NOT use for: "narrow the audience / add or remove a sector on an EXISTING lens" \u2192 \`leadbay_adjust_audience\`; "add <sector> to my <name> lens" \u2192 \`leadbay_adjust_audience\`; "focus on a qualitative trait beyond sector/size" \u2192 \`leadbay_refine_prompt\`; "show me / list / switch my lenses" \u2192 \`leadbay_my_lenses\`; "more leads on this lens" \u2192 \`leadbay_extend_lens\`.
|
|
7757
|
+
|
|
7758
|
+
Prefer when: user wants a brand-new lens (create/make/set up, often 'specialized in <X>'). Editing an existing lens \u2192 leadbay_adjust_audience (use lensName). Qualitative refinement \u2192 refine_prompt (admin-only).
|
|
7759
|
+
|
|
7760
|
+
Examples that SHOULD invoke this tool:
|
|
7761
|
+
- "Create a lens called Joinery for the fintech sector."
|
|
7762
|
+
- "Make me a new audience for healthcare companies, 30\u2013300 people."
|
|
7763
|
+
- "Set up a new lens named Nordics SaaS."
|
|
7764
|
+
|
|
7765
|
+
Examples that should NOT invoke this tool (sound similar, route elsewhere):
|
|
7766
|
+
- "Add fintech to my Joinery lens."
|
|
7767
|
+
- "Show me my lenses."
|
|
7768
|
+
- "I want more leads on this lens."
|
|
7769
|
+
|
|
7770
|
+
## RENDER (quick)
|
|
7771
|
+
|
|
7772
|
+
On \`preview\` (default \u2014 NOTHING created yet): show the lens that WILL be
|
|
7773
|
+
created (name + resolved sectors/sizes as chips) and ASK the user to confirm
|
|
7774
|
+
via ask_user_input_v0 ("Create this lens?" / "Change something"). Only on
|
|
7775
|
+
"yes" re-call with confirm:true. On \`created\`: confirm "Created **<name>**."
|
|
7776
|
+
On \`ambiguous_sectors\`: surface the candidate sectors to pick from.
|
|
7777
|
+
|
|
7778
|
+
---
|
|
7779
|
+
|
|
7780
|
+
Create a brand-new lens (saved audience) and apply its sector/size criteria. Clones a base lens (the user's active/default lens unless \`base\` is given), names it, and applies the filter.
|
|
7781
|
+
|
|
7782
|
+
**Confirm before creating \u2014 two-step by default.** A call WITHOUT \`confirm:true\` is a dry run: it resolves the sectors/sizes and returns \`status:"preview"\` with \`will_create\` (what it WOULD build) \u2014 **nothing is created**. Show that to the user, get an explicit yes (ask via \`ask_user_input_v0\`), then re-call the SAME args with \`confirm:true\` to actually create. Never pass \`confirm:true\` on the first call \u2014 the user must see the preview first. (Sector ambiguity is still surfaced in the preview step, so they pick before confirming.)
|
|
7783
|
+
|
|
7784
|
+
**Sectors resolve first.** Free-text \`sectors\`/\`exclude_sectors\` are auto-resolved against the taxonomy. If any don't resolve, the tool returns \`status:"ambiguous_sectors"\` with the candidates and **does NOT create the lens** \u2014 so re-calling after picking the right sector won't leave orphan half-built lenses. To discover valid sector labels up front, use \`leadbay_list_sectors\`.
|
|
7785
|
+
|
|
7786
|
+
**Does not switch the active lens.** The new lens is created but the user stays on their current one. Offer \`leadbay_my_lenses(switchToLensId=<new id>)\` as a next step if they want to start pulling from it.
|
|
7787
|
+
|
|
7788
|
+
WHEN TO USE: when the user wants a NEW lens. Canonical phrasings: "create a lens called X", "make a new audience for Y", "set up a lens for <sector>".
|
|
7789
|
+
|
|
7790
|
+
WHEN NOT TO USE: to EDIT an existing lens \u2014 use \`leadbay_adjust_audience\` (pass \`lensName\` to target one by name). Not for listing/switching (\`leadbay_my_lenses\`) or topping up (\`leadbay_extend_lens\`).
|
|
7791
|
+
|
|
7792
|
+
This tool MUTATES state. The caller (agent or human-in-the-loop) is responsible for confirming intent before invocation; the MCP server does not soft-prompt for confirmation. See \`annotations.destructiveHint\`.
|
|
7793
|
+
|
|
7794
|
+
|
|
7795
|
+
## GATE \u2014 PREFER BUILT-IN HOST WIDGETS
|
|
7796
|
+
|
|
7797
|
+
Modern chat hosts (Claude, ChatGPT) expose first-party widgets the agent can route into. These ALWAYS produce a better UX than markdown tables / inline prose for the data shapes they support \u2014 they're tappable on mobile, persistent across turns, and integrate with the host's quick-actions.
|
|
7798
|
+
|
|
7799
|
+
**The Big Three** \u2014 when a tool result fits, route there:
|
|
7800
|
+
|
|
7801
|
+
| Host widget | Use when | Field map (from Leadbay payload) |
|
|
7802
|
+
|---|---|---|
|
|
7803
|
+
| \`places_map_display_v0\` (Claude) | Result has \u22652 leads with \`location.city\` set, and the user's intent is geographic / "in person" / travel | \`{name: lead.company_name, address: "<city>, <country>", place_id: lead.location.place_id ?? omit, notes: <one-sentence pitch>}\` per location |
|
|
7804
|
+
| \`message_compose_v1\` (Claude) | You're about to draft outreach (email / message / call opener) | \`{kind: "email", summary_title, variants: [{label, body, subject}]}\` \u2014 2\u20133 variants, labels describe STRATEGY ("Push for alignment", "Reference the M&A signal"), not tone ("Friendly", "Formal") |
|
|
7805
|
+
| \`ask_user_input_v0\` (Claude) | The tool's NEXT STEPS block has 2\u20134 mutually-exclusive next moves and the user hasn't already chosen | \`{questions: [{question: "What next?", type: "single_select", options: [<2-4 short button labels>]}]}\`; max 3 questions per call |
|
|
7806
|
+
|
|
7807
|
+
ChatGPT exposes the same routing pattern via \`_meta.openai/outputTemplate\`. We don't ship any custom widgets ourselves \u2014 this gate is exclusively about routing into the host's first-party widgets when the data shape fits.
|
|
7808
|
+
|
|
7809
|
+
**Rules:**
|
|
7810
|
+
- The widget IS the visual. Do NOT emit a markdown table or prose list of the same data alongside \u2014 that produces two competing UIs.
|
|
7811
|
+
- Pass identifiers (place_id, lead.id, contact_id) verbatim. Don't rewrite.
|
|
7812
|
+
- When the host doesn't expose the named widget, the agent falls back to the prose/table rendering the per-tool description already specifies. The directive is host-conditional; the fallback is automatic.
|
|
7813
|
+
- One short intro sentence in chat is enough \u2014 "Here are your 5 NYC follow-ups." Then route into the widget.
|
|
7814
|
+
|
|
7815
|
+
|
|
7816
|
+
---
|
|
7817
|
+
|
|
7818
|
+
## NEXT STEPS \u2014 after \`leadbay_new_lens\`
|
|
7819
|
+
|
|
7820
|
+
**RENDER NEXT STEPS via \`ask_user_input_v0\` when the host exposes it.**
|
|
7821
|
+
|
|
7822
|
+
The (Observation, Suggest, Calls) table below is the source of truth for which moves are valid. Pick the 2\u20134 most relevant rows based on what the response actually contains, then surface them as a \`single_select\` quick-select widget:
|
|
7823
|
+
|
|
7824
|
+
\`\`\`
|
|
7825
|
+
ask_user_input_v0({
|
|
7826
|
+
questions: [{
|
|
7827
|
+
question: "What next?",
|
|
7828
|
+
type: "single_select",
|
|
7829
|
+
options: [
|
|
7830
|
+
"<Suggest column from row 1>",
|
|
7831
|
+
"<Suggest column from row 2>",
|
|
7832
|
+
"<Suggest column from row 3>"
|
|
7833
|
+
]
|
|
7834
|
+
}]
|
|
7835
|
+
})
|
|
7836
|
+
\`\`\`
|
|
7837
|
+
|
|
7838
|
+
When the user picks an option, you call the matching tool from the \`Calls\` column. Constraints carried over from the widget contract: 2\u20134 mutually-exclusive options per question, button-sized labels (\u22646 words), max 3 questions per call.
|
|
7839
|
+
|
|
7840
|
+
**Fallback prose mode** \u2014 when the host doesn't expose \`ask_user_input_v0\` (or it returned an error): surface the same 2\u20133 picks as a short bulleted list of "Suggest" phrasings. The table itself stays internal; never recite the whole table to the user.
|
|
7841
|
+
|
|
7842
|
+
---
|
|
7843
|
+
|
|
7844
|
+
|
|
7845
|
+
|
|
7846
|
+
Pick the rows that fit. On \`created\`, the switch + pull rows are the natural
|
|
7847
|
+
follow-ups. On \`ambiguous_sectors\`, the only move is to pick a sector and re-call.
|
|
7848
|
+
|
|
7849
|
+
| Observation | Suggest | Calls |
|
|
7850
|
+
|-----------------------------------|------------------------------------------|--------------------------------------------------------|
|
|
7851
|
+
| \`preview\` (not yet created) | "Yes, create this lens" | \`leadbay_new_lens(...same args..., confirm=true)\` |
|
|
7852
|
+
| \`preview\` (not yet created) | "Change the sectors/size first" | (re-ask the user, then \`leadbay_new_lens\` with new args) |
|
|
7853
|
+
| Lens created | "Switch to it and pull leads" | \`leadbay_my_lenses(switchToLensId=<new id>)\` then \`leadbay_pull_leads()\` |
|
|
7854
|
+
| Lens created | "Refine the audience further" | \`leadbay_adjust_audience(lensName=<new name>, ...)\` |
|
|
7855
|
+
| Lens created | "Leave it; keep my current lens active" | (no call) |
|
|
7856
|
+
| \`ambiguous_sectors\` | "Pick the right sector and create" | \`leadbay_new_lens(name=..., sectors=[<chosen id>])\` |
|
|
7857
|
+
|
|
7858
|
+
If nothing fits, default to "switch to the new lens and pull leads" \u2014 never
|
|
7859
|
+
invent a tool that doesn't exist.
|
|
7860
|
+
`;
|
|
7514
7861
|
var leadbay_open_billing_portal = `Generate a one-shot Stripe customer-portal URL. Wraps \`GET /1.5/stripe/portal\` \u2192 \`{url}\`. Returns a fresh Stripe-hosted URL the user can open to manage their existing Leadbay subscription: change plan tier, swap payment method, view invoices. The agent does NOT make subscription changes itself \u2014 it surfaces the URL and lets the user act.
|
|
7515
7862
|
|
|
7516
7863
|
Sibling of \`leadbay_create_topup_link\`. Use cases differ:
|
|
@@ -8143,7 +8490,33 @@ WHEN TO USE: before leadbay_enrich_titles, to plan which titles to order.
|
|
|
8143
8490
|
|
|
8144
8491
|
WHEN NOT TO USE: when you already know the exact titles you want to enrich.
|
|
8145
8492
|
`;
|
|
8146
|
-
var leadbay_refine_prompt =
|
|
8493
|
+
var leadbay_refine_prompt = `## WHEN TO USE
|
|
8494
|
+
|
|
8495
|
+
Trigger phrases: "focus on companies that <qualitative trait>", "I prefer leads that <behavior/characteristic>", "prioritize companies running their own IT", "deprioritize companies that just raised".
|
|
8496
|
+
|
|
8497
|
+
**Memory:** recall + capture via \`leadbay_agent_memory_*\` tools.
|
|
8498
|
+
|
|
8499
|
+
Do NOT use for: "create a new lens / a lens specialized into <X>" \u2192 \`leadbay_new_lens\`; "add/remove <sector> to/from my <name> lens" \u2192 \`leadbay_adjust_audience\`; "narrow the audience to <sector> / <size>" \u2192 \`leadbay_adjust_audience\`; "show me / list / switch my lenses" \u2192 \`leadbay_my_lenses\`.
|
|
8500
|
+
|
|
8501
|
+
Prefer when: ADMIN-ONLY. Qualitative refinement of the active lens that sector/size can't express. Creating/naming/listing/switching/sector-editing a lens routes elsewhere. Non-admin user \u2192 do NOT pick this.
|
|
8502
|
+
|
|
8503
|
+
Examples that SHOULD invoke this tool:
|
|
8504
|
+
- "Focus on hospitals that run their own IT in-house."
|
|
8505
|
+
- "Prioritize companies that have recently expanded headcount."
|
|
8506
|
+
|
|
8507
|
+
Examples that should NOT invoke this tool (sound similar, route elsewhere):
|
|
8508
|
+
- "Create a lens specialized in automobile."
|
|
8509
|
+
- "Add fintech to my Joinery lens."
|
|
8510
|
+
- "Show me my lenses."
|
|
8511
|
+
|
|
8512
|
+
## RENDER (quick)
|
|
8513
|
+
|
|
8514
|
+
On success: confirm the refinement applied to the active lens. If a
|
|
8515
|
+
clarification was raised, surface its question (route via ask_user_input_v0).
|
|
8516
|
+
|
|
8517
|
+
---
|
|
8518
|
+
|
|
8519
|
+
Refine the kind of leads Leadbay surfaces, beyond firmographics. Free-text instruction (e.g. "focus on hospitals running their own IT"). Sets the org's \`user_prompt\`; if the new prompt produces ambiguous criteria, Leadbay raises a clarification question, which this composite polls for and surfaces. Admin-only on the backend (will return 403 for non-admins).
|
|
8147
8520
|
|
|
8148
8521
|
WHEN TO USE: when audience filters (leadbay_adjust_audience) aren't enough.
|
|
8149
8522
|
|
|
@@ -10838,9 +11211,23 @@ async function completeUploadedChunk(client, upload, mappings, dryRun, perPhaseB
|
|
|
10838
11211
|
await pollPreprocess(client, importId, phaseBudget, ctx, signal);
|
|
10839
11212
|
ctx?.logger?.info?.(`import-leads: preprocess done for importId=${importId}`);
|
|
10840
11213
|
if (dryRun) {
|
|
10841
|
-
return { importId, records: [] };
|
|
11214
|
+
return { importId, records: [], notification_id: null };
|
|
11215
|
+
}
|
|
11216
|
+
let updateMappingsResp = null;
|
|
11217
|
+
try {
|
|
11218
|
+
updateMappingsResp = await client.request("POST", `/imports/${importId}/update_mappings`, mappings);
|
|
11219
|
+
} catch (err) {
|
|
11220
|
+
if (err?.code === "API_ERROR" || err?.code === "NOT_FOUND") {
|
|
11221
|
+
ctx?.logger?.warn?.(`import-leads: update_mappings raw error (${err?.code}); retrying void`);
|
|
11222
|
+
await client.requestVoid("POST", `/imports/${importId}/update_mappings`, mappings);
|
|
11223
|
+
} else {
|
|
11224
|
+
throw err;
|
|
11225
|
+
}
|
|
11226
|
+
}
|
|
11227
|
+
const importNotificationId = updateMappingsResp?.notification_id ?? null;
|
|
11228
|
+
if (importNotificationId) {
|
|
11229
|
+
ctx?.logger?.info?.(`import-leads: notification_id=${importNotificationId} importId=${importId}`);
|
|
10842
11230
|
}
|
|
10843
|
-
await client.requestVoid("POST", `/imports/${importId}/update_mappings`, mappings);
|
|
10844
11231
|
ctx?.logger?.info?.(`import-leads: mappings committed for importId=${importId}`);
|
|
10845
11232
|
const phaseBudget2 = Math.min(perPhaseBudgetMs, Math.max(1, totalDeadline - Date.now()));
|
|
10846
11233
|
await pollProcess(client, importId, phaseBudget2, ctx, signal);
|
|
@@ -10848,7 +11235,7 @@ async function completeUploadedChunk(client, upload, mappings, dryRun, perPhaseB
|
|
|
10848
11235
|
const phaseBudget3 = Math.min(perPhaseBudgetMs, Math.max(1, totalDeadline - Date.now()));
|
|
10849
11236
|
const records = await pollRecordsToTerminal(client, importId, phaseBudget3, chunk.length, ctx, signal);
|
|
10850
11237
|
ctx?.logger?.info?.(`import-leads: ${records.length} records terminal for importId=${importId}`);
|
|
10851
|
-
return { importId, records };
|
|
11238
|
+
return { importId, records, notification_id: importNotificationId };
|
|
10852
11239
|
}
|
|
10853
11240
|
function reconcileOneChunk(prep, chunk, matched, notImported) {
|
|
10854
11241
|
const seenInputIndex = /* @__PURE__ */ new Set();
|
|
@@ -10904,7 +11291,7 @@ function reconcileOneChunk(prep, chunk, matched, notImported) {
|
|
|
10904
11291
|
}
|
|
10905
11292
|
}
|
|
10906
11293
|
}
|
|
10907
|
-
function buildImportLeadsResult(client, prep, importIds, matched, notImported, dryRun, cancelled) {
|
|
11294
|
+
function buildImportLeadsResult(client, prep, importIds, matched, notImported, dryRun, cancelled, notificationIds) {
|
|
10908
11295
|
const leads = [];
|
|
10909
11296
|
const not_imported = [];
|
|
10910
11297
|
if (dryRun) {
|
|
@@ -10969,6 +11356,7 @@ function buildImportLeadsResult(client, prep, importIds, matched, notImported, d
|
|
|
10969
11356
|
leads,
|
|
10970
11357
|
not_imported,
|
|
10971
11358
|
importIds,
|
|
11359
|
+
notification_ids: notificationIds,
|
|
10972
11360
|
region: client.region,
|
|
10973
11361
|
cancelled: cancelled || void 0,
|
|
10974
11362
|
dry_run: dryRun || void 0,
|
|
@@ -11156,6 +11544,7 @@ var importLeads = {
|
|
|
11156
11544
|
leads: [],
|
|
11157
11545
|
not_imported,
|
|
11158
11546
|
importIds: [],
|
|
11547
|
+
notification_ids: [],
|
|
11159
11548
|
region: client.region,
|
|
11160
11549
|
dry_run: dryRun || void 0,
|
|
11161
11550
|
_meta: client.lastMeta ?? {
|
|
@@ -11210,6 +11599,10 @@ var importLeads = {
|
|
|
11210
11599
|
status: "running",
|
|
11211
11600
|
handle_id: reservation.record.bulk_id,
|
|
11212
11601
|
importIds: importIds2,
|
|
11602
|
+
// Notifications fire from update_mappings, which the background
|
|
11603
|
+
// task hasn't called yet at this point. They surface via the WS
|
|
11604
|
+
// listener / catch-up REST on subsequent agent turns.
|
|
11605
|
+
notification_ids: [],
|
|
11213
11606
|
progress: {
|
|
11214
11607
|
phase: reservation.record.status === "complete" ? "complete" : importIds2.length > 0 ? "preprocess" : "queued",
|
|
11215
11608
|
records_processed: reservation.record.status === "complete" ? reservation.record.records_total : 0,
|
|
@@ -11230,6 +11623,7 @@ var importLeads = {
|
|
|
11230
11623
|
}
|
|
11231
11624
|
ctx?.logger?.info?.(`import-leads(${prep.mode}): ${prep.validInputs.length} rows \u2192 ${chunks.length} chunk(s); dry_run=${dryRun}, totalBudgetMs=${totalBudget}`);
|
|
11232
11625
|
const importIds = [];
|
|
11626
|
+
const notificationIds = [];
|
|
11233
11627
|
const matched = /* @__PURE__ */ new Map();
|
|
11234
11628
|
const notImported = /* @__PURE__ */ new Map();
|
|
11235
11629
|
let cancelled = false;
|
|
@@ -11241,6 +11635,9 @@ var importLeads = {
|
|
|
11241
11635
|
for (let i = 0; i < chunks.length; i++) {
|
|
11242
11636
|
const chunk = chunks[i];
|
|
11243
11637
|
const out = await runOneChunk(client, chunk, i, chunks.length, prep.header, prep.mappings, dryRun, perPhaseBudget, totalDeadline, ctx, signal, recordImportId);
|
|
11638
|
+
if (out.notification_id && !notificationIds.includes(out.notification_id)) {
|
|
11639
|
+
notificationIds.push(out.notification_id);
|
|
11640
|
+
}
|
|
11244
11641
|
if (!dryRun) {
|
|
11245
11642
|
reconcileOneChunk(prep, out, matched, notImported);
|
|
11246
11643
|
}
|
|
@@ -11261,7 +11658,7 @@ var importLeads = {
|
|
|
11261
11658
|
throw err;
|
|
11262
11659
|
}
|
|
11263
11660
|
}
|
|
11264
|
-
return buildImportLeadsResult(client, prep, importIds, matched, notImported, dryRun, cancelled);
|
|
11661
|
+
return buildImportLeadsResult(client, prep, importIds, matched, notImported, dryRun, cancelled, notificationIds);
|
|
11265
11662
|
}
|
|
11266
11663
|
};
|
|
11267
11664
|
async function runImportInBackground(client, prep, uploadedChunks, opts, ctx, handleId) {
|
|
@@ -11278,17 +11675,21 @@ async function runImportInBackground(client, prep, uploadedChunks, opts, ctx, ha
|
|
|
11278
11675
|
void (async () => {
|
|
11279
11676
|
const bgCtx = { logger: ctx.logger, bulkTracker: tracker };
|
|
11280
11677
|
const importIds = uploadedChunks.map((chunk) => chunk.importId);
|
|
11678
|
+
const notificationIds = [];
|
|
11281
11679
|
const matched = /* @__PURE__ */ new Map();
|
|
11282
11680
|
const notImported = /* @__PURE__ */ new Map();
|
|
11283
11681
|
try {
|
|
11284
11682
|
const totalDeadline = Date.now() + opts.totalBudget;
|
|
11285
11683
|
for (const upload of uploadedChunks) {
|
|
11286
11684
|
const out = await completeUploadedChunk(client, upload, prep.mappings, opts.dryRun, opts.perPhaseBudget, totalDeadline, bgCtx, void 0);
|
|
11685
|
+
if (out.notification_id && !notificationIds.includes(out.notification_id)) {
|
|
11686
|
+
notificationIds.push(out.notification_id);
|
|
11687
|
+
}
|
|
11287
11688
|
if (!opts.dryRun) {
|
|
11288
11689
|
reconcileOneChunk(prep, out, matched, notImported);
|
|
11289
11690
|
}
|
|
11290
11691
|
}
|
|
11291
|
-
const result = buildImportLeadsResult(client, prep, importIds, matched, notImported, opts.dryRun, false);
|
|
11692
|
+
const result = buildImportLeadsResult(client, prep, importIds, matched, notImported, opts.dryRun, false, notificationIds);
|
|
11292
11693
|
await tracker.markImportComplete(handleId, {
|
|
11293
11694
|
leads: result.leads,
|
|
11294
11695
|
not_imported: result.not_imported,
|
|
@@ -11956,50 +12357,107 @@ var agentMemoryReview = {
|
|
|
11956
12357
|
}
|
|
11957
12358
|
};
|
|
11958
12359
|
|
|
11959
|
-
// ../core/dist/tools/
|
|
11960
|
-
var
|
|
11961
|
-
|
|
12360
|
+
// ../core/dist/tools/acknowledge-notification.js
|
|
12361
|
+
var UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
12362
|
+
var acknowledgeNotification = {
|
|
12363
|
+
name: "leadbay_acknowledge_notification",
|
|
11962
12364
|
annotations: {
|
|
11963
|
-
title: "
|
|
12365
|
+
title: "Acknowledge a Leadbay notification",
|
|
11964
12366
|
readOnlyHint: false,
|
|
11965
|
-
destructiveHint:
|
|
12367
|
+
destructiveHint: false,
|
|
11966
12368
|
idempotentHint: true,
|
|
11967
12369
|
openWorldHint: true
|
|
11968
12370
|
},
|
|
11969
|
-
description:
|
|
11970
|
-
optional: true,
|
|
12371
|
+
description: leadbay_acknowledge_notification,
|
|
11971
12372
|
write: true,
|
|
11972
12373
|
inputSchema: {
|
|
11973
12374
|
type: "object",
|
|
11974
12375
|
properties: {
|
|
11975
|
-
|
|
11976
|
-
type: "
|
|
11977
|
-
|
|
11978
|
-
|
|
11979
|
-
|
|
11980
|
-
|
|
12376
|
+
notification_id: {
|
|
12377
|
+
type: "string",
|
|
12378
|
+
description: "UUID of the notification to acknowledge. Use the notification_id from `_meta.notifications[]` or `account_status.notifications[]`."
|
|
12379
|
+
},
|
|
12380
|
+
archive: {
|
|
12381
|
+
type: "boolean",
|
|
12382
|
+
description: "If true, archive the notification (won't appear in `archived=false` listings). If false / omitted, mark seen (resets firstSeenAt)."
|
|
11981
12383
|
}
|
|
11982
12384
|
},
|
|
11983
|
-
required: ["
|
|
12385
|
+
required: ["notification_id"],
|
|
11984
12386
|
additionalProperties: false
|
|
11985
12387
|
},
|
|
11986
12388
|
outputSchema: {
|
|
11987
12389
|
type: "object",
|
|
11988
12390
|
properties: {
|
|
11989
|
-
|
|
11990
|
-
|
|
11991
|
-
|
|
11992
|
-
}
|
|
12391
|
+
acknowledged: { type: "boolean" },
|
|
12392
|
+
notification_id: { type: "string" },
|
|
12393
|
+
action: { type: "string", enum: ["seen", "archive"] }
|
|
11993
12394
|
},
|
|
11994
|
-
required: ["
|
|
12395
|
+
required: ["acknowledged", "notification_id", "action"]
|
|
11995
12396
|
},
|
|
11996
|
-
execute: async (client, params) => {
|
|
11997
|
-
|
|
11998
|
-
|
|
11999
|
-
|
|
12000
|
-
|
|
12001
|
-
|
|
12002
|
-
|
|
12397
|
+
execute: async (client, params, ctx) => {
|
|
12398
|
+
if (!UUID_RE.test(params.notification_id)) {
|
|
12399
|
+
return {
|
|
12400
|
+
error: true,
|
|
12401
|
+
code: "BAD_INPUT",
|
|
12402
|
+
message: "notification_id must be a UUID",
|
|
12403
|
+
hint: "Pass the notification_id verbatim from _meta.notifications[].notification_id or account_status.notifications[].notification_id."
|
|
12404
|
+
};
|
|
12405
|
+
}
|
|
12406
|
+
const action = params.archive ? "archive" : "seen";
|
|
12407
|
+
await client.acknowledgeNotification(params.notification_id, action);
|
|
12408
|
+
ctx?.notificationsInbox?.markSeen(params.notification_id);
|
|
12409
|
+
return {
|
|
12410
|
+
acknowledged: true,
|
|
12411
|
+
notification_id: params.notification_id,
|
|
12412
|
+
action
|
|
12413
|
+
};
|
|
12414
|
+
}
|
|
12415
|
+
};
|
|
12416
|
+
|
|
12417
|
+
// ../core/dist/tools/select-leads.js
|
|
12418
|
+
var selectLeads = {
|
|
12419
|
+
name: "leadbay_select_leads",
|
|
12420
|
+
annotations: {
|
|
12421
|
+
title: "Select leads",
|
|
12422
|
+
readOnlyHint: false,
|
|
12423
|
+
destructiveHint: true,
|
|
12424
|
+
idempotentHint: true,
|
|
12425
|
+
openWorldHint: true
|
|
12426
|
+
},
|
|
12427
|
+
description: leadbay_select_leads,
|
|
12428
|
+
optional: true,
|
|
12429
|
+
write: true,
|
|
12430
|
+
inputSchema: {
|
|
12431
|
+
type: "object",
|
|
12432
|
+
properties: {
|
|
12433
|
+
leadIds: {
|
|
12434
|
+
type: "array",
|
|
12435
|
+
items: { type: "string" },
|
|
12436
|
+
description: "Lead UUIDs to add to selection (1-1000)",
|
|
12437
|
+
minItems: 1,
|
|
12438
|
+
maxItems: 1e3
|
|
12439
|
+
}
|
|
12440
|
+
},
|
|
12441
|
+
required: ["leadIds"],
|
|
12442
|
+
additionalProperties: false
|
|
12443
|
+
},
|
|
12444
|
+
outputSchema: {
|
|
12445
|
+
type: "object",
|
|
12446
|
+
properties: {
|
|
12447
|
+
selected: {
|
|
12448
|
+
type: "number",
|
|
12449
|
+
description: "How many leadIds the call added to the selection (echoes input length)."
|
|
12450
|
+
}
|
|
12451
|
+
},
|
|
12452
|
+
required: ["selected"]
|
|
12453
|
+
},
|
|
12454
|
+
execute: async (client, params) => {
|
|
12455
|
+
const qs = params.leadIds.map((id) => `leadIds=${encodeURIComponent(id)}`).join("&");
|
|
12456
|
+
await client.requestVoid("POST", `/leads/selection/select?${qs}`);
|
|
12457
|
+
return { selected: params.leadIds.length };
|
|
12458
|
+
}
|
|
12459
|
+
};
|
|
12460
|
+
|
|
12003
12461
|
// ../core/dist/tools/deselect-leads.js
|
|
12004
12462
|
var deselectLeads = {
|
|
12005
12463
|
name: "leadbay_deselect_leads",
|
|
@@ -12118,7 +12576,7 @@ var createLens = {
|
|
|
12118
12576
|
},
|
|
12119
12577
|
execute: async (client, params) => {
|
|
12120
12578
|
const lens = await client.request("POST", "/lenses", {
|
|
12121
|
-
base: params.base,
|
|
12579
|
+
base: String(params.base),
|
|
12122
12580
|
name: params.name,
|
|
12123
12581
|
description: params.description
|
|
12124
12582
|
});
|
|
@@ -15028,6 +15486,11 @@ var accountStatus = {
|
|
|
15028
15486
|
type: ["object", "null"],
|
|
15029
15487
|
description: "Per-resource quota state (llm_completion, ai_rescore, web_fetch, LENS_EXTRA_REFILL) across daily/weekly/monthly windows. Null if /quota_status failed (logged in stderr). Pre-check the LENS_EXTRA_REFILL entry before calling leadbay_extend_lens."
|
|
15030
15488
|
},
|
|
15489
|
+
notifications: {
|
|
15490
|
+
type: "array",
|
|
15491
|
+
description: "Terminal bulk-progress notifications the MCP knows about (background work the user or agent started that has since completed). Each entry carries notification_id, kind (bulk_enrich | bulk_qualify | import | other), bulk_progress counters, and a revise_hint pointing at prior agent outputs the just-finished work might have made stale. After revising affected outputs, call leadbay_acknowledge_notification(notification_id) to clear the entry. Empty array when nothing has completed.",
|
|
15492
|
+
items: { type: "object" }
|
|
15493
|
+
},
|
|
15031
15494
|
_meta: {
|
|
15032
15495
|
type: "object",
|
|
15033
15496
|
properties: {
|
|
@@ -15087,6 +15550,13 @@ var accountStatus = {
|
|
|
15087
15550
|
// on /me are intentionally NOT surfaced — they're defunct (see
|
|
15088
15551
|
// SHAPE-DRIFT.md probe round 4).
|
|
15089
15552
|
quota,
|
|
15553
|
+
// Inbox of terminal bulk-progress notifications. Same shape the MCP
|
|
15554
|
+
// server attaches to `_meta.notifications` on every tool response —
|
|
15555
|
+
// duplicated here as a top-level field so the agent's daily-rhythm
|
|
15556
|
+
// check-in (this composite) sees them without having to read _meta.
|
|
15557
|
+
// Empty array when the WS listener isn't wired (OpenClaw, tests) OR
|
|
15558
|
+
// when nothing has completed since the last ack.
|
|
15559
|
+
notifications: ctx?.notificationsInbox?.list() ?? [],
|
|
15090
15560
|
_meta: {
|
|
15091
15561
|
region: client.region
|
|
15092
15562
|
}
|
|
@@ -15100,6 +15570,33 @@ var DEFAULT_COUNT = 10;
|
|
|
15100
15570
|
var MAX_COUNT = 25;
|
|
15101
15571
|
var DEFAULT_PER_LEAD_BUDGET_MS = 9e4;
|
|
15102
15572
|
var DEFAULT_TOTAL_BUDGET_MS2 = 5 * 6e4;
|
|
15573
|
+
async function launchBulkQualify(client, leadIds, ctx) {
|
|
15574
|
+
await client.acquireSelectionLock();
|
|
15575
|
+
try {
|
|
15576
|
+
try {
|
|
15577
|
+
const qs = leadIds.map((id) => `leadIds=${encodeURIComponent(id)}`).join("&");
|
|
15578
|
+
await client.requestVoid("POST", `/leads/selection/select?${qs}`);
|
|
15579
|
+
try {
|
|
15580
|
+
const resp = await client.request("POST", "/leads/selection/web_fetch?force_fetch=false", {});
|
|
15581
|
+
return { resp, quotaExceeded: false };
|
|
15582
|
+
} catch (err) {
|
|
15583
|
+
if (err?.code === "QUOTA_EXCEEDED") {
|
|
15584
|
+
ctx?.logger?.warn?.("bulk_qualify_leads: 429 on bulk /leads/selection/web_fetch \u2014 no leads queued");
|
|
15585
|
+
return { resp: null, quotaExceeded: true };
|
|
15586
|
+
}
|
|
15587
|
+
throw err;
|
|
15588
|
+
}
|
|
15589
|
+
} finally {
|
|
15590
|
+
try {
|
|
15591
|
+
await client.requestVoid("POST", "/leads/selection/clear");
|
|
15592
|
+
} catch (e) {
|
|
15593
|
+
ctx?.logger?.warn?.(`bulk_qualify_leads: selection.clear failed: ${e?.message ?? e?.code}`);
|
|
15594
|
+
}
|
|
15595
|
+
}
|
|
15596
|
+
} finally {
|
|
15597
|
+
client.releaseSelectionLock();
|
|
15598
|
+
}
|
|
15599
|
+
}
|
|
15103
15600
|
var bulkQualifyLeads = {
|
|
15104
15601
|
name: "leadbay_bulk_qualify_leads",
|
|
15105
15602
|
annotations: {
|
|
@@ -15267,69 +15764,50 @@ var bulkQualifyLeads = {
|
|
|
15267
15764
|
per_lead_budget_ms: perLeadBudget,
|
|
15268
15765
|
total_budget_ms: totalBudget
|
|
15269
15766
|
});
|
|
15270
|
-
|
|
15271
|
-
|
|
15767
|
+
let launchedCount = 0;
|
|
15768
|
+
let notificationId = null;
|
|
15272
15769
|
let quotaExceeded2 = false;
|
|
15770
|
+
let failed2 = [];
|
|
15273
15771
|
if (!reservation.reused) {
|
|
15274
|
-
|
|
15275
|
-
|
|
15276
|
-
|
|
15277
|
-
|
|
15278
|
-
|
|
15279
|
-
|
|
15280
|
-
|
|
15281
|
-
|
|
15282
|
-
|
|
15283
|
-
|
|
15284
|
-
failed2.push({ lead_id: leadId, error: "lead not found" });
|
|
15285
|
-
} else {
|
|
15286
|
-
failed2.push({
|
|
15287
|
-
lead_id: leadId,
|
|
15288
|
-
error: err?.message ?? err?.code ?? "unknown"
|
|
15289
|
-
});
|
|
15290
|
-
}
|
|
15291
|
-
}
|
|
15292
|
-
}
|
|
15293
|
-
if (failed2.length === candidates.length || launched2.length > 0 || quotaExceeded2) {
|
|
15294
|
-
await ctx.bulkTracker.markLaunched(reservation.record.bulk_id);
|
|
15772
|
+
const launch = await launchBulkQualify(client, candidates, ctx);
|
|
15773
|
+
quotaExceeded2 = launch.quotaExceeded;
|
|
15774
|
+
notificationId = launch.resp?.notification_id ?? null;
|
|
15775
|
+
const queuedIds = launch.resp?.queued_ids ?? [];
|
|
15776
|
+
const skippedIds = launch.resp?.skipped_ids ?? [];
|
|
15777
|
+
launchedCount = queuedIds.length;
|
|
15778
|
+
const seen = /* @__PURE__ */ new Set([...queuedIds, ...skippedIds]);
|
|
15779
|
+
failed2 = candidates.filter((id) => !seen.has(id)).map((id) => ({ lead_id: id, error: "not_queued" }));
|
|
15780
|
+
if (queuedIds.length > 0 || quotaExceeded2 || skippedIds.length > 0 || failed2.length === candidates.length) {
|
|
15781
|
+
await ctx.bulkTracker.markLaunched(reservation.record.bulk_id, notificationId);
|
|
15295
15782
|
}
|
|
15783
|
+
} else {
|
|
15784
|
+
notificationId = reservation.record.notification_id ?? null;
|
|
15785
|
+
launchedCount = reservation.record.lead_ids.length;
|
|
15296
15786
|
}
|
|
15297
15787
|
const out = {
|
|
15298
15788
|
status: "running",
|
|
15299
15789
|
handle_id: reservation.record.bulk_id,
|
|
15300
15790
|
qualify_id: reservation.record.bulk_id,
|
|
15301
15791
|
lead_ids: candidates,
|
|
15302
|
-
launched_count:
|
|
15792
|
+
launched_count: launchedCount,
|
|
15303
15793
|
failed: failed2,
|
|
15304
15794
|
quota_exceeded: quotaExceeded2,
|
|
15305
15795
|
lens_id: lensId,
|
|
15796
|
+
notification_id: notificationId,
|
|
15306
15797
|
_meta: { region: client.region }
|
|
15307
15798
|
};
|
|
15308
15799
|
return out;
|
|
15309
15800
|
}
|
|
15310
|
-
const
|
|
15311
|
-
const
|
|
15312
|
-
|
|
15313
|
-
|
|
15314
|
-
|
|
15315
|
-
|
|
15316
|
-
|
|
15317
|
-
await client.requestVoid("POST", `/leads/${leadId}/web_fetch?force_fetch=false`);
|
|
15318
|
-
launched.push(leadId);
|
|
15319
|
-
} catch (err) {
|
|
15320
|
-
if (err?.code === "QUOTA_EXCEEDED") {
|
|
15321
|
-
quotaExceeded = true;
|
|
15322
|
-
ctx?.logger?.warn?.(`bulk_qualify_leads: 429 mid-fanout after launching ${launched.length}/${candidates.length} \u2014 stopping further launches but polling those already in flight`);
|
|
15323
|
-
} else if (err?.code === "NOT_FOUND") {
|
|
15324
|
-
failed.push({ lead_id: leadId, error: "lead not found" });
|
|
15325
|
-
} else {
|
|
15326
|
-
failed.push({
|
|
15327
|
-
lead_id: leadId,
|
|
15328
|
-
error: err?.message ?? err?.code ?? "unknown"
|
|
15329
|
-
});
|
|
15330
|
-
}
|
|
15331
|
-
}
|
|
15801
|
+
const inlineLaunch = await launchBulkQualify(client, candidates, ctx);
|
|
15802
|
+
const quotaExceeded = inlineLaunch.quotaExceeded;
|
|
15803
|
+
const launched = inlineLaunch.resp?.queued_ids ?? [];
|
|
15804
|
+
const inlineSkipped = inlineLaunch.resp?.skipped_ids ?? [];
|
|
15805
|
+
const inlineNotificationId = inlineLaunch.resp?.notification_id ?? null;
|
|
15806
|
+
if (inlineNotificationId) {
|
|
15807
|
+
ctx?.logger?.info?.(`bulk_qualify_leads: launched bulk progress_notification_id=${inlineNotificationId} queued=${launched.length} skipped=${inlineSkipped.length}`);
|
|
15332
15808
|
}
|
|
15809
|
+
const inlineFailedSeen = /* @__PURE__ */ new Set([...launched, ...inlineSkipped]);
|
|
15810
|
+
const failed = candidates.filter((id) => !inlineFailedSeen.has(id)).map((id) => ({ lead_id: id, error: "not_queued" }));
|
|
15333
15811
|
let progressDone = 0;
|
|
15334
15812
|
const progressTotal = launched.length;
|
|
15335
15813
|
if (progressTotal > 0) {
|
|
@@ -16042,6 +16520,7 @@ var importAndQualify = {
|
|
|
16042
16520
|
...chosenBudgets ? { chosen_budgets: chosenBudgets } : {},
|
|
16043
16521
|
qualify_id: null,
|
|
16044
16522
|
import_ids: queued.importIds,
|
|
16523
|
+
notification_ids: queued.notification_ids ?? [],
|
|
16045
16524
|
imported: queued.leads.map((l) => ({
|
|
16046
16525
|
leadId: l.leadId,
|
|
16047
16526
|
...l.domain ? { domain: l.domain } : {},
|
|
@@ -16066,6 +16545,7 @@ var importAndQualify = {
|
|
|
16066
16545
|
...chosenBudgets ? { chosen_budgets: chosenBudgets } : {},
|
|
16067
16546
|
qualify_id: null,
|
|
16068
16547
|
import_ids: queued.importIds,
|
|
16548
|
+
notification_ids: queued.notification_ids ?? [],
|
|
16069
16549
|
imported: [],
|
|
16070
16550
|
not_imported: [],
|
|
16071
16551
|
qualified: [],
|
|
@@ -16103,6 +16583,7 @@ var importAndQualify = {
|
|
|
16103
16583
|
...chosenBudgets ? { chosen_budgets: chosenBudgets } : {},
|
|
16104
16584
|
qualify_id: null,
|
|
16105
16585
|
import_ids: importResult.importIds,
|
|
16586
|
+
notification_ids: importResult.notification_ids ?? [],
|
|
16106
16587
|
imported: [],
|
|
16107
16588
|
not_imported: importResult.not_imported.map(toNotImportedEntry),
|
|
16108
16589
|
qualified: [],
|
|
@@ -16140,6 +16621,7 @@ var importAndQualify = {
|
|
|
16140
16621
|
...chosenBudgets ? { chosen_budgets: chosenBudgets } : {},
|
|
16141
16622
|
qualify_id: null,
|
|
16142
16623
|
import_ids: importResult.importIds,
|
|
16624
|
+
notification_ids: importResult.notification_ids ?? [],
|
|
16143
16625
|
imported,
|
|
16144
16626
|
not_imported,
|
|
16145
16627
|
qualified: [],
|
|
@@ -16250,6 +16732,7 @@ var importAndQualify = {
|
|
|
16250
16732
|
...chosenBudgets ? { chosen_budgets: chosenBudgets } : {},
|
|
16251
16733
|
qualify_id: reservation.record.bulk_id,
|
|
16252
16734
|
import_ids: importResult.importIds,
|
|
16735
|
+
notification_ids: importResult.notification_ids ?? [],
|
|
16253
16736
|
imported,
|
|
16254
16737
|
not_imported,
|
|
16255
16738
|
qualified,
|
|
@@ -16591,6 +17074,14 @@ var importStatus = {
|
|
|
16591
17074
|
};
|
|
16592
17075
|
|
|
16593
17076
|
// ../core/dist/composite/qualify-status.js
|
|
17077
|
+
async function readNotification(client, notificationId) {
|
|
17078
|
+
try {
|
|
17079
|
+
const page = await client.listNotifications({ archived: false, count: 50 });
|
|
17080
|
+
return page.items.find((n) => n.id === notificationId) ?? null;
|
|
17081
|
+
} catch {
|
|
17082
|
+
return null;
|
|
17083
|
+
}
|
|
17084
|
+
}
|
|
16594
17085
|
var qualifyStatus = {
|
|
16595
17086
|
name: "leadbay_qualify_status",
|
|
16596
17087
|
annotations: {
|
|
@@ -16745,6 +17236,16 @@ var qualifyStatus = {
|
|
|
16745
17236
|
const { _stillRunning, _failedCode, ...rest } = r;
|
|
16746
17237
|
qualified.push(rest);
|
|
16747
17238
|
}
|
|
17239
|
+
let bulkProgress = null;
|
|
17240
|
+
let inProgressFlag = null;
|
|
17241
|
+
const notifId = record.notification_id ?? null;
|
|
17242
|
+
if (notifId) {
|
|
17243
|
+
const n = await readNotification(client, notifId);
|
|
17244
|
+
if (n) {
|
|
17245
|
+
bulkProgress = n.bulk_progress;
|
|
17246
|
+
inProgressFlag = n.in_progress;
|
|
17247
|
+
}
|
|
17248
|
+
}
|
|
16748
17249
|
const out = {
|
|
16749
17250
|
qualify_id: record.bulk_id,
|
|
16750
17251
|
launched_at: record.launched_at,
|
|
@@ -16756,6 +17257,9 @@ var qualifyStatus = {
|
|
|
16756
17257
|
still_running,
|
|
16757
17258
|
failed,
|
|
16758
17259
|
not_in_lens: [...notInLensSet],
|
|
17260
|
+
notification_id: notifId,
|
|
17261
|
+
bulk_progress: bulkProgress,
|
|
17262
|
+
in_progress: inProgressFlag,
|
|
16759
17263
|
region: client.region,
|
|
16760
17264
|
_meta: client.lastMeta ?? {
|
|
16761
17265
|
region: client.region,
|
|
@@ -16768,6 +17272,9 @@ var qualifyStatus = {
|
|
|
16768
17272
|
out.per_lead_budget_ms = record.per_lead_budget_ms;
|
|
16769
17273
|
if (record.total_budget_ms !== void 0)
|
|
16770
17274
|
out.total_budget_ms = record.total_budget_ms;
|
|
17275
|
+
if (bulkProgress && bulkProgress.quota_hit_count > 0) {
|
|
17276
|
+
out.quota_hit_hint = "Some leads hit the AI-credits quota during qualification. Top up via leadbay_create_topup_link to clear the throttle immediately, or wait until the daily/weekly window resets.";
|
|
17277
|
+
}
|
|
16771
17278
|
return out;
|
|
16772
17279
|
}
|
|
16773
17280
|
};
|
|
@@ -17038,6 +17545,7 @@ var enrichTitles = {
|
|
|
17038
17545
|
bulk_id: res.record.bulk_id,
|
|
17039
17546
|
launched_at: res.record.launched_at,
|
|
17040
17547
|
durability: res.record.durability,
|
|
17548
|
+
notification_id: res.record.notification_id ?? null,
|
|
17041
17549
|
seconds_since_original_launch: bulkSecondsSinceOriginal ?? 0,
|
|
17042
17550
|
titles: params.titles,
|
|
17043
17551
|
email,
|
|
@@ -17053,8 +17561,9 @@ var enrichTitles = {
|
|
|
17053
17561
|
total: 3,
|
|
17054
17562
|
message: `Launching enrichment for ${params.titles.length} title${params.titles.length === 1 ? "" : "s"}\u2026`
|
|
17055
17563
|
});
|
|
17564
|
+
let launchResp = null;
|
|
17056
17565
|
try {
|
|
17057
|
-
await client.
|
|
17566
|
+
launchResp = await client.request("POST", "/leads/selection/enrichment/launch", { titles: params.titles, email, phone });
|
|
17058
17567
|
} catch (err) {
|
|
17059
17568
|
const aborted = err?.name === "AbortError" || ctx?.signal?.aborted === true;
|
|
17060
17569
|
if (bulkRecord && tracker) {
|
|
@@ -17078,9 +17587,10 @@ var enrichTitles = {
|
|
|
17078
17587
|
}
|
|
17079
17588
|
throw err;
|
|
17080
17589
|
}
|
|
17590
|
+
const notificationId = launchResp?.notification_id ?? null;
|
|
17081
17591
|
if (bulkRecord && tracker) {
|
|
17082
17592
|
try {
|
|
17083
|
-
await tracker.markLaunched(bulkRecord.bulk_id);
|
|
17593
|
+
await tracker.markLaunched(bulkRecord.bulk_id, notificationId);
|
|
17084
17594
|
} catch (e) {
|
|
17085
17595
|
ctx?.logger?.warn?.(`enrich_titles: tracker.markLaunched failed: ${e?.message ?? e}`);
|
|
17086
17596
|
return {
|
|
@@ -17108,8 +17618,9 @@ var enrichTitles = {
|
|
|
17108
17618
|
bulk_id: bulkRecord?.bulk_id,
|
|
17109
17619
|
launched_at: bulkRecord?.launched_at,
|
|
17110
17620
|
durability: bulkRecord?.durability,
|
|
17111
|
-
|
|
17112
|
-
|
|
17621
|
+
notification_id: notificationId,
|
|
17622
|
+
message: notificationId ? "Enrichment job launched. The MCP is now listening for the backend notification \u2014 when enrichment finishes, a `_meta.notifications` entry will surface on your next tool response (also visible in `leadbay_account_status.notifications`)." : bulkRecord ? "Enrichment job launched. Backend did not return a notification id this time; poll via leadbay_bulk_enrich_status with the bulk_id." : "Enrichment job launched. No bulk_id tracker configured \u2014 poll leadbay_get_contacts per lead after ~60s; contact.enrichment.done flips to true.",
|
|
17623
|
+
next_action: notificationId ? "Wait for the next `_meta.notifications` entry (typically <2 min for a small batch). If you want progress sooner, call leadbay_bulk_enrich_status({bulk_id})." : bulkRecord ? "Call leadbay_bulk_enrich_status({bulk_id}) after ~60s; pass include_contacts=true for the final read." : "Wait ~60s, then call leadbay_research_lead_by_id or leadbay_get_contacts on the leads you care about."
|
|
17113
17624
|
};
|
|
17114
17625
|
} finally {
|
|
17115
17626
|
try {
|
|
@@ -17125,6 +17636,14 @@ var enrichTitles = {
|
|
|
17125
17636
|
};
|
|
17126
17637
|
|
|
17127
17638
|
// ../core/dist/composite/bulk-enrich-status.js
|
|
17639
|
+
async function readNotification2(client, notificationId) {
|
|
17640
|
+
try {
|
|
17641
|
+
const page = await client.listNotifications({ archived: false, count: 50 });
|
|
17642
|
+
return page.items.find((n) => n.id === notificationId) ?? null;
|
|
17643
|
+
} catch {
|
|
17644
|
+
return null;
|
|
17645
|
+
}
|
|
17646
|
+
}
|
|
17128
17647
|
var STATUS_FETCH_CONCURRENCY = 5;
|
|
17129
17648
|
async function pMap(items, fn, concurrency) {
|
|
17130
17649
|
const out = new Array(items.length);
|
|
@@ -17289,6 +17808,53 @@ var bulkEnrichStatus = {
|
|
|
17289
17808
|
launched_at: record.launched_at
|
|
17290
17809
|
};
|
|
17291
17810
|
}
|
|
17811
|
+
const notifId = record.notification_id ?? null;
|
|
17812
|
+
if (notifId) {
|
|
17813
|
+
const n = await readNotification2(client, notifId);
|
|
17814
|
+
if (n && n.bulk_progress) {
|
|
17815
|
+
const bp = n.bulk_progress;
|
|
17816
|
+
const inProgress = n.in_progress;
|
|
17817
|
+
let leads2 = [];
|
|
17818
|
+
if (!inProgress && includeContacts) {
|
|
17819
|
+
leads2 = await pMap(record.lead_ids, async (leadId) => {
|
|
17820
|
+
try {
|
|
17821
|
+
const out = await getContacts.execute(client, { leadId });
|
|
17822
|
+
const contacts = Array.isArray(out?.contacts) ? out.contacts : [];
|
|
17823
|
+
return { lead_id: leadId, contacts };
|
|
17824
|
+
} catch {
|
|
17825
|
+
return { lead_id: leadId };
|
|
17826
|
+
}
|
|
17827
|
+
}, STATUS_FETCH_CONCURRENCY);
|
|
17828
|
+
} else {
|
|
17829
|
+
leads2 = record.lead_ids.map((id) => ({ lead_id: id }));
|
|
17830
|
+
}
|
|
17831
|
+
ctx?.logger?.info?.(`bulk.status_checked_via_notification bulk_id=${record.bulk_id} notification_id=${notifId} done=${bp.success_count}/${bp.total_count} in_progress=${inProgress} wall_ms=${Date.now() - startMs}`);
|
|
17832
|
+
return {
|
|
17833
|
+
bulk_id: record.bulk_id,
|
|
17834
|
+
notification_id: notifId,
|
|
17835
|
+
launched_at: record.launched_at,
|
|
17836
|
+
status: record.status,
|
|
17837
|
+
durability: record.durability,
|
|
17838
|
+
titles: record.titles,
|
|
17839
|
+
email: record.email,
|
|
17840
|
+
phone: record.phone,
|
|
17841
|
+
lens_id: record.lens_id,
|
|
17842
|
+
leads: leads2,
|
|
17843
|
+
overall_progress: {
|
|
17844
|
+
done: bp.success_count + bp.failure_count + bp.quota_hit_count,
|
|
17845
|
+
total: bp.total_count,
|
|
17846
|
+
done_ratio: bp.total_count === 0 ? 0 : (bp.success_count + bp.failure_count + bp.quota_hit_count) / bp.total_count
|
|
17847
|
+
},
|
|
17848
|
+
bulk_progress: bp,
|
|
17849
|
+
in_progress: inProgress,
|
|
17850
|
+
all_done: !inProgress,
|
|
17851
|
+
...bp.quota_hit_count > 0 ? {
|
|
17852
|
+
quota_hit_hint: "Some contacts could not be enriched because the AI-credits quota was hit. Top up via leadbay_create_topup_link or wait for the window reset."
|
|
17853
|
+
} : {}
|
|
17854
|
+
};
|
|
17855
|
+
}
|
|
17856
|
+
ctx?.logger?.info?.(`bulk_enrich_status: notification ${notifId} not yet visible; falling back to per-lead fan-out`);
|
|
17857
|
+
}
|
|
17292
17858
|
let doneSoFar = 0;
|
|
17293
17859
|
const totalLeads = record.lead_ids.length;
|
|
17294
17860
|
const results = await pMap(record.lead_ids, async (leadId) => {
|
|
@@ -17373,6 +17939,8 @@ var bulkEnrichStatus = {
|
|
|
17373
17939
|
|
|
17374
17940
|
// ../core/dist/composite/adjust-audience.js
|
|
17375
17941
|
function tokens(s) {
|
|
17942
|
+
if (!s)
|
|
17943
|
+
return [];
|
|
17376
17944
|
return s.toLowerCase().split(/[^\p{L}\p{N}]+/u).filter(Boolean);
|
|
17377
17945
|
}
|
|
17378
17946
|
function bestMatches(text, taxonomy) {
|
|
@@ -17386,11 +17954,11 @@ function bestMatches(text, taxonomy) {
|
|
|
17386
17954
|
if (have.has(t))
|
|
17387
17955
|
overlap += 1;
|
|
17388
17956
|
const score = overlap / Math.max(want.size, 1);
|
|
17389
|
-
return { id: s.id, name: s.name, score };
|
|
17957
|
+
return { id: s.id, name: s.name ?? "", score };
|
|
17390
17958
|
}).filter((m) => m.score > 0).sort((a, b) => b.score - a.score);
|
|
17391
17959
|
return ranked.slice(0, 5);
|
|
17392
17960
|
}
|
|
17393
|
-
async function resolveSectors(client, texts) {
|
|
17961
|
+
async function resolveSectors(client, texts, ctx) {
|
|
17394
17962
|
const looksLikeId2 = (s) => /^\d+$/.test(s);
|
|
17395
17963
|
const direct = texts.filter(looksLikeId2);
|
|
17396
17964
|
const free = texts.filter((s) => !looksLikeId2(s));
|
|
@@ -17399,6 +17967,10 @@ async function resolveSectors(client, texts) {
|
|
|
17399
17967
|
const me = await client.resolveMe().catch(() => null);
|
|
17400
17968
|
const lang = me?.language ?? "en";
|
|
17401
17969
|
const taxonomy = await client.request("GET", `/sectors/all?lang=${encodeURIComponent(lang)}&includeInvisible=false`);
|
|
17970
|
+
const nullNames = taxonomy.filter((s) => !s.name).length;
|
|
17971
|
+
if (nullNames > 0) {
|
|
17972
|
+
ctx?.logger?.warn?.(`adjust_audience: /sectors/all returned ${nullNames}/${taxonomy.length} sector(s) with a null/missing name`);
|
|
17973
|
+
}
|
|
17402
17974
|
const resolved = [...direct];
|
|
17403
17975
|
const ambiguities = [];
|
|
17404
17976
|
for (const text of free) {
|
|
@@ -17411,6 +17983,22 @@ async function resolveSectors(client, texts) {
|
|
|
17411
17983
|
}
|
|
17412
17984
|
return { resolved, ambiguities };
|
|
17413
17985
|
}
|
|
17986
|
+
async function resolveLensByName(client, name) {
|
|
17987
|
+
const lenses = await client.request("GET", "/lenses");
|
|
17988
|
+
const all = lenses.map((l) => ({ id: l.id, name: l.name }));
|
|
17989
|
+
const needle = name.trim().toLowerCase();
|
|
17990
|
+
const exact = all.filter((l) => (l.name ?? "").trim().toLowerCase() === needle);
|
|
17991
|
+
if (exact.length === 1)
|
|
17992
|
+
return { ok: true, id: exact[0].id };
|
|
17993
|
+
if (exact.length > 1)
|
|
17994
|
+
return { ok: false, reason: "ambiguous", matches: exact };
|
|
17995
|
+
const partial = all.filter((l) => (l.name ?? "").toLowerCase().includes(needle));
|
|
17996
|
+
if (partial.length === 1)
|
|
17997
|
+
return { ok: true, id: partial[0].id };
|
|
17998
|
+
if (partial.length > 1)
|
|
17999
|
+
return { ok: false, reason: "ambiguous", matches: partial };
|
|
18000
|
+
return { ok: false, reason: "not_found", lenses: all };
|
|
18001
|
+
}
|
|
17414
18002
|
function mergeFilter(current, toAddSectors, toExcludeSectors, sizes) {
|
|
17415
18003
|
const items = current?.lens_filter?.items ?? [];
|
|
17416
18004
|
const item = items[0] ?? { criteria: [] };
|
|
@@ -17444,11 +18032,15 @@ function mergeFilter(current, toAddSectors, toExcludeSectors, sizes) {
|
|
|
17444
18032
|
}
|
|
17445
18033
|
}
|
|
17446
18034
|
if (sizes && sizes.length > 0) {
|
|
18035
|
+
const normalizedSizes = sizes.map((s) => ({
|
|
18036
|
+
min: s.min ?? 0,
|
|
18037
|
+
max: s.max ?? 1e6
|
|
18038
|
+
}));
|
|
17447
18039
|
const idx = criteria.findIndex((c) => c.type === "size");
|
|
17448
18040
|
if (idx >= 0) {
|
|
17449
|
-
criteria[idx] = { type: "size", is_excluded: false, sizes };
|
|
18041
|
+
criteria[idx] = { type: "size", is_excluded: false, sizes: normalizedSizes };
|
|
17450
18042
|
} else {
|
|
17451
|
-
criteria.push({ type: "size", is_excluded: false, sizes });
|
|
18043
|
+
criteria.push({ type: "size", is_excluded: false, sizes: normalizedSizes });
|
|
17452
18044
|
}
|
|
17453
18045
|
}
|
|
17454
18046
|
return {
|
|
@@ -17456,6 +18048,9 @@ function mergeFilter(current, toAddSectors, toExcludeSectors, sizes) {
|
|
|
17456
18048
|
locations: current.locations ?? { results: [], parents: [] }
|
|
17457
18049
|
};
|
|
17458
18050
|
}
|
|
18051
|
+
function filterWriteBody(filter) {
|
|
18052
|
+
return { items: filter.lens_filter.items };
|
|
18053
|
+
}
|
|
17459
18054
|
var adjustAudience = {
|
|
17460
18055
|
name: "leadbay_adjust_audience",
|
|
17461
18056
|
annotations: {
|
|
@@ -17497,6 +18092,10 @@ var adjustAudience = {
|
|
|
17497
18092
|
description: "Company size buckets, e.g. [{min:30,max:300}]"
|
|
17498
18093
|
},
|
|
17499
18094
|
lensId: { type: "number", description: "Lens id (escape hatch)" },
|
|
18095
|
+
lensName: {
|
|
18096
|
+
type: "string",
|
|
18097
|
+
description: "Target a lens BY NAME (e.g. 'Joinery') instead of the active one. Resolved against your lenses \u2014 edit-only, does NOT switch your active lens. Unknown/ambiguous names are surfaced to pick from. Takes effect only when lensId is not given."
|
|
18098
|
+
},
|
|
17500
18099
|
save_for_org: {
|
|
17501
18100
|
type: "boolean",
|
|
17502
18101
|
description: "Admin only \u2014 propagate the change to the org-level lens for everyone (default false: per-user draft)"
|
|
@@ -17510,21 +18109,35 @@ var adjustAudience = {
|
|
|
17510
18109
|
},
|
|
17511
18110
|
outputSchema: {
|
|
17512
18111
|
type: "object",
|
|
17513
|
-
description: "
|
|
18112
|
+
description: "Return shapes: 'applied' on success; 'ambiguous_sectors' when free-text sectors matched multiple candidates (re-call with sector_ids); 'lens_not_found' / 'ambiguous_lens' when a lensName didn't resolve to exactly one lens (re-call with lensId or an exact lensName).",
|
|
17514
18113
|
properties: {
|
|
17515
18114
|
status: {
|
|
17516
18115
|
type: "string",
|
|
17517
|
-
description: "'ambiguous_sectors' or '
|
|
18116
|
+
description: "'applied', 'ambiguous_sectors', 'lens_not_found', or 'ambiguous_lens'."
|
|
17518
18117
|
},
|
|
17519
18118
|
sector_ambiguities: {
|
|
17520
18119
|
type: "array",
|
|
17521
18120
|
description: "Per ambiguous text: {sector_text, matches:[{id, name, score}]}. Agent picks an id and re-calls.",
|
|
17522
18121
|
items: { type: "object" }
|
|
17523
18122
|
},
|
|
18123
|
+
lenses: {
|
|
18124
|
+
type: "array",
|
|
18125
|
+
description: "On 'lens_not_found': the user's lenses [{id, name}] to pick from.",
|
|
18126
|
+
items: { type: "object" }
|
|
18127
|
+
},
|
|
18128
|
+
matches: {
|
|
18129
|
+
type: "array",
|
|
18130
|
+
description: "On 'ambiguous_lens': the lenses [{id, name}] the name matched.",
|
|
18131
|
+
items: { type: "object" }
|
|
18132
|
+
},
|
|
18133
|
+
lens_query: {
|
|
18134
|
+
type: "string",
|
|
18135
|
+
description: "On 'lens_not_found' / 'ambiguous_lens': the lensName the user asked for."
|
|
18136
|
+
},
|
|
17524
18137
|
message: { type: "string" },
|
|
17525
18138
|
lens_used: {
|
|
17526
18139
|
type: "object",
|
|
17527
|
-
description: "Resolved lens metadata: {id, name, was_draft, was_new, save_for_org}."
|
|
18140
|
+
description: "Resolved lens metadata: {id, name, was_draft, was_new, active_lens_changed, save_for_org}."
|
|
17528
18141
|
},
|
|
17529
18142
|
filter_applied: {
|
|
17530
18143
|
type: "object",
|
|
@@ -17537,28 +18150,63 @@ var adjustAudience = {
|
|
|
17537
18150
|
execute: async (client, params, ctx) => {
|
|
17538
18151
|
const me = await client.resolveMe();
|
|
17539
18152
|
const isAdmin = me.admin === true;
|
|
17540
|
-
|
|
18153
|
+
let namedLensId;
|
|
18154
|
+
if (params.lensId == null && params.lensName != null && params.lensName.trim() !== "") {
|
|
18155
|
+
const res = await resolveLensByName(client, params.lensName);
|
|
18156
|
+
if (!res.ok && res.reason === "not_found") {
|
|
18157
|
+
return {
|
|
18158
|
+
status: "lens_not_found",
|
|
18159
|
+
lens_query: params.lensName,
|
|
18160
|
+
lenses: res.lenses,
|
|
18161
|
+
message: `No lens named "${params.lensName}". Pick one of the listed lenses (pass lensId or an exact lensName), or create it first.`
|
|
18162
|
+
};
|
|
18163
|
+
}
|
|
18164
|
+
if (!res.ok && res.reason === "ambiguous") {
|
|
18165
|
+
return {
|
|
18166
|
+
status: "ambiguous_lens",
|
|
18167
|
+
lens_query: params.lensName,
|
|
18168
|
+
matches: res.matches,
|
|
18169
|
+
message: `"${params.lensName}" matched multiple lenses. Re-call with the exact lensName or the lensId of the one you mean.`
|
|
18170
|
+
};
|
|
18171
|
+
}
|
|
18172
|
+
if (res.ok)
|
|
18173
|
+
namedLensId = res.id;
|
|
18174
|
+
}
|
|
18175
|
+
const startingLensId = params.lensId ?? namedLensId ?? me.last_requested_lens ?? await client.resolveDefaultLens();
|
|
18176
|
+
const isNamedEdit = namedLensId != null && params.lensId == null;
|
|
17541
18177
|
const includeTexts = [
|
|
17542
18178
|
...params.sectors ?? [],
|
|
17543
18179
|
...params.sector_ids ?? []
|
|
17544
18180
|
];
|
|
17545
18181
|
const excludeTexts = params.exclude_sectors ?? [];
|
|
17546
|
-
const includeRes = await resolveSectors(client, includeTexts);
|
|
17547
|
-
const excludeRes = await resolveSectors(client, excludeTexts);
|
|
18182
|
+
const includeRes = await resolveSectors(client, includeTexts, ctx);
|
|
18183
|
+
const excludeRes = await resolveSectors(client, excludeTexts, ctx);
|
|
17548
18184
|
const ambiguities = [
|
|
17549
18185
|
...includeRes.ambiguities,
|
|
17550
18186
|
...excludeRes.ambiguities
|
|
17551
18187
|
];
|
|
17552
18188
|
if (ambiguities.length > 0) {
|
|
18189
|
+
const noMatch = ambiguities.filter((a) => a.matches.length === 0);
|
|
18190
|
+
const multi = ambiguities.filter((a) => a.matches.length > 0);
|
|
18191
|
+
const parts = [];
|
|
18192
|
+
if (noMatch.length > 0) {
|
|
18193
|
+
const names = noMatch.map((a) => `"${a.sector_text}"`).join(", ");
|
|
18194
|
+
parts.push(`Couldn't find a sector matching ${names}. Ask the user to rephrase or pick a known sector, then re-call with sector_ids=...`);
|
|
18195
|
+
}
|
|
18196
|
+
if (multi.length > 0) {
|
|
18197
|
+
const names = multi.map((a) => `"${a.sector_text}"`).join(", ");
|
|
18198
|
+
parts.push(`${names} matched multiple sectors. Pick from the matches and re-call with sector_ids=...`);
|
|
18199
|
+
}
|
|
17553
18200
|
return {
|
|
17554
18201
|
status: "ambiguous_sectors",
|
|
17555
18202
|
sector_ambiguities: ambiguities,
|
|
17556
|
-
message: "
|
|
18203
|
+
message: parts.join(" ")
|
|
17557
18204
|
};
|
|
17558
18205
|
}
|
|
17559
18206
|
const lens = await client.request("GET", `/lenses/${startingLensId}`);
|
|
17560
18207
|
const currentFilter = await client.request("GET", `/lenses/${startingLensId}/filter`);
|
|
17561
18208
|
const merged = mergeFilter(currentFilter, includeRes.resolved, excludeRes.resolved, params.sizes);
|
|
18209
|
+
const mergedBody = filterWriteBody(merged);
|
|
17562
18210
|
const isDefault = lens.is_default || lens.default;
|
|
17563
18211
|
const isUserLevel = lens.user_id != null;
|
|
17564
18212
|
const isOrgLevel = !isUserLevel && !isDefault;
|
|
@@ -17567,24 +18215,28 @@ var adjustAudience = {
|
|
|
17567
18215
|
let wasNew = false;
|
|
17568
18216
|
if (isDefault) {
|
|
17569
18217
|
const name = params.newLensName ?? `Custom audience \u2014 ${(/* @__PURE__ */ new Date()).toISOString().slice(0, 10)}`;
|
|
17570
|
-
const
|
|
17571
|
-
base: startingLensId,
|
|
18218
|
+
const newLens2 = await client.request("POST", "/lenses", {
|
|
18219
|
+
base: String(startingLensId),
|
|
17572
18220
|
name
|
|
17573
18221
|
});
|
|
17574
|
-
targetLensId =
|
|
18222
|
+
targetLensId = newLens2.id;
|
|
17575
18223
|
wasNew = true;
|
|
17576
|
-
await client.requestVoid("POST", `/lenses/${targetLensId}/filter`,
|
|
17577
|
-
|
|
18224
|
+
await client.requestVoid("POST", `/lenses/${targetLensId}/filter`, mergedBody);
|
|
18225
|
+
if (!isNamedEdit) {
|
|
18226
|
+
await client.requestVoid("POST", `/lenses/${targetLensId}/update_last_requested`);
|
|
18227
|
+
}
|
|
17578
18228
|
} else if (isUserLevel) {
|
|
17579
18229
|
try {
|
|
17580
|
-
await client.requestVoid("POST", `/lenses/${startingLensId}/filter`,
|
|
18230
|
+
await client.requestVoid("POST", `/lenses/${startingLensId}/filter`, mergedBody);
|
|
17581
18231
|
} catch (err) {
|
|
17582
18232
|
if (err?.code === "FORBIDDEN") {
|
|
17583
18233
|
wasDraft = true;
|
|
17584
18234
|
const draft = await client.request("POST", `/lenses/${startingLensId}/draft`);
|
|
17585
18235
|
targetLensId = draft.id;
|
|
17586
|
-
await client.requestVoid("POST", `/lenses/${targetLensId}/filter`,
|
|
17587
|
-
|
|
18236
|
+
await client.requestVoid("POST", `/lenses/${targetLensId}/filter`, mergedBody);
|
|
18237
|
+
if (!isNamedEdit) {
|
|
18238
|
+
await client.requestVoid("POST", `/lenses/${targetLensId}/update_last_requested`);
|
|
18239
|
+
}
|
|
17588
18240
|
} else {
|
|
17589
18241
|
throw err;
|
|
17590
18242
|
}
|
|
@@ -17596,7 +18248,7 @@ var adjustAudience = {
|
|
|
17596
18248
|
const draft = await client.request("POST", `/lenses/${startingLensId}/draft`);
|
|
17597
18249
|
targetLensId = draft.id;
|
|
17598
18250
|
try {
|
|
17599
|
-
await client.requestVoid("POST", `/lenses/${targetLensId}/filter`,
|
|
18251
|
+
await client.requestVoid("POST", `/lenses/${targetLensId}/filter`, mergedBody);
|
|
17600
18252
|
} catch (err) {
|
|
17601
18253
|
ctx?.logger?.warn?.(`adjust_audience: filter on draft ${targetLensId} failed: ${err?.message}`);
|
|
17602
18254
|
try {
|
|
@@ -17612,17 +18264,21 @@ var adjustAudience = {
|
|
|
17612
18264
|
}
|
|
17613
18265
|
throw err;
|
|
17614
18266
|
}
|
|
17615
|
-
|
|
18267
|
+
if (!isNamedEdit) {
|
|
18268
|
+
await client.requestVoid("POST", `/lenses/${targetLensId}/update_last_requested`);
|
|
18269
|
+
}
|
|
17616
18270
|
} else {
|
|
17617
18271
|
try {
|
|
17618
|
-
await client.requestVoid("POST", `/lenses/${startingLensId}/filter`,
|
|
18272
|
+
await client.requestVoid("POST", `/lenses/${startingLensId}/filter`, mergedBody);
|
|
17619
18273
|
} catch (err) {
|
|
17620
18274
|
throw err;
|
|
17621
18275
|
}
|
|
17622
18276
|
}
|
|
17623
18277
|
}
|
|
17624
|
-
|
|
18278
|
+
if (!isNamedEdit)
|
|
18279
|
+
client.invalidateMe();
|
|
17625
18280
|
client.invalidateDefaultLens();
|
|
18281
|
+
const namedEditForkedMessage = isNamedEdit && (wasNew || wasDraft) ? ` Note: "${lens.name}" can't be edited in place, so the change was applied to a ${wasDraft ? "personal draft" : "new copy"} (id ${targetLensId}); your active lens is unchanged.` : "";
|
|
17626
18282
|
return {
|
|
17627
18283
|
status: "applied",
|
|
17628
18284
|
lens_used: {
|
|
@@ -17630,10 +18286,11 @@ var adjustAudience = {
|
|
|
17630
18286
|
name: lens.name,
|
|
17631
18287
|
was_draft: wasDraft,
|
|
17632
18288
|
was_new: wasNew,
|
|
18289
|
+
active_lens_changed: !isNamedEdit && (wasNew || wasDraft),
|
|
17633
18290
|
save_for_org: params.save_for_org === true && isAdmin && isOrgLevel
|
|
17634
18291
|
},
|
|
17635
18292
|
filter_applied: merged,
|
|
17636
|
-
message: wasDraft ? "Applied to your personal draft of the org lens (your view only)." : wasNew ? `Created a new user-level lens "${lens.name}" with the filter (you can rename via leadbay_update_lens).` : "Applied directly to the lens.",
|
|
18293
|
+
message: (wasDraft ? "Applied to your personal draft of the org lens (your view only)." : wasNew ? `Created a new user-level lens "${lens.name}" with the filter (you can rename via leadbay_update_lens).` : "Applied directly to the lens.") + namedEditForkedMessage,
|
|
17637
18294
|
_meta: { region: client.region }
|
|
17638
18295
|
};
|
|
17639
18296
|
}
|
|
@@ -18005,6 +18662,384 @@ var extendLens = {
|
|
|
18005
18662
|
}
|
|
18006
18663
|
};
|
|
18007
18664
|
|
|
18665
|
+
// ../core/dist/composite/my-lenses.js
|
|
18666
|
+
var sid = (v) => v == null ? null : String(v);
|
|
18667
|
+
async function listWithActive(client) {
|
|
18668
|
+
const lenses = await client.request("GET", "/lenses");
|
|
18669
|
+
const me = await client.resolveMe().catch(() => null);
|
|
18670
|
+
const activeFromMe = sid(me?.last_requested_lens);
|
|
18671
|
+
const active_lens_id = activeFromMe ?? sid(lenses.find((l) => l.is_last_active)?.id) ?? null;
|
|
18672
|
+
return {
|
|
18673
|
+
active_lens_id,
|
|
18674
|
+
lenses: lenses.map((l) => ({
|
|
18675
|
+
id: sid(l.id),
|
|
18676
|
+
name: l.name,
|
|
18677
|
+
description: l.description ?? null,
|
|
18678
|
+
is_active: sid(l.id) === active_lens_id,
|
|
18679
|
+
is_default: l.is_default === true || l.default === true
|
|
18680
|
+
}))
|
|
18681
|
+
};
|
|
18682
|
+
}
|
|
18683
|
+
var myLenses = {
|
|
18684
|
+
name: "leadbay_my_lenses",
|
|
18685
|
+
annotations: {
|
|
18686
|
+
title: "List, switch, edit, or delete your lenses",
|
|
18687
|
+
// No args → pure read. The delete mode issues DELETE /lenses/:id (an
|
|
18688
|
+
// irreversible side effect), so the tool is destructive — clients must
|
|
18689
|
+
// treat it as approval-required, not auto-run. The delete path is itself
|
|
18690
|
+
// confirm-gated (preview unless confirm:true). switch/edit are not
|
|
18691
|
+
// idempotent across modes either, so don't claim idempotency.
|
|
18692
|
+
readOnlyHint: false,
|
|
18693
|
+
destructiveHint: true,
|
|
18694
|
+
idempotentHint: false,
|
|
18695
|
+
openWorldHint: true
|
|
18696
|
+
},
|
|
18697
|
+
description: leadbay_my_lenses,
|
|
18698
|
+
inputSchema: {
|
|
18699
|
+
type: "object",
|
|
18700
|
+
properties: {
|
|
18701
|
+
switchToLensId: {
|
|
18702
|
+
type: ["string", "number"],
|
|
18703
|
+
description: "When set, switch the active lens to this id (must be one of the user's lenses), then return the refreshed list."
|
|
18704
|
+
},
|
|
18705
|
+
editLensId: {
|
|
18706
|
+
type: ["string", "number"],
|
|
18707
|
+
description: "When set, edit this lens's metadata \u2014 provide newName and/or newDescription. Must be one of the user's lenses."
|
|
18708
|
+
},
|
|
18709
|
+
newName: {
|
|
18710
|
+
type: "string",
|
|
18711
|
+
description: "New lens name (used with editLensId)."
|
|
18712
|
+
},
|
|
18713
|
+
newDescription: {
|
|
18714
|
+
type: "string",
|
|
18715
|
+
description: "New lens description (used with editLensId). Pass an empty string to clear it."
|
|
18716
|
+
},
|
|
18717
|
+
deleteLensId: {
|
|
18718
|
+
type: ["string", "number"],
|
|
18719
|
+
description: "When set, delete this lens. DESTRUCTIVE \u2014 returns a delete_preview unless confirm:true. Cannot delete the default lens."
|
|
18720
|
+
},
|
|
18721
|
+
confirm: {
|
|
18722
|
+
type: "boolean",
|
|
18723
|
+
description: "Required (=true) to actually delete. Without it, deleteLensId returns a preview to confirm with the user first."
|
|
18724
|
+
}
|
|
18725
|
+
},
|
|
18726
|
+
additionalProperties: false
|
|
18727
|
+
},
|
|
18728
|
+
outputSchema: {
|
|
18729
|
+
type: "object",
|
|
18730
|
+
properties: {
|
|
18731
|
+
status: {
|
|
18732
|
+
type: "string",
|
|
18733
|
+
description: "'listed', 'switched', 'already_active', 'edited', 'deleted', 'delete_preview' (confirm to proceed), 'cannot_delete_default', or 'not_found'."
|
|
18734
|
+
},
|
|
18735
|
+
switched: { type: "boolean", description: "True when this call changed the active lens." },
|
|
18736
|
+
edited: { type: "boolean", description: "True when this call renamed/re-described a lens." },
|
|
18737
|
+
deleted: { type: "boolean", description: "True when this call deleted a lens." },
|
|
18738
|
+
will_delete: {
|
|
18739
|
+
type: "object",
|
|
18740
|
+
description: "On 'delete_preview': the lens that WILL be deleted {id, name}. Nothing removed yet."
|
|
18741
|
+
},
|
|
18742
|
+
active_lens_id: { type: ["string", "null"] },
|
|
18743
|
+
lenses: {
|
|
18744
|
+
type: "array",
|
|
18745
|
+
description: "The user's lenses. Each: {id, name, description, is_active}.",
|
|
18746
|
+
items: { type: "object" }
|
|
18747
|
+
},
|
|
18748
|
+
message: { type: "string" }
|
|
18749
|
+
},
|
|
18750
|
+
required: ["status", "lenses", "active_lens_id"]
|
|
18751
|
+
},
|
|
18752
|
+
execute: async (client, params) => {
|
|
18753
|
+
if (params.deleteLensId != null) {
|
|
18754
|
+
const targetId = sid(params.deleteLensId);
|
|
18755
|
+
const before = await listWithActive(client);
|
|
18756
|
+
const target = before.lenses.find((l) => l.id === targetId);
|
|
18757
|
+
if (!target) {
|
|
18758
|
+
return {
|
|
18759
|
+
status: "not_found",
|
|
18760
|
+
switched: false,
|
|
18761
|
+
edited: false,
|
|
18762
|
+
deleted: false,
|
|
18763
|
+
active_lens_id: before.active_lens_id,
|
|
18764
|
+
lenses: before.lenses,
|
|
18765
|
+
message: `No lens with id ${targetId}. Pick one from the list.`
|
|
18766
|
+
};
|
|
18767
|
+
}
|
|
18768
|
+
if (target.is_default) {
|
|
18769
|
+
return {
|
|
18770
|
+
status: "cannot_delete_default",
|
|
18771
|
+
switched: false,
|
|
18772
|
+
edited: false,
|
|
18773
|
+
deleted: false,
|
|
18774
|
+
active_lens_id: before.active_lens_id,
|
|
18775
|
+
lenses: before.lenses,
|
|
18776
|
+
message: `"${target.name}" is the default lens and can't be deleted.`
|
|
18777
|
+
};
|
|
18778
|
+
}
|
|
18779
|
+
if (params.confirm !== true) {
|
|
18780
|
+
return {
|
|
18781
|
+
status: "delete_preview",
|
|
18782
|
+
switched: false,
|
|
18783
|
+
edited: false,
|
|
18784
|
+
deleted: false,
|
|
18785
|
+
active_lens_id: before.active_lens_id,
|
|
18786
|
+
lenses: before.lenses,
|
|
18787
|
+
will_delete: { id: target.id, name: target.name },
|
|
18788
|
+
message: `About to delete "${target.name}". This can't be undone. Confirm with the user, then re-call with confirm:true.`
|
|
18789
|
+
};
|
|
18790
|
+
}
|
|
18791
|
+
await client.requestVoid("DELETE", `/lenses/${targetId}`);
|
|
18792
|
+
client.invalidateMe();
|
|
18793
|
+
client.invalidateDefaultLens();
|
|
18794
|
+
const after = await listWithActive(client);
|
|
18795
|
+
return {
|
|
18796
|
+
status: "deleted",
|
|
18797
|
+
switched: false,
|
|
18798
|
+
edited: false,
|
|
18799
|
+
deleted: true,
|
|
18800
|
+
active_lens_id: after.active_lens_id,
|
|
18801
|
+
lenses: after.lenses,
|
|
18802
|
+
message: `Deleted "${target.name}".`
|
|
18803
|
+
};
|
|
18804
|
+
}
|
|
18805
|
+
if (params.editLensId != null) {
|
|
18806
|
+
const targetId = sid(params.editLensId);
|
|
18807
|
+
const before = await listWithActive(client);
|
|
18808
|
+
const target = before.lenses.find((l) => l.id === targetId);
|
|
18809
|
+
if (!target) {
|
|
18810
|
+
return {
|
|
18811
|
+
status: "not_found",
|
|
18812
|
+
switched: false,
|
|
18813
|
+
edited: false,
|
|
18814
|
+
active_lens_id: before.active_lens_id,
|
|
18815
|
+
lenses: before.lenses,
|
|
18816
|
+
message: `No lens with id ${targetId}. Pick one from the list.`
|
|
18817
|
+
};
|
|
18818
|
+
}
|
|
18819
|
+
const body = {};
|
|
18820
|
+
const newName = params.newName?.trim();
|
|
18821
|
+
if (newName)
|
|
18822
|
+
body.name = newName;
|
|
18823
|
+
if (params.newDescription !== void 0)
|
|
18824
|
+
body.description = params.newDescription;
|
|
18825
|
+
if (Object.keys(body).length === 0) {
|
|
18826
|
+
return {
|
|
18827
|
+
status: "not_found",
|
|
18828
|
+
switched: false,
|
|
18829
|
+
edited: false,
|
|
18830
|
+
active_lens_id: before.active_lens_id,
|
|
18831
|
+
lenses: before.lenses,
|
|
18832
|
+
message: `Nothing to change on "${target.name}" \u2014 provide newName and/or newDescription.`
|
|
18833
|
+
};
|
|
18834
|
+
}
|
|
18835
|
+
await client.requestVoid("POST", `/lenses/${targetId}`, body);
|
|
18836
|
+
client.invalidateDefaultLens();
|
|
18837
|
+
const changed = [
|
|
18838
|
+
body.name != null ? `renamed to "${body.name}"` : null,
|
|
18839
|
+
body.description !== void 0 ? "description updated" : null
|
|
18840
|
+
].filter(Boolean).join(", ");
|
|
18841
|
+
const after = await listWithActive(client);
|
|
18842
|
+
return {
|
|
18843
|
+
status: "edited",
|
|
18844
|
+
switched: false,
|
|
18845
|
+
edited: true,
|
|
18846
|
+
active_lens_id: after.active_lens_id,
|
|
18847
|
+
lenses: after.lenses,
|
|
18848
|
+
message: `"${target.name}" \u2014 ${changed}.`
|
|
18849
|
+
};
|
|
18850
|
+
}
|
|
18851
|
+
if (params.switchToLensId != null) {
|
|
18852
|
+
const targetId = sid(params.switchToLensId);
|
|
18853
|
+
const before = await listWithActive(client);
|
|
18854
|
+
const target = before.lenses.find((l) => l.id === targetId);
|
|
18855
|
+
if (!target) {
|
|
18856
|
+
return {
|
|
18857
|
+
status: "not_found",
|
|
18858
|
+
switched: false,
|
|
18859
|
+
edited: false,
|
|
18860
|
+
active_lens_id: before.active_lens_id,
|
|
18861
|
+
lenses: before.lenses,
|
|
18862
|
+
message: `No lens with id ${targetId}. Pick an id from the list.`
|
|
18863
|
+
};
|
|
18864
|
+
}
|
|
18865
|
+
if (target.is_active) {
|
|
18866
|
+
return {
|
|
18867
|
+
status: "already_active",
|
|
18868
|
+
switched: false,
|
|
18869
|
+
edited: false,
|
|
18870
|
+
active_lens_id: before.active_lens_id,
|
|
18871
|
+
lenses: before.lenses,
|
|
18872
|
+
message: `"${target.name}" is already your active lens.`
|
|
18873
|
+
};
|
|
18874
|
+
}
|
|
18875
|
+
await client.requestVoid("POST", `/lenses/${targetId}/update_last_requested`);
|
|
18876
|
+
client.invalidateMe();
|
|
18877
|
+
client.invalidateDefaultLens();
|
|
18878
|
+
const after = await listWithActive(client);
|
|
18879
|
+
return {
|
|
18880
|
+
status: "switched",
|
|
18881
|
+
switched: true,
|
|
18882
|
+
edited: false,
|
|
18883
|
+
active_lens_id: after.active_lens_id,
|
|
18884
|
+
lenses: after.lenses,
|
|
18885
|
+
message: `Now showing "${target.name}".`
|
|
18886
|
+
};
|
|
18887
|
+
}
|
|
18888
|
+
const { lenses, active_lens_id } = await listWithActive(client);
|
|
18889
|
+
return { status: "listed", switched: false, edited: false, active_lens_id, lenses };
|
|
18890
|
+
}
|
|
18891
|
+
};
|
|
18892
|
+
|
|
18893
|
+
// ../core/dist/composite/new-lens.js
|
|
18894
|
+
var EMPTY_FILTER = {
|
|
18895
|
+
lens_filter: { items: [{ criteria: [] }] },
|
|
18896
|
+
locations: { results: [], parents: [] }
|
|
18897
|
+
};
|
|
18898
|
+
var newLens = {
|
|
18899
|
+
name: "leadbay_new_lens",
|
|
18900
|
+
annotations: {
|
|
18901
|
+
title: "Create a new named lens",
|
|
18902
|
+
readOnlyHint: false,
|
|
18903
|
+
destructiveHint: false,
|
|
18904
|
+
idempotentHint: false,
|
|
18905
|
+
// each call creates a distinct lens
|
|
18906
|
+
openWorldHint: true
|
|
18907
|
+
},
|
|
18908
|
+
description: leadbay_new_lens,
|
|
18909
|
+
inputSchema: {
|
|
18910
|
+
type: "object",
|
|
18911
|
+
properties: {
|
|
18912
|
+
name: { type: "string", description: "Display name for the new lens (required)." },
|
|
18913
|
+
sectors: {
|
|
18914
|
+
type: "array",
|
|
18915
|
+
items: { type: "string" },
|
|
18916
|
+
description: "Sectors to include \u2014 free text (auto-resolved) or ids."
|
|
18917
|
+
},
|
|
18918
|
+
exclude_sectors: {
|
|
18919
|
+
type: "array",
|
|
18920
|
+
items: { type: "string" },
|
|
18921
|
+
description: "Sectors to exclude \u2014 free text or ids."
|
|
18922
|
+
},
|
|
18923
|
+
sizes: {
|
|
18924
|
+
type: "array",
|
|
18925
|
+
items: {
|
|
18926
|
+
type: "object",
|
|
18927
|
+
properties: { min: { type: "number" }, max: { type: "number" } }
|
|
18928
|
+
},
|
|
18929
|
+
description: "Company size buckets, e.g. [{min:30,max:300}]."
|
|
18930
|
+
},
|
|
18931
|
+
base: {
|
|
18932
|
+
type: "number",
|
|
18933
|
+
description: "Lens id to clone from. Defaults to the active/default lens."
|
|
18934
|
+
},
|
|
18935
|
+
description: { type: "string", description: "Optional lens description." },
|
|
18936
|
+
confirm: {
|
|
18937
|
+
type: "boolean",
|
|
18938
|
+
description: "Safety gate. Defaults to false \u2192 the tool returns a PREVIEW and creates nothing. Show the preview to the user, get their explicit go-ahead, then re-call the SAME args with confirm:true to actually create the lens."
|
|
18939
|
+
}
|
|
18940
|
+
},
|
|
18941
|
+
required: ["name"],
|
|
18942
|
+
additionalProperties: false
|
|
18943
|
+
},
|
|
18944
|
+
outputSchema: {
|
|
18945
|
+
type: "object",
|
|
18946
|
+
description: "'preview' (default, NOTHING created \u2014 confirm with the user then re-call with confirm:true); 'created' on success; 'ambiguous_sectors' when free-text sectors didn't resolve (re-call with sector ids \u2014 the lens was NOT created).",
|
|
18947
|
+
properties: {
|
|
18948
|
+
status: { type: "string", description: "'preview', 'created', 'ambiguous_sectors', or 'orphan_created' (filter write failed + cleanup failed)." },
|
|
18949
|
+
will_create: {
|
|
18950
|
+
type: "object",
|
|
18951
|
+
description: "On 'preview': what WILL be created \u2014 {name, description, sectors, exclude_sectors, sizes}. Nothing has been written yet."
|
|
18952
|
+
},
|
|
18953
|
+
filter_preview: { type: "object", description: "On 'preview': the FilterPayload that would be applied." },
|
|
18954
|
+
lens: {
|
|
18955
|
+
type: "object",
|
|
18956
|
+
description: "On 'created': the created lens {id, name}."
|
|
18957
|
+
},
|
|
18958
|
+
sector_ambiguities: {
|
|
18959
|
+
type: "array",
|
|
18960
|
+
description: "On 'ambiguous_sectors': per text {sector_text, matches:[{id,name,score}]}.",
|
|
18961
|
+
items: { type: "object" }
|
|
18962
|
+
},
|
|
18963
|
+
filter_applied: { type: "object", description: "On 'created': the FilterPayload POSTed to the new lens." },
|
|
18964
|
+
message: { type: "string" },
|
|
18965
|
+
_meta: { type: "object" }
|
|
18966
|
+
},
|
|
18967
|
+
required: ["status"]
|
|
18968
|
+
},
|
|
18969
|
+
execute: async (client, params, ctx) => {
|
|
18970
|
+
const includeRes = await resolveSectors(client, params.sectors ?? [], ctx);
|
|
18971
|
+
const excludeRes = await resolveSectors(client, params.exclude_sectors ?? [], ctx);
|
|
18972
|
+
const ambiguities = [...includeRes.ambiguities, ...excludeRes.ambiguities];
|
|
18973
|
+
if (ambiguities.length > 0) {
|
|
18974
|
+
const noMatch = ambiguities.filter((a) => a.matches.length === 0);
|
|
18975
|
+
const multi = ambiguities.filter((a) => a.matches.length > 0);
|
|
18976
|
+
const parts = [];
|
|
18977
|
+
if (noMatch.length > 0) {
|
|
18978
|
+
parts.push(`Couldn't find a sector matching ${noMatch.map((a) => `"${a.sector_text}"`).join(", ")}. Pick a known sector and re-call (lens not yet created).`);
|
|
18979
|
+
}
|
|
18980
|
+
if (multi.length > 0) {
|
|
18981
|
+
parts.push(`${multi.map((a) => `"${a.sector_text}"`).join(", ")} matched multiple sectors. Pick from the matches and re-call with the sector id.`);
|
|
18982
|
+
}
|
|
18983
|
+
return {
|
|
18984
|
+
status: "ambiguous_sectors",
|
|
18985
|
+
sector_ambiguities: ambiguities,
|
|
18986
|
+
message: parts.join(" ")
|
|
18987
|
+
};
|
|
18988
|
+
}
|
|
18989
|
+
const merged = mergeFilter(EMPTY_FILTER, includeRes.resolved, excludeRes.resolved, params.sizes);
|
|
18990
|
+
if (params.confirm !== true) {
|
|
18991
|
+
return {
|
|
18992
|
+
status: "preview",
|
|
18993
|
+
will_create: {
|
|
18994
|
+
name: params.name,
|
|
18995
|
+
description: params.description ?? null,
|
|
18996
|
+
sectors: includeRes.resolved,
|
|
18997
|
+
exclude_sectors: excludeRes.resolved,
|
|
18998
|
+
sizes: merged.lens_filter.items[0].criteria.find((c) => c.type === "size") ?? null
|
|
18999
|
+
},
|
|
19000
|
+
filter_preview: merged,
|
|
19001
|
+
message: `About to create "${params.name}". Confirm with the user, then re-call with confirm:true.`,
|
|
19002
|
+
_meta: { region: client.region }
|
|
19003
|
+
};
|
|
19004
|
+
}
|
|
19005
|
+
const base = params.base ?? await client.resolveDefaultLens();
|
|
19006
|
+
const created = await client.request("POST", "/lenses", {
|
|
19007
|
+
base: String(base),
|
|
19008
|
+
name: params.name,
|
|
19009
|
+
description: params.description
|
|
19010
|
+
});
|
|
19011
|
+
const hasCriteria = merged.lens_filter.items[0].criteria.length > 0;
|
|
19012
|
+
if (hasCriteria) {
|
|
19013
|
+
try {
|
|
19014
|
+
await client.requestVoid("POST", `/lenses/${created.id}/filter`, filterWriteBody(merged));
|
|
19015
|
+
} catch (err) {
|
|
19016
|
+
ctx?.logger?.warn?.(`new_lens: filter write on new lens ${created.id} failed: ${err?.message} \u2014 rolling back`);
|
|
19017
|
+
try {
|
|
19018
|
+
await client.requestVoid("DELETE", `/lenses/${created.id}`);
|
|
19019
|
+
} catch {
|
|
19020
|
+
client.invalidateDefaultLens();
|
|
19021
|
+
return {
|
|
19022
|
+
status: "orphan_created",
|
|
19023
|
+
lens: { id: created.id, name: created.name },
|
|
19024
|
+
message: `Created "${created.name}" but applying its filter failed, and cleanup also failed. The lens exists with no criteria \u2014 delete it via leadbay_my_lenses(deleteLensId:"${created.id}", confirm:true) or set its audience with leadbay_adjust_audience.`,
|
|
19025
|
+
_meta: { region: client.region }
|
|
19026
|
+
};
|
|
19027
|
+
}
|
|
19028
|
+
client.invalidateDefaultLens();
|
|
19029
|
+
throw err;
|
|
19030
|
+
}
|
|
19031
|
+
}
|
|
19032
|
+
client.invalidateDefaultLens();
|
|
19033
|
+
return {
|
|
19034
|
+
status: "created",
|
|
19035
|
+
lens: { id: created.id, name: created.name },
|
|
19036
|
+
filter_applied: merged,
|
|
19037
|
+
message: `Created "${created.name}".`,
|
|
19038
|
+
_meta: { region: client.region }
|
|
19039
|
+
};
|
|
19040
|
+
}
|
|
19041
|
+
};
|
|
19042
|
+
|
|
18008
19043
|
// ../core/dist/composite/answer-clarification.js
|
|
18009
19044
|
var answerClarification = {
|
|
18010
19045
|
name: "leadbay_answer_clarification",
|
|
@@ -18524,7 +19559,6 @@ var granularReadTools = [
|
|
|
18524
19559
|
getQuota,
|
|
18525
19560
|
getLensFilter,
|
|
18526
19561
|
getLensScoring,
|
|
18527
|
-
listSectors,
|
|
18528
19562
|
listLocations,
|
|
18529
19563
|
getUserPrompt,
|
|
18530
19564
|
getClarification,
|
|
@@ -18595,6 +19629,11 @@ var compositeReadTools = [
|
|
|
18595
19629
|
// it for discoverability; expose it always-on so agents can find custom fields
|
|
18596
19630
|
// without needing LEADBAY_MCP_ADVANCED=1.
|
|
18597
19631
|
listMappableFields,
|
|
19632
|
+
// listSectors is granular-shaped but ALWAYS exposed: it's the sector taxonomy
|
|
19633
|
+
// lookup the agent needs to STOP guessing sector names (and to feed
|
|
19634
|
+
// leadbay_new_lens / leadbay_adjust_audience). Without it the agent can only
|
|
19635
|
+
// probe sectors by trial-and-error or ask the user to read the web UI.
|
|
19636
|
+
listSectors,
|
|
18598
19637
|
// Billing / top-up tools — granular-shaped but ALWAYS exposed because
|
|
18599
19638
|
// they're the canonical recovery path from a QUOTA_EXCEEDED wall. If
|
|
18600
19639
|
// they were gated behind LEADBAY_MCP_ADVANCED=1 the agent would
|
|
@@ -18609,7 +19648,13 @@ var compositeReadTools = [
|
|
|
18609
19648
|
// didn't deliver"). Does not mutate Leadbay state; emits a PostHog
|
|
18610
19649
|
// event only. Companion to leadbay_report_outreach (which DOES write
|
|
18611
19650
|
// to the backend and stays gated behind LEADBAY_MCP_WRITE).
|
|
18612
|
-
reportFriction
|
|
19651
|
+
reportFriction,
|
|
19652
|
+
// Notification ack — ALWAYS exposed even though it POSTs to /seen.
|
|
19653
|
+
// _meta.notifications surfaces terminal bulk-progress notifications on
|
|
19654
|
+
// every tool response regardless of write gating; without ack the agent
|
|
19655
|
+
// sees the same entries on every call forever. Pairing the surfacing
|
|
19656
|
+
// channel with the clearing tool is non-optional.
|
|
19657
|
+
acknowledgeNotification
|
|
18613
19658
|
];
|
|
18614
19659
|
var compositeWriteTools = [
|
|
18615
19660
|
bulkQualifyLeads,
|
|
@@ -18637,7 +19682,16 @@ var compositeWriteTools = [
|
|
|
18637
19682
|
removeLeadsFromCampaign,
|
|
18638
19683
|
// Lens extend — agent-driven on-demand fill (additive). Gated behind
|
|
18639
19684
|
// LEADBAY_MCP_WRITE=1. Subject to per-org daily LENS_EXTRA_REFILL quota.
|
|
18640
|
-
extendLens
|
|
19685
|
+
extendLens,
|
|
19686
|
+
// Lens list/switch — read-first (no args = pure list); a switchToLensId
|
|
19687
|
+
// changes the active lens. In compositeWriteTools because the switch path
|
|
19688
|
+
// mutates last_requested_lens, but it stays on the default surface
|
|
19689
|
+
// (write is on by default since 0.3.0).
|
|
19690
|
+
myLenses,
|
|
19691
|
+
// Lens creation — make a brand-new named lens with sectors/sizes in one
|
|
19692
|
+
// call. Default-surface so "create a lens called X for Y" works without
|
|
19693
|
+
// the advanced gate.
|
|
19694
|
+
newLens
|
|
18641
19695
|
];
|
|
18642
19696
|
var compositeTools = [
|
|
18643
19697
|
...compositeReadTools,
|
|
@@ -19419,6 +20473,22 @@ function buildServer(client, opts = {}) {
|
|
|
19419
20473
|
});
|
|
19420
20474
|
}
|
|
19421
20475
|
};
|
|
20476
|
+
const maybeAttachNotifications = (result) => {
|
|
20477
|
+
const inbox = opts.notificationsInbox;
|
|
20478
|
+
if (!inbox) return;
|
|
20479
|
+
if (result === null || typeof result !== "object" || Array.isArray(result)) {
|
|
20480
|
+
return;
|
|
20481
|
+
}
|
|
20482
|
+
const entries = inbox.list();
|
|
20483
|
+
if (entries.length === 0) return;
|
|
20484
|
+
const envelope = result;
|
|
20485
|
+
const target = envelope.__markdown_envelope === true && envelope.structured !== null && typeof envelope.structured === "object" && !Array.isArray(envelope.structured) ? envelope.structured : envelope;
|
|
20486
|
+
const existingMeta = target._meta && typeof target._meta === "object" && !Array.isArray(target._meta) ? target._meta : {};
|
|
20487
|
+
target._meta = {
|
|
20488
|
+
...existingMeta,
|
|
20489
|
+
notifications: entries
|
|
20490
|
+
};
|
|
20491
|
+
};
|
|
19422
20492
|
const isLeadbayBusinessError = (err) => err != null && typeof err === "object" && err.error === true && typeof err.code === "string";
|
|
19423
20493
|
const buildBusinessCtx = (toolName, envelope, triggered_by) => {
|
|
19424
20494
|
const meta = envelope._meta ?? {};
|
|
@@ -19538,11 +20608,13 @@ function buildServer(client, opts = {}) {
|
|
|
19538
20608
|
const result = await tool.execute(client, args, {
|
|
19539
20609
|
logger: opts.logger,
|
|
19540
20610
|
bulkTracker: opts.bulkTracker,
|
|
20611
|
+
notificationsInbox: opts.notificationsInbox,
|
|
19541
20612
|
signal: extra.signal,
|
|
19542
20613
|
progress,
|
|
19543
20614
|
elicit
|
|
19544
20615
|
});
|
|
19545
20616
|
maybeAttachUpdate(name, result);
|
|
20617
|
+
maybeAttachNotifications(result);
|
|
19546
20618
|
if (result && typeof result === "object" && result.error === true) {
|
|
19547
20619
|
const envText = formatErrorForLLM(result);
|
|
19548
20620
|
const envDur = Date.now() - callStart;
|
|
@@ -19837,7 +20909,7 @@ function parseWriteEnv(env = process.env) {
|
|
|
19837
20909
|
}
|
|
19838
20910
|
|
|
19839
20911
|
// src/http-server.ts
|
|
19840
|
-
var VERSION = true ? "0.
|
|
20912
|
+
var VERSION = true ? "0.18.0" : "0.0.0-dev";
|
|
19841
20913
|
var PORT = Number(process.env.PORT ?? 8080);
|
|
19842
20914
|
var HOST = process.env.HOST ?? "0.0.0.0";
|
|
19843
20915
|
var sseSessions = /* @__PURE__ */ new Map();
|