@leadbay/mcp 0.34.1 → 0.35.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 +116 -0
- package/MIGRATION.md +51 -0
- package/README.md +1 -1
- package/dist/bin.js +1200 -1558
- package/dist/http-server.js +1061 -816
- package/dist/installer-electron.js +1 -1
- package/dist/installer-gui.js +1 -1
- package/package.json +1 -1
package/dist/bin.js
CHANGED
|
@@ -1513,7 +1513,7 @@ Some Leadbay tool responses include a \`_meta.notifications\` array listing **ba
|
|
|
1513
1513
|
- \`leadbay_qualify_status\` \u2192 \`still_running\` is empty: every launched lead has finished or failed. (\`in_progress\` also reads \`false\` on the fast path, but it can be \`null\` on the legacy/fallback read \u2014 so treat an empty \`still_running\` as terminal on its own; only require \`in_progress:false\` when that field is actually present.) LIKE imports, large qualification runs are async by design: \`leadbay_bulk_qualify_leads\` defaults to \`wait_for_completion:false\` for \`count > 5\` or chained workflows because blocking can time out, and \`leadbay_qualify_status\` may take minutes/hours. So don't force a long polling loop on a big run \u2014 return the handle/progress and let completion arrive via \`_meta.notifications\` \u2014 UNLESS the user explicitly asked to wait, or it's a small run that finishes quickly. A small \`wait_for_completion:true\` run you can poll to \`still_running\` empty inline.
|
|
1514
1514
|
- \`leadbay_import_status\` \u2192 \`status:"complete"\` (or \`"failed"\`). BUT imports are the exception to the stay-active loop: a large \`leadbay_import_leads({wait_for_completion:false})\` is meant to return a handle and resolve over minutes, and the tool does ONE refresh pass per call. Don't block the conversation looping on it \u2014 surface the returned progress/handle and let the completion arrive via \`_meta.notifications\` \u2014 UNLESS the user explicitly asked you to wait for the import, or it's a small import that finishes quickly.
|
|
1515
1515
|
|
|
1516
|
-
Enrichment polls to completion in-turn BY DEFAULT \u2014 the exception is when the user explicitly said to start it in the background / not wait ("kick it off, I'll check later"), in which case hand back the
|
|
1516
|
+
Enrichment polls to completion in-turn BY DEFAULT \u2014 the exception is when the user explicitly said to start it in the background / not wait ("kick it off, I'll check later"), in which case hand back the notification_id and let completion arrive via \`_meta.notifications\` (only when a notification id exists; if none was returned, tell the user to ask again / that you'll poll later, since nothing will auto-surface). For qualification and imports, poll inline only for small/quick runs or when the user explicitly asked you to wait; otherwise return the handle and let \`_meta.notifications\` deliver it. Either way, the user should never have to ask "is it done yet?" for work you kicked off in the same turn \u2014 you either report it or hand back a clear in-progress handle.
|
|
1517
1517
|
|
|
1518
1518
|
Also surfaced as a top-level \`notifications\` array on \`leadbay_account_status\` \u2014 same shape, same handling.
|
|
1519
1519
|
|
|
@@ -1820,23 +1820,57 @@ WHEN TO USE: the user asks for a clickable / interactive artifact, dashboard, or
|
|
|
1820
1820
|
|
|
1821
1821
|
WHEN NOT TO USE: the user wants a plain data answer (route to leadbay_pull_leads / leadbay_pull_followups) or to log a single real outreach you just did (leadbay_report_outreach).
|
|
1822
1822
|
`;
|
|
1823
|
-
leadbay_bulk_enrich_status = `Check status + per-lead contacts for a bulk enrichment you previously launched via leadbay_enrich_titles.
|
|
1823
|
+
leadbay_bulk_enrich_status = `Check status + per-lead contacts for a bulk enrichment you previously launched via leadbay_enrich_titles. Pass the \`notification_id\` for the job counters in one call, and/or the \`lead_ids\` + \`titles\` + \`email\` / \`phone\` the launch returned for per-lead progress. \`lead_ids\` alone is a valid call and is the reliable one: the job lookup is a scan of your recent notifications, so an archived job may not be found, and an enrichment notification does not always carry counters (then the tool answers \`ENRICH_JOB_NO_COUNTERS\` with the backend's running/finished flag and asks for \`lead_ids\`) \u2014 but the leads always answer. When \`include_contacts=true\` (opt-in), includes each contact's email/phone_number/job_title/enrichment.done.
|
|
1824
1824
|
|
|
1825
|
-
WHEN TO USE: poll this REPEATEDLY after leadbay_enrich_titles returns a \`
|
|
1825
|
+
WHEN TO USE: poll this REPEATEDLY after leadbay_enrich_titles returns a \`notification_id\`, staying active until the job is done \u2014 don't stop after one check, and don't hand the turn back to the user while progress is still climbing. "Done" = \`all_done:true\`, OR \`overall_progress.done\` has held steady across several SPACED polls (~15\u201330s apart) over at least ~90s\u20132 min of elapsed time (some contacts are unresolvable and never flip, so \`all_done\` can stay false forever \u2014 don't spin indefinitely). Do NOT declare a plateau from the first few back-to-back reads: right after launch, \`overall_progress.done\` can sit flat while the backend is still spinning the job up, so space your polls out and give it real elapsed time before treating a flat count as terminal. Also do NOT declare a plateau while the result carries \`partial_failures\` \u2014 a flat \`done\` there means a transient per-lead fetch error (e.g. a 429), NOT an unresolvable contact; keep polling (respecting any \`retry_after\`) or surface it as a temporary status failure, rather than reporting those leads as permanently unresolved. Default \`include_contacts=false\` for the cheap interim polls; set \`include_contacts=true\` on the read you report from to pull each lead's enriched contacts for the completion report.
|
|
1826
1826
|
|
|
1827
1827
|
WHEN NOT TO USE: as a substitute for leadbay_research_lead_by_id \u2014 that already includes enriched contacts for a single lead.
|
|
1828
1828
|
|
|
1829
|
+
## A launched job cannot be stopped
|
|
1830
|
+
|
|
1831
|
+
Leadbay has no cancel. A job started by \`leadbay_enrich_titles\`,
|
|
1832
|
+
\`leadbay_bulk_qualify_leads\`, \`leadbay_import_leads\` or
|
|
1833
|
+
\`leadbay_import_and_qualify\` runs to completion on Leadbay. The user cancelling
|
|
1834
|
+
in the chat, a request timeout, or a closed stream stops YOUR waiting, never the
|
|
1835
|
+
job, and \`cancelled: true\` on an earlier result means we stopped watching, not
|
|
1836
|
+
that the work stopped.
|
|
1837
|
+
|
|
1838
|
+
**This tool only reads.** Calling it again launches nothing and spends no quota,
|
|
1839
|
+
so poll it as often as the job needs \u2014 a timeout here is a reason to call it
|
|
1840
|
+
again, not a reason to stop.
|
|
1841
|
+
|
|
1842
|
+
One import state does NOT progress: a chunk cancelled before its mappings were
|
|
1843
|
+
committed reads \`running\` / \`committing\` forever. If the counts hold flat across
|
|
1844
|
+
several spaced polls, say so and stop, rather than polling on.
|
|
1845
|
+
|
|
1846
|
+
What must not be repeated is the LAUNCH \u2014 for work that actually launched. Re-run
|
|
1847
|
+
a launcher only for a subset that never started, never for the whole batch:
|
|
1848
|
+
|
|
1849
|
+
- \`failed[]\` entries with \`error:"not_queued"\`;
|
|
1850
|
+
- a \`rows_pending_upload\` count;
|
|
1851
|
+
- leads in \`still_running\` after a CANCELLED \`leadbay_import_and_qualify\`. Its
|
|
1852
|
+
fan-out is sequential, so an interruption leaves the remainder unlaunched and
|
|
1853
|
+
folds them in with the ones that did launch. Nothing in the result tells the
|
|
1854
|
+
two apart, and this tool cannot start either. Wait until the REST of the batch
|
|
1855
|
+
has settled: what launched settles in order, so leads still unanswered after
|
|
1856
|
+
that are the ones that never started. Only then call
|
|
1857
|
+
\`leadbay_bulk_qualify_leads({leadIds, lensId})\` for exactly those ids. A lead
|
|
1858
|
+
that is merely slow looks identical to one that never launched over a few
|
|
1859
|
+
polls, and re-launching it charges the user twice \u2014 when unsure, tell the user
|
|
1860
|
+
rather than guess.
|
|
1861
|
+
|
|
1862
|
+
|
|
1829
1863
|
## QUOTA \u2014 show where the user stands after the spend
|
|
1830
1864
|
|
|
1831
1865
|
Enrichment consumes QUOTA (the per-window allowance), not a separate credit wall. Once the job is done (all_done, or a plateau \u2014 see WHEN TO USE), show the user their refreshed quota: call \`leadbay_account_status\` and render the per-window quota it returns (the canonical surface). The result's \`credits_remaining\` field is **advisory internal context only \u2014 do NOT display it**: it comes from \`billing.ai_credits\` (a consumed counter, not remaining), so printing \`_(N credits remaining)_\` can show a fresh/quota-backed account a false "0 remaining." Never render a credits balance; the \`leadbay_account_status\` quota gauge is the only place the user's standing is shown. Do NOT report a "credits used" figure for this run either: the per-contact cost can't be scoped to this specific enrichment (a lead's contact list mixes in earlier runs), so any "X used" number would be misleading. Do the account_status refresh ONCE at completion \u2014 not on every in-progress poll.
|
|
1832
1866
|
|
|
1833
1867
|
## COMPLETION REPORT \u2014 what to tell the user when the job is done
|
|
1834
1868
|
|
|
1835
|
-
The result always carries \`overall_progress:{done,total,done_ratio}\` and, with \`include_contacts:true\`, \`leads[]\` each with contacts' \`email\` / \`phone_number\` / \`job_title\` / \`enrichment.done\`.
|
|
1869
|
+
The result always carries \`overall_progress:{done,total,done_ratio}\` and, with \`include_contacts:true\`, \`leads[]\` each with contacts' \`email\` / \`phone_number\` / \`job_title\` / \`enrichment.done\`. \`bulk_progress:{total_count,success_count,failure_count,quota_hit_count}\` is present only when you passed a \`notification_id\` AND the job was found; derive counts from \`overall_progress\` rather than assuming \`bulk_progress\` is there. With \`lead_ids\`, each entry carries \`enrichment_progress:{done,total}\` \u2014 \`done\` counts only contacts whose REQUESTED channel has landed, scoped to the \`titles\` this run enriched, so a lead's pre-existing CFO email cannot inflate a CEO run. A contact counts as done only when the REQUESTED channel actually landed \u2014 for a phone run, \`enrichment.done:true\` with no \`phone_number\` is NOT done (the contact may have been email-enriched earlier); read \`email\` / \`phone_number\` against the requested channels, don't rely on the \`enrichment.done\` flag alone (\`overall_progress\` already accounts for this). \`include_contacts\` returns each lead's FULL contact list (it fans out through \`leadbay_get_contacts\`), so it can include contacts of other roles that were enriched in earlier runs \u2014 filter your report to the \`titles\` this bulk enriched (match each contact's \`job_title\`), don't attribute a pre-existing email of an unrelated role to this run. Report it yourself in the SAME turn, without a reprompt and without deferring to a scheduled re-check: name which of the just-enriched contacts now have emails / phones, the done/total counts, and \u2014 if \`bulk_progress\` is present \u2014 any \`quota_hit_count\` (if non-zero, say some contacts were skipped because the quota window was exhausted, and point to \`leadbay_account_status\` for the wait-or-top-up choice). If you stopped on a plateau (not \`all_done\`), say so plainly \u2014 report the resolved contacts and name the ones that didn't resolve, keyed to the requested channel and the returned fields (no \`email\` \u2192 "no email found"; no \`phone_number\` \u2192 "no phone number found") \u2014 rather than implying the job fully finished. Then show refreshed quota via \`leadbay_account_status\` (see QUOTA above); do NOT print a credits-remaining line.
|
|
1836
1870
|
`;
|
|
1837
|
-
leadbay_bulk_qualify_leads = `Pick the next N unqualified leads in the active lens and qualify them (run AI rescore + web fetch). Pass \`wait_for_completion:false\` to return quickly with \`{status:'running',
|
|
1871
|
+
leadbay_bulk_qualify_leads = `Pick the next N unqualified leads in the active lens and qualify them (run AI rescore + web fetch). Pass \`wait_for_completion:false\` to return quickly with \`{status:'running', notification_id}\`; poll leadbay_qualify_status with that id. With \`wait_for_completion\` omitted/true, the legacy behavior polls until the answers are populated or a budget is exhausted. Already-qualified leads (those with a non-null \`ai_agent_lead_score\`) are silently no-ops on the backend, so this composite paginates past them to find fresh candidates. On 429 mid-fanout, stops launching but keeps polling already-launched leads.
|
|
1838
1872
|
|
|
1839
|
-
**Default to \`wait_for_completion:false\`** for any \`count > 5\` or when chained inside a multi-phase workflow \u2014 the blocking default can hit the MCP per-call timeout and surface as \`"Request timed out"\` even when the server is still working fine. The async pattern (capture \`
|
|
1873
|
+
**Default to \`wait_for_completion:false\`** for any \`count > 5\` or when chained inside a multi-phase workflow \u2014 the blocking default can hit the MCP per-call timeout and surface as \`"Request timed out"\` even when the server is still working fine. The async pattern (capture \`notification_id\`, poll \`leadbay_qualify_status\` every ~10s) is timeout-proof. Reserve the blocking form for tiny single-digit counts in interactive use.
|
|
1840
1874
|
|
|
1841
1875
|
Context: Leadbay auto-qualifies roughly the top 10 of each daily batch. Leads below the top ~10 are NOT worse \u2014 the system is saving resources. This tool is how the agent spends more resources to go deeper on promising-looking leads the user hasn't had time to surface yet.
|
|
1842
1876
|
|
|
@@ -1844,6 +1878,36 @@ WHEN TO USE: when the user wants more qualified leads than what's currently show
|
|
|
1844
1878
|
|
|
1845
1879
|
WHEN NOT TO USE: to qualify a single specific lead \u2014 that's leadbay_qualify_lead (granular, advanced).
|
|
1846
1880
|
|
|
1881
|
+
## A launched job cannot be stopped
|
|
1882
|
+
|
|
1883
|
+
Leadbay has no cancel. Once \`leadbay_enrich_titles\`, \`leadbay_bulk_qualify_leads\`,
|
|
1884
|
+
\`leadbay_import_leads\` or \`leadbay_import_and_qualify\` has returned a launched or
|
|
1885
|
+
running result, that work is queued on Leadbay and runs to completion, and the
|
|
1886
|
+
quota it costs is already committed. A discovery, preview or \`dry_run\` result
|
|
1887
|
+
launched nothing and is not covered here.
|
|
1888
|
+
|
|
1889
|
+
The user cancelling in the chat, a request timeout, or a closed stream stops YOUR
|
|
1890
|
+
waiting, never the job. \`cancelled: true\` means we stopped watching, not that the
|
|
1891
|
+
work stopped. What to do next depends on what you are holding:
|
|
1892
|
+
|
|
1893
|
+
- **A handle.** Poll the status tool with it, and do not launch the work that
|
|
1894
|
+
handle covers a second time \u2014 that spends the quota again on the same rows.
|
|
1895
|
+
\`leadbay_import_status\` takes \`importIds\`, so pass the values of \`import_ids\`
|
|
1896
|
+
under that name. A qualification started by \`leadbay_import_and_qualify\` has no
|
|
1897
|
+
notification of its own: resume it with
|
|
1898
|
+
\`leadbay_qualify_status({lead_ids, lens_id})\`.
|
|
1899
|
+
- **A handle AND a subset the result says never started** \u2014 \`failed[]\` entries
|
|
1900
|
+
with \`error:"not_queued"\`, or a \`rows_pending_upload\` count. Poll the handle
|
|
1901
|
+
for what was launched and re-run for that subset only, never for the whole
|
|
1902
|
+
batch.
|
|
1903
|
+
- **No result at all**, because the call timed out or the stream closed before it
|
|
1904
|
+
returned. Check \`leadbay_account_status\` first: the launch may have landed and
|
|
1905
|
+
finished. Calling the same tool again with the same arguments will usually hand
|
|
1906
|
+
back the job already launched rather than starting a second one, but that guard
|
|
1907
|
+
is in-memory, five minutes, and per process, so it is best-effort \u2014 say what you
|
|
1908
|
+
are about to re-run before you spend the user's quota on it.
|
|
1909
|
+
|
|
1910
|
+
|
|
1847
1911
|
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\`.
|
|
1848
1912
|
|
|
1849
1913
|
|
|
@@ -1865,7 +1929,7 @@ After the status line, propose the obvious refresh / progress-check / recovery a
|
|
|
1865
1929
|
|
|
1866
1930
|
Specifically for bulk qualify:
|
|
1867
1931
|
|
|
1868
|
-
- Kicked off async \u2192 \`"\u2713 Qualifying N lead(s) (
|
|
1932
|
+
- Kicked off async \u2192 \`"\u2713 Qualifying N lead(s) (notification_id <id>) \u2014 typically ~M minutes. I'll refresh your leads view when it's done."\`
|
|
1869
1933
|
- Blocking call returned with answers \u2192 \`"\u2713 Qualified N lead(s). Refresh your leads to see the new \u2756 caps."\`
|
|
1870
1934
|
- Already-qualified short-circuit \u2192 \`"All N leads are already qualified \u2014 no work to do."\`
|
|
1871
1935
|
- 429 mid-fanout \u2192 \`"\u26A0 Rate-limited after launching M of N \u2014 already-launched leads will complete; re-call later for the rest."\`
|
|
@@ -2328,6 +2392,23 @@ WHEN TO USE: when the user has already picked WHO they want on a company and you
|
|
|
2328
2392
|
|
|
2329
2393
|
WHEN NOT TO USE: for bulk enrichment by job title across many leads \u2014 use leadbay_enrich_titles, which handles the selection lifecycle and returns a clean preview/launch flow. Not to mark someone as the priority contact \u2014 that is leadbay_pin_contact, and pinning does not enrich anyone.
|
|
2330
2394
|
|
|
2395
|
+
## A launched job cannot be stopped, and this tool has no retry guard
|
|
2396
|
+
|
|
2397
|
+
Leadbay has no cancel. Once this call returns having actually launched, the work
|
|
2398
|
+
is queued on Leadbay and runs to completion, and the quota it costs is already
|
|
2399
|
+
committed. A \`dry_run\` result reached no backend and spent nothing. The user
|
|
2400
|
+
cancelling in the chat, a request timeout, or a closed stream stops YOUR waiting,
|
|
2401
|
+
never the job.
|
|
2402
|
+
|
|
2403
|
+
Unlike the composite launchers, this tool has **no double-launch guard**: calling
|
|
2404
|
+
it again always issues a new paid launch, even seconds later with identical
|
|
2405
|
+
arguments. So when a call returns nothing at all, do not simply retry. Read the
|
|
2406
|
+
record back first \u2014 \`leadbay_research_lead_by_id\` or \`leadbay_get_contacts\` for a
|
|
2407
|
+
lead, \`leadbay_account_status\` for background work that has since finished \u2014 to
|
|
2408
|
+
see whether the launch already landed, and tell the user what you are about to
|
|
2409
|
+
spend before spending it again.
|
|
2410
|
+
|
|
2411
|
+
|
|
2331
2412
|
## QUOTA, NOT CREDITS
|
|
2332
2413
|
|
|
2333
2414
|
Enrichment is gated by QUOTA (the per-window allowance in \`leadbay_account_status\`), not a credit balance. **Never pre-refuse because a credit number looks low or zero** \u2014 a freemium/fresh account with quota left can enrich even when its credit counter reads 0. The reveal either fits the remaining quota or the backend returns 429 (\`quota_exceeded\`); only THEN surface the exhausted window + wait-or-top-up choice. The \`credits_remaining\` field on the result is **advisory internal context only \u2014 do NOT display it**. Because it can read \`0\` on an account that still has quota, printing \`_(N credits remaining)_\` would falsely tell the user they're out. Do not render a credits balance at all; if the user asks where they stand, call \`leadbay_account_status\` and show the quota gauge instead. The actual per-contact cost (\`enrichment.credits_used\`) appears on the contact after enrichment.
|
|
@@ -2342,6 +2423,36 @@ WHEN TO USE: as the agent's go-to enrichment entry point, immediately before pro
|
|
|
2342
2423
|
|
|
2343
2424
|
WHEN NOT TO USE: to enrich a single named contact \u2014 that's leadbay_enrich_contacts. Speculatively, before the user has committed to outreaching \u2014 enrichment consumes quota. **NOT to add "titles" or "LinkedIn" to a list** \u2014 a contact's \`job_title\` and \`linkedin_page\` already ride on the contact record; they are FREE and need no enrichment. If the user asks for "title and LinkedIn only", read those fields directly (e.g. leadbay_get_contacts / leadbay_research_lead_by_id); do NOT launch a job here. This tool is strictly the email / phone reveal, which consumes quota.
|
|
2344
2425
|
|
|
2426
|
+
## A launched job cannot be stopped
|
|
2427
|
+
|
|
2428
|
+
Leadbay has no cancel. Once \`leadbay_enrich_titles\`, \`leadbay_bulk_qualify_leads\`,
|
|
2429
|
+
\`leadbay_import_leads\` or \`leadbay_import_and_qualify\` has returned a launched or
|
|
2430
|
+
running result, that work is queued on Leadbay and runs to completion, and the
|
|
2431
|
+
quota it costs is already committed. A discovery, preview or \`dry_run\` result
|
|
2432
|
+
launched nothing and is not covered here.
|
|
2433
|
+
|
|
2434
|
+
The user cancelling in the chat, a request timeout, or a closed stream stops YOUR
|
|
2435
|
+
waiting, never the job. \`cancelled: true\` means we stopped watching, not that the
|
|
2436
|
+
work stopped. What to do next depends on what you are holding:
|
|
2437
|
+
|
|
2438
|
+
- **A handle.** Poll the status tool with it, and do not launch the work that
|
|
2439
|
+
handle covers a second time \u2014 that spends the quota again on the same rows.
|
|
2440
|
+
\`leadbay_import_status\` takes \`importIds\`, so pass the values of \`import_ids\`
|
|
2441
|
+
under that name. A qualification started by \`leadbay_import_and_qualify\` has no
|
|
2442
|
+
notification of its own: resume it with
|
|
2443
|
+
\`leadbay_qualify_status({lead_ids, lens_id})\`.
|
|
2444
|
+
- **A handle AND a subset the result says never started** \u2014 \`failed[]\` entries
|
|
2445
|
+
with \`error:"not_queued"\`, or a \`rows_pending_upload\` count. Poll the handle
|
|
2446
|
+
for what was launched and re-run for that subset only, never for the whole
|
|
2447
|
+
batch.
|
|
2448
|
+
- **No result at all**, because the call timed out or the stream closed before it
|
|
2449
|
+
returned. Check \`leadbay_account_status\` first: the launch may have landed and
|
|
2450
|
+
finished. Calling the same tool again with the same arguments will usually hand
|
|
2451
|
+
back the job already launched rather than starting a second one, but that guard
|
|
2452
|
+
is in-memory, five minutes, and per process, so it is best-effort \u2014 say what you
|
|
2453
|
+
are about to re-run before you spend the user's quota on it.
|
|
2454
|
+
|
|
2455
|
+
|
|
2345
2456
|
## ENRICHMENT CONSUMES QUOTA \u2014 the model to reason with
|
|
2346
2457
|
|
|
2347
2458
|
Each email reveal and each phone reveal **consumes quota** (the per-window daily / weekly / monthly allowance shown in \`leadbay_account_status\`). That is the ONLY thing that gates enrichment. Do **NOT** reason about, mention, or block on "credits": there is no separate credit wall the user must clear first \u2014 enrichment either fits the user's remaining quota or the backend returns 429 (\`{status:'quota_exceeded'}\`) when a window is actually exhausted. **Never pre-refuse enrichment because a credit number looks low or zero** \u2014 a fresh/freemium account with quota still available can enrich even when its credit counter reads 0. If and only if the backend returns \`quota_exceeded\`, tell the user which window is exhausted and offer the wait-or-top-up choice (see \`leadbay_account_status\`).
|
|
@@ -2364,7 +2475,7 @@ Do NOT rely on a bare call (no \`confirm\`, no \`dry_run\`, no channels) as a "s
|
|
|
2364
2475
|
|
|
2365
2476
|
## AFTER LAUNCH \u2014 STAY ACTIVE UNTIL DONE
|
|
2366
2477
|
|
|
2367
|
-
When a launch returns \`mode:"launched"\` with a \`
|
|
2478
|
+
When a launch returns \`mode:"launched"\` with a \`notification_id\`, the enrichment runs ASYNC on the backend \u2014 the tool returns immediately, before any email/phone is attached. **Unless the user explicitly said to start it in the background / not to wait** (e.g. "kick it off, I'll check later", "don't wait for it"), stay active and report in-turn \u2014 do NOT end your turn on the ack, and do NOT say "I'll let you know when it's done." (If the user DID ask you not to wait, honor that: hand back the \`notification_id\` and a one-line "running \u2014 you can ask any time". Only promise that completion will auto-surface via \`_meta.notifications\` when the launch returned a non-null \`notification_id\`; if \`notification_id\` is null (the nullable-backend path), say instead that you'll re-check when asked / they should ask again later \u2014 nothing surfaces automatically without a notification id. Don't force a poll loop against explicit intent.) In the default (stay-active) case: call \`leadbay_bulk_enrich_status({notification_id})\` in a loop, re-polling until the job is done (small batches typically finish in under ~2 min). Pass \`include_contacts:true\` on the read you intend to report from, so you get each lead's enriched contacts back. Note that \`include_contacts\` returns each lead's FULL contact list (it fans out through \`leadbay_get_contacts\`), which can include contacts of OTHER roles that were already enriched in earlier runs \u2014 so **filter your report to the \`titles\` you just enriched** (match each contact's \`job_title\` to the requested titles). Don't present a pre-existing CFO/Sales email as part of this CEO/Owner/Manager run. Then \u2014 on your own, without waiting for the user to reprompt \u2014 report the enrichment: which of the just-enriched contacts now have emails / phones, and the counts from \`overall_progress\` (\`done\`/\`total\`). \`leadbay_bulk_enrich_status\` also returns \`bulk_progress.success_count\` / \`failure_count\` / \`quota_hit_count\` on the notification fast path \u2014 use those when present, but the per-lead path returns \`overall_progress\` only, so don't assume \`bulk_progress\` exists (see the status tool's COMPLETION REPORT). Then show refreshed quota via \`leadbay_account_status\` (see AFTER above).
|
|
2368
2479
|
|
|
2369
2480
|
**"Done" = \`all_done:true\` OR the resolvable work has plateaued.** Keep polling while \`overall_progress.done\` is still climbing. But \`total\` counts every matching contact, and some (unresolvable titles, contacts with no findable email) never flip to done \u2014 so a job can sit below 100% with \`all_done:false\` forever. A plateau is only real once the job has had time to run: do NOT declare it from the first few back-to-back reads (early on \`done\` can sit at its initial value while the backend is still spinning the job up). Give it at least ~90s\u20132 min of actual elapsed polling \u2014 space your polls out (~15\u201330s apart) rather than firing them back-to-back \u2014 and only treat the set as complete when \`overall_progress.done\` has held steady across several spaced polls over that window. Then stop polling and report what resolved, naming the ones that didn't. Key the "didn't resolve" wording off the channels the user actually requested and the returned contact fields (contacts carry \`email\` and \`phone_number\`) \u2014 a contact enriched for phone that came back with no \`phone_number\` is "no phone number found", one with no \`email\` is "no email found", email+phone that got neither is "no contact details found"; if \`quota_hit_count\` is non-zero say those were skipped because the quota window was exhausted. Do NOT hard-label every non-success as "no email found" when phone was requested. Do NOT spin indefinitely waiting for \`all_done\` on contacts the engine won't resolve, and do NOT \`ScheduleWakeup\` / defer the finished list to a later turn \u2014 deliver the resolved results in THIS reply.
|
|
2370
2481
|
|
|
@@ -3017,9 +3128,39 @@ WHEN TO USE: agent has a list of companies (domains, or CSV-shaped rows from the
|
|
|
3017
3128
|
|
|
3018
3129
|
WHEN NOT TO USE: discovery (use leadbay_pull_leads); single-lead deep dive (use leadbay_research_lead_by_id); high-cadence or untrusted automation \u2014 this mutates user state and consumes ai_rescore + web_fetch quota.
|
|
3019
3130
|
|
|
3020
|
-
|
|
3131
|
+
## A launched job cannot be stopped
|
|
3021
3132
|
|
|
3022
|
-
|
|
3133
|
+
Leadbay has no cancel. Once \`leadbay_enrich_titles\`, \`leadbay_bulk_qualify_leads\`,
|
|
3134
|
+
\`leadbay_import_leads\` or \`leadbay_import_and_qualify\` has returned a launched or
|
|
3135
|
+
running result, that work is queued on Leadbay and runs to completion, and the
|
|
3136
|
+
quota it costs is already committed. A discovery, preview or \`dry_run\` result
|
|
3137
|
+
launched nothing and is not covered here.
|
|
3138
|
+
|
|
3139
|
+
The user cancelling in the chat, a request timeout, or a closed stream stops YOUR
|
|
3140
|
+
waiting, never the job. \`cancelled: true\` means we stopped watching, not that the
|
|
3141
|
+
work stopped. What to do next depends on what you are holding:
|
|
3142
|
+
|
|
3143
|
+
- **A handle.** Poll the status tool with it, and do not launch the work that
|
|
3144
|
+
handle covers a second time \u2014 that spends the quota again on the same rows.
|
|
3145
|
+
\`leadbay_import_status\` takes \`importIds\`, so pass the values of \`import_ids\`
|
|
3146
|
+
under that name. A qualification started by \`leadbay_import_and_qualify\` has no
|
|
3147
|
+
notification of its own: resume it with
|
|
3148
|
+
\`leadbay_qualify_status({lead_ids, lens_id})\`.
|
|
3149
|
+
- **A handle AND a subset the result says never started** \u2014 \`failed[]\` entries
|
|
3150
|
+
with \`error:"not_queued"\`, or a \`rows_pending_upload\` count. Poll the handle
|
|
3151
|
+
for what was launched and re-run for that subset only, never for the whole
|
|
3152
|
+
batch.
|
|
3153
|
+
- **No result at all**, because the call timed out or the stream closed before it
|
|
3154
|
+
returned. Check \`leadbay_account_status\` first: the launch may have landed and
|
|
3155
|
+
finished. Calling the same tool again with the same arguments will usually hand
|
|
3156
|
+
back the job already launched rather than starting a second one, but that guard
|
|
3157
|
+
is in-memory, five minutes, and per process, so it is best-effort \u2014 say what you
|
|
3158
|
+
are about to re-run before you spend the user's quota on it.
|
|
3159
|
+
|
|
3160
|
+
|
|
3161
|
+
Budgets: \`total_budget_ms\` caps wall-clock; \`per_lead_budget_ms\` caps each lead's poll. For short transport timeouts, pass \`wait_for_completion:false\` and poll \`leadbay_import_status\`. Outputs \`qualified[]\`, \`still_running[]\`, \`not_imported[]\`, plus the ids that resume it: \`lead_ids\` + \`lens_id\` for leadbay_qualify_status, \`import_ids\` for leadbay_import_status. There is no qualification \`notification_id\` \u2014 the qualify phase runs per-lead, so no job notification exists; \`notification_ids[]\` are the file-import ones. Idempotent within a 5-min window. \`dry_run:'preview'\` returns mapping hints + custom-field candidates without importing.
|
|
3162
|
+
|
|
3163
|
+
\`not_imported\` rows with \`reason:"uncrawled"\` are **pending a background crawl**, NOT failures: Leadbay just hasn't matched/crawled that domain yet and will add the lead asynchronously (the label doesn't verify the URL resolves \u2014 don't call the site bad, but don't certify it valid either). Surface them as pending; the leads populate in the user's Leadbay account as the crawl completes (no tool here fetches them on demand \u2014 \`leadbay_import_status\` returns status/progress only, and \`leadbay_pull_leads\` reads the active lens's wishlist so an imported lead outside that lens may not appear). To pull those specific companies back through the MCP, re-run the import later. A large \`uncrawled\` share on a fresh list is normal.
|
|
3023
3164
|
|
|
3024
3165
|
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\`.
|
|
3025
3166
|
|
|
@@ -3044,9 +3185,9 @@ Otherwise, partition \`not_imported\` by \`reason\` into these buckets before yo
|
|
|
3044
3185
|
**Header \u2014 single line, choose by status:**
|
|
3045
3186
|
|
|
3046
3187
|
- Completed: \`"\u2713 Import complete \u2014 N imported \xB7 P pending crawl \xB7 Q need attention"\` (drop any segment whose count is 0)
|
|
3047
|
-
- Running
|
|
3048
|
-
- Running with \`timed_out:true\` (blocking call ran out of poll budget): the import is FINE and still running server-side \u2014 never render this as an error or a failure. \`"\u23F3 Import still running (the backend is slow today) \u2014 I'll check back."\` Then call \`leadbay_import_status({importIds})\`, do NOT re-run leadbay_import_leads. If \`rows_pending_upload\` is present, add \`"\u26A0 K rows weren't submitted \u2014 re-import just those."\`
|
|
3049
|
-
- Pending qualification (\`leadbay_import_and_qualify\`): \`"\u2713 Imported N leads \xB7 qualifying M of them \u2014
|
|
3188
|
+
- Running: \`"\u23F3 Import running \u2014 importIds <ids>; poll leadbay_import_status"\`
|
|
3189
|
+
- Running with \`timed_out:true\` (the blocking call ran out of poll budget): the import is FINE and still running server-side \u2014 never render this as an error or a failure. \`"\u23F3 Import still running (the backend is slow today) \u2014 I'll check back."\` Then call \`leadbay_import_status({importIds})\`, do NOT re-run leadbay_import_leads. If \`rows_pending_upload\` is present, add \`"\u26A0 K rows weren't submitted \u2014 re-import just those."\`
|
|
3190
|
+
- Pending qualification (\`leadbay_import_and_qualify\`): \`"\u2713 Imported N leads \xB7 qualifying M of them \u2014 I'll pick it up with leadbay_qualify_status"\` (its resume ids are \`lead_ids\` + \`lens_id\`; there is no qualification notification_id to quote)
|
|
3050
3191
|
|
|
3051
3192
|
Count \`uncrawled\` rows as **pending**, never as failures \u2014 never say "M failed" when the M is mostly/entirely uncrawled rows.
|
|
3052
3193
|
|
|
@@ -3094,7 +3235,7 @@ User picks \u2192 call the matching \`Calls\` tool. Constraints: 2\u20134 mutual
|
|
|
3094
3235
|
|
|
3095
3236
|
| Observation | Suggest | Calls |
|
|
3096
3237
|
|------------------------------------------------|---------------------------------------------------------------|--------------------------------------------------------|
|
|
3097
|
-
| Status: running
|
|
3238
|
+
| Status: running | "Check progress" | leadbay_import_status(importIds) |
|
|
3098
3239
|
| Status: running with \`timed_out:true\` | "Check progress" \u2014 NOT "retry the import" | leadbay_import_status(importIds, dry_run if the result carried it) after ~30s; \`result.leads\` carries the leadIds once complete |
|
|
3099
3240
|
| \`rows_pending_upload\` present | "Import the rows that never got submitted" | leadbay_import_leads (that subset only) |
|
|
3100
3241
|
| Status: complete, imports succeeded | "Run AI qualification on the imported leads" | leadbay_bulk_qualify_leads([leadIds]) \u2014 or use leadbay_import_and_qualify next time |
|
|
@@ -3104,9 +3245,9 @@ User picks \u2192 call the matching \`Calls\` tool. Constraints: 2\u20134 mutual
|
|
|
3104
3245
|
| User wants to see the imported leads | "See the imported leads in your view" | leadbay_pull_leads |
|
|
3105
3246
|
| User had follow-up intent for the imports | "Prep outreach for [a specific imported lead]" | leadbay_prepare_outreach(leadId) |
|
|
3106
3247
|
`;
|
|
3107
|
-
leadbay_import_leads = `Import leads into Leadbay's CRM via the file-import wizard. Returns stable Leadbay leadIds for downstream chaining into leadbay_bulk_qualify_leads / leadbay_research_lead_by_id. For MCP clients with short transport timeouts, pass \`wait_for_completion:false\` to return quickly with \`{status:'running',
|
|
3248
|
+
leadbay_import_leads = `Import leads into Leadbay's CRM via the file-import wizard. Returns stable Leadbay leadIds for downstream chaining into leadbay_bulk_qualify_leads / leadbay_research_lead_by_id. For MCP clients with short transport timeouts, pass \`wait_for_completion:false\` to return quickly with \`{status:'running', importIds}\`; poll leadbay_import_status with that handle. For end-to-end import+qualify in one call, prefer leadbay_import_and_qualify. For messy files, prefer the \`leadbay_import_file\` prompt which walks an agent through scan \u2192 resolve \u2192 preserve \u2192 commit phases.
|
|
3108
3249
|
|
|
3109
|
-
SLOW BACKEND \u21D2 \`{status:'running', timed_out:true, importIds}\`. The wizard is sometimes slow; when the poll budget runs out this tool returns that SUCCESS result, not an error. The import is still running server-side. **Do NOT call leadbay_import_leads again** \u2014 that re-uploads the file and leaves a duplicate CRM-imports row. Call \`leadbay_import_status({importIds})\` after ~30s \u2014 and pass \`dry_run:true\` too if the result carried it; on \`complete\` it returns \`result.leads\` with the leadIds, while \`phase:"committing"\` just means keep polling. Tell the user it's running and you'll check back \u2014 it is not a problem to report. Exception: \`rows_pending_upload\` rows never reached the backend and DO need a fresh call for that subset only. In records mode the result also carries \`row_ids\` \u2014 the synthetic id of each input row, in your \`records[]\` order \u2014 because \`leadbay_import_status\` reports recovered leads by that id; keep it to map them back to your source rows.
|
|
3250
|
+
SLOW BACKEND \u21D2 \`{status:'running', timed_out:true, importIds}\`. The wizard is sometimes slow; when the poll budget runs out this tool returns that SUCCESS result, not an error. The import is still running server-side. **Do NOT call leadbay_import_leads again** \u2014 that re-uploads the file and leaves a duplicate CRM-imports row. Leadbay has no cancel, so a Cancel or timeout is no reason to call it either. Sole exception: a \`wait_for_completion:false\` call that returned NOTHING \u2014 and even that can re-upload, so check CRM-imports. Call \`leadbay_import_status({importIds})\` after ~30s \u2014 and pass \`dry_run:true\` too if the result carried it; on \`complete\` it returns \`result.leads\` with the leadIds, while \`phase:"committing"\` just means keep polling. Tell the user it's running and you'll check back \u2014 it is not a problem to report. Exception: \`rows_pending_upload\` rows never reached the backend and DO need a fresh call for that subset only. In records mode the result also carries \`row_ids\` \u2014 the synthetic id of each input row, in your \`records[]\` order \u2014 because \`leadbay_import_status\` reports recovered leads by that id; keep it to map them back to your source rows.
|
|
3110
3251
|
|
|
3111
3252
|
TWO MODES: (A) Domain-list shortcut \u2014 pass \`domains: [{domain, name?}]\`. The tool builds a 2-column CSV (LEAD_NAME, LEAD_WEBSITE) and imports with the default mapping. (B) Custom records + mapping \u2014 pass \`records: [{Col1, Col2, ...}]\` plus \`mappings.fields: {Col1: 'LEAD_NAME', ...}\`. \`mappings.fields\` must include LEADBAY_ID, CRM_ID, SIREN, LEAD_NAME, or LEAD_WEBSITE (resolver needs at least one identity key). Pass exactly one of \`domains\` / \`records\`. Reserved column \`MCP_ROW_ID\` cannot appear in records/mappings \u2014 the tool injects it for stable reconciliation.
|
|
3112
3253
|
|
|
@@ -3141,9 +3282,9 @@ Otherwise, partition \`not_imported\` by \`reason\` into these buckets before yo
|
|
|
3141
3282
|
**Header \u2014 single line, choose by status:**
|
|
3142
3283
|
|
|
3143
3284
|
- Completed: \`"\u2713 Import complete \u2014 N imported \xB7 P pending crawl \xB7 Q need attention"\` (drop any segment whose count is 0)
|
|
3144
|
-
- Running
|
|
3145
|
-
- Running with \`timed_out:true\` (blocking call ran out of poll budget): the import is FINE and still running server-side \u2014 never render this as an error or a failure. \`"\u23F3 Import still running (the backend is slow today) \u2014 I'll check back."\` Then call \`leadbay_import_status({importIds})\`, do NOT re-run leadbay_import_leads. If \`rows_pending_upload\` is present, add \`"\u26A0 K rows weren't submitted \u2014 re-import just those."\`
|
|
3146
|
-
- Pending qualification (\`leadbay_import_and_qualify\`): \`"\u2713 Imported N leads \xB7 qualifying M of them \u2014
|
|
3285
|
+
- Running: \`"\u23F3 Import running \u2014 importIds <ids>; poll leadbay_import_status"\`
|
|
3286
|
+
- Running with \`timed_out:true\` (the blocking call ran out of poll budget): the import is FINE and still running server-side \u2014 never render this as an error or a failure. \`"\u23F3 Import still running (the backend is slow today) \u2014 I'll check back."\` Then call \`leadbay_import_status({importIds})\`, do NOT re-run leadbay_import_leads. If \`rows_pending_upload\` is present, add \`"\u26A0 K rows weren't submitted \u2014 re-import just those."\`
|
|
3287
|
+
- Pending qualification (\`leadbay_import_and_qualify\`): \`"\u2713 Imported N leads \xB7 qualifying M of them \u2014 I'll pick it up with leadbay_qualify_status"\` (its resume ids are \`lead_ids\` + \`lens_id\`; there is no qualification notification_id to quote)
|
|
3147
3288
|
|
|
3148
3289
|
Count \`uncrawled\` rows as **pending**, never as failures \u2014 never say "M failed" when the M is mostly/entirely uncrawled rows.
|
|
3149
3290
|
|
|
@@ -3191,7 +3332,7 @@ User picks \u2192 call the matching \`Calls\` tool. Constraints: 2\u20134 mutual
|
|
|
3191
3332
|
|
|
3192
3333
|
| Observation | Suggest | Calls |
|
|
3193
3334
|
|------------------------------------------------|---------------------------------------------------------------|--------------------------------------------------------|
|
|
3194
|
-
| Status: running
|
|
3335
|
+
| Status: running | "Check progress" | leadbay_import_status(importIds) |
|
|
3195
3336
|
| Status: running with \`timed_out:true\` | "Check progress" \u2014 NOT "retry the import" | leadbay_import_status(importIds, dry_run if the result carried it) after ~30s; \`result.leads\` carries the leadIds once complete |
|
|
3196
3337
|
| \`rows_pending_upload\` present | "Import the rows that never got submitted" | leadbay_import_leads (that subset only) |
|
|
3197
3338
|
| Status: complete, imports succeeded | "Run AI qualification on the imported leads" | leadbay_bulk_qualify_leads([leadIds]) \u2014 or use leadbay_import_and_qualify next time |
|
|
@@ -3201,11 +3342,45 @@ User picks \u2192 call the matching \`Calls\` tool. Constraints: 2\u20134 mutual
|
|
|
3201
3342
|
| User wants to see the imported leads | "See the imported leads in your view" | leadbay_pull_leads |
|
|
3202
3343
|
| User had follow-up intent for the imports | "Prep outreach for [a specific imported lead]" | leadbay_prepare_outreach(leadId) |
|
|
3203
3344
|
`;
|
|
3204
|
-
leadbay_import_status = `Retrieve the current **status/progress** of a lead import, and its leadIds once it finishes. Pass
|
|
3345
|
+
leadbay_import_status = `Retrieve the current **status/progress** of a lead import, and its leadIds once it finishes. Pass the \`importIds\` the launch returned \u2014 \`leadbay_import_leads\` returns \`importIds\`, \`leadbay_import_and_qualify\` returns \`import_ids\`. These are the backend's own import ids, so they resolve from a later message, a later conversation, or the next day; nothing is stored on the MCP side. Also pass the \`dry_run\` the import was launched with, so completion is judged against the right phase (a dry run finishes at preprocess, a real import at processing). This status call performs a single refresh pass and never polls in a loop.
|
|
3346
|
+
|
|
3347
|
+
WHEN TO USE: after an async import returns its ids \u2014 \`leadbay_import_leads\` as \`{status:'running', importIds}\`, \`leadbay_import_and_qualify\` as \`import_ids\` \u2014 poll with those; OR to check whether a finished import is still processing. This tool does NOT surface the leads Leadbay adds later for pending-crawl (\`uncrawled\`) rows \u2014 those populate in the user's Leadbay account as the crawl completes; no tool here fetches them on demand (re-run the import to pull them back through the MCP).
|
|
3348
|
+
|
|
3349
|
+
WHEN NOT TO USE: for the qualification half \u2014 use leadbay_qualify_status, with the \`lead_ids\` + \`lens_id\` an \`leadbay_import_and_qualify\` launch returned (it has no qualification \`notification_id\`; its \`notification_ids[]\` are these same file imports); or when you still want the legacy blocking behavior from leadbay_import_leads with \`wait_for_completion=true\`.
|
|
3350
|
+
|
|
3351
|
+
## A launched job cannot be stopped
|
|
3205
3352
|
|
|
3206
|
-
|
|
3353
|
+
Leadbay has no cancel. A job started by \`leadbay_enrich_titles\`,
|
|
3354
|
+
\`leadbay_bulk_qualify_leads\`, \`leadbay_import_leads\` or
|
|
3355
|
+
\`leadbay_import_and_qualify\` runs to completion on Leadbay. The user cancelling
|
|
3356
|
+
in the chat, a request timeout, or a closed stream stops YOUR waiting, never the
|
|
3357
|
+
job, and \`cancelled: true\` on an earlier result means we stopped watching, not
|
|
3358
|
+
that the work stopped.
|
|
3359
|
+
|
|
3360
|
+
**This tool only reads.** Calling it again launches nothing and spends no quota,
|
|
3361
|
+
so poll it as often as the job needs \u2014 a timeout here is a reason to call it
|
|
3362
|
+
again, not a reason to stop.
|
|
3363
|
+
|
|
3364
|
+
One import state does NOT progress: a chunk cancelled before its mappings were
|
|
3365
|
+
committed reads \`running\` / \`committing\` forever. If the counts hold flat across
|
|
3366
|
+
several spaced polls, say so and stop, rather than polling on.
|
|
3367
|
+
|
|
3368
|
+
What must not be repeated is the LAUNCH \u2014 for work that actually launched. Re-run
|
|
3369
|
+
a launcher only for a subset that never started, never for the whole batch:
|
|
3370
|
+
|
|
3371
|
+
- \`failed[]\` entries with \`error:"not_queued"\`;
|
|
3372
|
+
- a \`rows_pending_upload\` count;
|
|
3373
|
+
- leads in \`still_running\` after a CANCELLED \`leadbay_import_and_qualify\`. Its
|
|
3374
|
+
fan-out is sequential, so an interruption leaves the remainder unlaunched and
|
|
3375
|
+
folds them in with the ones that did launch. Nothing in the result tells the
|
|
3376
|
+
two apart, and this tool cannot start either. Wait until the REST of the batch
|
|
3377
|
+
has settled: what launched settles in order, so leads still unanswered after
|
|
3378
|
+
that are the ones that never started. Only then call
|
|
3379
|
+
\`leadbay_bulk_qualify_leads({leadIds, lensId})\` for exactly those ids. A lead
|
|
3380
|
+
that is merely slow looks identical to one that never launched over a few
|
|
3381
|
+
polls, and re-launching it charges the user twice \u2014 when unsure, tell the user
|
|
3382
|
+
rather than guess.
|
|
3207
3383
|
|
|
3208
|
-
WHEN NOT TO USE: for qualification handles returned as \`qualify_id\` \u2014 use leadbay_qualify_status for those; or when you still want the legacy blocking behavior from leadbay_import_leads with \`wait_for_completion=true\`.
|
|
3209
3384
|
|
|
3210
3385
|
---
|
|
3211
3386
|
|
|
@@ -3225,12 +3400,11 @@ After the status line, propose the obvious refresh / progress-check / recovery a
|
|
|
3225
3400
|
|
|
3226
3401
|
Specifically for import status:
|
|
3227
3402
|
|
|
3228
|
-
This tool returns \`status\`, \`importIds\`, and \`progress\` ({phase, records_processed, records_total}).
|
|
3403
|
+
This tool returns \`status\`, \`importIds\`, and \`progress\` ({phase, records_processed, records_total}). Once every named import is \`complete\` and it wasn't a dry run, it also reconciles the wizard's records and carries \`result\` ({leads, not_imported, importIds, still_settling?}) \u2014 that is how you recover the leadIds of an import you stopped watching, without re-importing. \`result.still_settling\` counts rows the wizard hasn't finished placing; they are neither imported nor failed, so poll again rather than reporting them. If \`result\` is absent on a \`complete\` import the records weren't readable \u2014 report completion without inventing counts. **Render only from the fields actually present; never invent counts.**
|
|
3229
3404
|
|
|
3230
3405
|
Caveat on \`progress\`: \`records_processed\` counts only the rows that MATCHED an existing lead (backend \`imported_records\`), not every row that finished processing \u2014 so for a complete import whose rows are mostly/all \`uncrawled\` (pending crawl), \`records_processed\` is legitimately low or 0. Never read a low \`records_processed\` on a \`complete\` import as "stuck" or "failed": once \`status:"complete"\`, processing is done; the pending-crawl rows just matched no existing lead yet.
|
|
3231
3406
|
|
|
3232
|
-
- Running \u2192 \`"\u23F3 Import still running \u2014 phase <phase>; check back in ~M minutes."\` (use the phase; don't turn the matched-count into an "X/Y processed" progress bar). \`phase:"committing"\`
|
|
3233
|
-
- Complete with **\`dry_run:true\`** on the response \u2192 a VALIDATION pass; nothing was committed. \`"\u{1F50E} Dry run complete \u2014 input validated, nothing imported. Re-run without dry_run to commit."\` Never render this as a completed import, and never quote a lead count.
|
|
3407
|
+
- Running \u2192 \`"\u23F3 Import still running \u2014 phase <phase>; check back in ~M minutes."\` (use the phase; don't turn the matched-count into an "X/Y processed" progress bar). \`phase:"committing"\` means the mappings are still being committed \u2014 say it's still being committed, never that it failed or finished empty.
|
|
3234
3408
|
- Complete, **no \`result\`** \u2192 \`"\u2713 Import complete."\` Do NOT append a \`records_processed/records_total\` fraction (it undercounts pending-crawl rows and looks stuck) and do NOT report pending-crawl / need-attention bucket counts \u2014 the row-level \`not_imported\` breakdown isn't in this response.
|
|
3235
3409
|
- Complete with \`result.still_settling > 0\` \u2192 say \`"\u2713 Import complete \u2014 N imported, S rows still being placed."\` Never count \`still_settling\` rows as failures.
|
|
3236
3410
|
- Complete, **\`result\` present AND it was a dry run** (\`result.dry_run:true\`, or every \`result.not_imported\` row has \`reason:"dry_run"\`) \u2192 this resolved handle was a VALIDATION pass, nothing committed. Render \`"\u{1F50E} Dry run complete \u2014 V rows validated, nothing imported. Re-run without dry_run to commit."\` \u2014 do NOT render it as a real import completion or use the pending/attention buckets.
|
|
@@ -3262,12 +3436,29 @@ How the OTHER reasons map to the "Need attention" bucket (see the render block a
|
|
|
3262
3436
|
| Status: running (incl. \`committing\`) | "Check again in N minutes" | leadbay_import_status \u2014 re-call (pass \`dry_run:true\` if the ids came from a dry run) |
|
|
3263
3437
|
| Status: error / failed (true error) | "Diagnose the failure" | leadbay_resolve_import_rows |
|
|
3264
3438
|
`;
|
|
3265
|
-
leadbay_launch_bulk_enrichment = `Launch a bulk-enrichment job against the current selection. The backend requires \`email=true\` OR \`phone=true\` (both can be true). Returns 204 with no body \u2014 there is no
|
|
3439
|
+
leadbay_launch_bulk_enrichment = `Launch a bulk-enrichment job against the current selection. The backend requires \`email=true\` OR \`phone=true\` (both can be true). Returns 204 with no body \u2014 there is no notification_id and no per-job status endpoint. Track results by polling individual leads via leadbay_get_contacts after ~60s; a contact is done for this run only when the REQUESTED channel landed (requested \`email\` and/or \`phone_number\` present), not \`contact.enrichment.done\` alone (that flag is already true for a contact enriched on the other channel earlier). \`dry_run:true\` returns the call shape without contacting the backend.
|
|
3266
3440
|
|
|
3267
3441
|
WHEN TO USE: low-level.
|
|
3268
3442
|
|
|
3269
3443
|
WHEN NOT TO USE: from agent flow \u2014 leadbay_enrich_titles handles selection lifecycle, preview, launch, and cleanup.
|
|
3270
3444
|
|
|
3445
|
+
## A launched job cannot be stopped, and this tool has no retry guard
|
|
3446
|
+
|
|
3447
|
+
Leadbay has no cancel. Once this call returns having actually launched, the work
|
|
3448
|
+
is queued on Leadbay and runs to completion, and the quota it costs is already
|
|
3449
|
+
committed. A \`dry_run\` result reached no backend and spent nothing. The user
|
|
3450
|
+
cancelling in the chat, a request timeout, or a closed stream stops YOUR waiting,
|
|
3451
|
+
never the job.
|
|
3452
|
+
|
|
3453
|
+
Unlike the composite launchers, this tool has **no double-launch guard**: calling
|
|
3454
|
+
it again always issues a new paid launch, even seconds later with identical
|
|
3455
|
+
arguments. So when a call returns nothing at all, do not simply retry. Read the
|
|
3456
|
+
record back first \u2014 \`leadbay_research_lead_by_id\` or \`leadbay_get_contacts\` for a
|
|
3457
|
+
lead, \`leadbay_account_status\` for background work that has since finished \u2014 to
|
|
3458
|
+
see whether the launch already landed, and tell the user what you are about to
|
|
3459
|
+
spend before spending it again.
|
|
3460
|
+
|
|
3461
|
+
|
|
3271
3462
|
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\`.
|
|
3272
3463
|
`;
|
|
3273
3464
|
leadbay_like_lead = `## WHEN TO USE
|
|
@@ -4337,13 +4528,70 @@ WHEN TO USE: low-level \u2014 when you need to kick qualification on exactly one
|
|
|
4337
4528
|
|
|
4338
4529
|
WHEN NOT TO USE: as the agent's bulk-qualify path \u2014 use leadbay_bulk_qualify_leads, which paginates past already-qualified leads, fans out, polls, and bails out cleanly on 429.
|
|
4339
4530
|
|
|
4531
|
+
## A launched job cannot be stopped, and this tool has no retry guard
|
|
4532
|
+
|
|
4533
|
+
Leadbay has no cancel. Once this call returns having actually launched, the work
|
|
4534
|
+
is queued on Leadbay and runs to completion, and the quota it costs is already
|
|
4535
|
+
committed. A \`dry_run\` result reached no backend and spent nothing. The user
|
|
4536
|
+
cancelling in the chat, a request timeout, or a closed stream stops YOUR waiting,
|
|
4537
|
+
never the job.
|
|
4538
|
+
|
|
4539
|
+
Unlike the composite launchers, this tool has **no double-launch guard**: calling
|
|
4540
|
+
it again always issues a new paid launch, even seconds later with identical
|
|
4541
|
+
arguments. So when a call returns nothing at all, do not simply retry. Read the
|
|
4542
|
+
record back first \u2014 \`leadbay_research_lead_by_id\` or \`leadbay_get_contacts\` for a
|
|
4543
|
+
lead, \`leadbay_account_status\` for background work that has since finished \u2014 to
|
|
4544
|
+
see whether the launch already landed, and tell the user what you are about to
|
|
4545
|
+
spend before spending it again.
|
|
4546
|
+
|
|
4547
|
+
|
|
4340
4548
|
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\`.
|
|
4341
4549
|
`;
|
|
4342
|
-
leadbay_qualify_status = `Retrieve the current state of
|
|
4550
|
+
leadbay_qualify_status = `Retrieve the current state of a bulk_qualify_leads or import_and_qualify launch. Which ids to pass depends on which tool launched it, because only one of them creates a qualification job on the backend:
|
|
4343
4551
|
|
|
4344
|
-
|
|
4552
|
+
- **\`leadbay_bulk_qualify_leads\`** returns a \`notification_id\`. Pass it for progress in ONE call, and add the \`lead_ids\` + \`lens_id\` it also returned for per-lead detail (which settled, which are still running).
|
|
4553
|
+
- **\`leadbay_import_and_qualify\`** returns NO qualification \`notification_id\` \u2014 its qualify phase runs per-lead, so no job notification exists. Pass the \`lead_ids\` + \`lens_id\` it returned. Its \`notification_ids[]\` are the FILE-IMPORT notifications; handing one of those to this tool is rejected as the wrong kind, and \`leadbay_import_status({importIds})\` is where the import half is polled.
|
|
4554
|
+
|
|
4555
|
+
**When it is finished:** \`status\` is always \`"launched"\` \u2014 it is not a progress field. On the \`notification_id\` path the job is done when \`in_progress\` is false (or \`bulk_progress.success_count + failure_count\` reaches \`total_count\`); \`still_running[]\` is empty on that path from the very first poll and must NOT be read as "done". On the \`lead_ids\` path the job is done when \`still_running[]\` is empty. Pass both and you get both signals in one call.
|
|
4556
|
+
|
|
4557
|
+
Everything comes straight out of the launch response \u2014 nothing is stored on the MCP side. A backend job is scoped to the user who launched it, so a \`notification_id\` resolves from a later message, a later conversation, or the next day.
|
|
4558
|
+
|
|
4559
|
+
WHEN TO USE: after leadbay_bulk_qualify_leads or leadbay_import_and_qualify came back with a non-empty \`still_running[]\`, call this tool a few minutes later (or hours) with those ids to retrieve the now-completed qualifications without re-running the import or re-spending qualify quota.
|
|
4345
4560
|
|
|
4346
4561
|
WHEN NOT TO USE: as a substitute for leadbay_research_lead_by_id \u2014 that's a deeper per-lead profile and includes contacts. This tool is purely the qualification answers + signals_count.
|
|
4562
|
+
|
|
4563
|
+
## A launched job cannot be stopped
|
|
4564
|
+
|
|
4565
|
+
Leadbay has no cancel. A job started by \`leadbay_enrich_titles\`,
|
|
4566
|
+
\`leadbay_bulk_qualify_leads\`, \`leadbay_import_leads\` or
|
|
4567
|
+
\`leadbay_import_and_qualify\` runs to completion on Leadbay. The user cancelling
|
|
4568
|
+
in the chat, a request timeout, or a closed stream stops YOUR waiting, never the
|
|
4569
|
+
job, and \`cancelled: true\` on an earlier result means we stopped watching, not
|
|
4570
|
+
that the work stopped.
|
|
4571
|
+
|
|
4572
|
+
**This tool only reads.** Calling it again launches nothing and spends no quota,
|
|
4573
|
+
so poll it as often as the job needs \u2014 a timeout here is a reason to call it
|
|
4574
|
+
again, not a reason to stop.
|
|
4575
|
+
|
|
4576
|
+
One import state does NOT progress: a chunk cancelled before its mappings were
|
|
4577
|
+
committed reads \`running\` / \`committing\` forever. If the counts hold flat across
|
|
4578
|
+
several spaced polls, say so and stop, rather than polling on.
|
|
4579
|
+
|
|
4580
|
+
What must not be repeated is the LAUNCH \u2014 for work that actually launched. Re-run
|
|
4581
|
+
a launcher only for a subset that never started, never for the whole batch:
|
|
4582
|
+
|
|
4583
|
+
- \`failed[]\` entries with \`error:"not_queued"\`;
|
|
4584
|
+
- a \`rows_pending_upload\` count;
|
|
4585
|
+
- leads in \`still_running\` after a CANCELLED \`leadbay_import_and_qualify\`. Its
|
|
4586
|
+
fan-out is sequential, so an interruption leaves the remainder unlaunched and
|
|
4587
|
+
folds them in with the ones that did launch. Nothing in the result tells the
|
|
4588
|
+
two apart, and this tool cannot start either. Wait until the REST of the batch
|
|
4589
|
+
has settled: what launched settles in order, so leads still unanswered after
|
|
4590
|
+
that are the ones that never started. Only then call
|
|
4591
|
+
\`leadbay_bulk_qualify_leads({leadIds, lensId})\` for exactly those ids. A lead
|
|
4592
|
+
that is merely slow looks identical to one that never launched over a few
|
|
4593
|
+
polls, and re-launching it charges the user twice \u2014 when unsure, tell the user
|
|
4594
|
+
rather than guess.
|
|
4347
4595
|
`;
|
|
4348
4596
|
leadbay_recall_ordered_titles = `Show job titles the org has previously enriched, so the agent can repeat the same titles for new leads (or skip already-saturated ones). Two implementation paths: (1) PREFERRED \u2014 a selection-scoped preview call that reads \`previously_enriched_titles\` from the backend (newer prod field). (2) FALLBACK \u2014 live aggregation across each lead's enriched contacts. The composite picks transparently.
|
|
4349
4597
|
|
|
@@ -5939,7 +6187,7 @@ Some Leadbay tool responses include a \`_meta.notifications\` array listing **ba
|
|
|
5939
6187
|
- \`leadbay_qualify_status\` \u2192 \`still_running\` is empty: every launched lead has finished or failed. (\`in_progress\` also reads \`false\` on the fast path, but it can be \`null\` on the legacy/fallback read \u2014 so treat an empty \`still_running\` as terminal on its own; only require \`in_progress:false\` when that field is actually present.) LIKE imports, large qualification runs are async by design: \`leadbay_bulk_qualify_leads\` defaults to \`wait_for_completion:false\` for \`count > 5\` or chained workflows because blocking can time out, and \`leadbay_qualify_status\` may take minutes/hours. So don't force a long polling loop on a big run \u2014 return the handle/progress and let completion arrive via \`_meta.notifications\` \u2014 UNLESS the user explicitly asked to wait, or it's a small run that finishes quickly. A small \`wait_for_completion:true\` run you can poll to \`still_running\` empty inline.
|
|
5940
6188
|
- \`leadbay_import_status\` \u2192 \`status:"complete"\` (or \`"failed"\`). BUT imports are the exception to the stay-active loop: a large \`leadbay_import_leads({wait_for_completion:false})\` is meant to return a handle and resolve over minutes, and the tool does ONE refresh pass per call. Don't block the conversation looping on it \u2014 surface the returned progress/handle and let the completion arrive via \`_meta.notifications\` \u2014 UNLESS the user explicitly asked you to wait for the import, or it's a small import that finishes quickly.
|
|
5941
6189
|
|
|
5942
|
-
Enrichment polls to completion in-turn BY DEFAULT \u2014 the exception is when the user explicitly said to start it in the background / not wait ("kick it off, I'll check later"), in which case hand back the
|
|
6190
|
+
Enrichment polls to completion in-turn BY DEFAULT \u2014 the exception is when the user explicitly said to start it in the background / not wait ("kick it off, I'll check later"), in which case hand back the notification_id and let completion arrive via \`_meta.notifications\` (only when a notification id exists; if none was returned, tell the user to ask again / that you'll poll later, since nothing will auto-surface). For qualification and imports, poll inline only for small/quick runs or when the user explicitly asked you to wait; otherwise return the handle and let \`_meta.notifications\` deliver it. Either way, the user should never have to ask "is it done yet?" for work you kicked off in the same turn \u2014 you either report it or hand back a clear in-progress handle.
|
|
5943
6191
|
|
|
5944
6192
|
Also surfaced as a top-level \`notifications\` array on \`leadbay_account_status\` \u2014 same shape, same handling.
|
|
5945
6193
|
|
|
@@ -6852,7 +7100,9 @@ var init_qualify_lead = __esm({
|
|
|
6852
7100
|
title: "Qualify a single lead",
|
|
6853
7101
|
readOnlyHint: false,
|
|
6854
7102
|
destructiveHint: true,
|
|
6855
|
-
|
|
7103
|
+
// No double-launch guard: this POSTs straight through, so an identical
|
|
7104
|
+
// repeat is a second paid launch, not a no-op (product#4039).
|
|
7105
|
+
idempotentHint: false,
|
|
6856
7106
|
openWorldHint: true
|
|
6857
7107
|
},
|
|
6858
7108
|
description: leadbay_qualify_lead,
|
|
@@ -6928,7 +7178,9 @@ var init_enrich_contacts = __esm({
|
|
|
6928
7178
|
title: "Enrich contacts for a lead",
|
|
6929
7179
|
readOnlyHint: false,
|
|
6930
7180
|
destructiveHint: true,
|
|
6931
|
-
|
|
7181
|
+
// No double-launch guard: this POSTs straight through, so an identical
|
|
7182
|
+
// repeat is a second paid launch, not a no-op (product#4039).
|
|
7183
|
+
idempotentHint: false,
|
|
6932
7184
|
openWorldHint: true
|
|
6933
7185
|
},
|
|
6934
7186
|
description: leadbay_enrich_contacts,
|
|
@@ -9012,6 +9264,74 @@ var init_qualify_helpers = __esm({
|
|
|
9012
9264
|
}
|
|
9013
9265
|
});
|
|
9014
9266
|
|
|
9267
|
+
// ../core/dist/jobs/launch-guard.js
|
|
9268
|
+
import { createHash as createHash2 } from "crypto";
|
|
9269
|
+
function launchFingerprint(parts) {
|
|
9270
|
+
const flat = parts.map((p) => Array.isArray(p) ? [...p].sort().join(",") : String(p)).join("|");
|
|
9271
|
+
return createHash2("sha256").update(flat).digest("hex");
|
|
9272
|
+
}
|
|
9273
|
+
function sweep(now) {
|
|
9274
|
+
for (const [k, v] of recent) {
|
|
9275
|
+
if (now - v.at >= WINDOW_MS)
|
|
9276
|
+
recent.delete(k);
|
|
9277
|
+
}
|
|
9278
|
+
}
|
|
9279
|
+
function beginLaunch(fingerprint, now = Date.now()) {
|
|
9280
|
+
const prior = recallLaunch(fingerprint, now);
|
|
9281
|
+
if (prior) {
|
|
9282
|
+
return prior.in_flight ? { state: "in_flight", seconds_since: prior.seconds_since } : { state: "settled", record: prior };
|
|
9283
|
+
}
|
|
9284
|
+
recent.set(fingerprint, {
|
|
9285
|
+
notification_id: null,
|
|
9286
|
+
in_flight: true,
|
|
9287
|
+
launched_at: new Date(now).toISOString(),
|
|
9288
|
+
at: now
|
|
9289
|
+
});
|
|
9290
|
+
return { state: "owned" };
|
|
9291
|
+
}
|
|
9292
|
+
function abandonLaunch(fingerprint) {
|
|
9293
|
+
const held = recent.get(fingerprint);
|
|
9294
|
+
if (held?.in_flight)
|
|
9295
|
+
recent.delete(fingerprint);
|
|
9296
|
+
}
|
|
9297
|
+
function recallLaunch(fingerprint, now = Date.now()) {
|
|
9298
|
+
sweep(now);
|
|
9299
|
+
const hit = recent.get(fingerprint);
|
|
9300
|
+
if (!hit)
|
|
9301
|
+
return void 0;
|
|
9302
|
+
return { ...hit, seconds_since: Math.round((now - hit.at) / 1e3) };
|
|
9303
|
+
}
|
|
9304
|
+
function rememberLaunch(fingerprint, notificationId, now = Date.now(), importIds) {
|
|
9305
|
+
sweep(now);
|
|
9306
|
+
while (recent.size >= MAX_ENTRIES) {
|
|
9307
|
+
const oldest = recent.keys().next();
|
|
9308
|
+
if (oldest.done)
|
|
9309
|
+
break;
|
|
9310
|
+
recent.delete(oldest.value);
|
|
9311
|
+
}
|
|
9312
|
+
const rec = {
|
|
9313
|
+
notification_id: notificationId,
|
|
9314
|
+
in_flight: false,
|
|
9315
|
+
...importIds ? { import_ids: importIds } : {},
|
|
9316
|
+
launched_at: new Date(now).toISOString(),
|
|
9317
|
+
at: now
|
|
9318
|
+
};
|
|
9319
|
+
recent.set(fingerprint, rec);
|
|
9320
|
+
return rec;
|
|
9321
|
+
}
|
|
9322
|
+
function resetLaunchGuard() {
|
|
9323
|
+
recent.clear();
|
|
9324
|
+
}
|
|
9325
|
+
var WINDOW_MS, MAX_ENTRIES, recent;
|
|
9326
|
+
var init_launch_guard = __esm({
|
|
9327
|
+
"../core/dist/jobs/launch-guard.js"() {
|
|
9328
|
+
"use strict";
|
|
9329
|
+
WINDOW_MS = 5 * 60 * 1e3;
|
|
9330
|
+
MAX_ENTRIES = 1e3;
|
|
9331
|
+
recent = /* @__PURE__ */ new Map();
|
|
9332
|
+
}
|
|
9333
|
+
});
|
|
9334
|
+
|
|
9015
9335
|
// ../core/dist/composite/_import-records.js
|
|
9016
9336
|
function normalizeDomain(input) {
|
|
9017
9337
|
if (!input || typeof input !== "string")
|
|
@@ -9215,7 +9535,7 @@ var init_import_records = __esm({
|
|
|
9215
9535
|
|
|
9216
9536
|
// ../core/dist/composite/_import-commit-log.js
|
|
9217
9537
|
function recordCommitFailure(importId, reason) {
|
|
9218
|
-
if (failures.size >=
|
|
9538
|
+
if (failures.size >= MAX_ENTRIES2) {
|
|
9219
9539
|
const oldest = failures.keys().next().value;
|
|
9220
9540
|
if (oldest !== void 0)
|
|
9221
9541
|
failures.delete(oldest);
|
|
@@ -9230,17 +9550,17 @@ function commitFailureFor(importIds) {
|
|
|
9230
9550
|
}
|
|
9231
9551
|
return void 0;
|
|
9232
9552
|
}
|
|
9233
|
-
var
|
|
9553
|
+
var MAX_ENTRIES2, failures;
|
|
9234
9554
|
var init_import_commit_log = __esm({
|
|
9235
9555
|
"../core/dist/composite/_import-commit-log.js"() {
|
|
9236
9556
|
"use strict";
|
|
9237
|
-
|
|
9557
|
+
MAX_ENTRIES2 = 500;
|
|
9238
9558
|
failures = /* @__PURE__ */ new Map();
|
|
9239
9559
|
}
|
|
9240
9560
|
});
|
|
9241
9561
|
|
|
9242
9562
|
// ../core/dist/composite/import-leads.js
|
|
9243
|
-
import { createHash as
|
|
9563
|
+
import { createHash as createHash3, randomUUID } from "crypto";
|
|
9244
9564
|
function isImportLeadsRunningResult(result) {
|
|
9245
9565
|
return "status" in result && result.status === "running";
|
|
9246
9566
|
}
|
|
@@ -9302,7 +9622,7 @@ function importFingerprint(params, prep) {
|
|
|
9302
9622
|
mappings: prep.mappings,
|
|
9303
9623
|
dry_run: Boolean(params.dry_run)
|
|
9304
9624
|
};
|
|
9305
|
-
return
|
|
9625
|
+
return createHash3("sha256").update(stableStringify(payload)).digest("hex");
|
|
9306
9626
|
}
|
|
9307
9627
|
function checkAborted(signal) {
|
|
9308
9628
|
if (signal?.aborted) {
|
|
@@ -9677,7 +9997,7 @@ async function commitMappings(client, importId, mappings, ctx) {
|
|
|
9677
9997
|
throw err;
|
|
9678
9998
|
}
|
|
9679
9999
|
}
|
|
9680
|
-
async function completeUploadedChunk(client, upload, mappings, dryRun, perPhaseBudgetMs, totalDeadline, ctx, signal, onNotificationId) {
|
|
10000
|
+
async function completeUploadedChunk(client, upload, mappings, dryRun, perPhaseBudgetMs, totalDeadline, ctx, signal, onNotificationId, onCommitSent) {
|
|
9681
10001
|
const { importId, chunk } = upload;
|
|
9682
10002
|
const phaseBudget = Math.min(perPhaseBudgetMs, Math.max(1, totalDeadline - Date.now()));
|
|
9683
10003
|
await pollPreprocess(client, importId, phaseBudget, ctx, signal);
|
|
@@ -9686,6 +10006,7 @@ async function completeUploadedChunk(client, upload, mappings, dryRun, perPhaseB
|
|
|
9686
10006
|
return { importId, records: [], notification_id: null };
|
|
9687
10007
|
}
|
|
9688
10008
|
const importNotificationId = await commitMappings(client, importId, mappings, ctx);
|
|
10009
|
+
onCommitSent?.();
|
|
9689
10010
|
if (importNotificationId) {
|
|
9690
10011
|
onNotificationId?.(importNotificationId);
|
|
9691
10012
|
ctx?.logger?.info?.(`import-leads: notification_id=${importNotificationId} importId=${importId}`);
|
|
@@ -9851,27 +10172,24 @@ function resumeParkedUpload(client, upload, mappings, ctx) {
|
|
|
9851
10172
|
});
|
|
9852
10173
|
}, 0);
|
|
9853
10174
|
}
|
|
9854
|
-
async function runImportInBackground(client, prep, uploadedChunks, opts, ctx
|
|
9855
|
-
const tracker = ctx.bulkTracker;
|
|
9856
|
-
if (!tracker)
|
|
9857
|
-
return;
|
|
9858
|
-
void tracker.setImportProgress(handleId, {
|
|
9859
|
-
phase: "preprocess",
|
|
9860
|
-
records_processed: 0,
|
|
9861
|
-
records_total: prep.validInputs.length
|
|
9862
|
-
}).catch(() => {
|
|
9863
|
-
});
|
|
10175
|
+
async function runImportInBackground(client, prep, uploadedChunks, opts, ctx) {
|
|
9864
10176
|
setTimeout(() => {
|
|
9865
10177
|
void (async () => {
|
|
9866
|
-
const bgCtx = { logger: ctx.logger
|
|
10178
|
+
const bgCtx = { logger: ctx.logger };
|
|
9867
10179
|
const importIds = uploadedChunks.map((chunk) => chunk.importId);
|
|
9868
10180
|
const notificationIds = [];
|
|
9869
10181
|
const matched = /* @__PURE__ */ new Map();
|
|
9870
10182
|
const notImported = /* @__PURE__ */ new Map();
|
|
10183
|
+
let inFlight = 0;
|
|
10184
|
+
let committed = false;
|
|
9871
10185
|
try {
|
|
9872
10186
|
const totalDeadline = Date.now() + opts.totalBudget;
|
|
9873
|
-
for (
|
|
9874
|
-
const
|
|
10187
|
+
for (inFlight = 0; inFlight < uploadedChunks.length; inFlight++) {
|
|
10188
|
+
const upload = uploadedChunks[inFlight];
|
|
10189
|
+
committed = false;
|
|
10190
|
+
const out = await completeUploadedChunk(client, upload, prep.mappings, opts.dryRun, opts.perPhaseBudget, totalDeadline, bgCtx, void 0, void 0, () => {
|
|
10191
|
+
committed = true;
|
|
10192
|
+
});
|
|
9875
10193
|
if (out.notification_id && !notificationIds.includes(out.notification_id)) {
|
|
9876
10194
|
notificationIds.push(out.notification_id);
|
|
9877
10195
|
}
|
|
@@ -9879,22 +10197,28 @@ async function runImportInBackground(client, prep, uploadedChunks, opts, ctx, ha
|
|
|
9879
10197
|
reconcileOneChunk(prep, out, matched, notImported);
|
|
9880
10198
|
}
|
|
9881
10199
|
}
|
|
9882
|
-
|
|
9883
|
-
await tracker.markImportComplete(handleId, {
|
|
9884
|
-
leads: result.leads,
|
|
9885
|
-
not_imported: result.not_imported,
|
|
9886
|
-
importIds: result.importIds
|
|
9887
|
-
});
|
|
10200
|
+
buildImportLeadsResult(client, prep, importIds, matched, notImported, opts.dryRun, false, notificationIds);
|
|
9888
10201
|
} catch (err) {
|
|
9889
|
-
|
|
10202
|
+
if (!opts.dryRun) {
|
|
10203
|
+
const parkedFrom = committed ? inFlight + 1 : inFlight;
|
|
10204
|
+
for (const parked of uploadedChunks.slice(parkedFrom)) {
|
|
10205
|
+
resumeParkedUpload(client, parked, prep.mappings, bgCtx);
|
|
10206
|
+
}
|
|
10207
|
+
}
|
|
10208
|
+
ctx?.logger?.warn?.(`import-leads: background import failed for ${importIds.join(",")}: ${err?.message ?? err?.code ?? err}`);
|
|
9890
10209
|
}
|
|
9891
|
-
})()
|
|
10210
|
+
})().catch((e) => (
|
|
10211
|
+
// Terminal guard. Nothing may escape a detached task — an unhandled
|
|
10212
|
+
// rejection would take the whole multi-tenant hosted process down.
|
|
10213
|
+
ctx?.logger?.error?.(`import-leads: background import crashed: ${e?.message ?? e}`)
|
|
10214
|
+
));
|
|
9892
10215
|
}, 0);
|
|
9893
10216
|
}
|
|
9894
10217
|
var CHUNK_SIZE, POLL_INTERVAL_MS2, DEFAULT_PER_PHASE_BUDGET_MS, DEFAULT_TOTAL_BUDGET_MS, STABILIZATION_POLLS, MAX_COLUMN_NAME_LEN, RESERVED_COLUMN_RE, CUSTOM_FIELD_RE, IMPORT_RESOLVER_FIELDS, ImportPhaseTimeout, LEAD_STATUSES, LEAD_STATUS_SET, importLeads, DEFAULT_RESUME_COMMIT_BUDGET_MS, resumeCommitBudgetMs;
|
|
9895
10218
|
var init_import_leads = __esm({
|
|
9896
10219
|
"../core/dist/composite/import-leads.js"() {
|
|
9897
10220
|
"use strict";
|
|
10221
|
+
init_launch_guard();
|
|
9898
10222
|
init_tool_descriptions_generated();
|
|
9899
10223
|
init_import_records();
|
|
9900
10224
|
init_import_commit_log();
|
|
@@ -10017,7 +10341,7 @@ var init_import_leads = __esm({
|
|
|
10017
10341
|
},
|
|
10018
10342
|
wait_for_completion: {
|
|
10019
10343
|
type: "boolean",
|
|
10020
|
-
description: "When false,
|
|
10344
|
+
description: "When false, upload the rows and return `{status:'running', importIds}` immediately. Poll leadbay_import_status({importIds, dry_run}) \u2014 those importIds are the backend's own and keep working from any conversation. Default is true."
|
|
10021
10345
|
}
|
|
10022
10346
|
},
|
|
10023
10347
|
// Neither field is "required" at the schema level; xor + presence is
|
|
@@ -10036,10 +10360,6 @@ var init_import_leads = __esm({
|
|
|
10036
10360
|
type: "string",
|
|
10037
10361
|
description: "`running` when wait_for_completion=false; absent on the legacy blocking result."
|
|
10038
10362
|
},
|
|
10039
|
-
handle_id: {
|
|
10040
|
-
type: "string",
|
|
10041
|
-
description: "Persisted UUID handle to pass to leadbay_import_status. Only on the wait_for_completion=false path; absent when a blocking call timed out (use importIds then)."
|
|
10042
|
-
},
|
|
10043
10363
|
timed_out: {
|
|
10044
10364
|
type: "boolean",
|
|
10045
10365
|
description: "True when a blocking call ran out of poll budget. The import is still running server-side \u2014 poll leadbay_import_status(importIds). Do NOT re-issue the import."
|
|
@@ -10070,7 +10390,7 @@ var init_import_leads = __esm({
|
|
|
10070
10390
|
region: { type: "string" },
|
|
10071
10391
|
cancelled: {
|
|
10072
10392
|
type: "boolean",
|
|
10073
|
-
description: "True when
|
|
10393
|
+
description: "True when the HOST cancelled the call (a user Cancel, or the host's own request timeout); a Leadbay-side or phase-budget timeout reports `timed_out` instead. Rows already uploaded keep importing on Leadbay \u2014 poll leadbay_import_status with importIds, but a chunk cancelled before its mappings were committed can read `running` without ever progressing; if the counts stop moving, say so rather than polling on. Rows past the interruption may never have been uploaded; re-run the import for those rows only."
|
|
10074
10394
|
},
|
|
10075
10395
|
dry_run: {
|
|
10076
10396
|
type: "boolean",
|
|
@@ -10138,62 +10458,72 @@ var init_import_leads = __esm({
|
|
|
10138
10458
|
}
|
|
10139
10459
|
const chunks = chunkAt100(prep.validInputs);
|
|
10140
10460
|
if (!waitForCompletion) {
|
|
10141
|
-
|
|
10142
|
-
|
|
10143
|
-
|
|
10144
|
-
|
|
10145
|
-
|
|
10146
|
-
|
|
10147
|
-
|
|
10148
|
-
|
|
10149
|
-
|
|
10150
|
-
const
|
|
10151
|
-
|
|
10152
|
-
|
|
10153
|
-
|
|
10154
|
-
|
|
10155
|
-
|
|
10156
|
-
|
|
10157
|
-
|
|
10158
|
-
|
|
10159
|
-
|
|
10160
|
-
|
|
10161
|
-
}
|
|
10162
|
-
await ctx.bulkTracker.setImportProgress(reservation.record.bulk_id, {
|
|
10461
|
+
const fingerprint = launchFingerprint([
|
|
10462
|
+
"import",
|
|
10463
|
+
prep.mode,
|
|
10464
|
+
dryRun,
|
|
10465
|
+
// Cached /users/me — the admin gate above already read it, so this
|
|
10466
|
+
// costs no round trip. Scopes the fingerprint to the organization.
|
|
10467
|
+
await client.resolveOrgId(),
|
|
10468
|
+
importFingerprint(params, prep)
|
|
10469
|
+
]);
|
|
10470
|
+
const claim = beginLaunch(fingerprint);
|
|
10471
|
+
if (claim.state === "in_flight") {
|
|
10472
|
+
throw client.makeError("IMPORT_LAUNCH_IN_FLIGHT", `An identical import was started ${claim.seconds_since}s ago and has not returned its import ids yet`, "Nothing was uploaded twice. Call leadbay_import_leads again with the same arguments in a few seconds to receive the importIds.", "");
|
|
10473
|
+
}
|
|
10474
|
+
const already = claim.state === "settled" ? claim.record : void 0;
|
|
10475
|
+
if (already) {
|
|
10476
|
+
return {
|
|
10477
|
+
status: "running",
|
|
10478
|
+
importIds: already.import_ids ?? [],
|
|
10479
|
+
notification_ids: [],
|
|
10480
|
+
progress: {
|
|
10163
10481
|
phase: "preprocess",
|
|
10164
10482
|
records_processed: 0,
|
|
10165
10483
|
records_total: prep.validInputs.length
|
|
10484
|
+
},
|
|
10485
|
+
region: client.region,
|
|
10486
|
+
reused: true,
|
|
10487
|
+
seconds_since_original: already.seconds_since,
|
|
10488
|
+
_meta: client.lastMeta ?? {
|
|
10489
|
+
region: client.region,
|
|
10490
|
+
endpoint: "POST /imports",
|
|
10491
|
+
latency_ms: null,
|
|
10492
|
+
retry_after: null
|
|
10493
|
+
}
|
|
10494
|
+
};
|
|
10495
|
+
}
|
|
10496
|
+
const importIds2 = [];
|
|
10497
|
+
const uploadedChunks = [];
|
|
10498
|
+
try {
|
|
10499
|
+
for (let i = 0; i < chunks.length; i++) {
|
|
10500
|
+
const upload = await uploadOneChunk(client, chunks[i], i, chunks.length, prep.header, ctx, (id) => {
|
|
10501
|
+
if (!importIds2.includes(id))
|
|
10502
|
+
importIds2.push(id);
|
|
10166
10503
|
});
|
|
10167
|
-
|
|
10168
|
-
await ctx.bulkTracker.markImportFailed(reservation.record.bulk_id, err?.message ?? err?.code ?? "unknown");
|
|
10169
|
-
throw err;
|
|
10504
|
+
uploadedChunks.push(upload);
|
|
10170
10505
|
}
|
|
10506
|
+
} catch (err) {
|
|
10507
|
+
abandonLaunch(fingerprint);
|
|
10508
|
+
throw err;
|
|
10171
10509
|
}
|
|
10510
|
+
rememberLaunch(fingerprint, null, void 0, importIds2);
|
|
10172
10511
|
if (uploadedChunks.length > 0) {
|
|
10173
|
-
void runImportInBackground(client, prep, uploadedChunks, {
|
|
10174
|
-
dryRun,
|
|
10175
|
-
perPhaseBudget,
|
|
10176
|
-
totalBudget
|
|
10177
|
-
}, ctx, reservation.record.bulk_id);
|
|
10512
|
+
void runImportInBackground(client, prep, uploadedChunks, { dryRun, perPhaseBudget, totalBudget }, ctx ?? {});
|
|
10178
10513
|
}
|
|
10179
10514
|
return {
|
|
10180
10515
|
status: "running",
|
|
10181
|
-
handle_id: reservation.record.bulk_id,
|
|
10182
10516
|
importIds: importIds2,
|
|
10183
|
-
// Notifications fire from update_mappings, which the background
|
|
10184
|
-
//
|
|
10185
|
-
//
|
|
10517
|
+
// Notifications fire from update_mappings, which the background task
|
|
10518
|
+
// hasn't called yet. They surface via the WS listener / catch-up REST
|
|
10519
|
+
// on subsequent turns.
|
|
10186
10520
|
notification_ids: [],
|
|
10187
10521
|
progress: {
|
|
10188
|
-
phase:
|
|
10189
|
-
records_processed:
|
|
10190
|
-
records_total:
|
|
10522
|
+
phase: importIds2.length > 0 ? "preprocess" : "queued",
|
|
10523
|
+
records_processed: 0,
|
|
10524
|
+
records_total: prep.validInputs.length
|
|
10191
10525
|
},
|
|
10192
10526
|
region: client.region,
|
|
10193
|
-
...reservation.reused ? {
|
|
10194
|
-
reused: true,
|
|
10195
|
-
seconds_since_original: reservation.seconds_since_original
|
|
10196
|
-
} : {},
|
|
10197
10527
|
_meta: client.lastMeta ?? {
|
|
10198
10528
|
region: client.region,
|
|
10199
10529
|
endpoint: "POST /imports",
|
|
@@ -11446,7 +11776,9 @@ var init_launch_bulk_enrichment = __esm({
|
|
|
11446
11776
|
title: "Launch bulk enrichment",
|
|
11447
11777
|
readOnlyHint: false,
|
|
11448
11778
|
destructiveHint: true,
|
|
11449
|
-
|
|
11779
|
+
// No double-launch guard: this POSTs straight through, so an identical
|
|
11780
|
+
// repeat is a second paid launch, not a no-op (product#4039).
|
|
11781
|
+
idempotentHint: false,
|
|
11450
11782
|
openWorldHint: true
|
|
11451
11783
|
},
|
|
11452
11784
|
description: leadbay_launch_bulk_enrichment,
|
|
@@ -15385,7 +15717,7 @@ var init_getting_started = __esm({
|
|
|
15385
15717
|
leadIds: "[<the ONE lead you drafted for at step 3>] \u2014 an ARRAY, always",
|
|
15386
15718
|
lensId: "<the pinned lens id from step 2>"
|
|
15387
15719
|
},
|
|
15388
|
-
spend: "TWO BEATS \u2014 free preview FIRST, the real reveal only after the user confirms. Beat 1: call leadbay_enrich_titles with the drafted lead's id + lensId and NO titles / NO confirm / NO email / NO phone. That returns mode:'discover' \u2014 the FREE list of job titles at that company. Say plainly that nothing has been spent yet. Beat 2: name the title the draft is addressed to, tell them BEFORE they decide what it costs (one credit per contact revealed \u2014 here that is ONE contact, one credit), and ask them to confirm. Only then call leadbay_enrich_titles AGAIN with leadIds: [<that lead id>] \u2014 ALWAYS the array, even for a single lead: `leadId` singular is not a key this tool reads, so it is dropped and the paid call falls back to the default wishlist selection, charging for the whole batch \u2014 plus the chosen title, confirm:true and email:true. Poll leadbay_bulk_enrich_status with the returned
|
|
15720
|
+
spend: "TWO BEATS \u2014 free preview FIRST, the real reveal only after the user confirms. Beat 1: call leadbay_enrich_titles with the drafted lead's id + lensId and NO titles / NO confirm / NO email / NO phone. That returns mode:'discover' \u2014 the FREE list of job titles at that company. Say plainly that nothing has been spent yet. Beat 2: name the title the draft is addressed to, tell them BEFORE they decide what it costs (one credit per contact revealed \u2014 here that is ONE contact, one credit), and ask them to confirm. Only then call leadbay_enrich_titles AGAIN with leadIds: [<that lead id>] \u2014 ALWAYS the array, even for a single lead: `leadId` singular is not a key this tool reads, so it is dropped and the paid call falls back to the default wishlist selection, charging for the whole batch \u2014 plus the chosen title, confirm:true and email:true. Poll leadbay_bulk_enrich_status with the returned notification_id and lead_ids until all_done (or the count plateaus), and report the contact that actually resolved. NEVER launch the reveal without an explicit confirm: silence is not consent, and neither is 'they clicked the gate'. If they decline, keep the draft and the title and move on \u2014 that is a normal outcome, not a failure.",
|
|
15389
15721
|
quota_note: "After the reveal, close the loop on gate 1 in one line: one credit per contact revealed, so this cost one. Then say the thing that makes it land \u2014 the draft from gate 3 now has a real person and a real address to go to. Re-check leadbay_account_status if you want to show the moved windows. This is where gate 1's numbers stop being abstract: they just watched them move, and got something for it. Keep it to a line; no pricing pitch."
|
|
15390
15722
|
}
|
|
15391
15723
|
],
|
|
@@ -16444,6 +16776,7 @@ var PAGE_SIZE, DEFAULT_COUNT2, MAX_COUNT, DEFAULT_PER_LEAD_BUDGET_MS, DEFAULT_TO
|
|
|
16444
16776
|
var init_bulk_qualify_leads = __esm({
|
|
16445
16777
|
"../core/dist/composite/bulk-qualify-leads.js"() {
|
|
16446
16778
|
"use strict";
|
|
16779
|
+
init_launch_guard();
|
|
16447
16780
|
init_tool_descriptions_generated();
|
|
16448
16781
|
PAGE_SIZE = 50;
|
|
16449
16782
|
DEFAULT_COUNT2 = 10;
|
|
@@ -16488,7 +16821,7 @@ var init_bulk_qualify_leads = __esm({
|
|
|
16488
16821
|
},
|
|
16489
16822
|
wait_for_completion: {
|
|
16490
16823
|
type: "boolean",
|
|
16491
|
-
description: "When false, launch qualification and return `{status:'running',
|
|
16824
|
+
description: "When false, launch qualification and return `{status:'running', notification_id, lead_ids, lens_id}` immediately. Poll leadbay_qualify_status with them. Default is true for 0.6.x backwards compatibility."
|
|
16492
16825
|
}
|
|
16493
16826
|
},
|
|
16494
16827
|
additionalProperties: false
|
|
@@ -16505,13 +16838,15 @@ var init_bulk_qualify_leads = __esm({
|
|
|
16505
16838
|
type: "string",
|
|
16506
16839
|
description: "`running` when wait_for_completion=false; absent on the legacy blocking result."
|
|
16507
16840
|
},
|
|
16508
|
-
|
|
16509
|
-
|
|
16841
|
+
notification_id: {
|
|
16842
|
+
type: ["string", "null"],
|
|
16843
|
+
description: "The backend's job id. Carry it to leadbay_qualify_status."
|
|
16844
|
+
},
|
|
16510
16845
|
lead_ids: { type: "array", items: { type: "string" } },
|
|
16511
16846
|
launched_count: { type: "number" },
|
|
16512
16847
|
still_running: {
|
|
16513
16848
|
type: "array",
|
|
16514
|
-
description: "Leads launched but whose qualification did not complete within budget. Re-poll via leadbay_qualify_status with
|
|
16849
|
+
description: "Leads launched but whose qualification did not complete within budget. Re-poll via leadbay_qualify_status with notification_id (when present) or lead_ids + lens_id.",
|
|
16515
16850
|
items: { type: "object" }
|
|
16516
16851
|
},
|
|
16517
16852
|
failed: {
|
|
@@ -16545,8 +16880,7 @@ var init_bulk_qualify_leads = __esm({
|
|
|
16545
16880
|
{
|
|
16546
16881
|
required: [
|
|
16547
16882
|
"status",
|
|
16548
|
-
"
|
|
16549
|
-
"qualify_id",
|
|
16883
|
+
"notification_id",
|
|
16550
16884
|
"lead_ids",
|
|
16551
16885
|
"launched_count",
|
|
16552
16886
|
"failed",
|
|
@@ -16606,45 +16940,45 @@ var init_bulk_qualify_leads = __esm({
|
|
|
16606
16940
|
};
|
|
16607
16941
|
}
|
|
16608
16942
|
if (!waitForCompletion) {
|
|
16609
|
-
|
|
16610
|
-
|
|
16943
|
+
const fingerprint = launchFingerprint(["qualify", candidates, lensId]);
|
|
16944
|
+
const claim = beginLaunch(fingerprint);
|
|
16945
|
+
if (claim.state === "in_flight") {
|
|
16946
|
+
throw client.makeError("QUALIFY_LAUNCH_IN_FLIGHT", `An identical qualification was started ${claim.seconds_since}s ago and has not returned its job id yet`, "Nothing was launched twice. Call leadbay_bulk_qualify_leads again with the same arguments in a few seconds to receive the notification_id.", "");
|
|
16611
16947
|
}
|
|
16612
|
-
const
|
|
16613
|
-
|
|
16614
|
-
|
|
16615
|
-
|
|
16616
|
-
|
|
16617
|
-
|
|
16618
|
-
|
|
16619
|
-
|
|
16620
|
-
|
|
16621
|
-
|
|
16622
|
-
|
|
16623
|
-
|
|
16624
|
-
|
|
16625
|
-
|
|
16626
|
-
|
|
16627
|
-
|
|
16628
|
-
|
|
16629
|
-
|
|
16630
|
-
|
|
16631
|
-
|
|
16632
|
-
|
|
16633
|
-
if (queuedIds.length > 0 || quotaExceeded2 || skippedIds.length > 0 || failed2.length === candidates.length) {
|
|
16634
|
-
await ctx.bulkTracker.markLaunched(reservation.record.bulk_id, notificationId);
|
|
16635
|
-
}
|
|
16636
|
-
} else {
|
|
16637
|
-
notificationId = reservation.record.notification_id ?? null;
|
|
16638
|
-
launchedCount = reservation.record.lead_ids.length;
|
|
16948
|
+
const already = claim.state === "settled" ? claim.record : void 0;
|
|
16949
|
+
if (already) {
|
|
16950
|
+
return {
|
|
16951
|
+
status: "running",
|
|
16952
|
+
lead_ids: candidates,
|
|
16953
|
+
launched_count: candidates.length,
|
|
16954
|
+
failed: [],
|
|
16955
|
+
quota_exceeded: false,
|
|
16956
|
+
lens_id: lensId,
|
|
16957
|
+
notification_id: already.notification_id,
|
|
16958
|
+
reused: true,
|
|
16959
|
+
seconds_since_original_launch: already.seconds_since,
|
|
16960
|
+
_meta: { region: client.region }
|
|
16961
|
+
};
|
|
16962
|
+
}
|
|
16963
|
+
let launch;
|
|
16964
|
+
try {
|
|
16965
|
+
launch = await launchBulkQualify(client, candidates, ctx);
|
|
16966
|
+
} catch (err) {
|
|
16967
|
+
abandonLaunch(fingerprint);
|
|
16968
|
+
throw err;
|
|
16639
16969
|
}
|
|
16970
|
+
const notificationId = launch.resp?.notification_id ?? null;
|
|
16971
|
+
const queuedIds = launch.resp?.queued_ids ?? [];
|
|
16972
|
+
const skippedIds = launch.resp?.skipped_ids ?? [];
|
|
16973
|
+
const seen = /* @__PURE__ */ new Set([...queuedIds, ...skippedIds]);
|
|
16974
|
+
const failed2 = candidates.filter((id) => !seen.has(id)).map((id) => ({ lead_id: id, error: "not_queued" }));
|
|
16975
|
+
rememberLaunch(fingerprint, notificationId);
|
|
16640
16976
|
const out = {
|
|
16641
16977
|
status: "running",
|
|
16642
|
-
handle_id: reservation.record.bulk_id,
|
|
16643
|
-
qualify_id: reservation.record.bulk_id,
|
|
16644
16978
|
lead_ids: candidates,
|
|
16645
|
-
launched_count:
|
|
16979
|
+
launched_count: queuedIds.length,
|
|
16646
16980
|
failed: failed2,
|
|
16647
|
-
quota_exceeded:
|
|
16981
|
+
quota_exceeded: launch.quotaExceeded,
|
|
16648
16982
|
lens_id: lensId,
|
|
16649
16983
|
notification_id: notificationId,
|
|
16650
16984
|
_meta: { region: client.region }
|
|
@@ -17255,6 +17589,7 @@ var DEFAULT_PER_LEAD_BUDGET_MS2, DEFAULT_TOTAL_BUDGET_MS3, DEFAULT_PER_PHASE_BUD
|
|
|
17255
17589
|
var init_import_and_qualify = __esm({
|
|
17256
17590
|
"../core/dist/composite/import-and-qualify.js"() {
|
|
17257
17591
|
"use strict";
|
|
17592
|
+
init_launch_guard();
|
|
17258
17593
|
init_tool_descriptions_generated();
|
|
17259
17594
|
init_import_leads();
|
|
17260
17595
|
init_qualify_helpers();
|
|
@@ -17341,7 +17676,7 @@ var init_import_and_qualify = __esm({
|
|
|
17341
17676
|
},
|
|
17342
17677
|
total_budget_ms: {
|
|
17343
17678
|
type: "number",
|
|
17344
|
-
description: `Total wall-clock budget across import + qualify in ms (default ${DEFAULT_TOTAL_BUDGET_MS3}). When exhausted, the response returns
|
|
17679
|
+
description: `Total wall-clock budget across import + qualify in ms (default ${DEFAULT_TOTAL_BUDGET_MS3}). When exhausted, the response returns lead_ids + lens_id for resume via leadbay_qualify_status.`
|
|
17345
17680
|
},
|
|
17346
17681
|
per_phase_budget_ms: {
|
|
17347
17682
|
type: "number",
|
|
@@ -17349,7 +17684,7 @@ var init_import_and_qualify = __esm({
|
|
|
17349
17684
|
},
|
|
17350
17685
|
wait_for_completion: {
|
|
17351
17686
|
type: "boolean",
|
|
17352
|
-
description: "When false,
|
|
17687
|
+
description: "When false, upload the rows and return `{kind:'result', status:'running', import_ids}` immediately. Poll leadbay_import_status({importIds, dry_run}); the qualify phase does NOT run on this path \u2014 call leadbay_bulk_qualify_leads on the imported leads yourself. Default is true."
|
|
17353
17688
|
},
|
|
17354
17689
|
lensId: {
|
|
17355
17690
|
type: "number",
|
|
@@ -17372,7 +17707,7 @@ var init_import_and_qualify = __esm({
|
|
|
17372
17707
|
},
|
|
17373
17708
|
outputSchema: {
|
|
17374
17709
|
type: "object",
|
|
17375
|
-
description: "Two return shapes: kind:'preview' (when dry_run='preview') with mapping hints; kind:'result' (default) with imported + qualified leads +
|
|
17710
|
+
description: "Two return shapes: kind:'preview' (when dry_run='preview') with mapping hints; kind:'result' (default) with imported + qualified leads + lead_ids/lens_id to resume via leadbay_qualify_status.",
|
|
17376
17711
|
properties: {
|
|
17377
17712
|
kind: {
|
|
17378
17713
|
type: "string",
|
|
@@ -17382,10 +17717,6 @@ var init_import_and_qualify = __esm({
|
|
|
17382
17717
|
type: "string",
|
|
17383
17718
|
description: "`running` when wait_for_completion=false."
|
|
17384
17719
|
},
|
|
17385
|
-
handle_id: {
|
|
17386
|
-
type: "string",
|
|
17387
|
-
description: "Import handle to pass to leadbay_import_status when wait_for_completion=false."
|
|
17388
|
-
},
|
|
17389
17720
|
timed_out: {
|
|
17390
17721
|
type: "boolean",
|
|
17391
17722
|
description: "True when the underlying import ran out of poll budget. Still running server-side \u2014 poll leadbay_import_status(import_ids). Do NOT re-issue the import."
|
|
@@ -17430,10 +17761,12 @@ var init_import_and_qualify = __esm({
|
|
|
17430
17761
|
type: "object",
|
|
17431
17762
|
description: "Adaptive budgets the composite selected (when caller didn't override): {per_lead_budget_ms, total_budget_ms, per_phase_budget_ms, wall_clock_estimate_ms, strategy}."
|
|
17432
17763
|
},
|
|
17433
|
-
|
|
17434
|
-
type:
|
|
17435
|
-
description: "
|
|
17764
|
+
lead_ids: {
|
|
17765
|
+
type: "array",
|
|
17766
|
+
description: "Leads the qualify phase covers. Pass to leadbay_qualify_status for per-lead detail.",
|
|
17767
|
+
items: { type: "string" }
|
|
17436
17768
|
},
|
|
17769
|
+
lens_id: { type: "number", description: "Lens the qualification ran against." },
|
|
17437
17770
|
import_ids: {
|
|
17438
17771
|
type: "array",
|
|
17439
17772
|
description: "Backend file-import handles (one per chunk).",
|
|
@@ -17456,7 +17789,7 @@ var init_import_and_qualify = __esm({
|
|
|
17456
17789
|
},
|
|
17457
17790
|
still_running: {
|
|
17458
17791
|
type: "array",
|
|
17459
|
-
description: "Leads still being qualified at deadline; agent calls leadbay_qualify_status with
|
|
17792
|
+
description: "Leads still being qualified at deadline; agent calls leadbay_qualify_status with lead_ids + lens_id.",
|
|
17460
17793
|
items: { type: "object" }
|
|
17461
17794
|
},
|
|
17462
17795
|
failed: {
|
|
@@ -17477,10 +17810,13 @@ var init_import_and_qualify = __esm({
|
|
|
17477
17810
|
},
|
|
17478
17811
|
reused: {
|
|
17479
17812
|
type: "boolean",
|
|
17480
|
-
description: "True when an identical
|
|
17813
|
+
description: "True when an identical launch was reused within the idempotency window."
|
|
17481
17814
|
},
|
|
17482
17815
|
seconds_since_original: { type: "number" },
|
|
17483
|
-
cancelled: {
|
|
17816
|
+
cancelled: {
|
|
17817
|
+
type: "boolean",
|
|
17818
|
+
description: "True when the HOST cancelled the call (a user Cancel, or the host's own request timeout); a budget timeout reports `budget_exhausted` instead. The import and the qualifications already launched keep running on Leadbay, except a chunk cancelled before its mappings were committed, which can read `running` without ever progressing \u2014 if the counts stop moving, say so rather than polling on. Poll leadbay_import_status with the `import_ids` values passed as `importIds`, and leadbay_qualify_status with lead_ids + lens_id. `still_running` can also hold leads whose qualification was never launched; those need a fresh qualification, which qualify_status cannot start."
|
|
17819
|
+
},
|
|
17484
17820
|
budget_exhausted: { type: "boolean", description: "True when total_budget_ms hit before all leads finished." },
|
|
17485
17821
|
quota_blocked: { type: "boolean", description: "True when quota was exhausted before launching all leads." },
|
|
17486
17822
|
region: { type: "string" },
|
|
@@ -17502,9 +17838,6 @@ var init_import_and_qualify = __esm({
|
|
|
17502
17838
|
if (params.dry_run === "preview") {
|
|
17503
17839
|
return await runPreview(client, params, ctx, perPhaseBudget, totalBudget);
|
|
17504
17840
|
}
|
|
17505
|
-
if (!ctx?.bulkTracker) {
|
|
17506
|
-
throw client.makeError("BULK_TRACKER_UNAVAILABLE", "No BulkTracker configured on this MCP instance", "leadbay_import_and_qualify needs a BulkTracker (qualify_id persistence). Upgrade to @leadbay/mcp \u22650.5.0 or set LEADBAY_BULK_STORE_ALLOW_MEMORY=1.", "");
|
|
17507
|
-
}
|
|
17508
17841
|
if (params.wait_for_completion === false) {
|
|
17509
17842
|
const queued = await importLeads.execute(client, {
|
|
17510
17843
|
domains: params.domains,
|
|
@@ -17519,7 +17852,8 @@ var init_import_and_qualify = __esm({
|
|
|
17519
17852
|
return {
|
|
17520
17853
|
kind: "result",
|
|
17521
17854
|
...chosenBudgets ? { chosen_budgets: chosenBudgets } : {},
|
|
17522
|
-
|
|
17855
|
+
lead_ids: [],
|
|
17856
|
+
lens_id: 0,
|
|
17523
17857
|
import_ids: queued.importIds,
|
|
17524
17858
|
notification_ids: queued.notification_ids ?? [],
|
|
17525
17859
|
imported: queued.leads.map((l) => ({
|
|
@@ -17542,9 +17876,9 @@ var init_import_and_qualify = __esm({
|
|
|
17542
17876
|
return {
|
|
17543
17877
|
kind: "result",
|
|
17544
17878
|
status: "running",
|
|
17545
|
-
...queued.handle_id ? { handle_id: queued.handle_id } : {},
|
|
17546
17879
|
...chosenBudgets ? { chosen_budgets: chosenBudgets } : {},
|
|
17547
|
-
|
|
17880
|
+
lead_ids: [],
|
|
17881
|
+
lens_id: 0,
|
|
17548
17882
|
import_ids: queued.importIds,
|
|
17549
17883
|
notification_ids: queued.notification_ids ?? [],
|
|
17550
17884
|
imported: [],
|
|
@@ -17577,7 +17911,6 @@ var init_import_and_qualify = __esm({
|
|
|
17577
17911
|
return {
|
|
17578
17912
|
kind: "result",
|
|
17579
17913
|
status: "running",
|
|
17580
|
-
...importResultRaw.handle_id ? { handle_id: importResultRaw.handle_id } : {},
|
|
17581
17914
|
// Everything the rendering contract keys off has to survive the
|
|
17582
17915
|
// wrapper. Without `timed_out` the agent can't tell this from a
|
|
17583
17916
|
// deliberate async launch; without `rows_pending_upload` a >100-row
|
|
@@ -17589,7 +17922,8 @@ var init_import_and_qualify = __esm({
|
|
|
17589
17922
|
...importResultRaw.dry_run ? { dry_run: true } : {},
|
|
17590
17923
|
...importResultRaw.row_ids ? { row_ids: importResultRaw.row_ids } : {},
|
|
17591
17924
|
...chosenBudgets ? { chosen_budgets: chosenBudgets } : {},
|
|
17592
|
-
|
|
17925
|
+
lead_ids: [],
|
|
17926
|
+
lens_id: 0,
|
|
17593
17927
|
import_ids: importResultRaw.importIds,
|
|
17594
17928
|
notification_ids: importResultRaw.notification_ids ?? [],
|
|
17595
17929
|
imported: [],
|
|
@@ -17610,7 +17944,8 @@ var init_import_and_qualify = __esm({
|
|
|
17610
17944
|
kind: "result",
|
|
17611
17945
|
...params.dry_run === true ? { dry_run: true } : {},
|
|
17612
17946
|
...chosenBudgets ? { chosen_budgets: chosenBudgets } : {},
|
|
17613
|
-
|
|
17947
|
+
lead_ids: [],
|
|
17948
|
+
lens_id: 0,
|
|
17614
17949
|
import_ids: importResult.importIds,
|
|
17615
17950
|
notification_ids: importResult.notification_ids ?? [],
|
|
17616
17951
|
imported: [],
|
|
@@ -17648,7 +17983,8 @@ var init_import_and_qualify = __esm({
|
|
|
17648
17983
|
kind: "result",
|
|
17649
17984
|
...params.dry_run === true ? { dry_run: true } : {},
|
|
17650
17985
|
...chosenBudgets ? { chosen_budgets: chosenBudgets } : {},
|
|
17651
|
-
|
|
17986
|
+
lead_ids: [],
|
|
17987
|
+
lens_id: 0,
|
|
17652
17988
|
import_ids: importResult.importIds,
|
|
17653
17989
|
notification_ids: importResult.notification_ids ?? [],
|
|
17654
17990
|
imported,
|
|
@@ -17694,29 +18030,17 @@ var init_import_and_qualify = __esm({
|
|
|
17694
18030
|
// targets do NOT collide on the same qualify_id.
|
|
17695
18031
|
buildFingerprintInput(params.mappings)
|
|
17696
18032
|
);
|
|
17697
|
-
const
|
|
17698
|
-
|
|
17699
|
-
|
|
17700
|
-
|
|
17701
|
-
|
|
17702
|
-
|
|
17703
|
-
|
|
17704
|
-
|
|
17705
|
-
|
|
17706
|
-
|
|
17707
|
-
|
|
17708
|
-
let launchMarked = false;
|
|
17709
|
-
for (const attempt of [1, 2]) {
|
|
17710
|
-
try {
|
|
17711
|
-
await ctx.bulkTracker.markLaunched(reservation.record.bulk_id);
|
|
17712
|
-
launchMarked = true;
|
|
17713
|
-
break;
|
|
17714
|
-
} catch (err) {
|
|
17715
|
-
ctx?.logger?.warn?.(`import_and_qualify: markLaunched attempt ${attempt} failed: ${err?.message ?? err}`);
|
|
17716
|
-
}
|
|
17717
|
-
}
|
|
17718
|
-
if (!launchMarked) {
|
|
17719
|
-
ctx?.logger?.warn?.(`import_and_qualify: markLaunched failed twice \u2014 qualify_status may BULK_PENDING-trap immediate retrieval; agent should poll, not relaunch`);
|
|
18033
|
+
const qualifyFingerprint = launchFingerprint([
|
|
18034
|
+
"import_qualify",
|
|
18035
|
+
leadIds,
|
|
18036
|
+
importResult.importIds,
|
|
18037
|
+
lensId,
|
|
18038
|
+
mappingFp
|
|
18039
|
+
]);
|
|
18040
|
+
const qualifyClaim = beginLaunch(qualifyFingerprint);
|
|
18041
|
+
const alreadyQualified = qualifyClaim.state === "settled" ? qualifyClaim.record : void 0;
|
|
18042
|
+
if (alreadyQualified) {
|
|
18043
|
+
ctx?.logger?.info?.(`import_and_qualify: identical qualify ran ${alreadyQualified.seconds_since}s ago; re-running the fan-out to report current per-lead state (already-qualified leads are skipped inside it)`);
|
|
17720
18044
|
}
|
|
17721
18045
|
let questionOrder = void 0;
|
|
17722
18046
|
try {
|
|
@@ -17730,22 +18054,24 @@ var init_import_and_qualify = __esm({
|
|
|
17730
18054
|
total: 3,
|
|
17731
18055
|
message: `Qualifying ${leadIds.length} lead${leadIds.length === 1 ? "" : "s"} (phase 3/3)`
|
|
17732
18056
|
});
|
|
17733
|
-
|
|
17734
|
-
|
|
17735
|
-
|
|
17736
|
-
|
|
17737
|
-
|
|
17738
|
-
|
|
17739
|
-
|
|
17740
|
-
|
|
17741
|
-
|
|
17742
|
-
|
|
17743
|
-
|
|
17744
|
-
|
|
17745
|
-
|
|
17746
|
-
|
|
17747
|
-
|
|
18057
|
+
let fanOut;
|
|
18058
|
+
try {
|
|
18059
|
+
fanOut = await fanOutWebFetchAndPoll(client, leadIds, {
|
|
18060
|
+
perLeadBudgetMs: perLeadBudget,
|
|
18061
|
+
totalDeadlineMs: totalDeadline,
|
|
18062
|
+
signal,
|
|
18063
|
+
ctx,
|
|
18064
|
+
skipAlreadyQualifiedLensId: lensId,
|
|
18065
|
+
skipAlreadyQualifiedLaunch: skipAlreadyQualified,
|
|
18066
|
+
...questionOrder ? { questionOrder } : {}
|
|
18067
|
+
});
|
|
18068
|
+
} catch (err) {
|
|
18069
|
+
if (!alreadyQualified)
|
|
18070
|
+
abandonLaunch(qualifyFingerprint);
|
|
18071
|
+
throw err;
|
|
17748
18072
|
}
|
|
18073
|
+
if (!alreadyQualified)
|
|
18074
|
+
rememberLaunch(qualifyFingerprint, null);
|
|
17749
18075
|
const qualified = fanOut.results.filter((r) => !r._stillRunning).map(({ _stillRunning, ...rest }) => rest);
|
|
17750
18076
|
const notInLensSet = new Set(fanOut.not_in_lens);
|
|
17751
18077
|
const stillRunningIds = new Set([
|
|
@@ -17759,7 +18085,8 @@ var init_import_and_qualify = __esm({
|
|
|
17759
18085
|
return {
|
|
17760
18086
|
kind: "result",
|
|
17761
18087
|
...chosenBudgets ? { chosen_budgets: chosenBudgets } : {},
|
|
17762
|
-
|
|
18088
|
+
lead_ids: leadIds,
|
|
18089
|
+
lens_id: lensId,
|
|
17763
18090
|
import_ids: importResult.importIds,
|
|
17764
18091
|
notification_ids: importResult.notification_ids ?? [],
|
|
17765
18092
|
imported,
|
|
@@ -17770,9 +18097,9 @@ var init_import_and_qualify = __esm({
|
|
|
17770
18097
|
quota_exceeded: fanOut.quota_exceeded,
|
|
17771
18098
|
skipped_already_qualified,
|
|
17772
18099
|
not_in_lens: fanOut.not_in_lens,
|
|
17773
|
-
...
|
|
18100
|
+
...alreadyQualified ? {
|
|
17774
18101
|
reused: true,
|
|
17775
|
-
seconds_since_original:
|
|
18102
|
+
seconds_since_original: alreadyQualified.seconds_since
|
|
17776
18103
|
} : {},
|
|
17777
18104
|
...fanOut.cancelled ? { cancelled: true } : {},
|
|
17778
18105
|
...budgetExhausted ? { budget_exhausted: true } : {},
|
|
@@ -17790,708 +18117,72 @@ var init_import_and_qualify = __esm({
|
|
|
17790
18117
|
}
|
|
17791
18118
|
});
|
|
17792
18119
|
|
|
17793
|
-
// ../core/dist/
|
|
17794
|
-
|
|
17795
|
-
|
|
17796
|
-
|
|
17797
|
-
|
|
17798
|
-
|
|
17799
|
-
|
|
17800
|
-
|
|
17801
|
-
|
|
17802
|
-
|
|
17803
|
-
|
|
17804
|
-
|
|
17805
|
-
|
|
17806
|
-
|
|
17807
|
-
|
|
17808
|
-
|
|
17809
|
-
|
|
17810
|
-
|
|
17811
|
-
|
|
17812
|
-
|
|
17813
|
-
|
|
17814
|
-
|
|
17815
|
-
|
|
17816
|
-
|
|
17817
|
-
|
|
17818
|
-
|
|
17819
|
-
|
|
17820
|
-
|
|
17821
|
-
}
|
|
17822
|
-
|
|
17823
|
-
const
|
|
17824
|
-
"import",
|
|
17825
|
-
args.mode,
|
|
17826
|
-
args.dry_run ? "dry1" : "dry0",
|
|
17827
|
-
args.import_fingerprint
|
|
17828
|
-
];
|
|
17829
|
-
return createHash3("sha256").update(parts.join("|")).digest("hex");
|
|
17830
|
-
}
|
|
17831
|
-
function normalizeLaunchInputs(args) {
|
|
18120
|
+
// ../core/dist/composite/import-status.js
|
|
18121
|
+
function summarizeImports(imports, dryRun) {
|
|
18122
|
+
let recordsTotal = 0;
|
|
18123
|
+
let recordsProcessed = 0;
|
|
18124
|
+
let hasPreprocess = false;
|
|
18125
|
+
let hasProcess = false;
|
|
18126
|
+
let hasFailed = false;
|
|
18127
|
+
for (const imp of imports) {
|
|
18128
|
+
recordsTotal += Number(imp.total_records ?? 0);
|
|
18129
|
+
recordsProcessed += Number(imp.imported_records ?? 0);
|
|
18130
|
+
if (!imp.pre_processing?.finished) {
|
|
18131
|
+
hasPreprocess = true;
|
|
18132
|
+
continue;
|
|
18133
|
+
}
|
|
18134
|
+
if (imp.pre_processing?.error) {
|
|
18135
|
+
hasFailed = true;
|
|
18136
|
+
continue;
|
|
18137
|
+
}
|
|
18138
|
+
if (dryRun === true) {
|
|
18139
|
+
continue;
|
|
18140
|
+
}
|
|
18141
|
+
if (!imp.processing?.finished) {
|
|
18142
|
+
if (dryRun === false || imp.processing != null)
|
|
18143
|
+
hasProcess = true;
|
|
18144
|
+
continue;
|
|
18145
|
+
}
|
|
18146
|
+
if (imp.processing?.error) {
|
|
18147
|
+
hasFailed = true;
|
|
18148
|
+
}
|
|
18149
|
+
}
|
|
18150
|
+
const phase = hasFailed ? "failed" : hasPreprocess ? "preprocess" : hasProcess ? "process" : imports.length > 0 ? "complete" : "queued";
|
|
17832
18151
|
return {
|
|
17833
|
-
|
|
17834
|
-
|
|
18152
|
+
phase,
|
|
18153
|
+
records_processed: recordsProcessed,
|
|
18154
|
+
records_total: recordsTotal
|
|
17835
18155
|
};
|
|
17836
18156
|
}
|
|
17837
|
-
|
|
17838
|
-
|
|
17839
|
-
return await fsOpen(path, fsConstants.O_CREAT | fsConstants.O_WRONLY | fsConstants.O_EXCL, 384);
|
|
17840
|
-
} catch (err) {
|
|
17841
|
-
if (err?.code === "EEXIST") {
|
|
17842
|
-
await unlink(path).catch(() => {
|
|
17843
|
-
});
|
|
17844
|
-
return fsOpen(path, fsConstants.O_CREAT | fsConstants.O_WRONLY | fsConstants.O_EXCL, 384);
|
|
17845
|
-
}
|
|
17846
|
-
throw err;
|
|
17847
|
-
}
|
|
18157
|
+
function isInProgress(err) {
|
|
18158
|
+
return /in_progress/i.test(String(err?.message ?? ""));
|
|
17848
18159
|
}
|
|
17849
|
-
async function
|
|
17850
|
-
const
|
|
17851
|
-
const
|
|
17852
|
-
|
|
17853
|
-
|
|
17854
|
-
|
|
17855
|
-
|
|
17856
|
-
|
|
17857
|
-
|
|
17858
|
-
|
|
17859
|
-
|
|
17860
|
-
|
|
17861
|
-
|
|
17862
|
-
await stat(dirname(path));
|
|
17863
|
-
return store;
|
|
17864
|
-
} catch (err) {
|
|
17865
|
-
if (!allowMemory) {
|
|
17866
|
-
const msg = `bulk store init failed at ${path}: ${err?.message ?? err}. Set LEADBAY_BULK_STORE_ALLOW_MEMORY=1 to fall back to in-memory (handles won't survive MCP restart), or set LEADBAY_BULK_STORE_PATH to a writable path.`;
|
|
17867
|
-
opts.logger?.error?.(msg);
|
|
17868
|
-
throw new Error(msg);
|
|
18160
|
+
async function fetchReconciledRecords(client, importIds, declaredTotal, ctx) {
|
|
18161
|
+
const canonicalLeadIds = /* @__PURE__ */ new Set();
|
|
18162
|
+
for (const importId of importIds) {
|
|
18163
|
+
try {
|
|
18164
|
+
const res = await client.request("GET", `/imports/${importId}/leads`);
|
|
18165
|
+
for (const id of res?.lead_ids ?? [])
|
|
18166
|
+
canonicalLeadIds.add(id);
|
|
18167
|
+
} catch (err) {
|
|
18168
|
+
if (isInProgress(err))
|
|
18169
|
+
throw new ImportNotReady();
|
|
18170
|
+
if (err?.code !== "NOT_FOUND" && err?._meta?.http_status !== 404)
|
|
18171
|
+
throw err;
|
|
18172
|
+
ctx?.logger?.warn?.(`import-status: /imports/${importId}/leads not available on this backend (404) \u2014 using records only`);
|
|
17869
18173
|
}
|
|
17870
|
-
opts.logger?.warn?.(`bulk.fallback_memory path=${path} reason=${err?.message ?? err}`);
|
|
17871
|
-
return new LocalBulkStore({ backend: "memory", logger: opts.logger });
|
|
17872
18174
|
}
|
|
17873
|
-
|
|
17874
|
-
|
|
17875
|
-
|
|
17876
|
-
|
|
17877
|
-
|
|
17878
|
-
|
|
17879
|
-
|
|
17880
|
-
|
|
17881
|
-
|
|
17882
|
-
|
|
17883
|
-
|
|
17884
|
-
async lock() {
|
|
17885
|
-
if (!this.locked) {
|
|
17886
|
-
this.locked = true;
|
|
17887
|
-
return;
|
|
17888
|
-
}
|
|
17889
|
-
return new Promise((resolve) => {
|
|
17890
|
-
this.queue.push(() => {
|
|
17891
|
-
this.locked = true;
|
|
17892
|
-
resolve();
|
|
17893
|
-
});
|
|
17894
|
-
});
|
|
17895
|
-
}
|
|
17896
|
-
unlock() {
|
|
17897
|
-
this.locked = false;
|
|
17898
|
-
const next = this.queue.shift();
|
|
17899
|
-
if (next)
|
|
17900
|
-
next();
|
|
17901
|
-
}
|
|
17902
|
-
async run(fn) {
|
|
17903
|
-
await this.lock();
|
|
17904
|
-
try {
|
|
17905
|
-
return await fn();
|
|
17906
|
-
} finally {
|
|
17907
|
-
this.unlock();
|
|
17908
|
-
}
|
|
17909
|
-
}
|
|
17910
|
-
};
|
|
17911
|
-
LocalBulkStore = class {
|
|
17912
|
-
backend;
|
|
17913
|
-
path;
|
|
17914
|
-
logger;
|
|
17915
|
-
allowUnsafePath;
|
|
17916
|
-
now;
|
|
17917
|
-
mutex = new AsyncMutex();
|
|
17918
|
-
memory = [];
|
|
17919
|
-
// Cached file resolution — computed lazily on first access.
|
|
17920
|
-
initialized = false;
|
|
17921
|
-
constructor(opts) {
|
|
17922
|
-
this.backend = opts.backend;
|
|
17923
|
-
this.logger = opts.logger;
|
|
17924
|
-
this.allowUnsafePath = !!opts.allowUnsafePath;
|
|
17925
|
-
this.now = opts.now ?? Date.now;
|
|
17926
|
-
if (this.backend === "file") {
|
|
17927
|
-
if (!opts.path) {
|
|
17928
|
-
throw new Error("LocalBulkStore: path is required when backend=file");
|
|
17929
|
-
}
|
|
17930
|
-
this.path = resolvePath(opts.path);
|
|
17931
|
-
this.validatePath(this.path);
|
|
17932
|
-
}
|
|
17933
|
-
}
|
|
17934
|
-
get durability() {
|
|
17935
|
-
return this.backend;
|
|
17936
|
-
}
|
|
17937
|
-
// Exposed for tests and ops tooling.
|
|
17938
|
-
get resolvedPath() {
|
|
17939
|
-
return this.path;
|
|
17940
|
-
}
|
|
17941
|
-
validatePath(p) {
|
|
17942
|
-
if (this.allowUnsafePath)
|
|
17943
|
-
return;
|
|
17944
|
-
const home = resolvePath(homedir());
|
|
17945
|
-
if (p !== home && !p.startsWith(home + "/") && !p.startsWith(home + "\\")) {
|
|
17946
|
-
throw new Error(`LocalBulkStore: path ${p} is outside $HOME (${home}). Set LEADBAY_BULK_STORE_PATH_UNSAFE=1 to override.`);
|
|
17947
|
-
}
|
|
17948
|
-
}
|
|
17949
|
-
async ensureInitialized() {
|
|
17950
|
-
if (this.initialized || this.backend !== "file") {
|
|
17951
|
-
this.initialized = true;
|
|
17952
|
-
return;
|
|
17953
|
-
}
|
|
17954
|
-
const dir = dirname(this.path);
|
|
17955
|
-
await mkdirAsync(dir, { recursive: true, mode: 448 });
|
|
17956
|
-
try {
|
|
17957
|
-
const st = await lstat(this.path);
|
|
17958
|
-
if (st.isSymbolicLink()) {
|
|
17959
|
-
throw new Error(`LocalBulkStore: refusing to use ${this.path} \u2014 path is a symlink. Set LEADBAY_BULK_STORE_PATH_UNSAFE=1 to override.`);
|
|
17960
|
-
}
|
|
17961
|
-
} catch (err) {
|
|
17962
|
-
if (err?.code !== "ENOENT")
|
|
17963
|
-
throw err;
|
|
17964
|
-
}
|
|
17965
|
-
this.initialized = true;
|
|
17966
|
-
}
|
|
17967
|
-
// ─── Storage layer (file or memory) ──────────────────────────────────────
|
|
17968
|
-
async readAll() {
|
|
17969
|
-
if (this.backend === "memory")
|
|
17970
|
-
return [...this.memory];
|
|
17971
|
-
await this.ensureInitialized();
|
|
17972
|
-
let raw;
|
|
17973
|
-
try {
|
|
17974
|
-
raw = await readFile(this.path, "utf8");
|
|
17975
|
-
} catch (err) {
|
|
17976
|
-
if (err?.code === "ENOENT")
|
|
17977
|
-
return [];
|
|
17978
|
-
throw err;
|
|
17979
|
-
}
|
|
17980
|
-
let parsed;
|
|
17981
|
-
try {
|
|
17982
|
-
parsed = JSON.parse(raw);
|
|
17983
|
-
} catch (err) {
|
|
17984
|
-
this.logger?.warn?.(`bulk.record_dropped file_parse_failed ${err?.message ?? err}`);
|
|
17985
|
-
return [];
|
|
17986
|
-
}
|
|
17987
|
-
if (!Array.isArray(parsed)) {
|
|
17988
|
-
this.logger?.warn?.("bulk.record_dropped file_not_array");
|
|
17989
|
-
return [];
|
|
17990
|
-
}
|
|
17991
|
-
const out = [];
|
|
17992
|
-
for (const entry of parsed) {
|
|
17993
|
-
try {
|
|
17994
|
-
out.push(this.validateRecord(entry));
|
|
17995
|
-
} catch (err) {
|
|
17996
|
-
this.logger?.warn?.(`bulk.record_dropped invalid_record ${err?.message ?? err}`);
|
|
17997
|
-
}
|
|
17998
|
-
}
|
|
17999
|
-
return out;
|
|
18000
|
-
}
|
|
18001
|
-
validateRecord(raw) {
|
|
18002
|
-
if (!raw || typeof raw !== "object")
|
|
18003
|
-
throw new Error("not an object");
|
|
18004
|
-
const r = raw;
|
|
18005
|
-
if (!isValidBulkId(r.bulk_id))
|
|
18006
|
-
throw new Error("invalid bulk_id");
|
|
18007
|
-
if (typeof r.launched_at !== "string")
|
|
18008
|
-
throw new Error("missing launched_at");
|
|
18009
|
-
if (!Array.isArray(r.lead_ids) || !r.lead_ids.every((x) => typeof x === "string"))
|
|
18010
|
-
throw new Error("invalid lead_ids");
|
|
18011
|
-
if (r.status !== "pending" && r.status !== "launched" && r.status !== "complete" && r.status !== "failed" && r.status !== "cancelled")
|
|
18012
|
-
throw new Error("invalid status");
|
|
18013
|
-
if (typeof r.idempotency_key !== "string")
|
|
18014
|
-
throw new Error("invalid idempotency_key");
|
|
18015
|
-
const kind = r.kind ?? "enrich";
|
|
18016
|
-
if (kind === "qualify") {
|
|
18017
|
-
if (!Array.isArray(r.import_ids) || !r.import_ids.every((x) => typeof x === "string"))
|
|
18018
|
-
throw new Error("invalid import_ids");
|
|
18019
|
-
if (typeof r.lens_id !== "number")
|
|
18020
|
-
throw new Error("invalid lens_id");
|
|
18021
|
-
const out = {
|
|
18022
|
-
kind: "qualify",
|
|
18023
|
-
bulk_id: r.bulk_id,
|
|
18024
|
-
launched_at: r.launched_at,
|
|
18025
|
-
lead_ids: r.lead_ids,
|
|
18026
|
-
import_ids: r.import_ids,
|
|
18027
|
-
lens_id: r.lens_id,
|
|
18028
|
-
status: r.status,
|
|
18029
|
-
idempotency_key: r.idempotency_key,
|
|
18030
|
-
durability: this.backend
|
|
18031
|
-
};
|
|
18032
|
-
if (typeof r.per_lead_budget_ms === "number")
|
|
18033
|
-
out.per_lead_budget_ms = r.per_lead_budget_ms;
|
|
18034
|
-
if (typeof r.total_budget_ms === "number")
|
|
18035
|
-
out.total_budget_ms = r.total_budget_ms;
|
|
18036
|
-
return out;
|
|
18037
|
-
}
|
|
18038
|
-
if (kind === "import") {
|
|
18039
|
-
if (!Array.isArray(r.import_ids) || !r.import_ids.every((x) => typeof x === "string"))
|
|
18040
|
-
throw new Error("invalid import_ids");
|
|
18041
|
-
if (r.mode !== "domains" && r.mode !== "records")
|
|
18042
|
-
throw new Error("invalid mode");
|
|
18043
|
-
if (typeof r.dry_run !== "boolean")
|
|
18044
|
-
throw new Error("invalid dry_run");
|
|
18045
|
-
if (typeof r.records_total !== "number")
|
|
18046
|
-
throw new Error("invalid records_total");
|
|
18047
|
-
const out = {
|
|
18048
|
-
kind: "import",
|
|
18049
|
-
bulk_id: r.bulk_id,
|
|
18050
|
-
launched_at: r.launched_at,
|
|
18051
|
-
lead_ids: r.lead_ids,
|
|
18052
|
-
import_ids: r.import_ids,
|
|
18053
|
-
mode: r.mode,
|
|
18054
|
-
dry_run: r.dry_run,
|
|
18055
|
-
records_total: r.records_total,
|
|
18056
|
-
status: r.status,
|
|
18057
|
-
idempotency_key: r.idempotency_key,
|
|
18058
|
-
durability: this.backend
|
|
18059
|
-
};
|
|
18060
|
-
if (r.result && typeof r.result === "object") {
|
|
18061
|
-
const result = r.result;
|
|
18062
|
-
if (Array.isArray(result.leads) && Array.isArray(result.not_imported) && Array.isArray(result.importIds) && result.importIds.every((x) => typeof x === "string")) {
|
|
18063
|
-
out.result = {
|
|
18064
|
-
leads: result.leads,
|
|
18065
|
-
not_imported: result.not_imported,
|
|
18066
|
-
importIds: result.importIds
|
|
18067
|
-
};
|
|
18068
|
-
}
|
|
18069
|
-
}
|
|
18070
|
-
if (r.progress && typeof r.progress === "object") {
|
|
18071
|
-
const p = r.progress;
|
|
18072
|
-
if (typeof p.phase === "string" && typeof p.records_processed === "number" && typeof p.records_total === "number") {
|
|
18073
|
-
out.progress = {
|
|
18074
|
-
phase: p.phase,
|
|
18075
|
-
records_processed: p.records_processed,
|
|
18076
|
-
records_total: p.records_total
|
|
18077
|
-
};
|
|
18078
|
-
}
|
|
18079
|
-
}
|
|
18080
|
-
if (typeof r.error === "string")
|
|
18081
|
-
out.error = r.error;
|
|
18082
|
-
return out;
|
|
18083
|
-
}
|
|
18084
|
-
if (kind === "enrich") {
|
|
18085
|
-
if (!Array.isArray(r.titles) || !r.titles.every((x) => typeof x === "string"))
|
|
18086
|
-
throw new Error("invalid titles");
|
|
18087
|
-
if (typeof r.email !== "boolean")
|
|
18088
|
-
throw new Error("invalid email");
|
|
18089
|
-
if (typeof r.phone !== "boolean")
|
|
18090
|
-
throw new Error("invalid phone");
|
|
18091
|
-
if (typeof r.lens_id !== "number")
|
|
18092
|
-
throw new Error("invalid lens_id");
|
|
18093
|
-
if (r.selection_source !== "explicit" && r.selection_source !== "wishlist")
|
|
18094
|
-
throw new Error("invalid selection_source");
|
|
18095
|
-
if (r.notification_id != null && typeof r.notification_id !== "string")
|
|
18096
|
-
throw new Error("invalid notification_id");
|
|
18097
|
-
return {
|
|
18098
|
-
kind: "enrich",
|
|
18099
|
-
bulk_id: r.bulk_id,
|
|
18100
|
-
launched_at: r.launched_at,
|
|
18101
|
-
lead_ids: r.lead_ids,
|
|
18102
|
-
titles: r.titles,
|
|
18103
|
-
email: r.email,
|
|
18104
|
-
phone: r.phone,
|
|
18105
|
-
lens_id: r.lens_id,
|
|
18106
|
-
selection_source: r.selection_source,
|
|
18107
|
-
status: r.status,
|
|
18108
|
-
idempotency_key: r.idempotency_key,
|
|
18109
|
-
// Preserve the persisted notification_id — dropping it on reload forced
|
|
18110
|
-
// bulk_enrich_status onto the legacy per-lead fallback every time
|
|
18111
|
-
// (the fast path keys off record.notification_id), even in production
|
|
18112
|
-
// where the default store is file-backed.
|
|
18113
|
-
...r.notification_id != null ? { notification_id: r.notification_id } : {},
|
|
18114
|
-
durability: this.backend
|
|
18115
|
-
};
|
|
18116
|
-
}
|
|
18117
|
-
throw new Error(`unknown kind: ${String(kind)}`);
|
|
18118
|
-
}
|
|
18119
|
-
async writeAll(records) {
|
|
18120
|
-
if (this.backend === "memory") {
|
|
18121
|
-
this.memory = records.map((r) => ({ ...r, durability: "memory" }));
|
|
18122
|
-
return;
|
|
18123
|
-
}
|
|
18124
|
-
await this.ensureInitialized();
|
|
18125
|
-
const payload = records.map((r) => ({ ...r, durability: "file" }));
|
|
18126
|
-
const json = JSON.stringify(payload, null, 2);
|
|
18127
|
-
const tmp = this.path + ".tmp";
|
|
18128
|
-
let fh = await openTmpFileExclusive(tmp);
|
|
18129
|
-
try {
|
|
18130
|
-
await fh.writeFile(json, { encoding: "utf8" });
|
|
18131
|
-
await fh.sync();
|
|
18132
|
-
} finally {
|
|
18133
|
-
await fh.close();
|
|
18134
|
-
}
|
|
18135
|
-
if (platform() === "win32") {
|
|
18136
|
-
try {
|
|
18137
|
-
await unlink(this.path);
|
|
18138
|
-
} catch (err) {
|
|
18139
|
-
if (err?.code !== "ENOENT")
|
|
18140
|
-
throw err;
|
|
18141
|
-
}
|
|
18142
|
-
}
|
|
18143
|
-
await rename(tmp, this.path);
|
|
18144
|
-
try {
|
|
18145
|
-
const dirFh = await fsOpen(dirname(this.path), "r");
|
|
18146
|
-
try {
|
|
18147
|
-
await dirFh.sync();
|
|
18148
|
-
} finally {
|
|
18149
|
-
await dirFh.close();
|
|
18150
|
-
}
|
|
18151
|
-
} catch {
|
|
18152
|
-
}
|
|
18153
|
-
}
|
|
18154
|
-
// ─── TTL cleanup ─────────────────────────────────────────────────────────
|
|
18155
|
-
prune(records) {
|
|
18156
|
-
const cutoff = this.now() - TTL_MS;
|
|
18157
|
-
const kept = [];
|
|
18158
|
-
for (const r of records) {
|
|
18159
|
-
const launched = Date.parse(r.launched_at);
|
|
18160
|
-
if (Number.isFinite(launched) && launched >= cutoff) {
|
|
18161
|
-
kept.push(r);
|
|
18162
|
-
} else {
|
|
18163
|
-
this.logger?.info?.(`bulk.ttl_dropped bulk_id=${r.bulk_id} launched_at=${r.launched_at}`);
|
|
18164
|
-
}
|
|
18165
|
-
}
|
|
18166
|
-
return kept;
|
|
18167
|
-
}
|
|
18168
|
-
// ─── BulkTracker API ────────────────────────────────────────────────────
|
|
18169
|
-
async findOrCreatePending(args) {
|
|
18170
|
-
const { lead_ids, titles } = normalizeLaunchInputs(args);
|
|
18171
|
-
const idempotency_key = computeIdempotencyKey({
|
|
18172
|
-
lead_ids,
|
|
18173
|
-
titles,
|
|
18174
|
-
email: args.email,
|
|
18175
|
-
phone: args.phone,
|
|
18176
|
-
lens_id: args.lens_id
|
|
18177
|
-
});
|
|
18178
|
-
const window = args.idempotency_window_ms ?? DEFAULT_IDEMPOTENCY_WINDOW_MS;
|
|
18179
|
-
return this.mutex.run(async () => {
|
|
18180
|
-
const all = this.prune(await this.readAll());
|
|
18181
|
-
const nowMs = this.now();
|
|
18182
|
-
const existing = all.find((r) => r.kind === "enrich" && r.idempotency_key === idempotency_key && r.status !== "failed" && nowMs - Date.parse(r.launched_at) < window);
|
|
18183
|
-
if (existing) {
|
|
18184
|
-
this.logger?.info?.(`bulk.reused bulk_id=${existing.bulk_id} seconds_since_original=${Math.round((nowMs - Date.parse(existing.launched_at)) / 1e3)}`);
|
|
18185
|
-
return {
|
|
18186
|
-
record: existing,
|
|
18187
|
-
reused: true,
|
|
18188
|
-
seconds_since_original: Math.round((nowMs - Date.parse(existing.launched_at)) / 1e3)
|
|
18189
|
-
};
|
|
18190
|
-
}
|
|
18191
|
-
const record = {
|
|
18192
|
-
kind: "enrich",
|
|
18193
|
-
bulk_id: randomUUID2(),
|
|
18194
|
-
launched_at: new Date(nowMs).toISOString(),
|
|
18195
|
-
lead_ids,
|
|
18196
|
-
titles,
|
|
18197
|
-
email: args.email,
|
|
18198
|
-
phone: args.phone,
|
|
18199
|
-
lens_id: args.lens_id,
|
|
18200
|
-
selection_source: args.selection_source,
|
|
18201
|
-
status: "pending",
|
|
18202
|
-
idempotency_key,
|
|
18203
|
-
durability: this.backend
|
|
18204
|
-
};
|
|
18205
|
-
all.push(record);
|
|
18206
|
-
await this.writeAll(all);
|
|
18207
|
-
this.logger?.info?.(`bulk.registered kind=enrich bulk_id=${record.bulk_id} lens_id=${record.lens_id} lead_count=${record.lead_ids.length} titles_count=${record.titles.length} durability=${record.durability}`);
|
|
18208
|
-
return { record, reused: false };
|
|
18209
|
-
});
|
|
18210
|
-
}
|
|
18211
|
-
async findOrCreatePendingQualify(args) {
|
|
18212
|
-
const lead_ids = [...new Set(args.lead_ids)].sort();
|
|
18213
|
-
const import_ids = [...new Set(args.import_ids)].sort();
|
|
18214
|
-
const idempotency_key = computeQualifyIdempotencyKey({
|
|
18215
|
-
lead_ids,
|
|
18216
|
-
import_ids,
|
|
18217
|
-
lens_id: args.lens_id,
|
|
18218
|
-
mapping_fingerprint: args.mapping_fingerprint
|
|
18219
|
-
});
|
|
18220
|
-
const window = args.idempotency_window_ms ?? DEFAULT_IDEMPOTENCY_WINDOW_MS;
|
|
18221
|
-
return this.mutex.run(async () => {
|
|
18222
|
-
const all = this.prune(await this.readAll());
|
|
18223
|
-
const nowMs = this.now();
|
|
18224
|
-
const existing = all.find((r) => r.kind === "qualify" && r.idempotency_key === idempotency_key && r.status !== "failed" && nowMs - Date.parse(r.launched_at) < window);
|
|
18225
|
-
if (existing) {
|
|
18226
|
-
this.logger?.info?.(`bulk.reused kind=qualify bulk_id=${existing.bulk_id} seconds_since_original=${Math.round((nowMs - Date.parse(existing.launched_at)) / 1e3)}`);
|
|
18227
|
-
return {
|
|
18228
|
-
record: existing,
|
|
18229
|
-
reused: true,
|
|
18230
|
-
seconds_since_original: Math.round((nowMs - Date.parse(existing.launched_at)) / 1e3)
|
|
18231
|
-
};
|
|
18232
|
-
}
|
|
18233
|
-
const record = {
|
|
18234
|
-
kind: "qualify",
|
|
18235
|
-
bulk_id: randomUUID2(),
|
|
18236
|
-
launched_at: new Date(nowMs).toISOString(),
|
|
18237
|
-
lead_ids,
|
|
18238
|
-
import_ids,
|
|
18239
|
-
lens_id: args.lens_id,
|
|
18240
|
-
status: "pending",
|
|
18241
|
-
idempotency_key,
|
|
18242
|
-
durability: this.backend
|
|
18243
|
-
};
|
|
18244
|
-
if (args.per_lead_budget_ms !== void 0)
|
|
18245
|
-
record.per_lead_budget_ms = args.per_lead_budget_ms;
|
|
18246
|
-
if (args.total_budget_ms !== void 0)
|
|
18247
|
-
record.total_budget_ms = args.total_budget_ms;
|
|
18248
|
-
all.push(record);
|
|
18249
|
-
await this.writeAll(all);
|
|
18250
|
-
this.logger?.info?.(`bulk.registered kind=qualify bulk_id=${record.bulk_id} lens_id=${record.lens_id} lead_count=${record.lead_ids.length} import_count=${record.import_ids.length} durability=${record.durability}`);
|
|
18251
|
-
return { record, reused: false };
|
|
18252
|
-
});
|
|
18253
|
-
}
|
|
18254
|
-
async findOrCreatePendingImport(args) {
|
|
18255
|
-
const idempotency_key = computeImportIdempotencyKey({
|
|
18256
|
-
import_fingerprint: args.import_fingerprint,
|
|
18257
|
-
mode: args.mode,
|
|
18258
|
-
dry_run: args.dry_run
|
|
18259
|
-
});
|
|
18260
|
-
const window = args.idempotency_window_ms ?? DEFAULT_IDEMPOTENCY_WINDOW_MS;
|
|
18261
|
-
return this.mutex.run(async () => {
|
|
18262
|
-
const all = this.prune(await this.readAll());
|
|
18263
|
-
const nowMs = this.now();
|
|
18264
|
-
const existing = all.find((r) => r.kind === "import" && r.idempotency_key === idempotency_key && r.status !== "failed" && r.status !== "cancelled" && nowMs - Date.parse(r.launched_at) < window);
|
|
18265
|
-
if (existing) {
|
|
18266
|
-
this.logger?.info?.(`bulk.reused kind=import bulk_id=${existing.bulk_id} seconds_since_original=${Math.round((nowMs - Date.parse(existing.launched_at)) / 1e3)}`);
|
|
18267
|
-
return {
|
|
18268
|
-
record: existing,
|
|
18269
|
-
reused: true,
|
|
18270
|
-
seconds_since_original: Math.round((nowMs - Date.parse(existing.launched_at)) / 1e3)
|
|
18271
|
-
};
|
|
18272
|
-
}
|
|
18273
|
-
const record = {
|
|
18274
|
-
kind: "import",
|
|
18275
|
-
bulk_id: randomUUID2(),
|
|
18276
|
-
launched_at: new Date(nowMs).toISOString(),
|
|
18277
|
-
lead_ids: [],
|
|
18278
|
-
import_ids: [],
|
|
18279
|
-
mode: args.mode,
|
|
18280
|
-
dry_run: args.dry_run,
|
|
18281
|
-
records_total: args.records_total,
|
|
18282
|
-
progress: {
|
|
18283
|
-
phase: "queued",
|
|
18284
|
-
records_processed: 0,
|
|
18285
|
-
records_total: args.records_total
|
|
18286
|
-
},
|
|
18287
|
-
status: "pending",
|
|
18288
|
-
idempotency_key,
|
|
18289
|
-
durability: this.backend
|
|
18290
|
-
};
|
|
18291
|
-
all.push(record);
|
|
18292
|
-
await this.writeAll(all);
|
|
18293
|
-
this.logger?.info?.(`bulk.registered kind=import bulk_id=${record.bulk_id} mode=${record.mode} records_total=${record.records_total} durability=${record.durability}`);
|
|
18294
|
-
return { record, reused: false };
|
|
18295
|
-
});
|
|
18296
|
-
}
|
|
18297
|
-
async getQualify(bulk_id) {
|
|
18298
|
-
const r = await this.get(bulk_id);
|
|
18299
|
-
return r && r.kind === "qualify" ? r : void 0;
|
|
18300
|
-
}
|
|
18301
|
-
async getImport(bulk_id) {
|
|
18302
|
-
const r = await this.get(bulk_id);
|
|
18303
|
-
return r && r.kind === "import" ? r : void 0;
|
|
18304
|
-
}
|
|
18305
|
-
async setImportIds(bulk_id, import_ids) {
|
|
18306
|
-
return this.mutex.run(async () => {
|
|
18307
|
-
const all = this.prune(await this.readAll());
|
|
18308
|
-
const idx = all.findIndex((r) => r.bulk_id === bulk_id && r.kind === "import");
|
|
18309
|
-
if (idx < 0)
|
|
18310
|
-
throw new Error(`import bulk_id not found: ${bulk_id}`);
|
|
18311
|
-
const record = all[idx];
|
|
18312
|
-
all[idx] = {
|
|
18313
|
-
...record,
|
|
18314
|
-
import_ids: [...new Set(import_ids)].sort(),
|
|
18315
|
-
status: record.status === "pending" ? "launched" : record.status
|
|
18316
|
-
};
|
|
18317
|
-
await this.writeAll(all);
|
|
18318
|
-
return all[idx];
|
|
18319
|
-
});
|
|
18320
|
-
}
|
|
18321
|
-
async setImportProgress(bulk_id, progress) {
|
|
18322
|
-
return this.mutex.run(async () => {
|
|
18323
|
-
const all = this.prune(await this.readAll());
|
|
18324
|
-
const idx = all.findIndex((r) => r.bulk_id === bulk_id && r.kind === "import");
|
|
18325
|
-
if (idx < 0)
|
|
18326
|
-
throw new Error(`import bulk_id not found: ${bulk_id}`);
|
|
18327
|
-
const record = all[idx];
|
|
18328
|
-
all[idx] = { ...record, progress };
|
|
18329
|
-
await this.writeAll(all);
|
|
18330
|
-
return all[idx];
|
|
18331
|
-
});
|
|
18332
|
-
}
|
|
18333
|
-
async markImportComplete(bulk_id, result) {
|
|
18334
|
-
return this.mutex.run(async () => {
|
|
18335
|
-
const all = this.prune(await this.readAll());
|
|
18336
|
-
const idx = all.findIndex((r) => r.bulk_id === bulk_id && r.kind === "import");
|
|
18337
|
-
if (idx < 0)
|
|
18338
|
-
throw new Error(`import bulk_id not found: ${bulk_id}`);
|
|
18339
|
-
const record = all[idx];
|
|
18340
|
-
all[idx] = {
|
|
18341
|
-
...record,
|
|
18342
|
-
import_ids: [...new Set(result.importIds)].sort(),
|
|
18343
|
-
result,
|
|
18344
|
-
progress: {
|
|
18345
|
-
phase: "complete",
|
|
18346
|
-
records_processed: record.records_total,
|
|
18347
|
-
records_total: record.records_total
|
|
18348
|
-
},
|
|
18349
|
-
status: "complete"
|
|
18350
|
-
};
|
|
18351
|
-
await this.writeAll(all);
|
|
18352
|
-
this.logger?.info?.(`bulk.import_complete bulk_id=${bulk_id}`);
|
|
18353
|
-
return all[idx];
|
|
18354
|
-
});
|
|
18355
|
-
}
|
|
18356
|
-
async markImportFailed(bulk_id, error) {
|
|
18357
|
-
return this.mutex.run(async () => {
|
|
18358
|
-
const all = this.prune(await this.readAll());
|
|
18359
|
-
const idx = all.findIndex((r) => r.bulk_id === bulk_id && r.kind === "import");
|
|
18360
|
-
if (idx < 0)
|
|
18361
|
-
return;
|
|
18362
|
-
all[idx] = { ...all[idx], status: "failed", error };
|
|
18363
|
-
await this.writeAll(all);
|
|
18364
|
-
this.logger?.info?.(`bulk.import_failed bulk_id=${bulk_id}`);
|
|
18365
|
-
});
|
|
18366
|
-
}
|
|
18367
|
-
async markLaunched(bulk_id, notification_id) {
|
|
18368
|
-
return this.mutex.run(async () => {
|
|
18369
|
-
const all = this.prune(await this.readAll());
|
|
18370
|
-
const idx = all.findIndex((r) => r.bulk_id === bulk_id);
|
|
18371
|
-
if (idx < 0) {
|
|
18372
|
-
throw new Error(`bulk_id not found: ${bulk_id}`);
|
|
18373
|
-
}
|
|
18374
|
-
all[idx] = {
|
|
18375
|
-
...all[idx],
|
|
18376
|
-
status: "launched",
|
|
18377
|
-
...notification_id ? { notification_id } : {}
|
|
18378
|
-
};
|
|
18379
|
-
await this.writeAll(all);
|
|
18380
|
-
this.logger?.info?.(`bulk.launched bulk_id=${bulk_id}${notification_id ? ` notification_id=${notification_id}` : ""}`);
|
|
18381
|
-
return all[idx];
|
|
18382
|
-
});
|
|
18383
|
-
}
|
|
18384
|
-
async markFailed(bulk_id) {
|
|
18385
|
-
return this.mutex.run(async () => {
|
|
18386
|
-
const all = this.prune(await this.readAll());
|
|
18387
|
-
const idx = all.findIndex((r) => r.bulk_id === bulk_id);
|
|
18388
|
-
if (idx < 0) {
|
|
18389
|
-
return;
|
|
18390
|
-
}
|
|
18391
|
-
all[idx] = { ...all[idx], status: "failed" };
|
|
18392
|
-
await this.writeAll(all);
|
|
18393
|
-
this.logger?.info?.(`bulk.launch_failed bulk_id=${bulk_id}`);
|
|
18394
|
-
});
|
|
18395
|
-
}
|
|
18396
|
-
async markCancelled(bulk_id) {
|
|
18397
|
-
return this.mutex.run(async () => {
|
|
18398
|
-
const all = this.prune(await this.readAll());
|
|
18399
|
-
const idx = all.findIndex((r) => r.bulk_id === bulk_id);
|
|
18400
|
-
if (idx < 0) {
|
|
18401
|
-
return;
|
|
18402
|
-
}
|
|
18403
|
-
all[idx] = { ...all[idx], status: "cancelled" };
|
|
18404
|
-
await this.writeAll(all);
|
|
18405
|
-
this.logger?.info?.(`bulk.cancelled bulk_id=${bulk_id}`);
|
|
18406
|
-
});
|
|
18407
|
-
}
|
|
18408
|
-
async get(bulk_id) {
|
|
18409
|
-
return this.mutex.run(async () => {
|
|
18410
|
-
const all = this.prune(await this.readAll());
|
|
18411
|
-
return all.find((r) => r.bulk_id === bulk_id);
|
|
18412
|
-
});
|
|
18413
|
-
}
|
|
18414
|
-
async list() {
|
|
18415
|
-
return this.mutex.run(async () => {
|
|
18416
|
-
const all = this.prune(await this.readAll());
|
|
18417
|
-
return [...all].sort((a, b) => Date.parse(b.launched_at) - Date.parse(a.launched_at));
|
|
18418
|
-
});
|
|
18419
|
-
}
|
|
18420
|
-
};
|
|
18421
|
-
InMemoryBulkStore = class extends LocalBulkStore {
|
|
18422
|
-
constructor(opts = {}) {
|
|
18423
|
-
super({ backend: "memory", logger: opts.logger, now: opts.now });
|
|
18424
|
-
}
|
|
18425
|
-
};
|
|
18426
|
-
}
|
|
18427
|
-
});
|
|
18428
|
-
|
|
18429
|
-
// ../core/dist/composite/import-status.js
|
|
18430
|
-
function summarizeImports(imports, dryRun) {
|
|
18431
|
-
let recordsTotal = 0;
|
|
18432
|
-
let recordsProcessed = 0;
|
|
18433
|
-
let hasPreprocess = false;
|
|
18434
|
-
let hasProcess = false;
|
|
18435
|
-
let hasFailed = false;
|
|
18436
|
-
for (const imp of imports) {
|
|
18437
|
-
recordsTotal += Number(imp.total_records ?? 0);
|
|
18438
|
-
recordsProcessed += Number(imp.imported_records ?? 0);
|
|
18439
|
-
if (!imp.pre_processing?.finished) {
|
|
18440
|
-
hasPreprocess = true;
|
|
18441
|
-
continue;
|
|
18442
|
-
}
|
|
18443
|
-
if (imp.pre_processing?.error) {
|
|
18444
|
-
hasFailed = true;
|
|
18445
|
-
continue;
|
|
18446
|
-
}
|
|
18447
|
-
if (dryRun === true) {
|
|
18448
|
-
continue;
|
|
18449
|
-
}
|
|
18450
|
-
if (!imp.processing?.finished) {
|
|
18451
|
-
if (dryRun === false || imp.processing != null)
|
|
18452
|
-
hasProcess = true;
|
|
18453
|
-
continue;
|
|
18454
|
-
}
|
|
18455
|
-
if (imp.processing?.error) {
|
|
18456
|
-
hasFailed = true;
|
|
18457
|
-
}
|
|
18458
|
-
}
|
|
18459
|
-
const phase = hasFailed ? "failed" : hasPreprocess ? "preprocess" : hasProcess ? "process" : imports.length > 0 ? "complete" : "queued";
|
|
18460
|
-
return {
|
|
18461
|
-
phase,
|
|
18462
|
-
records_processed: recordsProcessed,
|
|
18463
|
-
records_total: recordsTotal
|
|
18464
|
-
};
|
|
18465
|
-
}
|
|
18466
|
-
function isInProgress(err) {
|
|
18467
|
-
return /in_progress/i.test(String(err?.message ?? ""));
|
|
18468
|
-
}
|
|
18469
|
-
async function fetchReconciledRecords(client, importIds, declaredTotal, ctx) {
|
|
18470
|
-
const canonicalLeadIds = /* @__PURE__ */ new Set();
|
|
18471
|
-
for (const importId of importIds) {
|
|
18472
|
-
try {
|
|
18473
|
-
const res = await client.request("GET", `/imports/${importId}/leads`);
|
|
18474
|
-
for (const id of res?.lead_ids ?? [])
|
|
18475
|
-
canonicalLeadIds.add(id);
|
|
18476
|
-
} catch (err) {
|
|
18477
|
-
if (isInProgress(err))
|
|
18478
|
-
throw new ImportNotReady();
|
|
18479
|
-
if (err?.code !== "NOT_FOUND" && err?._meta?.http_status !== 404)
|
|
18480
|
-
throw err;
|
|
18481
|
-
ctx?.logger?.warn?.(`import-status: /imports/${importId}/leads not available on this backend (404) \u2014 using records only`);
|
|
18482
|
-
}
|
|
18483
|
-
}
|
|
18484
|
-
const all = [];
|
|
18485
|
-
for (const importId of importIds) {
|
|
18486
|
-
for (let page = 0; page < RECORDS_MAX_PAGES; page++) {
|
|
18487
|
-
const qs = `count=${RECORDS_PAGE_SIZE}&page=${page}&automatic_match=true&manual_match=true&no_match=true&matching=true&importing=true&imported=true`;
|
|
18488
|
-
let res;
|
|
18489
|
-
try {
|
|
18490
|
-
res = await client.request("GET", `/imports/${importId}/records?${qs}`);
|
|
18491
|
-
} catch (err) {
|
|
18492
|
-
if (isInProgress(err))
|
|
18493
|
-
throw new ImportNotReady();
|
|
18494
|
-
throw err;
|
|
18175
|
+
const all = [];
|
|
18176
|
+
for (const importId of importIds) {
|
|
18177
|
+
for (let page = 0; page < RECORDS_MAX_PAGES; page++) {
|
|
18178
|
+
const qs = `count=${RECORDS_PAGE_SIZE}&page=${page}&automatic_match=true&manual_match=true&no_match=true&matching=true&importing=true&imported=true`;
|
|
18179
|
+
let res;
|
|
18180
|
+
try {
|
|
18181
|
+
res = await client.request("GET", `/imports/${importId}/records?${qs}`);
|
|
18182
|
+
} catch (err) {
|
|
18183
|
+
if (isInProgress(err))
|
|
18184
|
+
throw new ImportNotReady();
|
|
18185
|
+
throw err;
|
|
18495
18186
|
}
|
|
18496
18187
|
all.push(...res.items);
|
|
18497
18188
|
const totalPages = res.pagination.pages ?? 0;
|
|
@@ -18529,7 +18220,6 @@ var RECORDS_PAGE_SIZE, RECORDS_MAX_PAGES, ImportNotReady, importStatus;
|
|
|
18529
18220
|
var init_import_status = __esm({
|
|
18530
18221
|
"../core/dist/composite/import-status.js"() {
|
|
18531
18222
|
"use strict";
|
|
18532
|
-
init_bulk_store();
|
|
18533
18223
|
init_import_records();
|
|
18534
18224
|
init_import_commit_log();
|
|
18535
18225
|
init_tool_descriptions_generated();
|
|
@@ -18550,10 +18240,6 @@ var init_import_status = __esm({
|
|
|
18550
18240
|
inputSchema: {
|
|
18551
18241
|
type: "object",
|
|
18552
18242
|
properties: {
|
|
18553
|
-
handle_id: {
|
|
18554
|
-
type: "string",
|
|
18555
|
-
description: "UUIDv4 handle returned by leadbay_import_leads when wait_for_completion=false."
|
|
18556
|
-
},
|
|
18557
18243
|
importIds: {
|
|
18558
18244
|
type: "array",
|
|
18559
18245
|
description: "Backend file-import ids to inspect directly \u2014 from a completed import's `importIds`, or from a `{status:'running', timed_out:true}` result.",
|
|
@@ -18570,12 +18256,11 @@ var init_import_status = __esm({
|
|
|
18570
18256
|
type: "object",
|
|
18571
18257
|
properties: {
|
|
18572
18258
|
status: { type: "string", description: "running, complete, or failed." },
|
|
18573
|
-
handle_id: { type: "string" },
|
|
18574
18259
|
importIds: { type: "array", items: { type: "string" } },
|
|
18575
18260
|
progress: { type: "object" },
|
|
18576
18261
|
result: {
|
|
18577
18262
|
type: "object",
|
|
18578
|
-
description: "Final import result: {leads, not_imported, importIds, still_settling?}. Present when
|
|
18263
|
+
description: "Final import result: {leads, not_imported, importIds, still_settling?}. Present when the importIds[] path finds every import complete and reconciles the wizard's records."
|
|
18579
18264
|
},
|
|
18580
18265
|
error: { type: "string" },
|
|
18581
18266
|
dry_run: {
|
|
@@ -18588,99 +18273,20 @@ var init_import_status = __esm({
|
|
|
18588
18273
|
required: ["status", "importIds", "progress", "region", "_meta"]
|
|
18589
18274
|
},
|
|
18590
18275
|
execute: async (client, params, ctx) => {
|
|
18591
|
-
let handleId = params.handle_id;
|
|
18592
18276
|
let importIds = params.importIds ?? [];
|
|
18593
|
-
let handleDryRun = params.dry_run;
|
|
18594
|
-
if (handleId) {
|
|
18595
|
-
if (!isValidBulkId(handleId)) {
|
|
18596
|
-
throw client.makeError("BULK_INVALID_ID", "handle_id is not a valid UUIDv4", "Pass the handle_id returned by leadbay_import_leads verbatim.", "");
|
|
18597
|
-
}
|
|
18598
|
-
if (!ctx?.bulkTracker) {
|
|
18599
|
-
throw client.makeError("BULK_TRACKER_UNAVAILABLE", "No BulkTracker configured on this MCP instance", "leadbay_import_status needs a BulkTracker to resolve handle_id. Pass importIds[] directly as a fallback.", "");
|
|
18600
|
-
}
|
|
18601
|
-
const record = await ctx.bulkTracker.getImport(handleId);
|
|
18602
|
-
if (!record) {
|
|
18603
|
-
const any = await ctx.bulkTracker.get(handleId);
|
|
18604
|
-
if (any && any.kind !== "import") {
|
|
18605
|
-
throw client.makeError("BULK_WRONG_KIND", "This handle was not created by leadbay_import_leads", "Use leadbay_qualify_status for qualify ids or leadbay_bulk_enrich_status for enrich ids.", "");
|
|
18606
|
-
}
|
|
18607
|
-
throw client.makeError("BULK_NOT_FOUND", "No import record for that handle_id", "It may have expired (30-day TTL) or the MCP process was restarted without persistence.", "");
|
|
18608
|
-
}
|
|
18609
|
-
importIds = record.import_ids;
|
|
18610
|
-
handleDryRun = record.dry_run ?? handleDryRun;
|
|
18611
|
-
if (record.status === "complete" && record.result) {
|
|
18612
|
-
return {
|
|
18613
|
-
status: "complete",
|
|
18614
|
-
handle_id: handleId,
|
|
18615
|
-
importIds,
|
|
18616
|
-
progress: record.progress ?? {
|
|
18617
|
-
phase: "complete",
|
|
18618
|
-
records_processed: record.records_total,
|
|
18619
|
-
records_total: record.records_total
|
|
18620
|
-
},
|
|
18621
|
-
result: record.result,
|
|
18622
|
-
region: client.region,
|
|
18623
|
-
_meta: client.lastMeta ?? {
|
|
18624
|
-
region: client.region,
|
|
18625
|
-
endpoint: "bulk-store",
|
|
18626
|
-
latency_ms: null,
|
|
18627
|
-
retry_after: null
|
|
18628
|
-
}
|
|
18629
|
-
};
|
|
18630
|
-
}
|
|
18631
|
-
if (record.status === "failed") {
|
|
18632
|
-
return {
|
|
18633
|
-
status: "failed",
|
|
18634
|
-
handle_id: handleId,
|
|
18635
|
-
importIds,
|
|
18636
|
-
progress: record.progress ?? {
|
|
18637
|
-
phase: "failed",
|
|
18638
|
-
records_processed: 0,
|
|
18639
|
-
records_total: record.records_total
|
|
18640
|
-
},
|
|
18641
|
-
error: record.error ?? "import failed",
|
|
18642
|
-
region: client.region,
|
|
18643
|
-
_meta: client.lastMeta ?? {
|
|
18644
|
-
region: client.region,
|
|
18645
|
-
endpoint: "bulk-store",
|
|
18646
|
-
latency_ms: null,
|
|
18647
|
-
retry_after: null
|
|
18648
|
-
}
|
|
18649
|
-
};
|
|
18650
|
-
}
|
|
18651
|
-
if (importIds.length === 0) {
|
|
18652
|
-
return {
|
|
18653
|
-
status: "running",
|
|
18654
|
-
handle_id: handleId,
|
|
18655
|
-
importIds,
|
|
18656
|
-
progress: record.progress ?? {
|
|
18657
|
-
phase: "queued",
|
|
18658
|
-
records_processed: 0,
|
|
18659
|
-
records_total: record.records_total
|
|
18660
|
-
},
|
|
18661
|
-
region: client.region,
|
|
18662
|
-
_meta: client.lastMeta ?? {
|
|
18663
|
-
region: client.region,
|
|
18664
|
-
endpoint: "bulk-store",
|
|
18665
|
-
latency_ms: null,
|
|
18666
|
-
retry_after: null
|
|
18667
|
-
}
|
|
18668
|
-
};
|
|
18669
|
-
}
|
|
18670
|
-
}
|
|
18671
18277
|
importIds = [...new Set(importIds)];
|
|
18672
18278
|
if (importIds.length === 0) {
|
|
18673
|
-
throw client.makeError("IMPORT_STATUS_INPUT_REQUIRED", "Pass
|
|
18279
|
+
throw client.makeError("IMPORT_STATUS_INPUT_REQUIRED", "Pass importIds[]", "Call leadbay_import_leads with wait_for_completion=false first, then pass back the importIds it returned.", "");
|
|
18674
18280
|
}
|
|
18675
18281
|
const imports = await Promise.all(importIds.map((id) => client.request("GET", `/imports/${id}`)));
|
|
18676
|
-
const progress = summarizeImports(imports,
|
|
18282
|
+
const progress = summarizeImports(imports, params.dry_run);
|
|
18677
18283
|
const failed = imports.find((i) => i.pre_processing?.error || i.processing?.error);
|
|
18678
18284
|
const complete = imports.every((i) => {
|
|
18679
18285
|
if (i.pre_processing?.error || i.processing?.error)
|
|
18680
18286
|
return false;
|
|
18681
|
-
if (
|
|
18287
|
+
if (params.dry_run === true)
|
|
18682
18288
|
return Boolean(i.pre_processing?.finished);
|
|
18683
|
-
if (
|
|
18289
|
+
if (params.dry_run === false)
|
|
18684
18290
|
return Boolean(i.processing?.finished);
|
|
18685
18291
|
return Boolean(i.processing?.finished || i.pre_processing?.finished && !i.processing);
|
|
18686
18292
|
});
|
|
@@ -18688,7 +18294,7 @@ var init_import_status = __esm({
|
|
|
18688
18294
|
const commitError = commitFailureFor(importIds);
|
|
18689
18295
|
let notReady = false;
|
|
18690
18296
|
const declaredTotal = imports.reduce((n, i) => n + Number(i.total_records ?? 0), 0);
|
|
18691
|
-
if (!failed && complete &&
|
|
18297
|
+
if (!failed && complete && params.dry_run !== true && importIds.length > 0) {
|
|
18692
18298
|
try {
|
|
18693
18299
|
reconciled = await fetchReconciledRecords(client, importIds, declaredTotal, ctx);
|
|
18694
18300
|
} catch (err) {
|
|
@@ -18703,9 +18309,8 @@ var init_import_status = __esm({
|
|
|
18703
18309
|
const settled = complete && !notReady;
|
|
18704
18310
|
return {
|
|
18705
18311
|
status: failed || commitError ? "failed" : settled ? "complete" : "running",
|
|
18706
|
-
...handleId ? { handle_id: handleId } : {},
|
|
18707
18312
|
importIds,
|
|
18708
|
-
...
|
|
18313
|
+
...params.dry_run === true ? { dry_run: true } : {},
|
|
18709
18314
|
progress: notReady ? { ...progress, phase: "committing" } : progress,
|
|
18710
18315
|
...reconciled ? {
|
|
18711
18316
|
result: {
|
|
@@ -18731,20 +18336,42 @@ var init_import_status = __esm({
|
|
|
18731
18336
|
}
|
|
18732
18337
|
});
|
|
18733
18338
|
|
|
18734
|
-
// ../core/dist/
|
|
18735
|
-
async function
|
|
18736
|
-
|
|
18737
|
-
|
|
18738
|
-
|
|
18739
|
-
|
|
18740
|
-
|
|
18339
|
+
// ../core/dist/notifications/read-by-id.js
|
|
18340
|
+
async function readNotificationById(client, notificationId) {
|
|
18341
|
+
for (const archived of [false, true]) {
|
|
18342
|
+
for (let page = 0; page < MAX_PAGES; page += 1) {
|
|
18343
|
+
let res;
|
|
18344
|
+
try {
|
|
18345
|
+
res = await client.listNotifications({ archived, page, count: PAGE_SIZE2 });
|
|
18346
|
+
} catch {
|
|
18347
|
+
return null;
|
|
18348
|
+
}
|
|
18349
|
+
const hit = res.items.find((n) => n.id === notificationId);
|
|
18350
|
+
if (hit)
|
|
18351
|
+
return hit;
|
|
18352
|
+
const pages = res.pagination?.pages ?? 1;
|
|
18353
|
+
if (page + 1 >= pages)
|
|
18354
|
+
break;
|
|
18355
|
+
}
|
|
18741
18356
|
}
|
|
18357
|
+
return null;
|
|
18742
18358
|
}
|
|
18359
|
+
var PAGE_SIZE2, MAX_PAGES;
|
|
18360
|
+
var init_read_by_id = __esm({
|
|
18361
|
+
"../core/dist/notifications/read-by-id.js"() {
|
|
18362
|
+
"use strict";
|
|
18363
|
+
PAGE_SIZE2 = 50;
|
|
18364
|
+
MAX_PAGES = 4;
|
|
18365
|
+
}
|
|
18366
|
+
});
|
|
18367
|
+
|
|
18368
|
+
// ../core/dist/composite/qualify-status.js
|
|
18743
18369
|
var qualifyStatus;
|
|
18744
18370
|
var init_qualify_status = __esm({
|
|
18745
18371
|
"../core/dist/composite/qualify-status.js"() {
|
|
18746
18372
|
"use strict";
|
|
18747
|
-
|
|
18373
|
+
init_read_by_id();
|
|
18374
|
+
init_revise_hint();
|
|
18748
18375
|
init_qualify_helpers();
|
|
18749
18376
|
init_tool_descriptions_generated();
|
|
18750
18377
|
qualifyStatus = {
|
|
@@ -18760,18 +18387,30 @@ var init_qualify_status = __esm({
|
|
|
18760
18387
|
inputSchema: {
|
|
18761
18388
|
type: "object",
|
|
18762
18389
|
properties: {
|
|
18763
|
-
|
|
18390
|
+
notification_id: {
|
|
18764
18391
|
type: "string",
|
|
18765
|
-
description: "
|
|
18392
|
+
description: "The `notification_id` returned by leadbay_import_and_qualify / leadbay_bulk_qualify_leads. Answers progress in ONE call."
|
|
18393
|
+
},
|
|
18394
|
+
lead_ids: {
|
|
18395
|
+
type: "array",
|
|
18396
|
+
description: "The `lead_ids` the launch returned. Supply them for per-lead detail (which settled, which are still running). Progress alone needs only notification_id.",
|
|
18397
|
+
items: { type: "string" }
|
|
18398
|
+
},
|
|
18399
|
+
lens_id: {
|
|
18400
|
+
type: "number",
|
|
18401
|
+
description: "The `lens_id` the launch returned. Used to flag leads no longer in the lens."
|
|
18766
18402
|
}
|
|
18767
18403
|
},
|
|
18768
|
-
required: ["
|
|
18404
|
+
anyOf: [{ required: ["notification_id"] }, { required: ["lead_ids"] }],
|
|
18769
18405
|
additionalProperties: false
|
|
18770
18406
|
},
|
|
18771
18407
|
outputSchema: {
|
|
18772
18408
|
type: "object",
|
|
18773
18409
|
properties: {
|
|
18774
|
-
|
|
18410
|
+
notification_id: {
|
|
18411
|
+
type: ["string", "null"],
|
|
18412
|
+
description: "The backend job id this status is for; pass it back to poll again."
|
|
18413
|
+
},
|
|
18775
18414
|
launched_at: { type: "string", description: "ISO timestamp of original launch." },
|
|
18776
18415
|
status: { type: "string", description: "'launched' on success (other states surface as error envelopes)." },
|
|
18777
18416
|
import_ids: {
|
|
@@ -18782,7 +18421,7 @@ var init_qualify_status = __esm({
|
|
|
18782
18421
|
lens_id: { type: "number", description: "Lens id the qualification ran against." },
|
|
18783
18422
|
lead_ids: {
|
|
18784
18423
|
type: "array",
|
|
18785
|
-
description: "Lead UUIDs covered by this
|
|
18424
|
+
description: "Lead UUIDs covered by this status (echoed from launch).",
|
|
18786
18425
|
items: { type: "string" }
|
|
18787
18426
|
},
|
|
18788
18427
|
qualified: {
|
|
@@ -18817,7 +18456,7 @@ var init_qualify_status = __esm({
|
|
|
18817
18456
|
_meta: { type: "object" }
|
|
18818
18457
|
},
|
|
18819
18458
|
required: [
|
|
18820
|
-
"
|
|
18459
|
+
"notification_id",
|
|
18821
18460
|
"status",
|
|
18822
18461
|
"import_ids",
|
|
18823
18462
|
"lens_id",
|
|
@@ -18831,29 +18470,55 @@ var init_qualify_status = __esm({
|
|
|
18831
18470
|
]
|
|
18832
18471
|
},
|
|
18833
18472
|
execute: async (client, params, ctx) => {
|
|
18834
|
-
|
|
18835
|
-
|
|
18836
|
-
|
|
18837
|
-
|
|
18838
|
-
throw client.makeError("BULK_TRACKER_UNAVAILABLE", "No BulkTracker configured on this MCP instance", "leadbay_qualify_status needs a BulkTracker. Upgrade to @leadbay/mcp \u22650.5.0 or set LEADBAY_BULK_STORE_ALLOW_MEMORY=1.", "");
|
|
18473
|
+
const notifId = params.notification_id ?? null;
|
|
18474
|
+
const leadIds = params.lead_ids ?? [];
|
|
18475
|
+
if (!notifId && leadIds.length === 0) {
|
|
18476
|
+
throw client.makeError("QUALIFY_STATUS_INPUT_REQUIRED", "Pass notification_id (for progress) and/or lead_ids (for per-lead detail)", "Both are in the launch response from leadbay_bulk_qualify_leads / leadbay_import_and_qualify. Re-read that result and pass them back.", "");
|
|
18839
18477
|
}
|
|
18840
|
-
|
|
18841
|
-
|
|
18842
|
-
|
|
18843
|
-
|
|
18844
|
-
|
|
18845
|
-
|
|
18478
|
+
let bulkProgress = null;
|
|
18479
|
+
let inProgressFlag = null;
|
|
18480
|
+
let launchedAt = null;
|
|
18481
|
+
if (notifId) {
|
|
18482
|
+
const n = await readNotificationById(client, notifId);
|
|
18483
|
+
if (!n && leadIds.length === 0) {
|
|
18484
|
+
throw client.makeError("QUALIFY_JOB_NOT_FOUND", "No job for that notification_id", "The lookup scans this user's recent notifications (archived included); a job behind many newer ones will not be found. Re-call with the `lead_ids` + `lens_id` the launch returned \u2014 that answers per lead without the notification.", "");
|
|
18485
|
+
}
|
|
18486
|
+
if (n && inferKind(n) !== "bulk_qualify") {
|
|
18487
|
+
const kind = inferKind(n);
|
|
18488
|
+
throw client.makeError("QUALIFY_JOB_WRONG_KIND", `That notification_id is a ${kind === "bulk_enrich" ? "contact enrichment" : kind === "import" ? "file import" : "non-bulk"} notification, not a lead qualification`, kind === "bulk_enrich" ? "Poll it with leadbay_bulk_enrich_status({notification_id, lead_ids, titles, email, phone}) instead \u2014 carry the titles and channel the enrichment was launched with, or a contact enriched earlier counts as done and all_done flips true before anything landed." : kind === "import" ? "Poll it with leadbay_import_status({importIds}) instead." : "Pass the notification_id returned by leadbay_bulk_qualify_leads.", "");
|
|
18489
|
+
}
|
|
18490
|
+
if (n) {
|
|
18491
|
+
bulkProgress = n.bulk_progress;
|
|
18492
|
+
inProgressFlag = n.in_progress;
|
|
18493
|
+
launchedAt = n.created_at;
|
|
18846
18494
|
}
|
|
18847
|
-
throw client.makeError("BULK_NOT_FOUND", "No qualify record for that qualify_id", "It may have expired (30-day TTL) or the MCP process was restarted without persistence. Re-launch via leadbay_import_and_qualify.", "");
|
|
18848
|
-
}
|
|
18849
|
-
if (record.status === "pending") {
|
|
18850
|
-
throw client.makeError("BULK_PENDING", "Qualify record is in 'pending' state \u2014 the launch may be in flight or crashed before launch ack", "Retry leadbay_qualify_status in a few seconds. If it persists >60s, relaunch via leadbay_import_and_qualify.", "");
|
|
18851
|
-
}
|
|
18852
|
-
if (record.status === "failed") {
|
|
18853
|
-
throw client.makeError("BULK_LAUNCH_FAILED", "The original import_and_qualify launch failed; no qualifications were ordered", "Call leadbay_import_and_qualify again \u2014 the failed record won't block a fresh launch.", "");
|
|
18854
18495
|
}
|
|
18855
|
-
if (
|
|
18856
|
-
|
|
18496
|
+
if (leadIds.length === 0) {
|
|
18497
|
+
const out2 = {
|
|
18498
|
+
notification_id: notifId,
|
|
18499
|
+
launched_at: launchedAt ?? "",
|
|
18500
|
+
status: "launched",
|
|
18501
|
+
import_ids: [],
|
|
18502
|
+
lens_id: params.lens_id ?? 0,
|
|
18503
|
+
lead_ids: [],
|
|
18504
|
+
qualified: [],
|
|
18505
|
+
still_running: [],
|
|
18506
|
+
failed: [],
|
|
18507
|
+
not_in_lens: [],
|
|
18508
|
+
bulk_progress: bulkProgress,
|
|
18509
|
+
in_progress: inProgressFlag,
|
|
18510
|
+
region: client.region,
|
|
18511
|
+
_meta: client.lastMeta ?? {
|
|
18512
|
+
region: client.region,
|
|
18513
|
+
endpoint: "GET /notifications",
|
|
18514
|
+
latency_ms: null,
|
|
18515
|
+
retry_after: null
|
|
18516
|
+
}
|
|
18517
|
+
};
|
|
18518
|
+
if (bulkProgress && bulkProgress.quota_hit_count > 0) {
|
|
18519
|
+
out2.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.";
|
|
18520
|
+
}
|
|
18521
|
+
return out2;
|
|
18857
18522
|
}
|
|
18858
18523
|
ctx?.progress?.({
|
|
18859
18524
|
progress: 1,
|
|
@@ -18869,20 +18534,20 @@ var init_qualify_status = __esm({
|
|
|
18869
18534
|
ctx?.progress?.({
|
|
18870
18535
|
progress: 2,
|
|
18871
18536
|
total: 3,
|
|
18872
|
-
message: `Checking lens membership for ${
|
|
18537
|
+
message: `Checking lens membership for ${leadIds.length} lead${leadIds.length === 1 ? "" : "s"}\u2026`
|
|
18873
18538
|
});
|
|
18874
18539
|
let notInLensSet = /* @__PURE__ */ new Set();
|
|
18875
18540
|
try {
|
|
18876
|
-
const pre = await prequalifiedLeads(client,
|
|
18541
|
+
const pre = await prequalifiedLeads(client, leadIds, params.lens_id ?? 0, ctx);
|
|
18877
18542
|
notInLensSet = pre.not_in_lens;
|
|
18878
18543
|
} catch {
|
|
18879
18544
|
}
|
|
18880
18545
|
ctx?.progress?.({
|
|
18881
18546
|
progress: 3,
|
|
18882
18547
|
total: 3,
|
|
18883
|
-
message: `Refreshing qualification state for ${
|
|
18548
|
+
message: `Refreshing qualification state for ${leadIds.length} lead${leadIds.length === 1 ? "" : "s"}\u2026`
|
|
18884
18549
|
});
|
|
18885
|
-
const fresh = await refreshLeadStates(client,
|
|
18550
|
+
const fresh = await refreshLeadStates(client, leadIds, questionOrder);
|
|
18886
18551
|
const failed = [];
|
|
18887
18552
|
const qualified = [];
|
|
18888
18553
|
const still_running = [];
|
|
@@ -18901,28 +18566,17 @@ var init_qualify_status = __esm({
|
|
|
18901
18566
|
const { _stillRunning, _failedCode, ...rest } = r;
|
|
18902
18567
|
qualified.push(rest);
|
|
18903
18568
|
}
|
|
18904
|
-
let bulkProgress = null;
|
|
18905
|
-
let inProgressFlag = null;
|
|
18906
|
-
const notifId = record.notification_id ?? null;
|
|
18907
|
-
if (notifId) {
|
|
18908
|
-
const n = await readNotification(client, notifId);
|
|
18909
|
-
if (n) {
|
|
18910
|
-
bulkProgress = n.bulk_progress;
|
|
18911
|
-
inProgressFlag = n.in_progress;
|
|
18912
|
-
}
|
|
18913
|
-
}
|
|
18914
18569
|
const out = {
|
|
18915
|
-
|
|
18916
|
-
launched_at:
|
|
18917
|
-
status:
|
|
18918
|
-
import_ids:
|
|
18919
|
-
lens_id:
|
|
18920
|
-
lead_ids:
|
|
18570
|
+
notification_id: notifId,
|
|
18571
|
+
launched_at: launchedAt ?? "",
|
|
18572
|
+
status: "launched",
|
|
18573
|
+
import_ids: [],
|
|
18574
|
+
lens_id: params.lens_id ?? 0,
|
|
18575
|
+
lead_ids: leadIds,
|
|
18921
18576
|
qualified,
|
|
18922
18577
|
still_running,
|
|
18923
18578
|
failed,
|
|
18924
18579
|
not_in_lens: [...notInLensSet],
|
|
18925
|
-
notification_id: notifId,
|
|
18926
18580
|
bulk_progress: bulkProgress,
|
|
18927
18581
|
in_progress: inProgressFlag,
|
|
18928
18582
|
region: client.region,
|
|
@@ -18933,10 +18587,6 @@ var init_qualify_status = __esm({
|
|
|
18933
18587
|
retry_after: null
|
|
18934
18588
|
}
|
|
18935
18589
|
};
|
|
18936
|
-
if (record.per_lead_budget_ms !== void 0)
|
|
18937
|
-
out.per_lead_budget_ms = record.per_lead_budget_ms;
|
|
18938
|
-
if (record.total_budget_ms !== void 0)
|
|
18939
|
-
out.total_budget_ms = record.total_budget_ms;
|
|
18940
18590
|
if (bulkProgress && bulkProgress.quota_hit_count > 0) {
|
|
18941
18591
|
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.";
|
|
18942
18592
|
}
|
|
@@ -18949,171 +18599,81 @@ var init_qualify_status = __esm({
|
|
|
18949
18599
|
// ../core/dist/composite/enrich-titles.js
|
|
18950
18600
|
async function launchOnSelection(client, args, ctx) {
|
|
18951
18601
|
const { leadIds, titles, email, phone, lensId, selectionSource, preview } = args;
|
|
18952
|
-
|
|
18953
|
-
|
|
18954
|
-
|
|
18955
|
-
|
|
18956
|
-
|
|
18957
|
-
|
|
18958
|
-
|
|
18959
|
-
|
|
18960
|
-
|
|
18961
|
-
|
|
18962
|
-
|
|
18963
|
-
|
|
18964
|
-
|
|
18965
|
-
|
|
18966
|
-
|
|
18967
|
-
|
|
18968
|
-
|
|
18969
|
-
|
|
18970
|
-
|
|
18971
|
-
|
|
18972
|
-
|
|
18973
|
-
|
|
18974
|
-
|
|
18975
|
-
|
|
18976
|
-
|
|
18977
|
-
|
|
18978
|
-
|
|
18979
|
-
|
|
18980
|
-
|
|
18981
|
-
|
|
18982
|
-
|
|
18983
|
-
|
|
18984
|
-
|
|
18985
|
-
|
|
18986
|
-
|
|
18987
|
-
|
|
18988
|
-
|
|
18989
|
-
|
|
18990
|
-
|
|
18991
|
-
|
|
18992
|
-
|
|
18993
|
-
|
|
18994
|
-
|
|
18995
|
-
|
|
18996
|
-
|
|
18997
|
-
|
|
18998
|
-
|
|
18999
|
-
|
|
19000
|
-
|
|
19001
|
-
lead_ids: leadIds,
|
|
19002
|
-
titles,
|
|
19003
|
-
email,
|
|
19004
|
-
phone,
|
|
19005
|
-
preview,
|
|
19006
|
-
message: "No new enrichment was ordered; quota not spent. A concurrent identical launch is already in flight. Unless the user asked NOT to wait, poll leadbay_bulk_enrich_status with this bulk_id for results (see next_action); if they asked not to wait, hand back the bulk_id.",
|
|
19007
|
-
next_action: "Unless the user explicitly asked NOT to wait, poll leadbay_bulk_enrich_status({bulk_id}) until all_done \u2014 OR until overall_progress.done plateaus across spaced polls (~90s\u20132min; unresolvable contacts never flip). include_contacts=true on the read you report from, then report the resolved enrichment in this turn. If the user asked not to wait, hand back the bulk_id instead."
|
|
19008
|
-
};
|
|
19009
|
-
}
|
|
19010
|
-
bulkRecord = {
|
|
19011
|
-
bulk_id: fresh.record.bulk_id,
|
|
19012
|
-
launched_at: fresh.record.launched_at,
|
|
19013
|
-
durability: fresh.record.durability
|
|
19014
|
-
};
|
|
19015
|
-
} else if (bulkReused && res.record.status !== "failed") {
|
|
19016
|
-
return {
|
|
19017
|
-
mode: "already_launched",
|
|
19018
|
-
re_used: true,
|
|
19019
|
-
bulk_id: res.record.bulk_id,
|
|
19020
|
-
launched_at: res.record.launched_at,
|
|
19021
|
-
durability: res.record.durability,
|
|
19022
|
-
notification_id: res.record.notification_id ?? null,
|
|
19023
|
-
seconds_since_original_launch: bulkSecondsSinceOriginal ?? 0,
|
|
19024
|
-
lead_ids: leadIds,
|
|
19025
|
-
titles,
|
|
19026
|
-
email,
|
|
19027
|
-
phone,
|
|
19028
|
-
preview,
|
|
19029
|
-
message: `No new enrichment was ordered; quota not spent. An identical bulk was launched ${bulkSecondsSinceOriginal ?? 0}s ago. Unless the user asked NOT to wait (background/'I'll check later'), poll leadbay_bulk_enrich_status with this bulk_id for results; if they DID ask not to wait, hand back the bulk_id instead.`,
|
|
19030
|
-
next_action: "Unless the user explicitly asked NOT to wait (background/'I'll check later'), poll leadbay_bulk_enrich_status({bulk_id}) until all_done \u2014 OR until overall_progress.done holds steady across several SPACED polls (~15\u201330s apart, ~90s\u20132min elapsed; unresolvable contacts never flip, so a reused bulk can stay all_done:false forever). include_contacts=true on the read you report from, then report the resolved enrichment in this turn \u2014 don't end your turn waiting or spin forever. If the user DID ask not to wait, hand back the bulk_id instead of polling."
|
|
19031
|
-
};
|
|
19032
|
-
}
|
|
19033
|
-
}
|
|
19034
|
-
ctx?.progress?.({
|
|
19035
|
-
progress: 3,
|
|
19036
|
-
total: 3,
|
|
19037
|
-
message: `Launching enrichment for ${titles.length} title${titles.length === 1 ? "" : "s"}\u2026`
|
|
19038
|
-
});
|
|
19039
|
-
let launchResp = null;
|
|
19040
|
-
try {
|
|
19041
|
-
launchResp = await client.request("POST", "/leads/selection/enrichment/launch", { titles, email, phone });
|
|
19042
|
-
} catch (err) {
|
|
19043
|
-
const aborted = err?.name === "AbortError" || ctx?.signal?.aborted === true;
|
|
19044
|
-
if (bulkRecord && tracker) {
|
|
19045
|
-
try {
|
|
19046
|
-
if (aborted) {
|
|
19047
|
-
await tracker.markCancelled(bulkRecord.bulk_id);
|
|
19048
|
-
} else {
|
|
19049
|
-
await tracker.markFailed(bulkRecord.bulk_id);
|
|
19050
|
-
}
|
|
19051
|
-
} catch (e) {
|
|
19052
|
-
ctx?.logger?.warn?.(`enrich_titles: tracker.${aborted ? "markCancelled" : "markFailed"} failed: ${e?.message ?? e}`);
|
|
19053
|
-
}
|
|
19054
|
-
}
|
|
19055
|
-
if (err?.code === "QUOTA_EXCEEDED") {
|
|
19056
|
-
return {
|
|
19057
|
-
status: "quota_exceeded",
|
|
19058
|
-
preview,
|
|
19059
|
-
message: "Quota exceeded on launch",
|
|
19060
|
-
retry_after_seconds: err?._meta?.retry_after ?? null
|
|
19061
|
-
};
|
|
19062
|
-
}
|
|
19063
|
-
throw err;
|
|
19064
|
-
}
|
|
19065
|
-
const notificationId = launchResp?.notification_id ?? null;
|
|
19066
|
-
if (bulkRecord && tracker) {
|
|
19067
|
-
try {
|
|
19068
|
-
await tracker.markLaunched(bulkRecord.bulk_id, notificationId);
|
|
19069
|
-
} catch (e) {
|
|
19070
|
-
ctx?.logger?.warn?.(`enrich_titles: tracker.markLaunched failed: ${e?.message ?? e}`);
|
|
19071
|
-
return {
|
|
19072
|
-
mode: "launched_tracker_pending",
|
|
19073
|
-
launched: true,
|
|
19074
|
-
preview,
|
|
19075
|
-
bulk_id: bulkRecord.bulk_id,
|
|
19076
|
-
launched_at: bulkRecord.launched_at,
|
|
19077
|
-
durability: bulkRecord.durability,
|
|
19078
|
-
// Surface the resolved lead IDs so the agent can follow the backend
|
|
19079
|
-
// job per-lead — bulk_enrich_status is unusable for this stuck handle,
|
|
19080
|
-
// and the caller may have omitted leadIds (wishlist default), so
|
|
19081
|
-
// without these it has no identifiers to poll.
|
|
19082
|
-
lead_ids: leadIds,
|
|
19083
|
-
titles,
|
|
19084
|
-
email,
|
|
19085
|
-
phone,
|
|
19086
|
-
message: "Enrichment job launched on the backend, but the local tracker record could not be flipped to 'launched' and will NOT heal on its own this session. leadbay_bulk_enrich_status({bulk_id}) will keep returning status:'pending' (BULK_PENDING) \u2014 do NOT poll it in a loop expecting completion. The backend job is running regardless; track it per-lead instead.",
|
|
19087
|
-
next_action: "Do NOT poll leadbay_bulk_enrich_status \u2014 this bulk_id is stuck 'pending' and won't flip. If the user asked NOT to wait (background/'I'll check later'), just hand back the returned lead_ids and let them re-check later. Otherwise track results per lead via leadbay_get_contacts(leadId) / leadbay_research_lead_by_id for the returned lead_ids (re-check every ~30s). get_contacts returns each lead's FULL contact list, so only count/report contacts whose job_title matches the enriched titles (" + titles.join(", ") + ") \u2014 don't attribute a pre-existing CFO/Sales email to this run \u2014 and a contact is done only when the REQUESTED channel landed (requested email and/or phone_number present, not contact.enrichment.done alone). Stop once the done set plateaus (~90s\u20132min), then report the resolved contacts and name the rest. (The launch already succeeded \u2014 do not relaunch.)"
|
|
19088
|
-
};
|
|
19089
|
-
}
|
|
19090
|
-
}
|
|
18602
|
+
const fingerprint = launchFingerprint([
|
|
18603
|
+
"enrich",
|
|
18604
|
+
leadIds,
|
|
18605
|
+
titles,
|
|
18606
|
+
email,
|
|
18607
|
+
phone,
|
|
18608
|
+
lensId
|
|
18609
|
+
]);
|
|
18610
|
+
const claim = beginLaunch(fingerprint);
|
|
18611
|
+
if (claim.state === "in_flight") {
|
|
18612
|
+
return {
|
|
18613
|
+
mode: "launch_in_flight",
|
|
18614
|
+
launched: false,
|
|
18615
|
+
preview,
|
|
18616
|
+
lead_ids: leadIds,
|
|
18617
|
+
titles,
|
|
18618
|
+
message: `An identical enrichment was started ${claim.seconds_since}s ago and has not returned its job id yet. Nothing was launched twice and no quota was spent.`,
|
|
18619
|
+
next_action: "Wait a few seconds and call leadbay_enrich_titles again with the same arguments \u2014 it will hand back the job id once the first call settles. Do not treat this as a running job; there is no id to poll yet."
|
|
18620
|
+
};
|
|
18621
|
+
}
|
|
18622
|
+
const already = claim.state === "settled" ? claim.record : void 0;
|
|
18623
|
+
if (already) {
|
|
18624
|
+
return {
|
|
18625
|
+
mode: "already_launched",
|
|
18626
|
+
launched: true,
|
|
18627
|
+
preview,
|
|
18628
|
+
reused: true,
|
|
18629
|
+
seconds_since_original_launch: already.seconds_since,
|
|
18630
|
+
lead_ids: leadIds,
|
|
18631
|
+
titles,
|
|
18632
|
+
email,
|
|
18633
|
+
phone,
|
|
18634
|
+
notification_id: already.notification_id,
|
|
18635
|
+
launched_at: already.launched_at,
|
|
18636
|
+
message: `An identical enrichment was launched ${already.seconds_since}s ago; this call did NOT spend quota again. Poll the original job rather than relaunching.`,
|
|
18637
|
+
next_action: "Poll leadbay_bulk_enrich_status({notification_id, lead_ids, titles, email, phone, include_contacts: true}) until all_done, or until overall_progress.done holds steady across spaced polls (~15-30s apart) \u2014 unresolvable contacts never flip. Pass titles/email/phone every time: they scope counting to the roles and channel THIS run asked for, so a contact enriched earlier cannot report the run as finished. Then report what landed and name what didn't."
|
|
18638
|
+
};
|
|
18639
|
+
}
|
|
18640
|
+
ctx?.progress?.({
|
|
18641
|
+
progress: 3,
|
|
18642
|
+
total: 3,
|
|
18643
|
+
message: `Launching enrichment for ${titles.length} title${titles.length === 1 ? "" : "s"}\u2026`
|
|
18644
|
+
});
|
|
18645
|
+
let launchResp = null;
|
|
18646
|
+
try {
|
|
18647
|
+
launchResp = await client.request("POST", "/leads/selection/enrichment/launch", { titles, email, phone });
|
|
18648
|
+
} catch (err) {
|
|
18649
|
+
abandonLaunch(fingerprint);
|
|
18650
|
+
if (err?.code === "QUOTA_EXCEEDED") {
|
|
19091
18651
|
return {
|
|
19092
|
-
|
|
18652
|
+
status: "quota_exceeded",
|
|
19093
18653
|
preview,
|
|
19094
|
-
|
|
19095
|
-
|
|
19096
|
-
email,
|
|
19097
|
-
phone,
|
|
19098
|
-
// Always surface the resolved lead IDs — in the no-tracker branch there's
|
|
19099
|
-
// no bulk_id to poll, and the caller may have omitted leadIds (wishlist
|
|
19100
|
-
// default), so without these the agent has no identifiers to follow the
|
|
19101
|
-
// job it just launched via leadbay_get_contacts / research_lead_by_id.
|
|
19102
|
-
lead_ids: leadIds,
|
|
19103
|
-
bulk_id: bulkRecord?.bulk_id,
|
|
19104
|
-
launched_at: bulkRecord?.launched_at,
|
|
19105
|
-
durability: bulkRecord?.durability,
|
|
19106
|
-
notification_id: notificationId,
|
|
19107
|
-
// Branch on bulkRecord FIRST: leadbay_bulk_enrich_status needs a real
|
|
19108
|
-
// bulk_id (tracker handle). A notification_id can come back even with no
|
|
19109
|
-
// tracker (legacy / OpenClaw raw-launch fall-through) — in that case
|
|
19110
|
-
// bulk_id is undefined, so the agent must use the per-lead fallback, not
|
|
19111
|
-
// poll a nonexistent bulk_id.
|
|
19112
|
-
message: bulkRecord ? notificationId ? "Enrichment job launched (runs async). Unless the user asked NOT to wait (background/'I'll check later'), do NOT end your turn here \u2014 poll leadbay_bulk_enrich_status({bulk_id}) until all_done OR until progress plateaus (overall_progress.done stops climbing across spaced polls \u2014 unresolvable contacts keep all_done:false forever), then report the finished contacts yourself. (If the user DID ask not to wait, hand back the bulk_id instead. Either way, if you leave the conversation the completion also surfaces later via _meta.notifications / leadbay_account_status.notifications \u2014 but for a job you launched this turn and were NOT told to background, poll it now.)" : "Enrichment job launched (runs async). Unless the user asked NOT to wait (background/'I'll check later'), do NOT end your turn here \u2014 poll leadbay_bulk_enrich_status({bulk_id}) until all_done OR until progress plateaus (overall_progress.done stops climbing across spaced polls \u2014 unresolvable contacts keep all_done:false forever), then report the finished contacts yourself. (No notification id was returned, so there is NO automatic _meta.notifications completion for this job \u2014 if you background it or don't finish this turn, you (or the user) must poll leadbay_bulk_enrich_status({bulk_id}) again later; it will NOT surface on its own.)" : "Enrichment job launched. No bulk_id tracker configured. Unless the user asked NOT to wait (background/'I'll check later' \u2014 in which case hand back the lead_ids and let them re-check later), poll leadbay_get_contacts per lead (re-check every ~30s). get_contacts returns each lead's FULL contact list, so only count/report contacts whose job_title matches the enriched titles (" + titles.join(", ") + ") \u2014 don't attribute a pre-existing email of an unrelated role to this run \u2014 and a contact is done only when the REQUESTED channel landed (requested email and/or phone_number present, not contact.enrichment.done alone). Then report the results. Stop once the set of done contacts stops growing across a couple of spaced re-checks (~90s\u20132min elapsed): some contacts are unresolvable and never flip, so report the resolved ones and name the rest rather than polling forever.",
|
|
19113
|
-
next_action: bulkRecord ? "Unless the user explicitly asked NOT to wait (background/'I'll check later'), poll leadbay_bulk_enrich_status({bulk_id}) in a loop until all_done \u2014 OR until overall_progress.done holds steady across several SPACED polls (~15\u201330s apart, ~90s\u20132min elapsed; don't call a plateau from the first back-to-back reads while the backend spins up, and don't call it a plateau while partial_failures is present \u2014 that's a transient fetch error, keep polling/respect retry_after). Pass include_contacts=true on the read you report from, then report the resolved enrichment in THIS turn (name what landed and what didn't). If the user DID ask not to wait, hand back the bulk_id instead of polling (and if notification_id is null, tell them to ask again later \u2014 nothing auto-surfaces)." : "Unless the user asked not to wait, re-check via leadbay_research_lead_by_id or leadbay_get_contacts for the returned lead_ids (every ~30s). get_contacts returns each lead's FULL contact list, so only count/report contacts whose job_title matches the enriched titles (" + titles.join(", ") + ") \u2014 don't attribute a pre-existing email of an unrelated role to this run. Treat a contact as done only when the REQUESTED channel landed \u2014 the requested email present and/or phone_number present \u2014 NOT contact.enrichment.done alone (it's already true for a contact enriched on the other channel earlier). Stop once the done set stops growing across a couple of spaced re-checks (~90s\u20132min elapsed) \u2014 unresolvable contacts never flip \u2014 then report the resolved ones and name the rest. Don't poll forever or end your turn waiting. If the user asked not to wait, hand back the lead_ids and let them re-check later."
|
|
18654
|
+
message: "Quota exceeded on launch",
|
|
18655
|
+
retry_after_seconds: err?._meta?.retry_after ?? null
|
|
19114
18656
|
};
|
|
19115
18657
|
}
|
|
18658
|
+
throw err;
|
|
19116
18659
|
}
|
|
18660
|
+
const notificationId = launchResp?.notification_id ?? null;
|
|
18661
|
+
const remembered = rememberLaunch(fingerprint, notificationId);
|
|
18662
|
+
return {
|
|
18663
|
+
mode: "launched",
|
|
18664
|
+
preview,
|
|
18665
|
+
launched: true,
|
|
18666
|
+
titles,
|
|
18667
|
+
email,
|
|
18668
|
+
phone,
|
|
18669
|
+
// Always surfaced: these are the coordinates the agent polls with. There is
|
|
18670
|
+
// no server-side handle to look them up from, by design.
|
|
18671
|
+
lead_ids: leadIds,
|
|
18672
|
+
notification_id: notificationId,
|
|
18673
|
+
launched_at: remembered.launched_at,
|
|
18674
|
+
message: notificationId ? "Enrichment job launched (runs async). Unless the user asked NOT to wait, do NOT end your turn here \u2014 poll leadbay_bulk_enrich_status({notification_id, lead_ids, titles, email, phone}) until all_done OR until progress plateaus (overall_progress.done stops climbing across spaced polls \u2014 unresolvable contacts keep all_done:false forever), then report the finished contacts yourself. The notification_id keeps working across conversations and days; completion also surfaces via _meta.notifications / leadbay_account_status.notifications." : "Enrichment job launched, but the backend returned no notification_id, so there is no job id to poll. Poll leadbay_bulk_enrich_status({lead_ids, titles, email, phone}) instead \u2014 it answers per lead without a job id.",
|
|
18675
|
+
next_action: notificationId ? "Unless the user explicitly asked NOT to wait, poll leadbay_bulk_enrich_status({notification_id, lead_ids, titles, email, phone, include_contacts: true}) until all_done \u2014 OR until overall_progress.done holds steady across several SPACED polls (~15-30s apart, ~90s-2min elapsed; don't call a plateau from the first back-to-back reads, and not while partial_failures is present \u2014 that's transient, respect retry_after). Carry titles/email/phone on every poll \u2014 they scope counting to the roles and channel THIS run asked for, and without them a contact enriched months ago counts as done and all_done flips true before anything landed. Then report the resolved enrichment in THIS turn, naming what landed and what didn't. If the user DID ask not to wait, hand back the notification_id \u2014 it resolves later from any conversation." : "Poll leadbay_bulk_enrich_status({lead_ids, titles, email, phone, include_contacts: true}) every ~30s. It scopes counting to these titles and treats a contact as done only when the REQUESTED channel landed, so you do not have to do that yourself. Stop once overall_progress.done stops growing across a couple of spaced re-checks (~90s-2min) \u2014 unresolvable contacts never flip."
|
|
18676
|
+
};
|
|
19117
18677
|
}
|
|
19118
18678
|
async function launchEnrichment(client, args, ctx) {
|
|
19119
18679
|
await client.acquireSelectionLock();
|
|
@@ -19137,6 +18697,7 @@ var DEFAULT_CANDIDATE_COUNT, enrichTitles;
|
|
|
19137
18697
|
var init_enrich_titles = __esm({
|
|
19138
18698
|
"../core/dist/composite/enrich-titles.js"() {
|
|
19139
18699
|
"use strict";
|
|
18700
|
+
init_launch_guard();
|
|
19140
18701
|
init_credits_helpers();
|
|
19141
18702
|
init_tool_descriptions_generated();
|
|
19142
18703
|
DEFAULT_CANDIDATE_COUNT = 25;
|
|
@@ -19150,7 +18711,7 @@ var init_enrich_titles = __esm({
|
|
|
19150
18711
|
// destructive because the dominant flow mutates state.
|
|
19151
18712
|
destructiveHint: true,
|
|
19152
18713
|
// Idempotent against the same selection + titles set (same hash → same
|
|
19153
|
-
//
|
|
18714
|
+
// the launch; backend silently no-ops on already-enriched contacts).
|
|
19154
18715
|
idempotentHint: true,
|
|
19155
18716
|
openWorldHint: true
|
|
19156
18717
|
},
|
|
@@ -19191,11 +18752,11 @@ var init_enrich_titles = __esm({
|
|
|
19191
18752
|
},
|
|
19192
18753
|
outputSchema: {
|
|
19193
18754
|
type: "object",
|
|
19194
|
-
description: "Branchy return shape; the `mode` (or `status`) field tells the agent which branch it got. Modes: 'discover' (no titles passed), 'preview_only' (no enrichable contacts), 'dry_run', 'needs_confirmation' (paid launch withheld pending user consent), 'already_launched' (idempotent reuse), '
|
|
18755
|
+
description: "Branchy return shape; the `mode` (or `status`) field tells the agent which branch it got. Modes: 'discover' (no titles passed), 'preview_only' (no enrichable contacts), 'dry_run', 'needs_confirmation' (paid launch withheld pending user consent), 'already_launched' (idempotent reuse), 'launch_in_flight' (an identical launch is mid-flight and has no id yet), 'launched' (happy path). Status: 'quota_exceeded' (429).",
|
|
19195
18756
|
properties: {
|
|
19196
18757
|
mode: {
|
|
19197
18758
|
type: "string",
|
|
19198
|
-
description: "'discover' | 'preview_only' | 'dry_run' | 'needs_confirmation' | 'already_launched' | '
|
|
18759
|
+
description: "'discover' | 'preview_only' | 'dry_run' | 'needs_confirmation' | 'already_launched' | 'launch_in_flight' | 'launched'."
|
|
19199
18760
|
},
|
|
19200
18761
|
status: {
|
|
19201
18762
|
type: "string",
|
|
@@ -19245,22 +18806,23 @@ var init_enrich_titles = __esm({
|
|
|
19245
18806
|
type: "object",
|
|
19246
18807
|
description: "What dry_run WOULD have launched (titles, email, phone)."
|
|
19247
18808
|
},
|
|
19248
|
-
|
|
19249
|
-
type: "
|
|
19250
|
-
description: "
|
|
18809
|
+
notification_id: {
|
|
18810
|
+
type: ["string", "null"],
|
|
18811
|
+
description: "The backend's job id. Carry it to leadbay_bulk_enrich_status. Null when the backend returned none \u2014 then poll by lead_ids instead."
|
|
19251
18812
|
},
|
|
19252
|
-
|
|
19253
|
-
type: "
|
|
19254
|
-
description: "
|
|
18813
|
+
lead_ids: {
|
|
18814
|
+
type: "array",
|
|
18815
|
+
description: "The leads this run enriched. Carry them to leadbay_bulk_enrich_status for per-lead progress; they also work when the notification is archived.",
|
|
18816
|
+
items: { type: "string" }
|
|
18817
|
+
},
|
|
18818
|
+
reused: {
|
|
18819
|
+
type: "boolean",
|
|
18820
|
+
description: "True when an identical launch inside the 5-minute window was reused instead of spending quota again."
|
|
19255
18821
|
},
|
|
19256
18822
|
launched_at: {
|
|
19257
18823
|
type: "string",
|
|
19258
18824
|
description: "ISO timestamp of the (re-used or fresh) launch."
|
|
19259
18825
|
},
|
|
19260
|
-
durability: {
|
|
19261
|
-
type: "string",
|
|
19262
|
-
description: "'file' (persisted bulks.json) or 'memory'."
|
|
19263
|
-
},
|
|
19264
18826
|
seconds_since_original_launch: {
|
|
19265
18827
|
type: "number",
|
|
19266
18828
|
description: "Age of the re-used bulk record (already_launched mode)."
|
|
@@ -19519,14 +19081,6 @@ var init_enrich_titles = __esm({
|
|
|
19519
19081
|
});
|
|
19520
19082
|
|
|
19521
19083
|
// ../core/dist/composite/bulk-enrich-status.js
|
|
19522
|
-
async function readNotification2(client, notificationId) {
|
|
19523
|
-
try {
|
|
19524
|
-
const page = await client.listNotifications({ archived: false, count: 50 });
|
|
19525
|
-
return page.items.find((n) => n.id === notificationId) ?? null;
|
|
19526
|
-
} catch {
|
|
19527
|
-
return null;
|
|
19528
|
-
}
|
|
19529
|
-
}
|
|
19530
19084
|
async function pMap(items, fn, concurrency) {
|
|
19531
19085
|
const out = new Array(items.length);
|
|
19532
19086
|
let next = 0;
|
|
@@ -19545,8 +19099,9 @@ var STATUS_FETCH_CONCURRENCY, bulkEnrichStatus;
|
|
|
19545
19099
|
var init_bulk_enrich_status = __esm({
|
|
19546
19100
|
"../core/dist/composite/bulk-enrich-status.js"() {
|
|
19547
19101
|
"use strict";
|
|
19102
|
+
init_read_by_id();
|
|
19103
|
+
init_revise_hint();
|
|
19548
19104
|
init_get_contacts();
|
|
19549
|
-
init_bulk_store();
|
|
19550
19105
|
init_credits_helpers();
|
|
19551
19106
|
init_tool_descriptions_generated();
|
|
19552
19107
|
STATUS_FETCH_CONCURRENCY = 5;
|
|
@@ -19563,44 +19118,45 @@ var init_bulk_enrich_status = __esm({
|
|
|
19563
19118
|
inputSchema: {
|
|
19564
19119
|
type: "object",
|
|
19565
19120
|
properties: {
|
|
19566
|
-
|
|
19121
|
+
notification_id: {
|
|
19567
19122
|
type: "string",
|
|
19568
|
-
description: "
|
|
19123
|
+
description: "The `notification_id` returned by leadbay_enrich_titles. Gives the job-level counters in one call."
|
|
19124
|
+
},
|
|
19125
|
+
lead_ids: {
|
|
19126
|
+
type: "array",
|
|
19127
|
+
description: "The `lead_ids` the launch returned. Gives per-lead progress, and answers on its own if the notification is archived or has aged off page 1.",
|
|
19128
|
+
items: { type: "string" }
|
|
19129
|
+
},
|
|
19130
|
+
titles: {
|
|
19131
|
+
type: "array",
|
|
19132
|
+
description: "The `titles` the launch returned. Scopes progress to the roles THIS run enriched, so a lead's pre-existing CFO email cannot inflate a CEO run.",
|
|
19133
|
+
items: { type: "string" }
|
|
19134
|
+
},
|
|
19135
|
+
email: {
|
|
19136
|
+
type: "boolean",
|
|
19137
|
+
description: "The `email` flag the launch returned. A contact counts as done only once the requested channel has landed."
|
|
19138
|
+
},
|
|
19139
|
+
phone: {
|
|
19140
|
+
type: "boolean",
|
|
19141
|
+
description: "The `phone` flag the launch returned. Same rule as `email`."
|
|
19569
19142
|
},
|
|
19570
19143
|
include_contacts: {
|
|
19571
19144
|
type: "boolean",
|
|
19572
19145
|
description: "If true, return the full contact list per lead (email, phone, enrichment.done). Default false \u2014 cheap status polls."
|
|
19573
19146
|
}
|
|
19574
19147
|
},
|
|
19575
|
-
required: ["
|
|
19148
|
+
anyOf: [{ required: ["notification_id"] }, { required: ["lead_ids"] }],
|
|
19576
19149
|
additionalProperties: false
|
|
19577
19150
|
},
|
|
19578
19151
|
outputSchema: {
|
|
19579
19152
|
type: "object",
|
|
19580
19153
|
properties: {
|
|
19581
|
-
|
|
19154
|
+
notification_id: { type: "string", description: "The backend job id; pass it back to poll again." },
|
|
19582
19155
|
launched_at: { type: "string", description: "ISO timestamp of /enrichment/launch ack." },
|
|
19583
19156
|
status: {
|
|
19584
19157
|
type: "string",
|
|
19585
19158
|
description: "'launched' on success. Errors return error envelopes (handled separately)."
|
|
19586
19159
|
},
|
|
19587
|
-
durability: {
|
|
19588
|
-
type: "string",
|
|
19589
|
-
description: "'persistent' (file-backed bulks.json) or 'memory' (LEADBAY_BULK_STORE_ALLOW_MEMORY)."
|
|
19590
|
-
},
|
|
19591
|
-
titles: {
|
|
19592
|
-
type: "array",
|
|
19593
|
-
description: "Titles ordered at launch time (echoed from the original enrich_titles call).",
|
|
19594
|
-
items: { type: "string" }
|
|
19595
|
-
},
|
|
19596
|
-
email: { type: "boolean", description: "True if email enrichment was requested." },
|
|
19597
|
-
phone: { type: "boolean", description: "True if phone enrichment was requested." },
|
|
19598
|
-
lens_id: { type: "number", description: "Lens id used to scope the enrichment." },
|
|
19599
|
-
leads: {
|
|
19600
|
-
type: "array",
|
|
19601
|
-
description: "Per-lead rollup: {lead_id, enrichment_progress:{done,total}, contacts? (when include_contacts=true)}.",
|
|
19602
|
-
items: { type: "object" }
|
|
19603
|
-
},
|
|
19604
19160
|
overall_progress: {
|
|
19605
19161
|
type: "object",
|
|
19606
19162
|
description: "Aggregate progress across all leads.",
|
|
@@ -19624,243 +19180,180 @@ var init_bulk_enrich_status = __esm({
|
|
|
19624
19180
|
items: { type: "object" }
|
|
19625
19181
|
}
|
|
19626
19182
|
},
|
|
19627
|
-
required: ["
|
|
19183
|
+
required: ["status", "leads", "overall_progress", "all_done"]
|
|
19628
19184
|
},
|
|
19629
19185
|
execute: async (client, params, ctx) => {
|
|
19630
|
-
if (!isValidBulkId(params.bulk_id)) {
|
|
19631
|
-
return {
|
|
19632
|
-
error: true,
|
|
19633
|
-
code: "BULK_INVALID_ID",
|
|
19634
|
-
message: "bulk_id is not a valid UUIDv4",
|
|
19635
|
-
hint: "Pass the bulk_id returned by leadbay_enrich_titles verbatim."
|
|
19636
|
-
};
|
|
19637
|
-
}
|
|
19638
|
-
if (!ctx?.bulkTracker) {
|
|
19639
|
-
return {
|
|
19640
|
-
error: true,
|
|
19641
|
-
code: "BULK_TRACKER_UNAVAILABLE",
|
|
19642
|
-
message: "No BulkTracker configured on this MCP instance",
|
|
19643
|
-
hint: "This composite requires a BulkTracker in ToolContext. Upgrade to @leadbay/mcp \u22650.3.0 or run with LEADBAY_BULK_STORE_ALLOW_MEMORY=1."
|
|
19644
|
-
};
|
|
19645
|
-
}
|
|
19646
19186
|
const includeContacts = params.include_contacts ?? false;
|
|
19187
|
+
const leadIds = params.lead_ids ?? [];
|
|
19647
19188
|
const startMs = Date.now();
|
|
19648
|
-
|
|
19649
|
-
try {
|
|
19650
|
-
record = await ctx.bulkTracker.get(params.bulk_id);
|
|
19651
|
-
} catch (err) {
|
|
19652
|
-
return {
|
|
19653
|
-
error: true,
|
|
19654
|
-
code: "BULK_STORE_UNAVAILABLE",
|
|
19655
|
-
message: `Bulk store read failed: ${err?.message ?? err}`,
|
|
19656
|
-
hint: "Check the file at $LEADBAY_BULK_STORE_PATH (default ~/.leadbay/bulks.json). Set LEADBAY_BULK_STORE_ALLOW_MEMORY=1 to fall back to in-memory storage on startup (handles won't survive restart)."
|
|
19657
|
-
};
|
|
19658
|
-
}
|
|
19659
|
-
if (!record) {
|
|
19660
|
-
return {
|
|
19661
|
-
error: true,
|
|
19662
|
-
code: "BULK_NOT_FOUND",
|
|
19663
|
-
message: "No bulk record for that bulk_id",
|
|
19664
|
-
hint: "The record may have aged out (30-day TTL) or the MCP process was restarted without persistence. Launch a new enrichment via leadbay_enrich_titles."
|
|
19665
|
-
};
|
|
19666
|
-
}
|
|
19667
|
-
if (record.kind !== "enrich") {
|
|
19668
|
-
return {
|
|
19669
|
-
error: true,
|
|
19670
|
-
code: "BULK_WRONG_KIND",
|
|
19671
|
-
message: `This bulk_id was created by ${record.kind === "qualify" ? "leadbay_import_and_qualify" : "leadbay_import_leads"}, not leadbay_enrich_titles.`,
|
|
19672
|
-
hint: record.kind === "qualify" ? "Call leadbay_qualify_status with this id instead." : "Call leadbay_import_status with this id instead.",
|
|
19673
|
-
bulk_id: record.bulk_id
|
|
19674
|
-
};
|
|
19675
|
-
}
|
|
19676
|
-
if (record.status === "pending") {
|
|
19677
|
-
return {
|
|
19678
|
-
error: true,
|
|
19679
|
-
code: "BULK_PENDING",
|
|
19680
|
-
message: "Bulk is in 'pending' state \u2014 the launch is in flight or the MCP crashed between launch and ack.",
|
|
19681
|
-
hint: "Retry leadbay_bulk_enrich_status in a few seconds. If it persists >60s, relaunch via leadbay_enrich_titles.",
|
|
19682
|
-
bulk_id: record.bulk_id,
|
|
19683
|
-
launched_at: record.launched_at
|
|
19684
|
-
};
|
|
19685
|
-
}
|
|
19686
|
-
if (record.status === "failed") {
|
|
19687
|
-
return {
|
|
19688
|
-
error: true,
|
|
19689
|
-
code: "BULK_LAUNCH_FAILED",
|
|
19690
|
-
message: "The original /enrichment/launch POST failed; no backend enrichment was ordered.",
|
|
19691
|
-
hint: "Call leadbay_enrich_titles again \u2014 the failed record won't block a fresh launch.",
|
|
19692
|
-
bulk_id: record.bulk_id,
|
|
19693
|
-
launched_at: record.launched_at
|
|
19694
|
-
};
|
|
19695
|
-
}
|
|
19696
|
-
if (record.status === "cancelled") {
|
|
19189
|
+
if (!params.notification_id && leadIds.length === 0) {
|
|
19697
19190
|
return {
|
|
19698
19191
|
error: true,
|
|
19699
|
-
code: "
|
|
19700
|
-
message: "
|
|
19701
|
-
hint: "
|
|
19702
|
-
bulk_id: record.bulk_id,
|
|
19703
|
-
launched_at: record.launched_at
|
|
19192
|
+
code: "ENRICH_STATUS_INPUT_REQUIRED",
|
|
19193
|
+
message: "Pass notification_id and/or lead_ids",
|
|
19194
|
+
hint: "Both are in the leadbay_enrich_titles result. notification_id gives the job counters; lead_ids gives per-lead progress and works even when the notification has been archived."
|
|
19704
19195
|
};
|
|
19705
19196
|
}
|
|
19706
|
-
|
|
19707
|
-
|
|
19708
|
-
|
|
19709
|
-
|
|
19710
|
-
|
|
19711
|
-
|
|
19712
|
-
|
|
19713
|
-
const fastPartialFailures = [];
|
|
19714
|
-
if (includeContacts) {
|
|
19715
|
-
leads2 = await pMap(record.lead_ids, async (leadId) => {
|
|
19716
|
-
try {
|
|
19717
|
-
const out = await getContacts.execute(client, { leadId });
|
|
19718
|
-
const contacts = Array.isArray(out?.contacts) ? out.contacts : [];
|
|
19719
|
-
const fe = Array.isArray(out?._fetch_errors) ? out._fetch_errors : [];
|
|
19720
|
-
if (fe.length > 0) {
|
|
19721
|
-
fastPartialFailures.push({
|
|
19722
|
-
lead_id: leadId,
|
|
19723
|
-
code: fe[0]?.code ?? "FETCH_ERROR",
|
|
19724
|
-
...fe[0]?.retry_after !== void 0 ? { retry_after: fe[0].retry_after } : {}
|
|
19725
|
-
});
|
|
19726
|
-
}
|
|
19727
|
-
return { lead_id: leadId, contacts };
|
|
19728
|
-
} catch (err) {
|
|
19729
|
-
fastPartialFailures.push({
|
|
19730
|
-
lead_id: leadId,
|
|
19731
|
-
code: err?.code ?? "UNKNOWN",
|
|
19732
|
-
...err?._meta?.retry_after !== void 0 ? { retry_after: err._meta.retry_after } : {}
|
|
19733
|
-
});
|
|
19734
|
-
return { lead_id: leadId };
|
|
19735
|
-
}
|
|
19736
|
-
}, STATUS_FETCH_CONCURRENCY);
|
|
19737
|
-
} else {
|
|
19738
|
-
leads2 = record.lead_ids.map((id) => ({ lead_id: id }));
|
|
19739
|
-
}
|
|
19740
|
-
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}`);
|
|
19741
|
-
const isReportRead = !inProgress || includeContacts;
|
|
19742
|
-
const creditsRemaining2 = isReportRead ? await readCreditsRemaining(client, true) : null;
|
|
19197
|
+
let bp = null;
|
|
19198
|
+
let inProgress = null;
|
|
19199
|
+
let launchedAt = null;
|
|
19200
|
+
if (params.notification_id) {
|
|
19201
|
+
const n = await readNotificationById(client, params.notification_id);
|
|
19202
|
+
if (n && inferKind(n) !== "bulk_enrich") {
|
|
19203
|
+
const kind = inferKind(n);
|
|
19743
19204
|
return {
|
|
19744
|
-
|
|
19745
|
-
|
|
19746
|
-
|
|
19747
|
-
|
|
19748
|
-
durability: record.durability,
|
|
19749
|
-
titles: record.titles,
|
|
19750
|
-
email: record.email,
|
|
19751
|
-
phone: record.phone,
|
|
19752
|
-
lens_id: record.lens_id,
|
|
19753
|
-
leads: leads2,
|
|
19754
|
-
overall_progress: {
|
|
19755
|
-
done: bp.success_count + bp.failure_count + bp.quota_hit_count,
|
|
19756
|
-
total: bp.total_count,
|
|
19757
|
-
done_ratio: bp.total_count === 0 ? 0 : (bp.success_count + bp.failure_count + bp.quota_hit_count) / bp.total_count
|
|
19758
|
-
},
|
|
19759
|
-
bulk_progress: bp,
|
|
19760
|
-
in_progress: inProgress,
|
|
19761
|
-
all_done: !inProgress,
|
|
19762
|
-
...fastPartialFailures.length > 0 ? { partial_failures: fastPartialFailures } : {},
|
|
19763
|
-
...isReportRead ? { credits_remaining: creditsRemaining2 } : {},
|
|
19764
|
-
...bp.quota_hit_count > 0 ? {
|
|
19765
|
-
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."
|
|
19766
|
-
} : {}
|
|
19205
|
+
error: true,
|
|
19206
|
+
code: "ENRICH_JOB_WRONG_KIND",
|
|
19207
|
+
message: `That notification_id is a ${kind === "bulk_qualify" ? "lead qualification" : kind === "import" ? "file import" : "non-bulk"} notification, not a contact enrichment`,
|
|
19208
|
+
hint: kind === "bulk_qualify" ? "Poll it with leadbay_qualify_status({notification_id}) instead." : kind === "import" ? "Poll it with leadbay_import_status({importIds}) instead \u2014 the import ids came back from the import launch." : "Pass the notification_id returned by leadbay_enrich_titles."
|
|
19767
19209
|
};
|
|
19768
19210
|
}
|
|
19769
|
-
|
|
19770
|
-
|
|
19771
|
-
|
|
19772
|
-
|
|
19773
|
-
|
|
19774
|
-
try {
|
|
19775
|
-
const out = await getContacts.execute(client, { leadId });
|
|
19776
|
-
const contacts = Array.isArray(out?.contacts) ? out.contacts : [];
|
|
19777
|
-
const wantTitles = new Set((record.titles ?? []).map((t) => t.trim().toLowerCase()));
|
|
19778
|
-
const enrichable = contacts.filter((c) => c && c.enrichment && (wantTitles.size === 0 || typeof c.job_title === "string" && wantTitles.has(c.job_title.trim().toLowerCase())));
|
|
19779
|
-
const channelResolved = (c) => {
|
|
19780
|
-
if (c.enrichment?.done !== true)
|
|
19781
|
-
return false;
|
|
19782
|
-
if (record.email && !c.email)
|
|
19783
|
-
return false;
|
|
19784
|
-
if (record.phone && !c.phone_number)
|
|
19785
|
-
return false;
|
|
19786
|
-
return true;
|
|
19787
|
-
};
|
|
19788
|
-
const done = enrichable.filter(channelResolved).length;
|
|
19789
|
-
const total = enrichable.length;
|
|
19790
|
-
doneSoFar += 1;
|
|
19791
|
-
ctx?.progress?.({
|
|
19792
|
-
progress: doneSoFar,
|
|
19793
|
-
total: totalLeads,
|
|
19794
|
-
message: `Fetched contacts for ${leadId} (${doneSoFar}/${totalLeads})`
|
|
19795
|
-
});
|
|
19796
|
-
return {
|
|
19797
|
-
kind: "ok",
|
|
19798
|
-
lead_id: leadId,
|
|
19799
|
-
done,
|
|
19800
|
-
total,
|
|
19801
|
-
contacts: includeContacts ? contacts : void 0
|
|
19802
|
-
};
|
|
19803
|
-
} catch (err) {
|
|
19804
|
-
doneSoFar += 1;
|
|
19805
|
-
ctx?.progress?.({
|
|
19806
|
-
progress: doneSoFar,
|
|
19807
|
-
total: totalLeads,
|
|
19808
|
-
message: `Fetch failed for ${leadId} (${doneSoFar}/${totalLeads}): ${err?.code ?? "UNKNOWN"}`
|
|
19809
|
-
});
|
|
19211
|
+
if (n) {
|
|
19212
|
+
bp = n.bulk_progress;
|
|
19213
|
+
inProgress = n.in_progress;
|
|
19214
|
+
launchedAt = n.created_at;
|
|
19215
|
+
} else if (leadIds.length === 0) {
|
|
19810
19216
|
return {
|
|
19811
|
-
|
|
19812
|
-
|
|
19813
|
-
|
|
19814
|
-
|
|
19217
|
+
error: true,
|
|
19218
|
+
code: "ENRICH_JOB_NOT_FOUND",
|
|
19219
|
+
message: "That notification_id is not in the recent notification list",
|
|
19220
|
+
hint: "The lookup scans your recent unarchived notifications; an archived job, or one behind many newer ones, will not be found. Re-call with the `lead_ids` the launch returned \u2014 that answers without the notification."
|
|
19815
19221
|
};
|
|
19816
19222
|
}
|
|
19817
|
-
}
|
|
19818
|
-
|
|
19819
|
-
|
|
19820
|
-
|
|
19821
|
-
|
|
19822
|
-
|
|
19823
|
-
|
|
19824
|
-
|
|
19223
|
+
}
|
|
19224
|
+
if (leadIds.length > 0) {
|
|
19225
|
+
const wantTitles = new Set((params.titles ?? []).map((t) => t.trim().toLowerCase()));
|
|
19226
|
+
const channelResolved = (c) => {
|
|
19227
|
+
if (c?.enrichment?.done !== true)
|
|
19228
|
+
return false;
|
|
19229
|
+
if (params.email && !c.email)
|
|
19230
|
+
return false;
|
|
19231
|
+
if (params.phone && !c.phone_number)
|
|
19232
|
+
return false;
|
|
19233
|
+
return true;
|
|
19234
|
+
};
|
|
19235
|
+
let doneSoFar = 0;
|
|
19236
|
+
const totalLeads = leadIds.length;
|
|
19237
|
+
const results = await pMap(leadIds, async (leadId) => {
|
|
19238
|
+
try {
|
|
19239
|
+
const out = await getContacts.execute(client, { leadId });
|
|
19240
|
+
const contacts = Array.isArray(out?.contacts) ? out.contacts : [];
|
|
19241
|
+
const enrichable = contacts.filter((c) => c && c.enrichment && (wantTitles.size === 0 || typeof c.job_title === "string" && wantTitles.has(c.job_title.trim().toLowerCase())));
|
|
19242
|
+
const fe = Array.isArray(out?._fetch_errors) ? out._fetch_errors : [];
|
|
19243
|
+
doneSoFar += 1;
|
|
19244
|
+
ctx?.progress?.({
|
|
19245
|
+
progress: doneSoFar,
|
|
19246
|
+
total: totalLeads,
|
|
19247
|
+
message: `Fetched contacts for ${leadId} (${doneSoFar}/${totalLeads})`
|
|
19248
|
+
});
|
|
19249
|
+
if (fe.length > 0) {
|
|
19250
|
+
return {
|
|
19251
|
+
kind: "fail",
|
|
19252
|
+
lead_id: leadId,
|
|
19253
|
+
code: fe[0]?.code ?? "FETCH_ERROR",
|
|
19254
|
+
...fe[0]?.retry_after !== void 0 ? { retry_after: fe[0].retry_after } : {}
|
|
19255
|
+
};
|
|
19256
|
+
}
|
|
19257
|
+
return {
|
|
19258
|
+
kind: "ok",
|
|
19259
|
+
lead_id: leadId,
|
|
19260
|
+
done: enrichable.filter(channelResolved).length,
|
|
19261
|
+
total: enrichable.length,
|
|
19262
|
+
...includeContacts ? { contacts } : {}
|
|
19263
|
+
};
|
|
19264
|
+
} catch (err) {
|
|
19265
|
+
doneSoFar += 1;
|
|
19266
|
+
ctx?.progress?.({
|
|
19267
|
+
progress: doneSoFar,
|
|
19268
|
+
total: totalLeads,
|
|
19269
|
+
message: `Fetch failed for ${leadId} (${doneSoFar}/${totalLeads}): ${err?.code ?? "UNKNOWN"}`
|
|
19270
|
+
});
|
|
19271
|
+
return {
|
|
19272
|
+
kind: "fail",
|
|
19273
|
+
lead_id: leadId,
|
|
19274
|
+
code: err?.code ?? "UNKNOWN",
|
|
19275
|
+
...err?._meta?.retry_after !== void 0 ? { retry_after: err._meta.retry_after } : {}
|
|
19276
|
+
};
|
|
19277
|
+
}
|
|
19278
|
+
}, STATUS_FETCH_CONCURRENCY);
|
|
19279
|
+
const leads = [];
|
|
19280
|
+
const partialFailures = [];
|
|
19281
|
+
let totalDone = 0;
|
|
19282
|
+
let totalAll = 0;
|
|
19283
|
+
for (const r of results) {
|
|
19284
|
+
if (r.kind === "fail") {
|
|
19285
|
+
partialFailures.push({
|
|
19286
|
+
lead_id: r.lead_id,
|
|
19287
|
+
code: r.code,
|
|
19288
|
+
...r.retry_after !== void 0 ? { retry_after: r.retry_after } : {}
|
|
19289
|
+
});
|
|
19290
|
+
continue;
|
|
19291
|
+
}
|
|
19292
|
+
leads.push({
|
|
19825
19293
|
lead_id: r.lead_id,
|
|
19826
|
-
|
|
19827
|
-
|
|
19294
|
+
...r.contacts ? { contacts: r.contacts } : {},
|
|
19295
|
+
enrichment_progress: { done: r.done, total: r.total }
|
|
19828
19296
|
});
|
|
19829
|
-
|
|
19297
|
+
totalDone += r.done;
|
|
19298
|
+
totalAll += r.total;
|
|
19830
19299
|
}
|
|
19831
|
-
|
|
19832
|
-
|
|
19833
|
-
|
|
19834
|
-
|
|
19835
|
-
|
|
19836
|
-
|
|
19837
|
-
|
|
19300
|
+
const allDone = totalAll > 0 && totalDone === totalAll && partialFailures.length === 0;
|
|
19301
|
+
ctx?.logger?.info?.(`bulk.status leads=${leadIds.length} done=${totalDone}/${totalAll} wall_ms=${Date.now() - startMs}`);
|
|
19302
|
+
const creditsRemaining2 = allDone ? await readCreditsRemaining(client, true) : null;
|
|
19303
|
+
return {
|
|
19304
|
+
...params.notification_id ? { notification_id: params.notification_id } : {},
|
|
19305
|
+
...launchedAt ? { launched_at: launchedAt } : {},
|
|
19306
|
+
status: allDone ? "complete" : "launched",
|
|
19307
|
+
// Echo what was asked for, so the reply states its own scope.
|
|
19308
|
+
...params.titles ? { titles: params.titles } : {},
|
|
19309
|
+
...params.email !== void 0 ? { email: params.email } : {},
|
|
19310
|
+
...params.phone !== void 0 ? { phone: params.phone } : {},
|
|
19311
|
+
leads,
|
|
19312
|
+
overall_progress: {
|
|
19313
|
+
done: totalDone,
|
|
19314
|
+
total: totalAll,
|
|
19315
|
+
done_ratio: totalAll === 0 ? 0 : totalDone / totalAll
|
|
19316
|
+
},
|
|
19317
|
+
...bp ? { bulk_progress: bp } : {},
|
|
19318
|
+
...inProgress !== null ? { in_progress: inProgress } : {},
|
|
19319
|
+
all_done: allDone,
|
|
19320
|
+
...partialFailures.length > 0 ? { partial_failures: partialFailures } : {},
|
|
19321
|
+
...allDone ? { credits_remaining: creditsRemaining2 } : {},
|
|
19322
|
+
...bp && bp.quota_hit_count > 0 ? {
|
|
19323
|
+
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."
|
|
19324
|
+
} : {}
|
|
19325
|
+
};
|
|
19838
19326
|
}
|
|
19839
|
-
|
|
19840
|
-
|
|
19841
|
-
|
|
19842
|
-
|
|
19843
|
-
|
|
19844
|
-
|
|
19845
|
-
|
|
19846
|
-
|
|
19847
|
-
|
|
19848
|
-
creditsRemaining = await readCreditsRemaining(client, true);
|
|
19327
|
+
if (!bp) {
|
|
19328
|
+
return {
|
|
19329
|
+
error: true,
|
|
19330
|
+
code: "ENRICH_JOB_NO_COUNTERS",
|
|
19331
|
+
message: `This enrichment notification carries no per-contact counters; the backend reports it as ${inProgress ? "still running" : "finished"}`,
|
|
19332
|
+
hint: "Re-call with the `lead_ids` returned by leadbay_enrich_titles (plus titles/email/phone) \u2014 that path counts contacts directly and works whether or not the notification has counters.",
|
|
19333
|
+
...inProgress !== null ? { in_progress: inProgress } : {},
|
|
19334
|
+
...launchedAt ? { launched_at: launchedAt } : {}
|
|
19335
|
+
};
|
|
19849
19336
|
}
|
|
19337
|
+
const done = bp.success_count + bp.failure_count + bp.quota_hit_count;
|
|
19338
|
+
const isReportRead = !inProgress;
|
|
19339
|
+
const creditsRemaining = isReportRead ? await readCreditsRemaining(client, true) : null;
|
|
19850
19340
|
return {
|
|
19851
|
-
|
|
19852
|
-
launched_at:
|
|
19853
|
-
status:
|
|
19854
|
-
|
|
19855
|
-
|
|
19856
|
-
|
|
19857
|
-
|
|
19858
|
-
|
|
19859
|
-
|
|
19860
|
-
|
|
19861
|
-
|
|
19862
|
-
|
|
19863
|
-
...
|
|
19341
|
+
notification_id: params.notification_id,
|
|
19342
|
+
launched_at: launchedAt,
|
|
19343
|
+
status: inProgress ? "launched" : "complete",
|
|
19344
|
+
leads: [],
|
|
19345
|
+
overall_progress: {
|
|
19346
|
+
done,
|
|
19347
|
+
total: bp.total_count,
|
|
19348
|
+
done_ratio: bp.total_count === 0 ? 0 : done / bp.total_count
|
|
19349
|
+
},
|
|
19350
|
+
bulk_progress: bp,
|
|
19351
|
+
in_progress: inProgress,
|
|
19352
|
+
all_done: !inProgress,
|
|
19353
|
+
...isReportRead ? { credits_remaining: creditsRemaining } : {},
|
|
19354
|
+
...bp.quota_hit_count > 0 ? {
|
|
19355
|
+
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."
|
|
19356
|
+
} : {}
|
|
19864
19357
|
};
|
|
19865
19358
|
}
|
|
19866
19359
|
};
|
|
@@ -21991,7 +21484,7 @@ var init_artifact_runtime_generated = __esm({
|
|
|
21991
21484
|
"../core/dist/artifact-runtime.generated.js"() {
|
|
21992
21485
|
"use strict";
|
|
21993
21486
|
ARTIFACT_KIT_VERSION = "0.5.0";
|
|
21994
|
-
ARTIFACT_RUNTIME = '"use strict";(()=>{var L=Object.defineProperty;var A=(e,r,t)=>r in e?L(e,r,{enumerable:!0,configurable:!0,writable:!0,value:t}):e[r]=t;var a=(e,r,t)=>A(e,typeof r!="symbol"?r+"":r,t);var S=`\n:root{\n--color-black:#191919;--color-white:#fff;\n--color-gray-1:#f9f9f9;--color-gray-2:#f0f0f0;--color-gray-3:#e0e0e0;--color-gray-4:#cecece;\n--color-gray-5:#c4c4c4;--color-gray-6:#8d8d8d;--color-gray-7:#787878;--color-gray-8:#646464;\n--color-gray-9:#202020;\n--color-linkedin:#0a66c2;\n--color-blue-background:oklch(0.947 0.029 251);--color-blue-foreground:oklch(0.564 0.181 251);\n--color-green-background:oklch(0.947 0.029 141);--color-green-foreground:oklch(0.564 0.181 141);\n--color-red-background:oklch(0.947 0.029 26);--color-red-foreground:oklch(0.564 0.191 26);\n--color-gold-background:oklch(0.972 0.049 91);--color-gold-foreground:oklch(0.667 0.177 91);\n--color-cherry-background:oklch(0.947 0.029 15);--color-cherry-foreground:oklch(0.44 0.146 15);\n--color-red-like:var(--color-cherry-foreground);\n--lb-font:"Nikkei Maru",system-ui,-apple-system,"Segoe UI",Roboto,sans-serif;\n--lb-radius:1rem;--lb-radius-sm:0.625rem;--lb-gap:0.75rem;\n--lb-surface:var(--color-gray-1);--lb-border:var(--color-gray-3);\n--lb-fg:var(--color-black);--lb-muted:var(--color-gray-8);--lb-field:var(--color-white);\n}\n:root[data-theme=dark],:root[data-lb-theme=dark]{\n--lb-surface:var(--color-gray-9);--lb-border:var(--color-gray-8);\n--lb-fg:var(--color-white);--lb-muted:var(--color-gray-3);--lb-field:var(--color-gray-9);\n}\n@media(prefers-color-scheme:dark){:root:not([data-theme=light]):not([data-lb-theme=light]){\n--lb-surface:var(--color-gray-9);--lb-border:var(--color-gray-8);\n--lb-fg:var(--color-white);--lb-muted:var(--color-gray-3);--lb-field:var(--color-gray-9);\n}}\n.lb-card{display:grid;gap:var(--lb-gap);padding:0.875rem;\nbackground-color:var(--lb-surface);border:1px solid var(--lb-border);\nborder-radius:var(--lb-radius);corner-shape:squircle;color:var(--lb-fg);\nfont-family:var(--lb-font);\nbox-shadow:0 1rem 2.5rem color-mix(in srgb,var(--color-gray-9) 6%,transparent),\n0 0.125rem 0.5rem color-mix(in srgb,var(--color-gray-9) 4%,transparent)}\n.lb-card-head{display:flex;justify-content:space-between;align-items:baseline;gap:var(--lb-gap)}\n.lb-title{font-size:0.875rem;font-weight:600;line-height:1.25rem;color:var(--lb-fg)}\n.lb-sub{font-size:0.8125rem;line-height:1.125rem;color:var(--lb-muted)}\n.lb-row{display:flex;align-items:center;gap:var(--lb-gap);flex-wrap:wrap}\n.lb-stack{display:grid;gap:var(--lb-gap)}\n.lb-select,.lb-input{font:inherit;font-family:var(--lb-font);font-size:0.8125rem;color:var(--lb-fg);\nbackground-color:var(--lb-field);border:1px solid var(--lb-border);\nborder-radius:var(--lb-radius-sm);corner-shape:squircle;padding:0.4rem 0.55rem;min-height:2.125rem}\n.lb-btn{font:inherit;font-family:var(--lb-font);font-size:0.8125rem;font-weight:600;\ncolor:var(--lb-fg);background-color:var(--lb-field);border:1px solid var(--lb-border);\nborder-radius:var(--lb-radius-sm);corner-shape:squircle;padding:0.4rem 0.85rem;min-height:2.125rem;\ncursor:pointer;transition:background-color .15s,border-color .15s,color .15s}\n.lb-btn:hover:not([disabled]){border-color:var(--color-gray-6)}\n.lb-btn:focus-visible,.lb-select:focus-visible,.lb-input:focus-visible{\noutline:2px solid var(--color-blue-foreground);outline-offset:1px}\n.lb-btn[data-lb-state=loading]{opacity:.55;cursor:progress}\n.lb-btn[data-lb-state=success]{background-color:var(--color-green-background);\nborder-color:var(--color-green-foreground);color:var(--color-green-foreground)}\n.lb-btn[data-lb-state=error],.lb-select[data-lb-state=error]{\nbackground-color:var(--color-red-background);border-color:var(--color-red-foreground);\ncolor:var(--color-red-foreground)}\n.lb-btn[data-lb-state=unavailable],.lb-btn[disabled]{opacity:.5;cursor:not-allowed}\n.lb-msg{font-size:0.8125rem;line-height:1.125rem;color:var(--lb-muted)}\n.lb-msg[data-tone=error]{color:var(--color-red-foreground)}\n.lb-msg[data-tone=ok]{color:var(--color-green-foreground)}\n.lb-chip{display:inline-flex;align-items:center;gap:.25rem;white-space:nowrap;\nfont-size:0.75rem;font-weight:600;line-height:1rem;padding:0.125rem 0.5rem;\nborder-radius:var(--lb-radius-sm);corner-shape:squircle;\nbackground-color:var(--color-gray-2);color:var(--lb-muted)}\n.lb-chip[data-status=WANTED]{background-color:var(--color-blue-background);color:var(--color-blue-foreground)}\n.lb-chip[data-status=WON]{background-color:var(--color-green-background);color:var(--color-green-foreground)}\n.lb-chip[data-status=LOST]{background-color:var(--color-red-background);color:var(--color-red-foreground)}\n.lb-chip[data-status=UNWANTED]{background-color:var(--color-gray-2);color:var(--color-gray-7)}\n.lb-chip[data-taste=liked]{background-color:var(--color-cherry-background);color:var(--color-red-like)}\n.lb-chip[data-taste=disliked]{background-color:var(--color-gray-2);color:var(--color-gray-7)}\n.lb-chips{display:flex;align-items:center;gap:.35rem;flex-wrap:wrap}\n.lb-chip[hidden]{display:none}\n.lb-table{width:100%;border-collapse:collapse;font-family:var(--lb-font);color:var(--lb-fg)}\n.lb-table th,.lb-table td{text-align:left;padding:0.5rem 0.4rem;\nborder-bottom:1px solid var(--lb-border);vertical-align:middle;font-size:0.8125rem}\n.lb-table th{font-size:0.75rem;font-weight:600;color:var(--lb-muted);\ntext-transform:uppercase;letter-spacing:.04em}\n.lb-link{color:var(--color-blue-foreground);text-decoration:none}\n.lb-link:hover{text-decoration:underline}\n/* Quiet text link out of the artifact. Button-height so it shares the row\'s\n baseline; understated so it never competes with the actions beside it. The\n arrow is a bare diagonal stroke \\u2014 an escape-hatch marker, not an icon that\n asks to be read. */\n.lb-link-out{display:inline-flex;align-items:center;gap:.3rem;\nfont-size:0.75rem;line-height:1rem;min-height:2.125rem;\ncolor:var(--lb-fg);text-decoration:none;opacity:.65;transition:opacity .15s}\n.lb-link-out:hover{opacity:1;text-decoration:underline}\n.lb-link-out:focus-visible{outline:2px solid var(--color-blue-foreground);outline-offset:1px;\nborder-radius:var(--lb-radius-sm)}\n.lb-link-out svg{width:.85em;height:.85em;flex-shrink:0}\n/* Pushes whatever follows it to the right edge of an .lb-row, so a trailing\n link sits on the SAME baseline as the row\'s buttons instead of on its own\n line below them. */\n.lb-spacer{flex:1 1 auto}\n.lb-spinner{display:inline-block;width:.7em;height:.7em;border:2px solid var(--lb-border);\nborder-top-color:var(--color-blue-foreground);border-radius:50%;animation:lb-spin .8s linear infinite}\n@keyframes lb-spin{to{transform:rotate(1turn)}}\n@media(prefers-reduced-motion:reduce){.lb-spinner{animation:none}\n.lb-btn{transition-property:none}}\n`,p="lb-styles";var x="0.5.0",c=class extends Error{constructor(t,o={}){super(t);a(this,"code");a(this,"raw");this.name="LbError",this.code=o.code,this.raw=o.raw}},y=null,f=3e4;function C(){let e=globalThis.cowork;return e&&typeof e.callMcpTool=="function"?(r,t)=>e.callMcpTool(r,t):null}function _(e){if(e&&typeof e=="object"&&"content"in e){let r=e.content;if(Array.isArray(r)&&r[0]&&typeof r[0].text=="string")return r[0].text}return null}function E(e){if(!e||typeof e!="object")return e;let r=e;if(r.isError)throw new c(_(e)??"tool call failed",{raw:e});if("structuredContent"in r&&r.structuredContent!=null)return r.structuredContent;let t=_(e);if(t!=null)try{return JSON.parse(t)}catch{return t}return e}function I(e){return e instanceof Error?e.message:String(e)}function v(e){let r=e instanceof c?e.code:void 0;return{message:I(e),unavailable:r==="unavailable",code:r}}function O(e={}){y=e.call??null,f=e.timeoutMs??3e4}function R(){if(typeof document>"u"||!document.head)return null;let e=document.getElementById(p);if(e)return e;let r=document.createElement("style");return r.id=p,r.textContent=S,document.head.appendChild(r),r}async function T(e,r){if(!f||f<=0)return e;let t,o=new Promise((n,i)=>{t=setTimeout(()=>i(new c(`"${r}" timed out after ${f}ms`,{code:"timeout"})),f)});try{return await Promise.race([e,o])}finally{t&&clearTimeout(t)}}async function l(e,r={}){if(y)return E(await T(Promise.resolve(y(e,r)),e));let t=C();if(!t)throw new c("Leadbay bridge unavailable (window.cowork absent)",{code:"unavailable"});return E(await T(Promise.resolve(t(e,r)),e))}var g=class{constructor(){a(this,"subs",new Set)}subscribe(r){return this.subs.add(r),r(this),()=>this.subs.delete(r)}emit(){for(let r of this.subs)r(this)}};function N(e){return Array.isArray(e)?e.map(r=>r&&typeof r=="object"?r:{value:r,label:String(r)}):[]}var b=class extends g{constructor(t={}){super();a(this,"kind");a(this,"value");a(this,"options",[]);a(this,"loading",!1);a(this,"error",null);a(this,"ready",!1);a(this,"cfg");a(this,"depUnsubs",[]);a(this,"seq",0);this.cfg=t,this.kind=t.kind,this.value=t.value??"";for(let o of t.dependsOn??[]){let n=o.value;this.depUnsubs.push(o.subscribe(()=>{o.value!==n&&(n=o.value,this.cfg.load&&this.load())}))}t.load&&(t.autoLoad??!0)&&this.load()}async load(){if(!this.cfg.load)return;let t=++this.seq;this.loading=!0,this.error=null,this.emit();try{let o=await this.cfg.load();if(t!==this.seq)return;this.options=this.cfg.options?this.cfg.options(o):N(o),this.ready=!0;let n=this.value==null?"":String(this.value);this.options.length&&(n===""||!this.options.some(i=>String(i.value)===n))&&(this.value=this.options[0].value)}catch(o){if(t!==this.seq)return;this.options=[],this.error=v(o)}finally{t===this.seq&&(this.loading=!1,this.emit())}}setValue(t){this.value=t;let o=this.validate();this.error=o?{message:o,unavailable:!1}:null,this.emit()}validate(){return this.cfg.validate?this.cfg.validate(this.value):null}get valid(){return this.validate()==null}reset(){this.value=this.cfg.value??"",this.error=null,this.emit()}dispose(){for(let t of this.depUnsubs)t();this.depUnsubs=[]}};function P(e){if(!e||typeof e!="object")return null;let r=e;if(r.error!==!0)return null;let t=typeof r.message=="string"&&r.message?r.message:"tool call failed",o=typeof r.hint=="string"&&r.hint?` \\u2014 ${r.hint}`:"";return`${t}${o}`}var s=class extends g{constructor(t){super();a(this,"loading",!1);a(this,"error",null);a(this,"lastResult",null);a(this,"cfg");this.cfg=t}async run(){if(this.loading)return;for(let n of this.cfg.fields??[]){let i=n.validate();if(i!=null){this.error={message:i,unavailable:!1},this.emit();return}}if(this.cfg.confirm&&typeof globalThis.confirm=="function"&&!globalThis.confirm(this.cfg.confirm))return;this.loading=!0,this.error=null,this.emit();let t;try{let n=typeof this.cfg.args=="function"?this.cfg.args():this.cfg.args??{};t=await l(this.cfg.tool,n)}catch(n){this.error=v(n),this.loading=!1,this.emit(),this.cfg.onError?.(this.error);return}let o=P(t)??this.cfg.checkResult?.(t)??null;if(o!=null){this.error={message:o,unavailable:!1},this.loading=!1,this.emit(),this.cfg.onError?.(this.error);return}return this.lastResult=t,this.loading=!1,this.emit(),this.cfg.onSuccess?.(t),t}reset(){this.error=null,this.lastResult=null,this.emit()}},d=class extends g{constructor(t){super();a(this,"data",null);a(this,"loading",!1);a(this,"refreshing",!1);a(this,"error",null);a(this,"done",!1);a(this,"cfg");a(this,"timer",null);a(this,"seq",0);this.cfg=t,(t.autoLoad??!0)&&this.load()}async load(){this.clearTimer();let t=++this.seq;this.data==null?this.loading=!0:this.refreshing=!0,this.error=null,this.emit();try{let n=await this.cfg.load();if(t!==this.seq)return;this.data=n,this.done=this.cfg.until?this.cfg.until(n):!0,this.cfg.pollEvery&&!this.done&&(this.timer=setTimeout(()=>void this.load(),this.cfg.pollEvery))}catch(n){if(t!==this.seq)return;this.error=v(n)}finally{t===this.seq&&(this.loading=!1,this.refreshing=!1,this.emit())}}refresh(){return this.load()}stop(){this.clearTimer()}clearTimer(){this.timer&&(clearTimeout(this.timer),this.timer=null)}},h=class extends g{constructor(t){super();a(this,"items",[]);a(this,"page",0);a(this,"pageSize");a(this,"total",0);a(this,"loading",!1);a(this,"error",null);a(this,"cfg");a(this,"seq",0);this.cfg=t,this.pageSize=t.pageSize??20,(t.autoLoad??!0)&&this.loadPage(0)}async loadPage(t){let o=++this.seq;this.loading=!0,this.error=null,this.emit();try{let n=await this.cfg.load({page:t,pageSize:this.pageSize});if(o!==this.seq)return;this.items=n.items??[],this.total=n.total??this.items.length,this.page=t}catch(n){if(o!==this.seq)return;this.error=v(n)}finally{o===this.seq&&(this.loading=!1,this.emit())}}next(){return this.loadPage(this.page+1)}prev(){return this.loadPage(Math.max(0,this.page-1))}get hasMore(){return(this.page+1)*this.pageSize<this.total}};function M(e,r){let t=r.error?.unavailable?"unavailable":r.loading?"loading":r.error?"error":"ready";e.setAttribute("data-lb-state",t),r.error?e.setAttribute("data-lb-error",r.error.message):e.removeAttribute("data-lb-error")}function D(e,r){let t=()=>r.setValue(e.value);e.addEventListener("change",t);let o=r.subscribe(()=>{M(e,r),e.disabled=r.loading,e.innerHTML="";for(let n of r.options){let i=document.createElement("option");i.value=String(n.value),i.textContent=n.label,e.appendChild(i)}e.value=r.value==null?"":String(r.value)});return()=>{e.removeEventListener("change",t),o()}}function z(e,r){let t=e.type==="checkbox",o=e.tagName==="SELECT"?"change":"input",n=()=>r.setValue(t?e.checked:e.value);e.addEventListener(o,n);let i=r.subscribe(()=>{if(t)e.checked=!!r.value;else{let u=r.value==null?"":String(r.value);e.value!==u&&(e.value=u)}e.setAttribute("data-lb-state",r.error?"error":"ready"),r.error?e.setAttribute("data-lb-error",r.error.message):e.removeAttribute("data-lb-error")});return()=>{e.removeEventListener(o,n),i()}}function F(e,r){let t=n=>{n.preventDefault(),r.run()};e.addEventListener("click",t);let o=r.subscribe(()=>{let n=r.error?.unavailable?"unavailable":r.loading?"loading":r.error?"error":r.lastResult!=null?"success":"idle";e.setAttribute("data-lb-state",n),"disabled"in e&&(e.disabled=r.loading),r.error?e.setAttribute("data-lb-error",r.error.message):e.removeAttribute("data-lb-error")});return()=>{e.removeEventListener("click",t),o()}}var q=["STILL_CHASING","COULD_NOT_REACH_STILL_TRYING","INTEREST_VALIDATED_OR_MEETING_PLANED","NOT_INTERESTED_LOST"],k=[{value:"",label:"Default ranking"},{value:"SCORE:DESC",label:"Score \\u2193"},{value:"SCORE:ASC",label:"Score \\u2191"},{value:"NAME:ASC",label:"Name A\\u2192Z"},{value:"NAME:DESC",label:"Name Z\\u2192A"},{value:"SIZE:DESC",label:"Size \\u2193"},{value:"SIZE:ASC",label:"Size \\u2191"},{value:"SECTOR:ASC",label:"Sector A\\u2192Z"},{value:"STATUS:ASC",label:"Status A\\u2192Z"},{value:"CONTACT_COUNT:DESC",label:"Contacts \\u2193"},{value:"LAST_PROSPECTING_ACTION_AT:DESC",label:"Last action \\u2193"},{value:"LAST_PROSPECTING_ACTION_AT:ASC",label:"Last action \\u2191"},{value:"EPILOGUE_STATUS_SET_AT:DESC",label:"Outcome set \\u2193"},{value:"LIKED:DESC",label:"Liked first"},{value:"DISLIKED:DESC",label:"Disliked first"}];function U(e){let r=String(e??"").trim().toUpperCase(),t=k.some(o=>o.value===r);return new b({kind:"select",value:t?r:"",load:async()=>k.slice()})}var m=[{value:"WANTED",label:"Wanted"},{value:"WON",label:"Won"},{value:"LOST",label:"Lost"},{value:"UNWANTED",label:"Unwanted"}],H={value:"",label:"\\u2014 Not set \\u2014"};function W(e){let r=String(e??"").trim().toUpperCase(),t=m.some(o=>o.value===r);return new b({kind:"select",value:t?r:"",validate:o=>String(o??"")===""?"Pick a status":null,load:async()=>t?m.slice():[H,...m]})}function $(e){let r=()=>{let t=typeof e.leadIds=="function"?e.leadIds():e.leadIds;return Array.isArray(t)?t:e.leadId?[e.leadId]:[]};return new s({tool:"leadbay_set_lead_status",fields:e.date?[e.status,e.date]:[e.status],confirm:e.confirm,args:()=>({lead_ids:r(),status:e.status.value,...e.date&&e.date.value?{status_date:e.date.value}:{},...e.ask?{_triggered_by:e.ask}:{}}),checkResult:t=>{let o=t?.failed;if(!Array.isArray(o)||o.length===0)return null;let n=r().length,i=o[0]?.message??"write rejected";return o.length===n?`Status not applied: ${i}`:`${o.length} of ${n} leads failed: ${i}`}})}function j(e){return new b({kind:"select",load:()=>l("leadbay_list_campaigns",{_triggered_by:e}),options:r=>(r?.campaigns??[]).map(o=>{let n=o?.campaign??o;return n?.id?{value:n.id,label:n.name??n.ai_generated_name??String(n.id)}:null}).filter(o=>o!=null)})}function V(e){return new s({tool:"leadbay_report_outreach",fields:e.note?[e.note]:[],args:()=>({lead_id:e.leadId,...e.status?{epilogue_status:e.status.value}:{},note:e.note?e.note.value:"",verification:{source:"user_confirmed",ref:e.ref??"logged from artifact"},_triggered_by:e.ask})})}function G(e){return new s({tool:"leadbay_add_note",fields:[e.note],args:()=>({leadId:e.leadId,note:e.note.value})})}function Z(e){return new s({tool:"leadbay_like_lead",args:{lead_id:e}})}function Y(e){return new s({tool:"leadbay_dislike_lead",args:{lead_id:e}})}function B(e,r){return new d({autoLoad:!1,load:()=>l("leadbay_account_history",{leadId:e,_triggered_by:r})})}function K(e,r){return new d({autoLoad:!1,load:()=>l("leadbay_research_lead_by_id",{leadId:e,_triggered_by:r})})}function J(e){let r=null;return new d({...e.autoLoad!==void 0?{autoLoad:e.autoLoad}:{},pollEvery:e.pollEvery??4e3,until:t=>!!t?.all_done,load:async()=>{if(!r){let t=await l("leadbay_enrich_titles",{...e.leadIds?{leadIds:e.leadIds}:{},titles:e.titles,...e.email!==void 0?{email:e.email}:{},...e.phone!==void 0?{phone:e.phone}:{},...e.confirm!==void 0?{confirm:e.confirm}:{},_triggered_by:e.ask});if(r=t?.bulk_id??null,!r)return{...t,all_done:!0,no_job:!0}}return l("leadbay_bulk_enrich_status",{bulk_id:r,_triggered_by:e.ask})}})}function Q(e){let r=()=>typeof e.order=="string"?e.order:String(e.order?.value??"");return new h({pageSize:e.pageSize??20,load:async({page:t,pageSize:o})=>{let n=await l("leadbay_pull_leads",{page:t,count:o,...e.lensId?{lensId:e.lensId}:{},...r()?{order:r()}:{},_triggered_by:e.ask}),i=n.leads??[];return{items:i,total:n.pagination?.total??i.length}}})}function X(e){let r=e.source??"followups",t=()=>typeof e.order=="string"?e.order:String(e.order?.value??"");return new h({pageSize:e.pageSize??20,load:async({page:o,pageSize:n})=>{let u=r==="campaign"?await l("leadbay_campaign_call_sheet",{campaign_id:e.campaignId,page:o,count:n,_triggered_by:e.ask}):await l("leadbay_pull_followups",{page:o,count:n,...e.city?{city:e.city}:{},...t()?{order:t()}:{},_triggered_by:e.ask}),w=u.leads??u.items??[];return{items:w,total:u.total_leads??u.pagination?.total??w.length}}})}function ee(e){return new d({load:()=>l("leadbay_team_activity",{weeks:e.weeks??4,_triggered_by:e.ask})})}var re={VERSION:x,configure:O,styles:R,call:l,field:e=>new b(e),action:e=>new s(e),resource:e=>new d(e),list:e=>new h(e),bindSelect:D,bindValue:z,bindAction:F,campaigns:j,outreach:V,note:G,like:Z,dislike:Y,leadStatus:W,setStatus:$,sortOrder:U,leadHistory:B,leadProfile:K,enrichment:J,callList:X,leadList:Q,teamActivity:ee,EPILOGUE_STATUSES:q,LEAD_STATUSES:m,SORT_ORDERS:k};typeof globalThis<"u"&&(globalThis.LeadbayArtifacts=re);})();';
|
|
21487
|
+
ARTIFACT_RUNTIME = '"use strict";(()=>{var L=Object.defineProperty;var A=(e,r,t)=>r in e?L(e,r,{enumerable:!0,configurable:!0,writable:!0,value:t}):e[r]=t;var a=(e,r,t)=>A(e,typeof r!="symbol"?r+"":r,t);var _=`\n:root{\n--color-black:#191919;--color-white:#fff;\n--color-gray-1:#f9f9f9;--color-gray-2:#f0f0f0;--color-gray-3:#e0e0e0;--color-gray-4:#cecece;\n--color-gray-5:#c4c4c4;--color-gray-6:#8d8d8d;--color-gray-7:#787878;--color-gray-8:#646464;\n--color-gray-9:#202020;\n--color-linkedin:#0a66c2;\n--color-blue-background:oklch(0.947 0.029 251);--color-blue-foreground:oklch(0.564 0.181 251);\n--color-green-background:oklch(0.947 0.029 141);--color-green-foreground:oklch(0.564 0.181 141);\n--color-red-background:oklch(0.947 0.029 26);--color-red-foreground:oklch(0.564 0.191 26);\n--color-gold-background:oklch(0.972 0.049 91);--color-gold-foreground:oklch(0.667 0.177 91);\n--color-cherry-background:oklch(0.947 0.029 15);--color-cherry-foreground:oklch(0.44 0.146 15);\n--color-red-like:var(--color-cherry-foreground);\n--lb-font:"Nikkei Maru",system-ui,-apple-system,"Segoe UI",Roboto,sans-serif;\n--lb-radius:1rem;--lb-radius-sm:0.625rem;--lb-gap:0.75rem;\n--lb-surface:var(--color-gray-1);--lb-border:var(--color-gray-3);\n--lb-fg:var(--color-black);--lb-muted:var(--color-gray-8);--lb-field:var(--color-white);\n}\n:root[data-theme=dark],:root[data-lb-theme=dark]{\n--lb-surface:var(--color-gray-9);--lb-border:var(--color-gray-8);\n--lb-fg:var(--color-white);--lb-muted:var(--color-gray-3);--lb-field:var(--color-gray-9);\n}\n@media(prefers-color-scheme:dark){:root:not([data-theme=light]):not([data-lb-theme=light]){\n--lb-surface:var(--color-gray-9);--lb-border:var(--color-gray-8);\n--lb-fg:var(--color-white);--lb-muted:var(--color-gray-3);--lb-field:var(--color-gray-9);\n}}\n.lb-card{display:grid;gap:var(--lb-gap);padding:0.875rem;\nbackground-color:var(--lb-surface);border:1px solid var(--lb-border);\nborder-radius:var(--lb-radius);corner-shape:squircle;color:var(--lb-fg);\nfont-family:var(--lb-font);\nbox-shadow:0 1rem 2.5rem color-mix(in srgb,var(--color-gray-9) 6%,transparent),\n0 0.125rem 0.5rem color-mix(in srgb,var(--color-gray-9) 4%,transparent)}\n.lb-card-head{display:flex;justify-content:space-between;align-items:baseline;gap:var(--lb-gap)}\n.lb-title{font-size:0.875rem;font-weight:600;line-height:1.25rem;color:var(--lb-fg)}\n.lb-sub{font-size:0.8125rem;line-height:1.125rem;color:var(--lb-muted)}\n.lb-row{display:flex;align-items:center;gap:var(--lb-gap);flex-wrap:wrap}\n.lb-stack{display:grid;gap:var(--lb-gap)}\n.lb-select,.lb-input{font:inherit;font-family:var(--lb-font);font-size:0.8125rem;color:var(--lb-fg);\nbackground-color:var(--lb-field);border:1px solid var(--lb-border);\nborder-radius:var(--lb-radius-sm);corner-shape:squircle;padding:0.4rem 0.55rem;min-height:2.125rem}\n.lb-btn{font:inherit;font-family:var(--lb-font);font-size:0.8125rem;font-weight:600;\ncolor:var(--lb-fg);background-color:var(--lb-field);border:1px solid var(--lb-border);\nborder-radius:var(--lb-radius-sm);corner-shape:squircle;padding:0.4rem 0.85rem;min-height:2.125rem;\ncursor:pointer;transition:background-color .15s,border-color .15s,color .15s}\n.lb-btn:hover:not([disabled]){border-color:var(--color-gray-6)}\n.lb-btn:focus-visible,.lb-select:focus-visible,.lb-input:focus-visible{\noutline:2px solid var(--color-blue-foreground);outline-offset:1px}\n.lb-btn[data-lb-state=loading]{opacity:.55;cursor:progress}\n.lb-btn[data-lb-state=success]{background-color:var(--color-green-background);\nborder-color:var(--color-green-foreground);color:var(--color-green-foreground)}\n.lb-btn[data-lb-state=error],.lb-select[data-lb-state=error]{\nbackground-color:var(--color-red-background);border-color:var(--color-red-foreground);\ncolor:var(--color-red-foreground)}\n.lb-btn[data-lb-state=unavailable],.lb-btn[disabled]{opacity:.5;cursor:not-allowed}\n.lb-msg{font-size:0.8125rem;line-height:1.125rem;color:var(--lb-muted)}\n.lb-msg[data-tone=error]{color:var(--color-red-foreground)}\n.lb-msg[data-tone=ok]{color:var(--color-green-foreground)}\n.lb-chip{display:inline-flex;align-items:center;gap:.25rem;white-space:nowrap;\nfont-size:0.75rem;font-weight:600;line-height:1rem;padding:0.125rem 0.5rem;\nborder-radius:var(--lb-radius-sm);corner-shape:squircle;\nbackground-color:var(--color-gray-2);color:var(--lb-muted)}\n.lb-chip[data-status=WANTED]{background-color:var(--color-blue-background);color:var(--color-blue-foreground)}\n.lb-chip[data-status=WON]{background-color:var(--color-green-background);color:var(--color-green-foreground)}\n.lb-chip[data-status=LOST]{background-color:var(--color-red-background);color:var(--color-red-foreground)}\n.lb-chip[data-status=UNWANTED]{background-color:var(--color-gray-2);color:var(--color-gray-7)}\n.lb-chip[data-taste=liked]{background-color:var(--color-cherry-background);color:var(--color-red-like)}\n.lb-chip[data-taste=disliked]{background-color:var(--color-gray-2);color:var(--color-gray-7)}\n.lb-chips{display:flex;align-items:center;gap:.35rem;flex-wrap:wrap}\n.lb-chip[hidden]{display:none}\n.lb-table{width:100%;border-collapse:collapse;font-family:var(--lb-font);color:var(--lb-fg)}\n.lb-table th,.lb-table td{text-align:left;padding:0.5rem 0.4rem;\nborder-bottom:1px solid var(--lb-border);vertical-align:middle;font-size:0.8125rem}\n.lb-table th{font-size:0.75rem;font-weight:600;color:var(--lb-muted);\ntext-transform:uppercase;letter-spacing:.04em}\n.lb-link{color:var(--color-blue-foreground);text-decoration:none}\n.lb-link:hover{text-decoration:underline}\n/* Quiet text link out of the artifact. Button-height so it shares the row\'s\n baseline; understated so it never competes with the actions beside it. The\n arrow is a bare diagonal stroke \\u2014 an escape-hatch marker, not an icon that\n asks to be read. */\n.lb-link-out{display:inline-flex;align-items:center;gap:.3rem;\nfont-size:0.75rem;line-height:1rem;min-height:2.125rem;\ncolor:var(--lb-fg);text-decoration:none;opacity:.65;transition:opacity .15s}\n.lb-link-out:hover{opacity:1;text-decoration:underline}\n.lb-link-out:focus-visible{outline:2px solid var(--color-blue-foreground);outline-offset:1px;\nborder-radius:var(--lb-radius-sm)}\n.lb-link-out svg{width:.85em;height:.85em;flex-shrink:0}\n/* Pushes whatever follows it to the right edge of an .lb-row, so a trailing\n link sits on the SAME baseline as the row\'s buttons instead of on its own\n line below them. */\n.lb-spacer{flex:1 1 auto}\n.lb-spinner{display:inline-block;width:.7em;height:.7em;border:2px solid var(--lb-border);\nborder-top-color:var(--color-blue-foreground);border-radius:50%;animation:lb-spin .8s linear infinite}\n@keyframes lb-spin{to{transform:rotate(1turn)}}\n@media(prefers-reduced-motion:reduce){.lb-spinner{animation:none}\n.lb-btn{transition-property:none}}\n`,p="lb-styles";var x="0.5.0",c=class extends Error{constructor(t,o={}){super(t);a(this,"code");a(this,"raw");this.name="LbError",this.code=o.code,this.raw=o.raw}},y=null,f=3e4;function C(){let e=globalThis.cowork;return e&&typeof e.callMcpTool=="function"?(r,t)=>e.callMcpTool(r,t):null}function S(e){if(e&&typeof e=="object"&&"content"in e){let r=e.content;if(Array.isArray(r)&&r[0]&&typeof r[0].text=="string")return r[0].text}return null}function E(e){if(!e||typeof e!="object")return e;let r=e;if(r.isError)throw new c(S(e)??"tool call failed",{raw:e});if("structuredContent"in r&&r.structuredContent!=null)return r.structuredContent;let t=S(e);if(t!=null)try{return JSON.parse(t)}catch{return t}return e}function I(e){return e instanceof Error?e.message:String(e)}function v(e){let r=e instanceof c?e.code:void 0;return{message:I(e),unavailable:r==="unavailable",code:r}}function O(e={}){y=e.call??null,f=e.timeoutMs??3e4}function R(){if(typeof document>"u"||!document.head)return null;let e=document.getElementById(p);if(e)return e;let r=document.createElement("style");return r.id=p,r.textContent=_,document.head.appendChild(r),r}async function T(e,r){if(!f||f<=0)return e;let t,o=new Promise((n,i)=>{t=setTimeout(()=>i(new c(`"${r}" timed out after ${f}ms`,{code:"timeout"})),f)});try{return await Promise.race([e,o])}finally{t&&clearTimeout(t)}}async function l(e,r={}){if(y)return E(await T(Promise.resolve(y(e,r)),e));let t=C();if(!t)throw new c("Leadbay bridge unavailable (window.cowork absent)",{code:"unavailable"});return E(await T(Promise.resolve(t(e,r)),e))}var g=class{constructor(){a(this,"subs",new Set)}subscribe(r){return this.subs.add(r),r(this),()=>this.subs.delete(r)}emit(){for(let r of this.subs)r(this)}};function N(e){return Array.isArray(e)?e.map(r=>r&&typeof r=="object"?r:{value:r,label:String(r)}):[]}var b=class extends g{constructor(t={}){super();a(this,"kind");a(this,"value");a(this,"options",[]);a(this,"loading",!1);a(this,"error",null);a(this,"ready",!1);a(this,"cfg");a(this,"depUnsubs",[]);a(this,"seq",0);this.cfg=t,this.kind=t.kind,this.value=t.value??"";for(let o of t.dependsOn??[]){let n=o.value;this.depUnsubs.push(o.subscribe(()=>{o.value!==n&&(n=o.value,this.cfg.load&&this.load())}))}t.load&&(t.autoLoad??!0)&&this.load()}async load(){if(!this.cfg.load)return;let t=++this.seq;this.loading=!0,this.error=null,this.emit();try{let o=await this.cfg.load();if(t!==this.seq)return;this.options=this.cfg.options?this.cfg.options(o):N(o),this.ready=!0;let n=this.value==null?"":String(this.value);this.options.length&&(n===""||!this.options.some(i=>String(i.value)===n))&&(this.value=this.options[0].value)}catch(o){if(t!==this.seq)return;this.options=[],this.error=v(o)}finally{t===this.seq&&(this.loading=!1,this.emit())}}setValue(t){this.value=t;let o=this.validate();this.error=o?{message:o,unavailable:!1}:null,this.emit()}validate(){return this.cfg.validate?this.cfg.validate(this.value):null}get valid(){return this.validate()==null}reset(){this.value=this.cfg.value??"",this.error=null,this.emit()}dispose(){for(let t of this.depUnsubs)t();this.depUnsubs=[]}};function P(e){if(!e||typeof e!="object")return null;let r=e;if(r.error!==!0)return null;let t=typeof r.message=="string"&&r.message?r.message:"tool call failed",o=typeof r.hint=="string"&&r.hint?` \\u2014 ${r.hint}`:"";return`${t}${o}`}var s=class extends g{constructor(t){super();a(this,"loading",!1);a(this,"error",null);a(this,"lastResult",null);a(this,"cfg");this.cfg=t}async run(){if(this.loading)return;for(let n of this.cfg.fields??[]){let i=n.validate();if(i!=null){this.error={message:i,unavailable:!1},this.emit();return}}if(this.cfg.confirm&&typeof globalThis.confirm=="function"&&!globalThis.confirm(this.cfg.confirm))return;this.loading=!0,this.error=null,this.emit();let t;try{let n=typeof this.cfg.args=="function"?this.cfg.args():this.cfg.args??{};t=await l(this.cfg.tool,n)}catch(n){this.error=v(n),this.loading=!1,this.emit(),this.cfg.onError?.(this.error);return}let o=P(t)??this.cfg.checkResult?.(t)??null;if(o!=null){this.error={message:o,unavailable:!1},this.loading=!1,this.emit(),this.cfg.onError?.(this.error);return}return this.lastResult=t,this.loading=!1,this.emit(),this.cfg.onSuccess?.(t),t}reset(){this.error=null,this.lastResult=null,this.emit()}},u=class extends g{constructor(t){super();a(this,"data",null);a(this,"loading",!1);a(this,"refreshing",!1);a(this,"error",null);a(this,"done",!1);a(this,"cfg");a(this,"timer",null);a(this,"seq",0);this.cfg=t,(t.autoLoad??!0)&&this.load()}async load(){this.clearTimer();let t=++this.seq;this.data==null?this.loading=!0:this.refreshing=!0,this.error=null,this.emit();try{let n=await this.cfg.load();if(t!==this.seq)return;this.data=n,this.done=this.cfg.until?this.cfg.until(n):!0,this.cfg.pollEvery&&!this.done&&(this.timer=setTimeout(()=>void this.load(),this.cfg.pollEvery))}catch(n){if(t!==this.seq)return;this.error=v(n)}finally{t===this.seq&&(this.loading=!1,this.refreshing=!1,this.emit())}}refresh(){return this.load()}stop(){this.clearTimer()}clearTimer(){this.timer&&(clearTimeout(this.timer),this.timer=null)}},h=class extends g{constructor(t){super();a(this,"items",[]);a(this,"page",0);a(this,"pageSize");a(this,"total",0);a(this,"loading",!1);a(this,"error",null);a(this,"cfg");a(this,"seq",0);this.cfg=t,this.pageSize=t.pageSize??20,(t.autoLoad??!0)&&this.loadPage(0)}async loadPage(t){let o=++this.seq;this.loading=!0,this.error=null,this.emit();try{let n=await this.cfg.load({page:t,pageSize:this.pageSize});if(o!==this.seq)return;this.items=n.items??[],this.total=n.total??this.items.length,this.page=t}catch(n){if(o!==this.seq)return;this.error=v(n)}finally{o===this.seq&&(this.loading=!1,this.emit())}}next(){return this.loadPage(this.page+1)}prev(){return this.loadPage(Math.max(0,this.page-1))}get hasMore(){return(this.page+1)*this.pageSize<this.total}};function M(e,r){let t=r.error?.unavailable?"unavailable":r.loading?"loading":r.error?"error":"ready";e.setAttribute("data-lb-state",t),r.error?e.setAttribute("data-lb-error",r.error.message):e.removeAttribute("data-lb-error")}function D(e,r){let t=()=>r.setValue(e.value);e.addEventListener("change",t);let o=r.subscribe(()=>{M(e,r),e.disabled=r.loading,e.innerHTML="";for(let n of r.options){let i=document.createElement("option");i.value=String(n.value),i.textContent=n.label,e.appendChild(i)}e.value=r.value==null?"":String(r.value)});return()=>{e.removeEventListener("change",t),o()}}function z(e,r){let t=e.type==="checkbox",o=e.tagName==="SELECT"?"change":"input",n=()=>r.setValue(t?e.checked:e.value);e.addEventListener(o,n);let i=r.subscribe(()=>{if(t)e.checked=!!r.value;else{let d=r.value==null?"":String(r.value);e.value!==d&&(e.value=d)}e.setAttribute("data-lb-state",r.error?"error":"ready"),r.error?e.setAttribute("data-lb-error",r.error.message):e.removeAttribute("data-lb-error")});return()=>{e.removeEventListener(o,n),i()}}function F(e,r){let t=n=>{n.preventDefault(),r.run()};e.addEventListener("click",t);let o=r.subscribe(()=>{let n=r.error?.unavailable?"unavailable":r.loading?"loading":r.error?"error":r.lastResult!=null?"success":"idle";e.setAttribute("data-lb-state",n),"disabled"in e&&(e.disabled=r.loading),r.error?e.setAttribute("data-lb-error",r.error.message):e.removeAttribute("data-lb-error")});return()=>{e.removeEventListener("click",t),o()}}var q=["STILL_CHASING","COULD_NOT_REACH_STILL_TRYING","INTEREST_VALIDATED_OR_MEETING_PLANED","NOT_INTERESTED_LOST"],k=[{value:"",label:"Default ranking"},{value:"SCORE:DESC",label:"Score \\u2193"},{value:"SCORE:ASC",label:"Score \\u2191"},{value:"NAME:ASC",label:"Name A\\u2192Z"},{value:"NAME:DESC",label:"Name Z\\u2192A"},{value:"SIZE:DESC",label:"Size \\u2193"},{value:"SIZE:ASC",label:"Size \\u2191"},{value:"SECTOR:ASC",label:"Sector A\\u2192Z"},{value:"STATUS:ASC",label:"Status A\\u2192Z"},{value:"CONTACT_COUNT:DESC",label:"Contacts \\u2193"},{value:"LAST_PROSPECTING_ACTION_AT:DESC",label:"Last action \\u2193"},{value:"LAST_PROSPECTING_ACTION_AT:ASC",label:"Last action \\u2191"},{value:"EPILOGUE_STATUS_SET_AT:DESC",label:"Outcome set \\u2193"},{value:"LIKED:DESC",label:"Liked first"},{value:"DISLIKED:DESC",label:"Disliked first"}];function U(e){let r=String(e??"").trim().toUpperCase(),t=k.some(o=>o.value===r);return new b({kind:"select",value:t?r:"",load:async()=>k.slice()})}var m=[{value:"WANTED",label:"Wanted"},{value:"WON",label:"Won"},{value:"LOST",label:"Lost"},{value:"UNWANTED",label:"Unwanted"}],H={value:"",label:"\\u2014 Not set \\u2014"};function j(e){let r=String(e??"").trim().toUpperCase(),t=m.some(o=>o.value===r);return new b({kind:"select",value:t?r:"",validate:o=>String(o??"")===""?"Pick a status":null,load:async()=>t?m.slice():[H,...m]})}function W(e){let r=()=>{let t=typeof e.leadIds=="function"?e.leadIds():e.leadIds;return Array.isArray(t)?t:e.leadId?[e.leadId]:[]};return new s({tool:"leadbay_set_lead_status",fields:e.date?[e.status,e.date]:[e.status],confirm:e.confirm,args:()=>({lead_ids:r(),status:e.status.value,...e.date&&e.date.value?{status_date:e.date.value}:{},...e.ask?{_triggered_by:e.ask}:{}}),checkResult:t=>{let o=t?.failed;if(!Array.isArray(o)||o.length===0)return null;let n=r().length,i=o[0]?.message??"write rejected";return o.length===n?`Status not applied: ${i}`:`${o.length} of ${n} leads failed: ${i}`}})}function $(e){return new b({kind:"select",load:()=>l("leadbay_list_campaigns",{_triggered_by:e}),options:r=>(r?.campaigns??[]).map(o=>{let n=o?.campaign??o;return n?.id?{value:n.id,label:n.name??n.ai_generated_name??String(n.id)}:null}).filter(o=>o!=null)})}function V(e){return new s({tool:"leadbay_report_outreach",fields:e.note?[e.note]:[],args:()=>({lead_id:e.leadId,...e.status?{epilogue_status:e.status.value}:{},note:e.note?e.note.value:"",verification:{source:"user_confirmed",ref:e.ref??"logged from artifact"},_triggered_by:e.ask})})}function G(e){return new s({tool:"leadbay_add_note",fields:[e.note],args:()=>({leadId:e.leadId,note:e.note.value})})}function Z(e){return new s({tool:"leadbay_like_lead",args:{lead_id:e}})}function Y(e){return new s({tool:"leadbay_dislike_lead",args:{lead_id:e}})}function B(e,r){return new u({autoLoad:!1,load:()=>l("leadbay_account_history",{leadId:e,_triggered_by:r})})}function K(e,r){return new u({autoLoad:!1,load:()=>l("leadbay_research_lead_by_id",{leadId:e,_triggered_by:r})})}function J(e){let r=null;return new u({...e.autoLoad!==void 0?{autoLoad:e.autoLoad}:{},pollEvery:e.pollEvery??4e3,until:t=>!!t?.all_done,load:async()=>{if(!r){let t=await l("leadbay_enrich_titles",{...e.leadIds?{leadIds:e.leadIds}:{},titles:e.titles,...e.email!==void 0?{email:e.email}:{},...e.phone!==void 0?{phone:e.phone}:{},...e.confirm!==void 0?{confirm:e.confirm}:{},_triggered_by:e.ask}),o=Array.isArray(t?.lead_ids)?t.lead_ids:[],n=t?.notification_id??null;if(r=n||o.length>0?{notification_id:n,lead_ids:o}:null,!r)return{...t,all_done:!0,no_job:!0}}return l("leadbay_bulk_enrich_status",{...r.notification_id?{notification_id:r.notification_id}:{},...r.lead_ids.length>0?{lead_ids:r.lead_ids}:{},...e.titles?{titles:e.titles}:{},...e.email!==void 0?{email:e.email}:{},...e.phone!==void 0?{phone:e.phone}:{},_triggered_by:e.ask})}})}function Q(e){let r=()=>typeof e.order=="string"?e.order:String(e.order?.value??"");return new h({pageSize:e.pageSize??20,load:async({page:t,pageSize:o})=>{let n=await l("leadbay_pull_leads",{page:t,count:o,...e.lensId?{lensId:e.lensId}:{},...r()?{order:r()}:{},_triggered_by:e.ask}),i=n.leads??[];return{items:i,total:n.pagination?.total??i.length}}})}function X(e){let r=e.source??"followups",t=()=>typeof e.order=="string"?e.order:String(e.order?.value??"");return new h({pageSize:e.pageSize??20,load:async({page:o,pageSize:n})=>{let d=r==="campaign"?await l("leadbay_campaign_call_sheet",{campaign_id:e.campaignId,page:o,count:n,_triggered_by:e.ask}):await l("leadbay_pull_followups",{page:o,count:n,...e.city?{city:e.city}:{},...t()?{order:t()}:{},_triggered_by:e.ask}),w=d.leads??d.items??[];return{items:w,total:d.total_leads??d.pagination?.total??w.length}}})}function ee(e){return new u({load:()=>l("leadbay_team_activity",{weeks:e.weeks??4,_triggered_by:e.ask})})}var re={VERSION:x,configure:O,styles:R,call:l,field:e=>new b(e),action:e=>new s(e),resource:e=>new u(e),list:e=>new h(e),bindSelect:D,bindValue:z,bindAction:F,campaigns:$,outreach:V,note:G,like:Z,dislike:Y,leadStatus:j,setStatus:W,sortOrder:U,leadHistory:B,leadProfile:K,enrichment:J,callList:X,leadList:Q,teamActivity:ee,EPILOGUE_STATUSES:q,LEAD_STATUSES:m,SORT_ORDERS:k};typeof globalThis<"u"&&(globalThis.LeadbayArtifacts=re);})();';
|
|
21995
21488
|
ARTIFACT_USAGE_GUIDE = '# Leadbay Artifact Kit \u2014 headless domain components\n\nYou are building a single-file HTML **artifact** the user runs inside cowork. This\nkit gives you **headless view-models** that own a control\'s whole data lifecycle \u2014\nload/populate from a Leadbay call, hold value/state, poll, validate, and\nencapsulate the API call + business rules. **You own 100% of markup/layout/style.**\nThe library renders nothing. Inline the runtime once as a `<script>`; it exposes\none global `window.LeadbayArtifacts` (call it `lb`). Vanilla, no React, no build.\n\nPass every tool you use as the artifact\'s `mcp_tools` so the host permits it.\n\n## Two layers\n\n**Primitives** (generic):\n- `lb.field({ load, options, value, validate, dependsOn })` \u2014 a value + optionally\n API-populated options. `.value/.setValue/.options/.loading/.error/.valid/.subscribe`.\n- `lb.action({ tool, args, fields, confirm, onSuccess, onError })` \u2014 a write/submit.\n `.run()/.loading/.error/.lastResult/.subscribe`.\n- `lb.resource({ load, pollEvery?, until?, autoLoad? })` \u2014 one read that may change:\n load-on-click or poll-until-`until`. `.data/.loading/.refreshing/.error/.done/.load()/.refresh()/.stop()/.subscribe`.\n- `lb.list({ load, pageSize })` \u2014 paginated rows. `.items/.page/.total/.loading/.loadPage(n)/.next()/.prev()/.hasMore/.subscribe`.\n\n`.error` is `{ message, unavailable } | null`. `subscribe(cb)` fires immediately\nthen on every change \u2014 render your own DOM from it.\n\n**Domain components** (pre-wired \u2014 bake in the tool name, arg shape, and footguns):\n\n| Call | Returns | For |\n|---|---|---|\n| `lb.campaigns(ask)` | field | a campaign `<select>`, options from `leadbay_list_campaigns` |\n| `lb.outreach({leadId, ask, status?, note?})` | action | log a call \u2192 `report_outreach` (verification + `_triggered_by` baked in) |\n| `lb.note({leadId, note})` | action | add a note \u2192 `add_note` |\n| `lb.like(leadId)` / `lb.dislike(leadId)` | action | taste signal |\n| `lb.leadStatus(current?)` | field | a status `<select>` (Wanted/Won/Lost/Unwanted) |\n| `lb.setStatus({leadId or leadIds, status, date?, ask})` | action | write the org CRM status \u2192 `set_lead_status` |\n| `lb.leadHistory(leadId, ask)` | resource (lazy) | notes + activities + engagement \u2192 `account_history` |\n| `lb.leadProfile(leadId, ask)` | resource (lazy) | full lead profile \u2192 `research_lead_by_id` |\n| `lb.sortOrder(current?)` | field | a sort `<select>` mirroring the app\'s TableSort |\n| `lb.leadList({lensId?, order?, ask})` | list | a sortable Discover batch \u2192 `pull_leads` |\n| `lb.callList({source:\'followups\'\\|\'campaign\', campaignId?, city?, ask})` | list | a cold-call list (Monitor or a campaign) |\n| `lb.enrichment({leadIds, titles, ask, pollEvery?})` | resource (polling) | launch + watch contact enrichment |\n| `lb.teamActivity({weeks, ask})` | resource | manager leaderboard + activity trend \u2192 `leadbay_team_activity` |\n\n`lb.EPILOGUE_STATUSES` = the 4 disposition values\n(`STILL_CHASING`, `COULD_NOT_REACH_STILL_TRYING`, `INTEREST_VALIDATED_OR_MEETING_PLANED`, `NOT_INTERESTED_LOST`).\n`lb.LEAD_STATUSES` = the 4 org CRM statuses as `{value,label}` (`WANTED`, `WON`, `LOST`, `UNWANTED`).\n`lb.SORT_ORDERS` = the sort options as `{value,label}`; values are the backend `FIELD:ASC|DESC` enum.\n\n**Sorting is a SERVER concern.** `lb.leadList` and `lb.callList` take an `order`\n(a `lb.sortOrder()` field or a literal) and send it upstream; the backend sorts\nthe whole lens / Monitor and returns the requested page of that. Never re-sort\nrows in the browser \u2014 you would be reordering one page of a larger set, showing\nleads that do not belong at that position. The empty value means "no order\nparam", i.e. the tab\'s own ranking, which is the right default. Changing the\nsort should reset to page 0. Campaign call sheets cannot sort:\n`leadbay_campaign_call_sheet` has no `order` param, and `lb.callList` drops it\nfor that source rather than sending something the tool would reject.\n\n**Two different systems.** Epilogue = how one outreach attempt went (drives\nfollow-up ranking). Lead status = the commercial outcome, org-wide \u2014 the same\nfield the website\'s status selector writes. A won deal is a LEAD STATUS;\n"she didn\'t pick up" is an EPILOGUE. Setting one never sets the other, so when\nthe user reports both in one breath, fire both actions.\n\n**Binding sugar** (optional; binds a view-model to YOUR native element, no style):\n`lb.bindSelect(selectEl, field)` (populates options + value), `lb.bindValue(inputEl, field)`,\n`lb.bindAction(buttonEl, action)`. They set `data-lb-state`\n(`ready|loading|error|success|unavailable`) + `data-lb-error` on your element as\nstyling hooks. For lists/resources, use `.subscribe()` and render yourself.\n\n`ask` is the user\'s request this artifact serves \u2014 it becomes `_triggered_by`.\n\n## The skin (optional) \u2014 `lb.styles()`\n\nCall it once and you get a small `lb-*` stylesheet, so every artifact you build\nshares one visual language instead of re-inventing padding and colours. It is\n**opt-in**: skip it and you get exactly the unstyled HTML you wrote. It injects\nno markup and never touches your `class` attributes.\n\n```js\nlb.styles(); // idempotent \u2014 safe to call per row\n```\n\n| Class | For |\n|---|---|\n| `lb-card` / `lb-card-head` / `lb-title` / `lb-sub` | a lead card + its header |\n| `lb-row` / `lb-stack` / `lb-spacer` | control row / vertical spacing / flex filler that right-aligns what follows |\n| `lb-link-out` | quiet external link (icon inherits currentColor) \u2014 "Open in Leadbay" |\n| `lb-select` / `lb-input` / `lb-btn` | form controls (state-aware, see below) |\n| `lb-msg` (`data-tone="error\\|ok"`) | inline feedback |\n| `lb-chip` (`data-status="WON\\|LOST"`) | a status pill |\n| `lb-table` | leads table |\n| `lb-spinner` | inline busy indicator |\n\nControls react to the `data-lb-state` the bind helpers already set \u2014 a bound\n`lb-btn` dims while loading, goes green on success, red on error, all with no\nextra CSS from you.\n\nThe palette is the **product design system**, ported from\n`frontend/packages/style/color.css` \u2014 same `--color-gray-1\u20269` ramp, same\nsemantic `--color-{green,red,blue,gold}-{background,foreground}` pairs, same\n`1rem` / `0.625rem` radii and `corner-shape: squircle` as the app\'s components.\nAn artifact therefore looks like Leadbay, not like a generic page.\n\nUse the tokens rather than hardcoded colours \u2014 the same rule the style package\nenforces. Re-theme by overriding them; don\'t fight specificity:\n\n```css\n:root { --lb-surface: var(--color-gray-2); --lb-radius: 0.5rem; }\n```\n\nDark mode works two ways: `data-theme="dark"` on `<html>` (the frontend\'s own\nhook) **and** `prefers-color-scheme`, because an artifact renders inside a host\nwhose theme attribute it cannot set. Never hardcode a light background over the\nskin.\n\nThe product face is `Nikkei Maru`; the stack names it first and falls back to\nthe system UI font. Do **not** add an `@font-face` \u2014 artifacts are inline-only\nand a remote font URL will silently fail.\n\n## What every lead card MUST carry\n\nA card is the artifact form of the `pull_leads` table, and it inherits that\ntable\'s rules. A card with a name and a button is not enough: the rep cannot\ntell *why* this lead is on screen. Four lines, in this order.\n\n```html\n<div class="lb-card">\n <div class="lb-card-head">\n <span class="lb-title"></span> <!-- 1. company -->\n <span class="lb-chips"> <!-- 2. state -->\n <span class="lb-chip" data-taste hidden></span>\n <span class="lb-chip" data-status hidden></span>\n </span>\n </div>\n <div class="lb-sub"></div> <!-- 3. firmographics -->\n <div class="lb-sub" data-why></div> <!-- 4. why it fits -->\n <div class="lb-row"><!-- actions --></div>\n</div>\n```\n\n1. **Company** \u2014 `name`, linked to `website` (prefix `https://` on a bare host).\n Never render the numeric `score`; use the `\u25B0\u2756\u25B1` bar if you want the signal.\n\n Also give every card an **Open in Leadbay** link to the lead\'s panel in the\n product. Put it at the **right-hand end of the card\'s last action row** \u2014\n same row as the buttons, pushed right by an `lb-spacer`, not on a line of\n its own. Style it `lb-link-out`: quiet text plus a plain arrow-up-right,\n never a filled button. It is an escape hatch, not a call to action.\n\n ```html\n <div class="lb-row">\n <button class="lb-btn">Like</button>\n <button class="lb-btn">Set status</button>\n <span class="lb-spacer"></span> <!-- pushes the link right -->\n <a class="lb-link-out" data-k="open" target="_blank" rel="noopener">\n Open in Leadbay\n <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"\n stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">\n <line x1="7" y1="17" x2="17" y2="7"/><polyline points="7 7 17 7 17 17"/>\n </svg>\n </a>\n </div>\n ```\n\n Keep the arrow a bare diagonal stroke \u2014 the text already says where the link\n goes, so the glyph only has to mark "leaves this page". Mark the `<svg>`\n `aria-hidden="true"`: it is decorative, and the link text is the accessible\n name.\n **Pick the view the lead actually lives in** \u2014 the URL is\n `/app/<view>?lead=<uuid>`, and the three views are `discover`, `monitor`,\n `campaign`. Landing a Monitor lead on Discover drops the rep into a list\n that does not contain it:\n\n ```js\n function leadUrl(lead, campaignId) {\n const id = encodeURIComponent(lead.id);\n // A campaign card carries TWO params \u2014 the campaign selects the list, the\n // lead opens the panel inside it. Campaign wins even when in_monitor is\n // also true, because that is the list the rep is looking at.\n if (campaignId) {\n return `https://leadbay.app/app/campaign?campaign=${encodeURIComponent(campaignId)}&lead=${id}`;\n }\n const view = lead.in_monitor ? "monitor" : "discover";\n return `https://leadbay.app/app/${view}?lead=${id}`;\n }\n openEl.href = leadUrl(lead, campaignId);\n ```\n\n `in_monitor` / `in_discover` are booleans on the `pull_followups` payload \u2014\n every follow-up carries `in_monitor: true`, so a call sheet must link to\n `monitor`. `pull_leads` omits both flags entirely; its leads are the Discover\n batch by definition, so `discover` is the default. A campaign card\n (`lb.callList({source:"campaign", campaignId})`) needs `?campaign=<id>&lead=<id>`\n \u2014 the param names are `CAMPAIGN_QUERY_PARAM` and `LEAD_QUERY_PARAM`, and the\n app\'s own `useLeadPanel` preserves whatever params are already set, so the\n two coexist by design. Omitting `campaign=` opens an empty campaign view.\n\n Inline the glyph as SVG rather than an emoji or `\u2197` \u2014 it inherits\n `currentColor` and scales with the text, so it stays legible in both themes.\n `?lead=<uuid>` is the real deep-link (`LEAD_QUERY_PARAM` in the web app, read\n on load; the panel is an overlay, so the view choice only decides what sits\n behind it). This is the ONE place a card may use `lead.id`: as a link target,\n never as visible text.\n2. **State chips** \u2014 taste (`data-taste`) and CRM status (`data-status`) are\n INDEPENDENT axes; render both, hide the empty one. Never collapse to one chip.\n3. **Firmographics** \u2014 sector of activity first, then city, then size, then the\n contact. `sector_id` is a RAW ID (`"5136"`), not a label: resolve it via\n `leadbay_list_sectors` (1346 rows \u2014 fetch once, cache, never inline the lot)\n or omit it. Never print the raw id.\n\n **Always show whether the lead is reachable \u2014 and never merge the person\n with the company\'s switchboard.** These are two separate lines:\n\n ```js\n // WHO \u2014 recommended_contact. Name, and job_title ONLY when present; on list\n // payloads it is usually null, and inventing one is worse than omitting it.\n const rc = lead.recommended_contact;\n const who = rc ? [rc.first_name, rc.last_name].filter(Boolean).join(" ") : null;\n const whoLine = who ? who + (rc.job_title ? " \xB7 " + rc.job_title : "") : "No named contact";\n\n // HOW \u2014 company-level channels. `phone_numbers` and `email` belong to the\n // COMPANY, not to `recommended_contact`. Rendering "Jean \xB7 \u260E 0123\u2026" claims a\n // direct line that does not exist; it is the switchboard.\n const phone = (lead.phone_numbers || [])[0] || null;\n // The API returns the STRING "null" for a missing email \u2014 guard for it or\n // you will print the word "null" as an address.\n const email = lead.email && lead.email !== "null" ? lead.email : null;\n const howLine = [phone && "\u260E " + phone, email && "\u2709 " + email].filter(Boolean)\n .join(" \xB7 ") || "No direct channel \u2014 enrich to reveal";\n ```\n\n ```html\n <div class="lb-sub">Sector \xB7 City \xB7 Size</div>\n <div class="lb-sub">\u{1F464} Jean-Fran\xE7ois Froemer \xB7 G\xE9rant</div> <!-- WHO -->\n <div class="lb-sub">\u{1F3E2} \u260E 01 23 45 67 89</div> <!-- HOW: company -->\n ```\n\n Label the channel line as the **company\'s**, so a rep reading fast cannot\n mistake it for a direct line. A per-contact email or phone exists only after\n enrichment \u2014 `research_lead_by_id` exposes it as `contacts.reachable[]`, and\n `_meta.has_reachable_contact` is the authoritative flag. The list payloads\n carry neither, so a card built from `pull_leads` / `pull_followups` can only\n ever show company channels. Say "enrich to reveal" rather than implying the\n contact is callable.\n\n Two things that look like reachability and are not: a `linkedin_page` alone\n (the rep cannot message a URL without leaving the artifact \u2014 same rule\n `research_lead_by_id` applies), and `contacts_count > 0` (it counts known\n people, not people you can contact; a lead can show 2518 contacts and zero\n channels). `pull_followups` carries `has_phone` as a ready-made boolean;\n `pull_leads` omits it, so derive from `phone_numbers` there.\n4. **Why it fits** \u2014 one sentence, \u226420 words. Walk this chain and stop at the\n first hit:\n\n 1. `short_description`\n 2. `description` (longer; only on `research_lead_by_id` /\n `research_lead_by_name_fuzzy` \u2014 the trim payloads omit it)\n 3. top 2 `tags[].display_name`\n 4. `qualification_summary.best_response_excerpt`, trimmed to one sentence\n 5. `keywords`, first 3, joined with ` \xB7 `\n 6. the resolved sector label \u2014 better than nothing, and if step 3 already\n printed the sector on the firmographics line, skip to step 7\n 7. the literal *"No description yet \u2014 run qualification to generate one"*\n\n Never leave this line blank: a silent gap reads as a rendering bug, whereas\n the fallback tells the rep the data is missing and what fixes it.\n\n **The two list payloads are complementary, so the chain must span both.**\n `pull_leads` returns `short_description` on every lead but no `sector_id`;\n `pull_followups` returns `sector_id` but no `short_description` at all. A\n card fed by one will fall through to a different step than the same card fed\n by the other \u2014 that is expected, not a bug. Never call\n `research_lead_by_id` per row just to fill this line: it is one request per\n lead. Fetch it lazily when the rep expands a card.\n\n**Never show** on a card: `id`, `sector_id`, `location.pos`, `location.country`\n(unless city and state are both missing), `is_hq`, `*_in_progress`,\n`highlighted_fields`, `custom_fields`, `stale_at`, `deal_insights`,\n`need_attention*`, any count that is 0, any value that is the string `"null"`.\n\n**Minimum actions.** A card that only displays is a table row that costs more \u2014\nif you are not wiring an action, render the markdown table instead. Wire at\nleast one write, and prefer the set the rep actually needs:\n\n| Card is for | Wire |\n|---|---|\n| triage a discovery batch | `lb.like` / `lb.dislike` + `lb.setStatus` |\n| working a call list | `lb.outreach` (gated on a note) + `lb.leadHistory` |\n| pipeline review | `lb.setStatus` + `lb.note` |\n\nAlways render the `.error` branch of every view-model \u2014 a control that cannot\nreach the host must say so, not sit silent.\n\n## Recipe: cold-call sheet (one row per lead)\n\n```js\nconst lb = window.LeadbayArtifacts; lb.configure();\nconst ASK = "<the user\'s request>";\n\nconst list = lb.callList({ source: "campaign", campaignId: CID, ask: ASK });\nlist.subscribe((l) => renderRows(l.items, l.loading)); // your render\n\n// per lead row (call when you build a row):\nfunction wireRow(lead, els) {\n const status = lb.field({ value: "STILL_CHASING" }); // static-enum <select>\n const note = lb.field({ validate: (v) => (v && v.trim() ? null : "Add a note") });\n lb.bindValue(els.status, status);\n lb.bindValue(els.note, note);\n lb.bindAction(els.log, lb.outreach({ leadId: lead.id, ask: ASK, status, note }));\n lb.bindAction(els.like, lb.like(lead.id));\n\n const history = lb.leadHistory(lead.id, ASK); // lazy\n history.subscribe((h) => renderHistory(els.history, h));\n els.expand.onclick = () => history.load(); // load on click\n}\n```\n\n## Recipe: lead-status dropdown (Wanted / Won / Lost)\n\nThe org-wide CRM status, as a `<select>` + Apply button. You write the markup;\n`lb.leadStatus` fills the options and holds the value, `lb.setStatus` does the write.\n\n```html\n<div class="lb-card">\n <div class="lb-card-head">\n <span class="lb-title">Acme Corp</span>\n <span class="lb-chips">\n <span id="taste" class="lb-chip" data-taste="liked">Liked</span>\n <span id="crm" class="lb-chip" data-status="WANTED">Wanted</span>\n </span>\n </div>\n <div class="lb-row">\n <select id="st" class="lb-select"></select>\n <button id="go" class="lb-btn">Apply</button>\n <span id="msg" class="lb-msg"></span>\n </div>\n</div>\n```\n\n**Two badges, never one.** Taste (`liked`/`disliked`, from `lb.like`/`lb.dislike`)\nand CRM status (`WANTED`/`WON`/`LOST`/`UNWANTED`, from `lb.setStatus`) are\nindependent axes \u2014 a lead can be liked *and* lost. Collapsing them into a single\nchip destroys information: the rep can no longer see that a lead they liked went\nnowhere. Render `data-taste` and `data-status` as separate chips inside\n`lb-chips`, and hide the one that has no value rather than reusing it.\n\n```js\nlb.styles(); // once per artifact \u2014 see below\n\nconst status = lb.leadStatus(lead.org_lead_status); // seed with the CURRENT value\nconst save = lb.setStatus({ leadId: lead.id, status, ask: ASK });\n\nlb.bindSelect(document.getElementById("st"), status); // populates the 4 options\nlb.bindAction(document.getElementById("go"), save); // click \u2192 write\n\nsave.subscribe((a) => { // render your own feedback\n msg.textContent = a.loading ? "Saving\u2026"\n : a.error ? a.error.message // includes partial failures\n : a.lastResult ? `Set to ${a.lastResult.status}` : "";\n msg.dataset.tone = a.error ? "error" : a.lastResult ? "ok" : "";\n});\n```\n\nLoading / success / error styling comes free: `bindAction` and `bindSelect` set\n`data-lb-state` (`ready|loading|error|success|unavailable`) and the skin already\ntargets those attributes. No extra wiring.\n\nSave-on-change instead of an Apply button \u2014 drop `bindAction` and run it yourself:\n\n```js\ndocument.getElementById("st").addEventListener("change", () => save.run());\n```\n\n**Bulk apply** across checked rows \u2014 pass `leadIds` and a `confirm`, since one\nclick rewrites a field every rep in the org sees:\n\n```js\nconst bulk = lb.setStatus({\n leadIds: () => checkedIds, // \u2190 read at run() time, not at build time\n status, ask: ASK,\n confirm: "Set this status on every selected lead?",\n});\n```\n\n`leadIds` is read when the action runs, so a live selection works \u2014 but pass the\narray itself if your selection is fixed. A partial write (some leads rejected)\nsurfaces as `.error`, never as a green button: `setStatus` checks the `failed[]`\nthe tool returns.\n\nThe backend stamps the status date as "now" on every write, which is what a rep\nclicking a dropdown means. Don\'t add a date picker unless the user asks to\nbackdate \u2014 then pass an optional `date` field holding `YYYY-MM-DD`:\n`lb.setStatus({ leadId, status, date, ask })`.\n\n## Recipe: manager dashboard\n\n```js\nconst team = lb.teamActivity({ weeks: 4, ask: ASK });\nteam.subscribe((t) => {\n if (t.loading) showSpinner();\n if (t.data) {\n renderLeaderboard(t.data.reps); // sorted by total_activities; cols: name, notes, meetings_or_interest, lost\u2026\n renderTrendChart(t.data.trend); // [{date,count}] \u2192 Chart.js (allowed from CDN)\n }\n});\nrefreshBtn.onclick = () => team.refresh();\n```\n\n## Recipe: live enrichment\n\n```js\nconst job = lb.enrichment({ leadIds: [LEAD], titles: ["CEO", "VP Sales"], ask: ASK });\njob.subscribe((j) => {\n const p = j.data && j.data.overall_progress; // {done,total,done_ratio}\n renderBar(p);\n if (j.done) renderContacts(j.data.leads); // enriched contacts\n});\nrefreshBtn.onclick = () => job.refresh();\n```\n\n## Write-call rules\n\nThe domain factories handle these for you. If you hand-roll an action:\n`leadbay_report_outreach` args MUST include `verification:{source:"user_confirmed", ref}`\nAND `_triggered_by`; `leadbay_add_leads_to_campaign` needs `_triggered_by`;\n`add_note`/`like_lead`/`dislike_lead` take only their own args. `epilogue_status` is\none of `lb.EPILOGUE_STATUSES`. Snoozing (pushback) is advanced-gated \u2014 not\ncallable from a default artifact. Org lead status IS on the default surface:\nuse `lb.setStatus`, which owns the arg shape AND the partial-write check \u2014\n`leadbay_set_lead_status` writes each lead separately, so it can resolve 200\nwith a non-empty `failed[]`. Hand-rolling that action will report a green\nbutton over a write that never landed.\n\n## Degradation + live updates\n\nIf the host bridge is absent, a view-model\'s `.error` is set with `.error.unavailable\n=== true` (bind helpers set `data-lb-state="unavailable"`) \u2014 nothing throws. Every\ncall also has a **30s timeout** (configurable via `lb.configure({ timeoutMs })`): a\nhost call that never settles becomes `.error` with `code:"timeout"`, so a control is\nnever stuck loading forever \u2014 always render the `.error` branch so the user can retry.\nAuto-poll (`pollEvery`) depends on the cowork host serving FRESH reads; `.refresh()`\nis the guaranteed manual path \u2014 always wire a Refresh control for polling resources.';
|
|
21996
21489
|
}
|
|
21997
21490
|
});
|
|
@@ -22042,13 +21535,12 @@ __export(dist_exports, {
|
|
|
22042
21535
|
COMPOSITE_FILE_TOOL_NAMES: () => COMPOSITE_FILE_TOOL_NAMES,
|
|
22043
21536
|
DEFAULT_REQUEST_TIMEOUT_MS: () => DEFAULT_REQUEST_TIMEOUT_MS,
|
|
22044
21537
|
GETTING_STARTED_MANIFEST: () => GETTING_STARTED_MANIFEST,
|
|
22045
|
-
InMemoryBulkStore: () => InMemoryBulkStore,
|
|
22046
21538
|
LeadbayClient: () => LeadbayClient,
|
|
22047
|
-
LocalBulkStore: () => LocalBulkStore,
|
|
22048
21539
|
NO_COMMERCE_TOOL_DESCRIPTIONS: () => NO_COMMERCE_TOOL_DESCRIPTIONS,
|
|
22049
21540
|
NotificationsInbox: () => NotificationsInbox,
|
|
22050
21541
|
NotificationsWsClient: () => NotificationsWsClient,
|
|
22051
21542
|
REGIONS: () => REGIONS,
|
|
21543
|
+
abandonLaunch: () => abandonLaunch,
|
|
22052
21544
|
accountHistory: () => accountHistory,
|
|
22053
21545
|
accountStatus: () => accountStatus,
|
|
22054
21546
|
acknowledgeNotification: () => acknowledgeNotification,
|
|
@@ -22058,6 +21550,7 @@ __export(dist_exports, {
|
|
|
22058
21550
|
anchorIdFor: () => anchorIdFor,
|
|
22059
21551
|
answerClarification: () => answerClarification,
|
|
22060
21552
|
artifactKit: () => artifactKit,
|
|
21553
|
+
beginLaunch: () => beginLaunch,
|
|
22061
21554
|
bulkEnrichStatus: () => bulkEnrichStatus,
|
|
22062
21555
|
bulkQualifyLeads: () => bulkQualifyLeads,
|
|
22063
21556
|
campaignCallSheet: () => campaignCallSheet,
|
|
@@ -22072,7 +21565,6 @@ __export(dist_exports, {
|
|
|
22072
21565
|
createCampaign: () => createCampaign,
|
|
22073
21566
|
createClient: () => createClient,
|
|
22074
21567
|
createCustomField: () => createCustomField,
|
|
22075
|
-
createDefaultBulkStore: () => createDefaultBulkStore,
|
|
22076
21568
|
createLens: () => createLens,
|
|
22077
21569
|
createLensDraft: () => createLensDraft,
|
|
22078
21570
|
createTopupLink: () => createTopupLink,
|
|
@@ -22112,8 +21604,8 @@ __export(dist_exports, {
|
|
|
22112
21604
|
importLeads: () => importLeads,
|
|
22113
21605
|
importStatus: () => importStatus,
|
|
22114
21606
|
inferKind: () => inferKind,
|
|
22115
|
-
isValidBulkId: () => isValidBulkId,
|
|
22116
21607
|
launchBulkEnrichment: () => launchBulkEnrichment,
|
|
21608
|
+
launchFingerprint: () => launchFingerprint,
|
|
22117
21609
|
likeLead: () => likeLead,
|
|
22118
21610
|
listCampaigns: () => listCampaigns,
|
|
22119
21611
|
listLenses: () => listLenses,
|
|
@@ -22130,8 +21622,10 @@ __export(dist_exports, {
|
|
|
22130
21622
|
pullLeads: () => pullLeads,
|
|
22131
21623
|
qualifyLead: () => qualifyLead,
|
|
22132
21624
|
qualifyStatus: () => qualifyStatus,
|
|
21625
|
+
recallLaunch: () => recallLaunch,
|
|
22133
21626
|
recallOrderedTitles: () => recallOrderedTitles,
|
|
22134
21627
|
refinePrompt: () => refinePrompt,
|
|
21628
|
+
rememberLaunch: () => rememberLaunch,
|
|
22135
21629
|
removeEpilogue: () => removeEpilogue,
|
|
22136
21630
|
removeLeadsFromCampaign: () => removeLeadsFromCampaign,
|
|
22137
21631
|
removePushback: () => removePushback,
|
|
@@ -22139,6 +21633,7 @@ __export(dist_exports, {
|
|
|
22139
21633
|
reportOutreach: () => reportOutreach,
|
|
22140
21634
|
researchLeadById: () => researchLeadById,
|
|
22141
21635
|
researchLeadByNameFuzzy: () => researchLeadByNameFuzzy,
|
|
21636
|
+
resetLaunchGuard: () => resetLaunchGuard,
|
|
22142
21637
|
resolveImportRows: () => resolveImportRows,
|
|
22143
21638
|
resolveRegion: () => resolveRegion,
|
|
22144
21639
|
reviseHintFor: () => reviseHintFor,
|
|
@@ -22269,9 +21764,9 @@ var init_dist = __esm({
|
|
|
22269
21764
|
init_team_activity();
|
|
22270
21765
|
init_send_feedback();
|
|
22271
21766
|
init_artifact_kit();
|
|
22272
|
-
init_bulk_store();
|
|
22273
21767
|
init_getting_started();
|
|
22274
21768
|
init_tool_descriptions_generated();
|
|
21769
|
+
init_launch_guard();
|
|
22275
21770
|
granularReadTools = [
|
|
22276
21771
|
listLenses,
|
|
22277
21772
|
discoverLeads,
|
|
@@ -22549,7 +22044,7 @@ If the prompt's body and the tool's RENDERING appear to conflict, the tool's REN
|
|
|
22549
22044
|
|
|
22550
22045
|
# Resilience rules for Leadbay long-running tools
|
|
22551
22046
|
|
|
22552
|
-
These
|
|
22047
|
+
These rules apply to every Leadbay workflow that calls \`leadbay_pull_leads\`, \`leadbay_bulk_qualify_leads\`, \`leadbay_research_lead_by_id\`, \`leadbay_import_and_qualify\`, or \`leadbay_enrich_titles\`. **Treat timeouts and stream-closed errors as transient, not as signals to replan.**
|
|
22553
22048
|
|
|
22554
22049
|
## Rule 1 \u2014 Pin the lens
|
|
22555
22050
|
|
|
@@ -22557,7 +22052,7 @@ After your first \`leadbay_pull_leads\` call, capture \`response.lens.id\` into
|
|
|
22557
22052
|
|
|
22558
22053
|
## Rule 2 \u2014 Prefer async for bulk operations
|
|
22559
22054
|
|
|
22560
|
-
\`leadbay_bulk_qualify_leads\` and \`leadbay_import_and_qualify\` accept \`wait_for_completion:false
|
|
22055
|
+
\`leadbay_bulk_qualify_leads\` and \`leadbay_import_and_qualify\` accept \`wait_for_completion:false\` and return immediately. They hand back different ids: \`bulk_qualify_leads\` returns \`{status:'running', notification_id, lead_ids, lens_id}\` \u2014 poll \`leadbay_qualify_status\` with those. \`import_and_qualify\` returns \`{status:'running', import_ids}\` and no \`notification_id\` at all \u2014 poll \`leadbay_import_status({importIds, dry_run})\` with those. Poll every ~10s until the job completes. **Use the async pattern by default** \u2014 the blocking default can exceed the MCP client's per-call timeout on large batches and produce a misleading \`"Request timed out"\` even though the server is still working.
|
|
22561
22056
|
|
|
22562
22057
|
## Rule 3 \u2014 Serialize \`leadbay_research_lead_by_id\` fan-out
|
|
22563
22058
|
|
|
@@ -22573,6 +22068,36 @@ If a Leadbay tool returns \`"Request timed out"\`, \`"stream closed"\`, or any o
|
|
|
22573
22068
|
|
|
22574
22069
|
If \`pull_leads\` itself fails and you have no prior batch, then yes \u2014 retry it, explicitly pass the lensId you captured (if any), and continue.
|
|
22575
22070
|
|
|
22071
|
+
## A launched job cannot be stopped
|
|
22072
|
+
|
|
22073
|
+
Leadbay has no cancel. Once \`leadbay_enrich_titles\`, \`leadbay_bulk_qualify_leads\`,
|
|
22074
|
+
\`leadbay_import_leads\` or \`leadbay_import_and_qualify\` has returned a launched or
|
|
22075
|
+
running result, that work is queued on Leadbay and runs to completion, and the
|
|
22076
|
+
quota it costs is already committed. A discovery, preview or \`dry_run\` result
|
|
22077
|
+
launched nothing and is not covered here.
|
|
22078
|
+
|
|
22079
|
+
The user cancelling in the chat, a request timeout, or a closed stream stops YOUR
|
|
22080
|
+
waiting, never the job. \`cancelled: true\` means we stopped watching, not that the
|
|
22081
|
+
work stopped. What to do next depends on what you are holding:
|
|
22082
|
+
|
|
22083
|
+
- **A handle.** Poll the status tool with it, and do not launch the work that
|
|
22084
|
+
handle covers a second time \u2014 that spends the quota again on the same rows.
|
|
22085
|
+
\`leadbay_import_status\` takes \`importIds\`, so pass the values of \`import_ids\`
|
|
22086
|
+
under that name. A qualification started by \`leadbay_import_and_qualify\` has no
|
|
22087
|
+
notification of its own: resume it with
|
|
22088
|
+
\`leadbay_qualify_status({lead_ids, lens_id})\`.
|
|
22089
|
+
- **A handle AND a subset the result says never started** \u2014 \`failed[]\` entries
|
|
22090
|
+
with \`error:"not_queued"\`, or a \`rows_pending_upload\` count. Poll the handle
|
|
22091
|
+
for what was launched and re-run for that subset only, never for the whole
|
|
22092
|
+
batch.
|
|
22093
|
+
- **No result at all**, because the call timed out or the stream closed before it
|
|
22094
|
+
returned. Check \`leadbay_account_status\` first: the launch may have landed and
|
|
22095
|
+
finished. Calling the same tool again with the same arguments will usually hand
|
|
22096
|
+
back the job already launched rather than starting a second one, but that guard
|
|
22097
|
+
is in-memory, five minutes, and per process, so it is best-effort \u2014 say what you
|
|
22098
|
+
are about to re-run before you spend the user's quota on it.
|
|
22099
|
+
|
|
22100
|
+
|
|
22576
22101
|
|
|
22577
22102
|
# PHASE 0 \u2014 STATE + AUDIENCE
|
|
22578
22103
|
|
|
@@ -22741,7 +22266,7 @@ Run the Leadbay daily check-in for me. Treat this prompt the same way for any eq
|
|
|
22741
22266
|
|
|
22742
22267
|
# Resilience rules for Leadbay long-running tools
|
|
22743
22268
|
|
|
22744
|
-
These
|
|
22269
|
+
These rules apply to every Leadbay workflow that calls \`leadbay_pull_leads\`, \`leadbay_bulk_qualify_leads\`, \`leadbay_research_lead_by_id\`, \`leadbay_import_and_qualify\`, or \`leadbay_enrich_titles\`. **Treat timeouts and stream-closed errors as transient, not as signals to replan.**
|
|
22745
22270
|
|
|
22746
22271
|
## Rule 1 \u2014 Pin the lens
|
|
22747
22272
|
|
|
@@ -22749,7 +22274,7 @@ After your first \`leadbay_pull_leads\` call, capture \`response.lens.id\` into
|
|
|
22749
22274
|
|
|
22750
22275
|
## Rule 2 \u2014 Prefer async for bulk operations
|
|
22751
22276
|
|
|
22752
|
-
\`leadbay_bulk_qualify_leads\` and \`leadbay_import_and_qualify\` accept \`wait_for_completion:false
|
|
22277
|
+
\`leadbay_bulk_qualify_leads\` and \`leadbay_import_and_qualify\` accept \`wait_for_completion:false\` and return immediately. They hand back different ids: \`bulk_qualify_leads\` returns \`{status:'running', notification_id, lead_ids, lens_id}\` \u2014 poll \`leadbay_qualify_status\` with those. \`import_and_qualify\` returns \`{status:'running', import_ids}\` and no \`notification_id\` at all \u2014 poll \`leadbay_import_status({importIds, dry_run})\` with those. Poll every ~10s until the job completes. **Use the async pattern by default** \u2014 the blocking default can exceed the MCP client's per-call timeout on large batches and produce a misleading \`"Request timed out"\` even though the server is still working.
|
|
22753
22278
|
|
|
22754
22279
|
## Rule 3 \u2014 Serialize \`leadbay_research_lead_by_id\` fan-out
|
|
22755
22280
|
|
|
@@ -22765,6 +22290,36 @@ If a Leadbay tool returns \`"Request timed out"\`, \`"stream closed"\`, or any o
|
|
|
22765
22290
|
|
|
22766
22291
|
If \`pull_leads\` itself fails and you have no prior batch, then yes \u2014 retry it, explicitly pass the lensId you captured (if any), and continue.
|
|
22767
22292
|
|
|
22293
|
+
## A launched job cannot be stopped
|
|
22294
|
+
|
|
22295
|
+
Leadbay has no cancel. Once \`leadbay_enrich_titles\`, \`leadbay_bulk_qualify_leads\`,
|
|
22296
|
+
\`leadbay_import_leads\` or \`leadbay_import_and_qualify\` has returned a launched or
|
|
22297
|
+
running result, that work is queued on Leadbay and runs to completion, and the
|
|
22298
|
+
quota it costs is already committed. A discovery, preview or \`dry_run\` result
|
|
22299
|
+
launched nothing and is not covered here.
|
|
22300
|
+
|
|
22301
|
+
The user cancelling in the chat, a request timeout, or a closed stream stops YOUR
|
|
22302
|
+
waiting, never the job. \`cancelled: true\` means we stopped watching, not that the
|
|
22303
|
+
work stopped. What to do next depends on what you are holding:
|
|
22304
|
+
|
|
22305
|
+
- **A handle.** Poll the status tool with it, and do not launch the work that
|
|
22306
|
+
handle covers a second time \u2014 that spends the quota again on the same rows.
|
|
22307
|
+
\`leadbay_import_status\` takes \`importIds\`, so pass the values of \`import_ids\`
|
|
22308
|
+
under that name. A qualification started by \`leadbay_import_and_qualify\` has no
|
|
22309
|
+
notification of its own: resume it with
|
|
22310
|
+
\`leadbay_qualify_status({lead_ids, lens_id})\`.
|
|
22311
|
+
- **A handle AND a subset the result says never started** \u2014 \`failed[]\` entries
|
|
22312
|
+
with \`error:"not_queued"\`, or a \`rows_pending_upload\` count. Poll the handle
|
|
22313
|
+
for what was launched and re-run for that subset only, never for the whole
|
|
22314
|
+
batch.
|
|
22315
|
+
- **No result at all**, because the call timed out or the stream closed before it
|
|
22316
|
+
returned. Check \`leadbay_account_status\` first: the launch may have landed and
|
|
22317
|
+
finished. Calling the same tool again with the same arguments will usually hand
|
|
22318
|
+
back the job already launched rather than starting a second one, but that guard
|
|
22319
|
+
is in-memory, five minutes, and per process, so it is best-effort \u2014 say what you
|
|
22320
|
+
are about to re-run before you spend the user's quota on it.
|
|
22321
|
+
|
|
22322
|
+
|
|
22768
22323
|
|
|
22769
22324
|
# PHASE 0 \u2014 RESUME CHECK
|
|
22770
22325
|
|
|
@@ -22859,7 +22414,7 @@ When the response carries \`social_urls\` (the post-fix multi-platform URL block
|
|
|
22859
22414
|
|
|
22860
22415
|
ABOVE the table, add a 2\u20134 sentence "Today's nudges" paragraph for the 3 most-promising rows. The nudges speak to urgency / opportunity / freshness \u2014 what makes acting on these RIGHT NOW the right call. Do NOT repeat the "why it fits" column from the table; the nudges should add fresh framing the table doesn't carry (e.g., recent news from the \`qualification_summary\` excerpt, a window closing, a competitor activity the user mentioned earlier in the session). One sentence per nudge, salesperson voice, not coachspeak.
|
|
22861
22416
|
|
|
22862
|
-
If the batch returns fewer than 10 qualified leads, top it up: call \`leadbay_bulk_qualify_leads\` with \`lensId:<captured>\`, \`count:<1.5x deficit, capped at 25>\`, and **\`wait_for_completion:false\`**. Capture \`
|
|
22417
|
+
If the batch returns fewer than 10 qualified leads, top it up: call \`leadbay_bulk_qualify_leads\` with \`lensId:<captured>\`, \`count:<1.5x deficit, capped at 25>\`, and **\`wait_for_completion:false\`**. Capture \`notification_id\` from the response and poll \`leadbay_qualify_status\` every ~10s until \`status:'done'\`. Then re-pull with the same \`lensId\` to pick up the newly qualified leads. **Never re-pull without \`lensId\` \u2014 you will lose your batch to a lens shift.** (The \`leadbay_qualify_top_n\` slash-prompt wraps this same tool with a friendlier surface for users; agents should call the underlying tool directly here.)
|
|
22863
22418
|
|
|
22864
22419
|
# PHASE 4 \u2014 DEEP DIVE (every promising lead)
|
|
22865
22420
|
|
|
@@ -22973,7 +22528,7 @@ If the prompt's body and the tool's RENDERING appear to conflict, the tool's REN
|
|
|
22973
22528
|
|
|
22974
22529
|
# Resilience rules for Leadbay long-running tools
|
|
22975
22530
|
|
|
22976
|
-
These
|
|
22531
|
+
These rules apply to every Leadbay workflow that calls \`leadbay_pull_leads\`, \`leadbay_bulk_qualify_leads\`, \`leadbay_research_lead_by_id\`, \`leadbay_import_and_qualify\`, or \`leadbay_enrich_titles\`. **Treat timeouts and stream-closed errors as transient, not as signals to replan.**
|
|
22977
22532
|
|
|
22978
22533
|
## Rule 1 \u2014 Pin the lens
|
|
22979
22534
|
|
|
@@ -22981,7 +22536,7 @@ After your first \`leadbay_pull_leads\` call, capture \`response.lens.id\` into
|
|
|
22981
22536
|
|
|
22982
22537
|
## Rule 2 \u2014 Prefer async for bulk operations
|
|
22983
22538
|
|
|
22984
|
-
\`leadbay_bulk_qualify_leads\` and \`leadbay_import_and_qualify\` accept \`wait_for_completion:false
|
|
22539
|
+
\`leadbay_bulk_qualify_leads\` and \`leadbay_import_and_qualify\` accept \`wait_for_completion:false\` and return immediately. They hand back different ids: \`bulk_qualify_leads\` returns \`{status:'running', notification_id, lead_ids, lens_id}\` \u2014 poll \`leadbay_qualify_status\` with those. \`import_and_qualify\` returns \`{status:'running', import_ids}\` and no \`notification_id\` at all \u2014 poll \`leadbay_import_status({importIds, dry_run})\` with those. Poll every ~10s until the job completes. **Use the async pattern by default** \u2014 the blocking default can exceed the MCP client's per-call timeout on large batches and produce a misleading \`"Request timed out"\` even though the server is still working.
|
|
22985
22540
|
|
|
22986
22541
|
## Rule 3 \u2014 Serialize \`leadbay_research_lead_by_id\` fan-out
|
|
22987
22542
|
|
|
@@ -22997,6 +22552,36 @@ If a Leadbay tool returns \`"Request timed out"\`, \`"stream closed"\`, or any o
|
|
|
22997
22552
|
|
|
22998
22553
|
If \`pull_leads\` itself fails and you have no prior batch, then yes \u2014 retry it, explicitly pass the lensId you captured (if any), and continue.
|
|
22999
22554
|
|
|
22555
|
+
## A launched job cannot be stopped
|
|
22556
|
+
|
|
22557
|
+
Leadbay has no cancel. Once \`leadbay_enrich_titles\`, \`leadbay_bulk_qualify_leads\`,
|
|
22558
|
+
\`leadbay_import_leads\` or \`leadbay_import_and_qualify\` has returned a launched or
|
|
22559
|
+
running result, that work is queued on Leadbay and runs to completion, and the
|
|
22560
|
+
quota it costs is already committed. A discovery, preview or \`dry_run\` result
|
|
22561
|
+
launched nothing and is not covered here.
|
|
22562
|
+
|
|
22563
|
+
The user cancelling in the chat, a request timeout, or a closed stream stops YOUR
|
|
22564
|
+
waiting, never the job. \`cancelled: true\` means we stopped watching, not that the
|
|
22565
|
+
work stopped. What to do next depends on what you are holding:
|
|
22566
|
+
|
|
22567
|
+
- **A handle.** Poll the status tool with it, and do not launch the work that
|
|
22568
|
+
handle covers a second time \u2014 that spends the quota again on the same rows.
|
|
22569
|
+
\`leadbay_import_status\` takes \`importIds\`, so pass the values of \`import_ids\`
|
|
22570
|
+
under that name. A qualification started by \`leadbay_import_and_qualify\` has no
|
|
22571
|
+
notification of its own: resume it with
|
|
22572
|
+
\`leadbay_qualify_status({lead_ids, lens_id})\`.
|
|
22573
|
+
- **A handle AND a subset the result says never started** \u2014 \`failed[]\` entries
|
|
22574
|
+
with \`error:"not_queued"\`, or a \`rows_pending_upload\` count. Poll the handle
|
|
22575
|
+
for what was launched and re-run for that subset only, never for the whole
|
|
22576
|
+
batch.
|
|
22577
|
+
- **No result at all**, because the call timed out or the stream closed before it
|
|
22578
|
+
returned. Check \`leadbay_account_status\` first: the launch may have landed and
|
|
22579
|
+
finished. Calling the same tool again with the same arguments will usually hand
|
|
22580
|
+
back the job already launched rather than starting a second one, but that guard
|
|
22581
|
+
is in-memory, five minutes, and per process, so it is best-effort \u2014 say what you
|
|
22582
|
+
are about to re-run before you spend the user's quota on it.
|
|
22583
|
+
|
|
22584
|
+
|
|
23000
22585
|
|
|
23001
22586
|
# THE ONE-FORWARD-OPTION RULE \u2014 the structural contract of this walkthrough
|
|
23002
22587
|
|
|
@@ -23432,7 +23017,7 @@ account's **default wishlist selection** while \`confirm\`/\`email\` are set \u2
|
|
|
23432
23017
|
it would reveal and charge for the whole batch instead of the one lead the user
|
|
23433
23018
|
agreed to.
|
|
23434
23019
|
|
|
23435
|
-
It returns a \`
|
|
23020
|
+
It returns a \`notification_id\` and runs async \u2014 poll \`leadbay_bulk_enrich_status\`
|
|
23436
23021
|
with that id (\`include_contacts=true\`) until \`all_done\`, or until the resolved
|
|
23437
23022
|
count plateaus across a few spaced polls. Then report the contact that actually
|
|
23438
23023
|
resolved: name, title, and the email/phone that came back. Contacts sometimes
|
|
@@ -23670,7 +23255,7 @@ Build the final mappings yourself. Start from \`leadbay_resolve_import_rows.mapp
|
|
|
23670
23255
|
|
|
23671
23256
|
# PHASE 5 \u2014 QUALIFY (optional) + REPORT
|
|
23672
23257
|
|
|
23673
|
-
Prefer \`leadbay_import_and_qualify\` when the user asks to qualify/research after import; otherwise use \`leadbay_import_leads\`. For large files or short client timeouts, pass \`wait_for_completion=false\` and poll \`leadbay_import_status\`. After import, qualify only lead IDs returned by the import. Rows that came back \`uncrawled\` are pending a background crawl (not failures); the leads Leadbay adds for them populate in the user's Leadbay account as the crawl completes \u2014 tell the user that, not that a tool call will fetch them (\`import_status\`
|
|
23258
|
+
Prefer \`leadbay_import_and_qualify\` when the user asks to qualify/research after import; otherwise use \`leadbay_import_leads\`. For large files or short client timeouts, pass \`wait_for_completion=false\` and poll \`leadbay_import_status\`. After import, qualify only lead IDs returned by the import. Rows that came back \`uncrawled\` are pending a background crawl (not failures); the leads Leadbay adds for them populate in the user's Leadbay account as the crawl completes \u2014 tell the user that, not that a tool call will fetch them (\`import_status\` refreshes status/progress only; \`pull_leads\` reads the active lens, so an imported lead outside it may not appear; re-running the import later re-reconciles those companies).
|
|
23674
23259
|
|
|
23675
23260
|
**Deliver the augmented file back to the user**: the original file plus a new \`LEADBAY_ID\` column populated from the resolution step. This is the second deliverable of a job well done.
|
|
23676
23261
|
|
|
@@ -23842,7 +23427,7 @@ You are working with Leadbay through the \`leadbay_*\` MCP tools. This prompt or
|
|
|
23842
23427
|
|
|
23843
23428
|
# Resilience rules for Leadbay long-running tools
|
|
23844
23429
|
|
|
23845
|
-
These
|
|
23430
|
+
These rules apply to every Leadbay workflow that calls \`leadbay_pull_leads\`, \`leadbay_bulk_qualify_leads\`, \`leadbay_research_lead_by_id\`, \`leadbay_import_and_qualify\`, or \`leadbay_enrich_titles\`. **Treat timeouts and stream-closed errors as transient, not as signals to replan.**
|
|
23846
23431
|
|
|
23847
23432
|
## Rule 1 \u2014 Pin the lens
|
|
23848
23433
|
|
|
@@ -23850,7 +23435,7 @@ After your first \`leadbay_pull_leads\` call, capture \`response.lens.id\` into
|
|
|
23850
23435
|
|
|
23851
23436
|
## Rule 2 \u2014 Prefer async for bulk operations
|
|
23852
23437
|
|
|
23853
|
-
\`leadbay_bulk_qualify_leads\` and \`leadbay_import_and_qualify\` accept \`wait_for_completion:false
|
|
23438
|
+
\`leadbay_bulk_qualify_leads\` and \`leadbay_import_and_qualify\` accept \`wait_for_completion:false\` and return immediately. They hand back different ids: \`bulk_qualify_leads\` returns \`{status:'running', notification_id, lead_ids, lens_id}\` \u2014 poll \`leadbay_qualify_status\` with those. \`import_and_qualify\` returns \`{status:'running', import_ids}\` and no \`notification_id\` at all \u2014 poll \`leadbay_import_status({importIds, dry_run})\` with those. Poll every ~10s until the job completes. **Use the async pattern by default** \u2014 the blocking default can exceed the MCP client's per-call timeout on large batches and produce a misleading \`"Request timed out"\` even though the server is still working.
|
|
23854
23439
|
|
|
23855
23440
|
## Rule 3 \u2014 Serialize \`leadbay_research_lead_by_id\` fan-out
|
|
23856
23441
|
|
|
@@ -23866,6 +23451,36 @@ If a Leadbay tool returns \`"Request timed out"\`, \`"stream closed"\`, or any o
|
|
|
23866
23451
|
|
|
23867
23452
|
If \`pull_leads\` itself fails and you have no prior batch, then yes \u2014 retry it, explicitly pass the lensId you captured (if any), and continue.
|
|
23868
23453
|
|
|
23454
|
+
## A launched job cannot be stopped
|
|
23455
|
+
|
|
23456
|
+
Leadbay has no cancel. Once \`leadbay_enrich_titles\`, \`leadbay_bulk_qualify_leads\`,
|
|
23457
|
+
\`leadbay_import_leads\` or \`leadbay_import_and_qualify\` has returned a launched or
|
|
23458
|
+
running result, that work is queued on Leadbay and runs to completion, and the
|
|
23459
|
+
quota it costs is already committed. A discovery, preview or \`dry_run\` result
|
|
23460
|
+
launched nothing and is not covered here.
|
|
23461
|
+
|
|
23462
|
+
The user cancelling in the chat, a request timeout, or a closed stream stops YOUR
|
|
23463
|
+
waiting, never the job. \`cancelled: true\` means we stopped watching, not that the
|
|
23464
|
+
work stopped. What to do next depends on what you are holding:
|
|
23465
|
+
|
|
23466
|
+
- **A handle.** Poll the status tool with it, and do not launch the work that
|
|
23467
|
+
handle covers a second time \u2014 that spends the quota again on the same rows.
|
|
23468
|
+
\`leadbay_import_status\` takes \`importIds\`, so pass the values of \`import_ids\`
|
|
23469
|
+
under that name. A qualification started by \`leadbay_import_and_qualify\` has no
|
|
23470
|
+
notification of its own: resume it with
|
|
23471
|
+
\`leadbay_qualify_status({lead_ids, lens_id})\`.
|
|
23472
|
+
- **A handle AND a subset the result says never started** \u2014 \`failed[]\` entries
|
|
23473
|
+
with \`error:"not_queued"\`, or a \`rows_pending_upload\` count. Poll the handle
|
|
23474
|
+
for what was launched and re-run for that subset only, never for the whole
|
|
23475
|
+
batch.
|
|
23476
|
+
- **No result at all**, because the call timed out or the stream closed before it
|
|
23477
|
+
returned. Check \`leadbay_account_status\` first: the launch may have landed and
|
|
23478
|
+
finished. Calling the same tool again with the same arguments will usually hand
|
|
23479
|
+
back the job already launched rather than starting a second one, but that guard
|
|
23480
|
+
is in-memory, five minutes, and per process, so it is best-effort \u2014 say what you
|
|
23481
|
+
are about to re-run before you spend the user's quota on it.
|
|
23482
|
+
|
|
23483
|
+
|
|
23869
23484
|
|
|
23870
23485
|
## The two entry points
|
|
23871
23486
|
|
|
@@ -24026,7 +23641,7 @@ If the prompt's body and the tool's RENDERING appear to conflict, the tool's REN
|
|
|
24026
23641
|
# PHASE 1 \u2014 LAUNCH
|
|
24027
23642
|
Call \`leadbay_bulk_qualify_leads\` with \`count={{arg:count_or_default}}\` and \`wait_for_completion=true\` (synchronous mode \u2014 waits for results before returning).
|
|
24028
23643
|
|
|
24029
|
-
**Resilience rule:** If \`leadbay_bulk_qualify_leads\` returns
|
|
23644
|
+
**Resilience rule:** If \`leadbay_bulk_qualify_leads\` returns an infrastructure error, do NOT retry with \`wait_for_completion=false\`. Instead, proceed directly to Phase 3 and call \`leadbay_pull_leads\` to surface the already-qualified leads in the current batch.
|
|
24030
23645
|
|
|
24031
23646
|
# PHASE 2 \u2014 POLL
|
|
24032
23647
|
While it polls, expect notifications / progress events showing per-lead transitions. Surface meaningful ones (e.g. "lead X just finished") to me as they arrive \u2014 one inline status sentence per check, never expanded into a card:
|
|
@@ -24525,7 +24140,7 @@ If the prompt's body and the tool's RENDERING appear to conflict, the tool's REN
|
|
|
24525
24140
|
|
|
24526
24141
|
# Resilience rules for Leadbay long-running tools
|
|
24527
24142
|
|
|
24528
|
-
These
|
|
24143
|
+
These rules apply to every Leadbay workflow that calls \`leadbay_pull_leads\`, \`leadbay_bulk_qualify_leads\`, \`leadbay_research_lead_by_id\`, \`leadbay_import_and_qualify\`, or \`leadbay_enrich_titles\`. **Treat timeouts and stream-closed errors as transient, not as signals to replan.**
|
|
24529
24144
|
|
|
24530
24145
|
## Rule 1 \u2014 Pin the lens
|
|
24531
24146
|
|
|
@@ -24533,7 +24148,7 @@ After your first \`leadbay_pull_leads\` call, capture \`response.lens.id\` into
|
|
|
24533
24148
|
|
|
24534
24149
|
## Rule 2 \u2014 Prefer async for bulk operations
|
|
24535
24150
|
|
|
24536
|
-
\`leadbay_bulk_qualify_leads\` and \`leadbay_import_and_qualify\` accept \`wait_for_completion:false
|
|
24151
|
+
\`leadbay_bulk_qualify_leads\` and \`leadbay_import_and_qualify\` accept \`wait_for_completion:false\` and return immediately. They hand back different ids: \`bulk_qualify_leads\` returns \`{status:'running', notification_id, lead_ids, lens_id}\` \u2014 poll \`leadbay_qualify_status\` with those. \`import_and_qualify\` returns \`{status:'running', import_ids}\` and no \`notification_id\` at all \u2014 poll \`leadbay_import_status({importIds, dry_run})\` with those. Poll every ~10s until the job completes. **Use the async pattern by default** \u2014 the blocking default can exceed the MCP client's per-call timeout on large batches and produce a misleading \`"Request timed out"\` even though the server is still working.
|
|
24537
24152
|
|
|
24538
24153
|
## Rule 3 \u2014 Serialize \`leadbay_research_lead_by_id\` fan-out
|
|
24539
24154
|
|
|
@@ -24549,6 +24164,36 @@ If a Leadbay tool returns \`"Request timed out"\`, \`"stream closed"\`, or any o
|
|
|
24549
24164
|
|
|
24550
24165
|
If \`pull_leads\` itself fails and you have no prior batch, then yes \u2014 retry it, explicitly pass the lensId you captured (if any), and continue.
|
|
24551
24166
|
|
|
24167
|
+
## A launched job cannot be stopped
|
|
24168
|
+
|
|
24169
|
+
Leadbay has no cancel. Once \`leadbay_enrich_titles\`, \`leadbay_bulk_qualify_leads\`,
|
|
24170
|
+
\`leadbay_import_leads\` or \`leadbay_import_and_qualify\` has returned a launched or
|
|
24171
|
+
running result, that work is queued on Leadbay and runs to completion, and the
|
|
24172
|
+
quota it costs is already committed. A discovery, preview or \`dry_run\` result
|
|
24173
|
+
launched nothing and is not covered here.
|
|
24174
|
+
|
|
24175
|
+
The user cancelling in the chat, a request timeout, or a closed stream stops YOUR
|
|
24176
|
+
waiting, never the job. \`cancelled: true\` means we stopped watching, not that the
|
|
24177
|
+
work stopped. What to do next depends on what you are holding:
|
|
24178
|
+
|
|
24179
|
+
- **A handle.** Poll the status tool with it, and do not launch the work that
|
|
24180
|
+
handle covers a second time \u2014 that spends the quota again on the same rows.
|
|
24181
|
+
\`leadbay_import_status\` takes \`importIds\`, so pass the values of \`import_ids\`
|
|
24182
|
+
under that name. A qualification started by \`leadbay_import_and_qualify\` has no
|
|
24183
|
+
notification of its own: resume it with
|
|
24184
|
+
\`leadbay_qualify_status({lead_ids, lens_id})\`.
|
|
24185
|
+
- **A handle AND a subset the result says never started** \u2014 \`failed[]\` entries
|
|
24186
|
+
with \`error:"not_queued"\`, or a \`rows_pending_upload\` count. Poll the handle
|
|
24187
|
+
for what was launched and re-run for that subset only, never for the whole
|
|
24188
|
+
batch.
|
|
24189
|
+
- **No result at all**, because the call timed out or the stream closed before it
|
|
24190
|
+
returned. Check \`leadbay_account_status\` first: the launch may have landed and
|
|
24191
|
+
finished. Calling the same tool again with the same arguments will usually hand
|
|
24192
|
+
back the job already launched rather than starting a second one, but that guard
|
|
24193
|
+
is in-memory, five minutes, and per process, so it is best-effort \u2014 say what you
|
|
24194
|
+
are about to re-run before you spend the user's quota on it.
|
|
24195
|
+
|
|
24196
|
+
|
|
24552
24197
|
|
|
24553
24198
|
# PHASE 0 \u2014 SCOPE + STATE
|
|
24554
24199
|
|
|
@@ -24646,7 +24291,7 @@ So: **read the persisted filter first** (the response reports \`active_filters\`
|
|
|
24646
24291
|
|
|
24647
24292
|
\u26A0 **Pass explicit \`leadIds\` whenever the cohort isn't simply "the next N on the lens"** \u2014 e.g. after you've selected a shortlist, or when the plan mixes Monitor and Discover rows. The \`count\`-based path selects the next *unqualified leads from the lens wishlist*, so on any other cohort it qualifies unrelated leads and hands you handles whose pills belong to different companies. Use \`leadbay_bulk_qualify_leads({leadIds:[\u2026\u226425 of the cohort], wait_for_completion:false})\` and chunk through the cohort's own ids. The \`{lensId, count}\` form is only right when the cohort genuinely *is* the lens's top N.
|
|
24648
24293
|
|
|
24649
|
-
**Qualify the plan cohort, not the whole base.** Select your ~{{arg:count_or_default}} candidates (plus a modest buffer for drop-outs) BEFORE qualifying \u2014 qualification is async and quota-bearing, so running it across an entire portfolio to produce a top-{{arg:count_or_default}} burns the user's quota for rows that will never appear. **Keep every returned \`
|
|
24294
|
+
**Qualify the plan cohort, not the whole base.** Select your ~{{arg:count_or_default}} candidates (plus a modest buffer for drop-outs) BEFORE qualifying \u2014 qualification is async and quota-bearing, so running it across an entire portfolio to produce a top-{{arg:count_or_default}} burns the user's quota for rows that will never appear. **Keep every returned \`notification_id\`** \u2014 the deck's live qualification layer is wired from those handles, and a deck with none is a dead deck that still looks finished. Never ship a plan whose lower ranks have empty qualification pills because only the first 25 were ever qualified.
|
|
24650
24295
|
|
|
24651
24296
|
**Signals \u2014 scoped to the cohort.** \u26A0 **Always pass the selected \`leadIds\`.** With \`leadIds\` omitted, \`leadbay_scan_portfolio_signals\` builds its own portfolio by paging \`/monitor\` \u2014 so on an imported cohort or a freshly-pulled Discover set it would scan a *different population* and you'd render dashes for accounts whose signals were never read.
|
|
24652
24297
|
|
|
@@ -24773,7 +24418,7 @@ Each card needs a reachable decision-maker. \`leadbay_enrich_titles({leadIds, le
|
|
|
24773
24418
|
|
|
24774
24419
|
\u26A0 **Do NOT quote a cost or a credits figure.** The per-reveal rate is backend-side and enrichment is gated by quota, not a credit balance; \`credits_remaining\` is advisory context only. A spend number invented to make the offer concrete is the same failure as an invented euro on a card.
|
|
24775
24420
|
|
|
24776
|
-
On an explicit yes, launch with the agreed \`titles\` + channels, then poll \`leadbay_bulk_enrich_status\` until done and **keep the \`
|
|
24421
|
+
On an explicit yes, launch with the agreed \`titles\` + channels, then poll \`leadbay_bulk_enrich_status\` until done and **keep the \`notification_id\` handles** for the deck.
|
|
24777
24422
|
|
|
24778
24423
|
\u26A0 **Render only the channels that actually came back.** The default reveal is email-only unless phone was explicitly requested, so never emit a \`tel:\` link for a contact whose phone was never revealed \u2014 show the channels enrichment returned and mark the rest omitted. A fabricated phone link is the same failure as a fabricated euro.
|
|
24779
24424
|
|
|
@@ -24919,9 +24564,9 @@ ChatGPT exposes the same routing pattern via \`_meta.openai/outputTemplate\`. We
|
|
|
24919
24564
|
- One short intro sentence in chat is enough \u2014 "Here are your 5 NYC follow-ups." Then route into the widget.
|
|
24920
24565
|
|
|
24921
24566
|
|
|
24922
|
-
\u26A0 **The deck's contact layer depends on what actually happened in Phase 5.** Bind a \`leadbay_bulk_enrich_status\` resource ONLY if a paid reveal was launched and you hold a \`
|
|
24567
|
+
\u26A0 **The deck's contact layer depends on what actually happened in Phase 5.** Bind a \`leadbay_bulk_enrich_status\` resource ONLY if a paid reveal was launched and you hold a \`notification_id\`. If the user accepted the deck but not the reveal, render the contacts already on record and carry the paid-reveal offer inside the deck \u2014 never wire a status resource with no handle (it renders permanently empty) and never launch enrichment from the deck to manufacture one.
|
|
24923
24568
|
|
|
24924
|
-
On acceptance, call \`leadbay_artifact_kit\`, read its \`usage_guide\` before writing any code, and build a single-file deck. Wire the live layer from the handles you kept: a poll-until-done resource per \`
|
|
24569
|
+
On acceptance, call \`leadbay_artifact_kit\`, read its \`usage_guide\` before writing any code, and build a single-file deck. Wire the live layer from the handles you kept: a poll-until-done resource per \`notification_id\` for the qualification pills, and one over \`leadbay_bulk_enrich_status\` for the contacts. \u26A0 **If enrichment already ran this session, bind the existing \`notification_id\` \u2014 re-launching enrichment from the deck double-spends my quota.** Per-card notes and outcomes go through the pre-wired note/outreach view-models (they carry the required verification and \`_triggered_by\` fields; hand-rolling those is where it breaks). Keep the checklists in local storage, and always wire a Refresh \u2014 auto-poll is host-dependent. List every tool the deck calls in its \`mcp_tools\`, and render the bridge-unavailable branch, or the pills silently show empty.
|
|
24925
24570
|
|
|
24926
24571
|
# Iron laws
|
|
24927
24572
|
|
|
@@ -26197,7 +25842,7 @@ function buildProtocolPrimitivesParagraph(has) {
|
|
|
26197
25842
|
}
|
|
26198
25843
|
if (longRunners.length > 0) {
|
|
26199
25844
|
parts.push(
|
|
26200
|
-
"(2) `notifications/cancelled` \u2014 when the user clicks Cancel in the host UI, the polling loop exits within \u22642 seconds
|
|
25845
|
+
"(2) `notifications/cancelled` \u2014 when the user clicks Cancel in the host UI, the polling loop exits within \u22642 seconds. The job itself keeps running on the backend; poll its notification_id / importIds later to pick it up."
|
|
26201
25846
|
);
|
|
26202
25847
|
} else {
|
|
26203
25848
|
parts.push(
|
|
@@ -26717,7 +26362,6 @@ ${url}
|
|
|
26717
26362
|
const shapeError = findShapeMismatch(tool, args);
|
|
26718
26363
|
const result = shapeError ?? await runWithRequestSignal(extra.signal, () => tool.execute(client, args, {
|
|
26719
26364
|
logger: opts.logger,
|
|
26720
|
-
bulkTracker: opts.bulkTracker,
|
|
26721
26365
|
notificationsInbox: opts.notificationsInbox,
|
|
26722
26366
|
signal: extra.signal,
|
|
26723
26367
|
progress,
|
|
@@ -26971,10 +26615,10 @@ ${url}
|
|
|
26971
26615
|
import { spawn } from "child_process";
|
|
26972
26616
|
import { existsSync as existsSync2, readFileSync as readFileSync2, readdirSync as readdirSync2 } from "fs";
|
|
26973
26617
|
import { join as join2 } from "path";
|
|
26974
|
-
import { homedir
|
|
26618
|
+
import { homedir } from "os";
|
|
26975
26619
|
var HOSTED_MCP_URL_CHATGPT = "https://mcp.leadbay.app/chatgpt/mcp";
|
|
26976
|
-
function formatInstallOsLabel(
|
|
26977
|
-
const name =
|
|
26620
|
+
function formatInstallOsLabel(platform = process.platform, arch = process.arch) {
|
|
26621
|
+
const name = platform === "darwin" ? "macOS" : platform === "win32" ? "Windows" : platform === "linux" ? "Linux" : platform;
|
|
26978
26622
|
return `${name} (${arch})`;
|
|
26979
26623
|
}
|
|
26980
26624
|
function detectClaudeDesktopMode(claudeSupportDir) {
|
|
@@ -27089,7 +26733,7 @@ async function isCursorInstalled(home) {
|
|
|
27089
26733
|
}
|
|
27090
26734
|
async function detectClients() {
|
|
27091
26735
|
const out = [];
|
|
27092
|
-
const home =
|
|
26736
|
+
const home = homedir();
|
|
27093
26737
|
const claudeBin = await findOnPath("claude");
|
|
27094
26738
|
if (claudeBin) {
|
|
27095
26739
|
out.push({ id: "claude-code", label: "Claude Code", detail: `${claudeBin} mcp add ...` });
|
|
@@ -27198,7 +26842,7 @@ async function uninstallFromClaudeCode() {
|
|
|
27198
26842
|
async function installInJsonConfig(configPath, token, region, includeWrite, telemetryEnabled, localBinPath) {
|
|
27199
26843
|
try {
|
|
27200
26844
|
const { readFileSync: readFileSync3, writeFileSync, existsSync: existsSync4, mkdirSync, statSync } = await import("fs");
|
|
27201
|
-
const { dirname:
|
|
26845
|
+
const { dirname: dirname2 } = await import("path");
|
|
27202
26846
|
let parsed = {};
|
|
27203
26847
|
let preserved = {};
|
|
27204
26848
|
const existed = existsSync4(configPath);
|
|
@@ -27211,7 +26855,7 @@ async function installInJsonConfig(configPath, token, region, includeWrite, tele
|
|
|
27211
26855
|
return { ok: false, message: `existing ${configPath} is not valid JSON; refusing to overwrite` };
|
|
27212
26856
|
}
|
|
27213
26857
|
} else {
|
|
27214
|
-
mkdirSync(
|
|
26858
|
+
mkdirSync(dirname2(configPath), { recursive: true });
|
|
27215
26859
|
}
|
|
27216
26860
|
parsed.mcpServers = parsed.mcpServers ?? {};
|
|
27217
26861
|
const env = {
|
|
@@ -27336,13 +26980,13 @@ function stripShellExportBlock(existing) {
|
|
|
27336
26980
|
async function installInCodexConfig(configPath, includeWrite, telemetryEnabled, localBinPath) {
|
|
27337
26981
|
try {
|
|
27338
26982
|
const { readFileSync: readFileSync3, writeFileSync, existsSync: existsSync4, mkdirSync, statSync, renameSync, chmodSync } = await import("fs");
|
|
27339
|
-
const { dirname:
|
|
26983
|
+
const { dirname: dirname2 } = await import("path");
|
|
27340
26984
|
let existing = "";
|
|
27341
26985
|
const existed = existsSync4(configPath);
|
|
27342
26986
|
if (existed) {
|
|
27343
26987
|
existing = readFileSync3(configPath, "utf8");
|
|
27344
26988
|
} else {
|
|
27345
|
-
mkdirSync(
|
|
26989
|
+
mkdirSync(dirname2(configPath), { recursive: true });
|
|
27346
26990
|
}
|
|
27347
26991
|
const hadLeadbayConfig = /(^|\r?\n)\[mcp_servers\.leadbay\]\r?\n/.test(existing);
|
|
27348
26992
|
const next = mergeCodexConfig(
|
|
@@ -27660,17 +27304,17 @@ function parseWriteEnv(env = process.env) {
|
|
|
27660
27304
|
|
|
27661
27305
|
// src/update-state.ts
|
|
27662
27306
|
import {
|
|
27663
|
-
mkdir as
|
|
27664
|
-
lstat
|
|
27665
|
-
open as
|
|
27666
|
-
readFile
|
|
27667
|
-
rename
|
|
27668
|
-
stat
|
|
27669
|
-
unlink
|
|
27307
|
+
mkdir as mkdirAsync,
|
|
27308
|
+
lstat,
|
|
27309
|
+
open as fsOpen,
|
|
27310
|
+
readFile,
|
|
27311
|
+
rename,
|
|
27312
|
+
stat,
|
|
27313
|
+
unlink
|
|
27670
27314
|
} from "fs/promises";
|
|
27671
|
-
import { constants as
|
|
27672
|
-
import { dirname
|
|
27673
|
-
import { homedir as
|
|
27315
|
+
import { constants as fsConstants } from "fs";
|
|
27316
|
+
import { dirname, resolve as resolvePath } from "path";
|
|
27317
|
+
import { homedir as homedir2 } from "os";
|
|
27674
27318
|
function emptyState() {
|
|
27675
27319
|
return {
|
|
27676
27320
|
last_check_time: 0,
|
|
@@ -27694,7 +27338,7 @@ var UpdateStateStore = class {
|
|
|
27694
27338
|
if (!opts.path) {
|
|
27695
27339
|
throw new Error("UpdateStateStore: path is required when backend=file");
|
|
27696
27340
|
}
|
|
27697
|
-
this.path =
|
|
27341
|
+
this.path = resolvePath(opts.path);
|
|
27698
27342
|
this.validatePath(this.path);
|
|
27699
27343
|
}
|
|
27700
27344
|
}
|
|
@@ -27706,7 +27350,7 @@ var UpdateStateStore = class {
|
|
|
27706
27350
|
}
|
|
27707
27351
|
validatePath(p) {
|
|
27708
27352
|
if (this.allowUnsafePath) return;
|
|
27709
|
-
const home =
|
|
27353
|
+
const home = resolvePath(homedir2());
|
|
27710
27354
|
if (p !== home && !p.startsWith(home + "/") && !p.startsWith(home + "\\")) {
|
|
27711
27355
|
throw new Error(
|
|
27712
27356
|
`UpdateStateStore: path ${p} is outside $HOME (${home}). Set LEADBAY_UPDATE_STATE_PATH_UNSAFE=1 to override.`
|
|
@@ -27718,10 +27362,10 @@ var UpdateStateStore = class {
|
|
|
27718
27362
|
this.initialized = true;
|
|
27719
27363
|
return;
|
|
27720
27364
|
}
|
|
27721
|
-
const dir =
|
|
27722
|
-
await
|
|
27365
|
+
const dir = dirname(this.path);
|
|
27366
|
+
await mkdirAsync(dir, { recursive: true, mode: 448 });
|
|
27723
27367
|
try {
|
|
27724
|
-
const st = await
|
|
27368
|
+
const st = await lstat(this.path);
|
|
27725
27369
|
if (st.isSymbolicLink()) {
|
|
27726
27370
|
throw new Error(
|
|
27727
27371
|
`UpdateStateStore: refusing to use ${this.path} \u2014 path is a symlink. Set LEADBAY_UPDATE_STATE_PATH_UNSAFE=1 to override.`
|
|
@@ -27737,7 +27381,7 @@ var UpdateStateStore = class {
|
|
|
27737
27381
|
await this.ensureInitialized();
|
|
27738
27382
|
let raw;
|
|
27739
27383
|
try {
|
|
27740
|
-
raw = await
|
|
27384
|
+
raw = await readFile(this.path, "utf8");
|
|
27741
27385
|
} catch (err) {
|
|
27742
27386
|
if (err?.code === "ENOENT") return emptyState();
|
|
27743
27387
|
throw err;
|
|
@@ -27759,13 +27403,13 @@ var UpdateStateStore = class {
|
|
|
27759
27403
|
}
|
|
27760
27404
|
await this.ensureInitialized();
|
|
27761
27405
|
const tmp = `${this.path}.tmp.${process.pid}.${this.now()}`;
|
|
27762
|
-
const handle = await
|
|
27406
|
+
const handle = await openTmpFileExclusive(tmp);
|
|
27763
27407
|
try {
|
|
27764
27408
|
await handle.writeFile(JSON.stringify(state, null, 2));
|
|
27765
27409
|
} finally {
|
|
27766
27410
|
await handle.close();
|
|
27767
27411
|
}
|
|
27768
|
-
await
|
|
27412
|
+
await rename(tmp, this.path);
|
|
27769
27413
|
}
|
|
27770
27414
|
/**
|
|
27771
27415
|
* Apply a partial mutation atomically (read → merge → write). Caller
|
|
@@ -27811,20 +27455,20 @@ var UpdateStateStore = class {
|
|
|
27811
27455
|
return out;
|
|
27812
27456
|
}
|
|
27813
27457
|
};
|
|
27814
|
-
async function
|
|
27458
|
+
async function openTmpFileExclusive(path) {
|
|
27815
27459
|
try {
|
|
27816
|
-
return await
|
|
27460
|
+
return await fsOpen(
|
|
27817
27461
|
path,
|
|
27818
|
-
|
|
27462
|
+
fsConstants.O_CREAT | fsConstants.O_WRONLY | fsConstants.O_EXCL,
|
|
27819
27463
|
384
|
|
27820
27464
|
);
|
|
27821
27465
|
} catch (err) {
|
|
27822
27466
|
if (err?.code === "EEXIST") {
|
|
27823
|
-
await
|
|
27467
|
+
await unlink(path).catch(() => {
|
|
27824
27468
|
});
|
|
27825
|
-
return
|
|
27469
|
+
return fsOpen(
|
|
27826
27470
|
path,
|
|
27827
|
-
|
|
27471
|
+
fsConstants.O_CREAT | fsConstants.O_WRONLY | fsConstants.O_EXCL,
|
|
27828
27472
|
384
|
|
27829
27473
|
);
|
|
27830
27474
|
}
|
|
@@ -27834,7 +27478,7 @@ async function openTmpFileExclusive2(path) {
|
|
|
27834
27478
|
async function createDefaultUpdateStateStore(opts = {}) {
|
|
27835
27479
|
const env = opts.env ?? process.env;
|
|
27836
27480
|
const allowUnsafePath = env.LEADBAY_UPDATE_STATE_PATH_UNSAFE === "1";
|
|
27837
|
-
const path = env.LEADBAY_UPDATE_STATE_PATH ??
|
|
27481
|
+
const path = env.LEADBAY_UPDATE_STATE_PATH ?? resolvePath(homedir2(), ".leadbay", "update-state.json");
|
|
27838
27482
|
try {
|
|
27839
27483
|
const store = new UpdateStateStore({
|
|
27840
27484
|
backend: "file",
|
|
@@ -27843,7 +27487,7 @@ async function createDefaultUpdateStateStore(opts = {}) {
|
|
|
27843
27487
|
allowUnsafePath
|
|
27844
27488
|
});
|
|
27845
27489
|
await store.ensureInitialized();
|
|
27846
|
-
await
|
|
27490
|
+
await stat(dirname(path));
|
|
27847
27491
|
return store;
|
|
27848
27492
|
} catch (err) {
|
|
27849
27493
|
opts.logger?.warn?.(
|
|
@@ -28154,14 +27798,14 @@ async function exchangeCodeForToken(opts) {
|
|
|
28154
27798
|
return { accessToken: parsed.access_token };
|
|
28155
27799
|
}
|
|
28156
27800
|
function browserOpenCandidates(url) {
|
|
28157
|
-
const
|
|
28158
|
-
if (
|
|
27801
|
+
const platform = process.platform;
|
|
27802
|
+
if (platform === "darwin") {
|
|
28159
27803
|
return [
|
|
28160
27804
|
{ cmd: "/usr/bin/open", args: [url] },
|
|
28161
27805
|
{ cmd: "open", args: [url] }
|
|
28162
27806
|
];
|
|
28163
27807
|
}
|
|
28164
|
-
if (
|
|
27808
|
+
if (platform === "win32") {
|
|
28165
27809
|
const sysRoot = process.env.SystemRoot || process.env.windir || "C:\\Windows";
|
|
28166
27810
|
const cmdExe = `${sysRoot}\\System32\\cmd.exe`;
|
|
28167
27811
|
const quoted = `"${url}"`;
|
|
@@ -28401,7 +28045,7 @@ var OAUTH_BASE_URLS = {
|
|
|
28401
28045
|
fr: "https://staging.api.leadbay.app"
|
|
28402
28046
|
}
|
|
28403
28047
|
};
|
|
28404
|
-
var VERSION = "0.
|
|
28048
|
+
var VERSION = "0.35.0";
|
|
28405
28049
|
var HELP = `
|
|
28406
28050
|
leadbay-mcp ${VERSION} \u2014 Leadbay Model Context Protocol server
|
|
28407
28051
|
|
|
@@ -28524,9 +28168,9 @@ function hydrateEnvFromCredentialsFile() {
|
|
|
28524
28168
|
function resolveOAuthBootstrapCredentialsPath() {
|
|
28525
28169
|
const resolved = resolveDefaultCredentialsPath();
|
|
28526
28170
|
if (process.env.LEADBAY_OAUTH_STAGING !== "1") return resolved;
|
|
28527
|
-
const { dirname:
|
|
28171
|
+
const { dirname: dirname2, join: join3 } = require_("node:path");
|
|
28528
28172
|
return {
|
|
28529
|
-
path: join3(
|
|
28173
|
+
path: join3(dirname2(resolved.path), "credentials.staging.json"),
|
|
28530
28174
|
legacy: resolved.legacy
|
|
28531
28175
|
};
|
|
28532
28176
|
}
|
|
@@ -28537,8 +28181,8 @@ function bootstrapDebug(msg) {
|
|
|
28537
28181
|
try {
|
|
28538
28182
|
const { appendFileSync, mkdirSync } = require_("node:fs");
|
|
28539
28183
|
const { join: join3 } = require_("node:path");
|
|
28540
|
-
const { homedir:
|
|
28541
|
-
const dir = join3(
|
|
28184
|
+
const { homedir: homedir3 } = require_("node:os");
|
|
28185
|
+
const dir = join3(homedir3(), ".leadbay");
|
|
28542
28186
|
mkdirSync(dir, { recursive: true });
|
|
28543
28187
|
const ts = (/* @__PURE__ */ new Date()).toISOString();
|
|
28544
28188
|
appendFileSync(join3(dir, "oauth-bootstrap-debug.log"), `${ts} [pid ${process.pid}] ${msg}
|
|
@@ -28548,8 +28192,8 @@ function bootstrapDebug(msg) {
|
|
|
28548
28192
|
}
|
|
28549
28193
|
function oauthClientCachePath() {
|
|
28550
28194
|
const { join: join3 } = require_("node:path");
|
|
28551
|
-
const { homedir:
|
|
28552
|
-
return join3(
|
|
28195
|
+
const { homedir: homedir3 } = require_("node:os");
|
|
28196
|
+
return join3(homedir3(), ".leadbay", "oauth-client.json");
|
|
28553
28197
|
}
|
|
28554
28198
|
function getCachedOAuthClientId(authServerBaseUrl, port) {
|
|
28555
28199
|
try {
|
|
@@ -28565,7 +28209,7 @@ function getCachedOAuthClientId(authServerBaseUrl, port) {
|
|
|
28565
28209
|
function cacheOAuthClientId(authServerBaseUrl, clientId, port) {
|
|
28566
28210
|
try {
|
|
28567
28211
|
const { readFileSync: readFileSync3, writeFileSync, mkdirSync } = require_("node:fs");
|
|
28568
|
-
const { dirname:
|
|
28212
|
+
const { dirname: dirname2 } = require_("node:path");
|
|
28569
28213
|
const path = oauthClientCachePath();
|
|
28570
28214
|
let data = { clients: {} };
|
|
28571
28215
|
try {
|
|
@@ -28577,7 +28221,7 @@ function cacheOAuthClientId(authServerBaseUrl, clientId, port) {
|
|
|
28577
28221
|
const byPort = server && typeof server === "object" && server.byPort && typeof server.byPort === "object" ? server.byPort : {};
|
|
28578
28222
|
byPort[String(port)] = clientId;
|
|
28579
28223
|
data.clients[authServerBaseUrl] = { byPort };
|
|
28580
|
-
mkdirSync(
|
|
28224
|
+
mkdirSync(dirname2(path), { recursive: true });
|
|
28581
28225
|
writeFileSync(path, JSON.stringify(data, null, 2) + "\n", { mode: 384 });
|
|
28582
28226
|
} catch {
|
|
28583
28227
|
}
|
|
@@ -28670,7 +28314,7 @@ async function bootstrapOAuthIfMissing(logger) {
|
|
|
28670
28314
|
});
|
|
28671
28315
|
try {
|
|
28672
28316
|
const { writeFileSync, mkdirSync, chmodSync } = require_("node:fs");
|
|
28673
|
-
const { dirname:
|
|
28317
|
+
const { dirname: dirname2 } = require_("node:path");
|
|
28674
28318
|
const { path } = resolveOAuthBootstrapCredentialsPath();
|
|
28675
28319
|
const envBlock = {
|
|
28676
28320
|
LEADBAY_TOKEN: accessToken,
|
|
@@ -28686,7 +28330,7 @@ async function bootstrapOAuthIfMissing(logger) {
|
|
|
28686
28330
|
}
|
|
28687
28331
|
}
|
|
28688
28332
|
};
|
|
28689
|
-
mkdirSync(
|
|
28333
|
+
mkdirSync(dirname2(path), { recursive: true });
|
|
28690
28334
|
writeFileSync(path, JSON.stringify(config, null, 2) + "\n", { mode: 384 });
|
|
28691
28335
|
try {
|
|
28692
28336
|
chmodSync(path, 384);
|
|
@@ -29127,8 +28771,8 @@ Restart your MCP client to pick up the new server.
|
|
|
29127
28771
|
let actualMode;
|
|
29128
28772
|
try {
|
|
29129
28773
|
const { writeFileSync, chmodSync, mkdirSync, renameSync, statSync, unlinkSync } = await import("fs");
|
|
29130
|
-
const { dirname:
|
|
29131
|
-
mkdirSync(
|
|
28774
|
+
const { dirname: dirname2 } = await import("path");
|
|
28775
|
+
mkdirSync(dirname2(targetPath), { recursive: true });
|
|
29132
28776
|
const tmp = targetPath + ".tmp." + process.pid;
|
|
29133
28777
|
writeFileSync(tmp, JSON.stringify(config, null, 2) + "\n", {
|
|
29134
28778
|
encoding: "utf8",
|
|
@@ -29586,7 +29230,6 @@ async function main() {
|
|
|
29586
29230
|
});
|
|
29587
29231
|
const includeAdvanced = process.env.LEADBAY_MCP_ADVANCED === "1";
|
|
29588
29232
|
const includeWrite = parseWriteEnv();
|
|
29589
|
-
const bulkTracker = await createDefaultBulkStore({ logger });
|
|
29590
29233
|
const updateStateStore = await createDefaultUpdateStateStore({ logger });
|
|
29591
29234
|
void recordRunningVersion(VERSION, updateStateStore, telemetry).catch((err) => {
|
|
29592
29235
|
logger.warn?.(
|
|
@@ -29623,7 +29266,6 @@ async function main() {
|
|
|
29623
29266
|
includeAdvanced,
|
|
29624
29267
|
includeWrite,
|
|
29625
29268
|
logger,
|
|
29626
|
-
bulkTracker,
|
|
29627
29269
|
notificationsInbox,
|
|
29628
29270
|
version: VERSION,
|
|
29629
29271
|
telemetry,
|
|
@@ -29643,7 +29285,7 @@ async function main() {
|
|
|
29643
29285
|
});
|
|
29644
29286
|
const transport = new StdioServerTransport();
|
|
29645
29287
|
logger.info?.(
|
|
29646
|
-
`Starting MCP server v${VERSION} (advanced=${includeAdvanced}, write=${includeWrite}, baseUrl=${client.baseUrl},
|
|
29288
|
+
`Starting MCP server v${VERSION} (advanced=${includeAdvanced}, write=${includeWrite}, baseUrl=${client.baseUrl}, notifications_ws=${WS_DISABLED ? "disabled" : "enabled"}, auth_state=${authState})`
|
|
29647
29289
|
);
|
|
29648
29290
|
await server.connect(transport);
|
|
29649
29291
|
if (bootstrapPending) {
|