@leadbay/mcp 0.38.2 → 0.39.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/bin.js CHANGED
@@ -1369,12 +1369,12 @@ var init_ws_client = __esm({
1369
1369
  this.pingTimer = setInterval(() => this.sendPing(), PING_INTERVAL_MS);
1370
1370
  });
1371
1371
  ws.addEventListener("message", (ev) => {
1372
- const text2 = typeof ev.data === "string" ? ev.data : String(ev.data ?? "");
1373
- if (!text2)
1372
+ const text3 = typeof ev.data === "string" ? ev.data : String(ev.data ?? "");
1373
+ if (!text3)
1374
1374
  return;
1375
1375
  let msg;
1376
1376
  try {
1377
- msg = JSON.parse(text2);
1377
+ msg = JSON.parse(text3);
1378
1378
  } catch {
1379
1379
  this.logger?.warn?.("notifications.ws non_json_frame");
1380
1380
  return;
@@ -2843,7 +2843,7 @@ measured:
2843
2843
  \`employees_max: number\`. FLAT numbers \u2014 nested \`employees: {min, max}\`
2844
2844
  exists only in RESULT payloads. \`example_lead.employees\` does not filter.
2845
2845
  \`locations\` take city/state/region names ("Dallas, TX", "\xCEle-de-France");
2846
- a country name is refused in code \u2014 whole-country intent = omit it.
2846
+ never a country: this workspace's own is dropped, any other is refused.
2847
2847
  7. **Prefer \`example_lead\` over \`query\`.** Query matches topic *vocabulary*:
2848
2848
  "gyms that need durable flooring" surfaced flooring VENDORS, 0 delivered.
2849
2849
  Use \`query\` only for signal an example can't express.
@@ -3913,8 +3913,31 @@ server-side: past the 30-min wall clock a job reads \`completed_partial\`
3913
3913
  delivered leads via leadbay_qualify_leads \`prior_deliveries\`). A 404 means
3914
3914
  unknown job or another org's job.
3915
3915
 
3916
+ \`compact: true\` (pass it whenever \`next_poll\` carries it) returns the rows of
3917
+ a free identity pass, 100 at a time from \`offset\`, instead of leads.
3918
+
3916
3919
  ---
3917
3920
 
3921
+ ## RENDERING \u2014 identity pass (\`rows[]\`)
3922
+
3923
+ \`rows[]\` replaces \`leads[]\`: one row per company, in the user's order. One
3924
+ table: **Company** (\`input\`, plus \`name\` when it differs) \xB7 **Website** \xB7
3925
+ **LinkedIn**. Skipped rows stay in it: \`not_in_universe\` \u2192 "not in Leadbay",
3926
+ \`low_confidence_identity\` \u2192 "several matches \u2014 add a city or website".
3927
+
3928
+ Then ONE coverage line from \`summary\`: found \`resolved\` of \`rows_total\` \xB7
3929
+ \`with_website\` with a website \xB7 \`with_linkedin\` with a LinkedIn \xB7
3930
+ \`ambiguous\` unclear \xB7 \`not_found\` not found. When most rows lack what the
3931
+ user asked for, that line is the answer: say so, and do not look the
3932
+ missing ones up one by one.
3933
+
3934
+ Done job with \`next_poll.offset\`: say "showing N of \`rows_total\`" and offer
3935
+ the rest via \`leadbay_lead_job_status(job_id, compact: true, offset)\`.
3936
+
3937
+ \`file\` set (local install only): every row is saved there as a CSV. Give the
3938
+ user that path.
3939
+
3940
+
3918
3941
  ## RENDERING \u2014 delivery table + honest funnel line
3919
3942
 
3920
3943
  Render delivered leads (\`leads[]\`, i.e. items with status \`delivered\` or
@@ -5157,13 +5180,14 @@ This tool MUTATES state. The caller (agent or human-in-the-loop) is responsible
5157
5180
  `;
5158
5181
  leadbay_qualify_leads = `## WHEN TO USE
5159
5182
 
5160
- Trigger phrases: "qualify these companies", "vet this list", "which of these fit our ICP", "score these websites / accounts", "get me the right contact at these companies", "re-qualify what you delivered last week".
5183
+ Trigger phrases: "for each of these companies, give me the website / LinkedIn", "qualify these companies", "vet this list", "which of these fit our ICP", "score these websites / accounts", "get me the right contact at these companies", "re-qualify what you delivered last week".
5161
5184
 
5162
5185
  Do NOT use for: "find me new leads / companies that <profile>" \u2192 \`leadbay_find_new_leads\`; "qualify the top N of my lens batch" \u2192 \`leadbay_bulk_qualify_leads\`; "import this CSV file" \u2192 \`leadbay_import_leads\`; "tell me about <one company> in depth" \u2192 \`leadbay_research_lead_by_name_fuzzy\`; "add emails to the contacts I selected" \u2192 \`leadbay_enrich_titles\`.
5163
5186
 
5164
5187
  Prefer when: the user points at SPECIFIC companies (ids, websites, names, a pasted list, "what you found yesterday") and wants fit verdicts and/or the right person to talk to.
5165
5188
 
5166
5189
  Examples that SHOULD invoke this tool:
5190
+ - "For each of these 300 brokers, give me the website and LinkedIn, nothing else."
5167
5191
  - "Here are 60 restaurant websites from my Austin sweep \u2014 which fit, and who's the owner?"
5168
5192
  - "Re-qualify last week's delivery and get phone numbers for the good ones."
5169
5193
  - "Vet these 12 accounts from my spreadsheet against our criteria."
@@ -5175,7 +5199,8 @@ Examples that should NOT invoke this tool (sound similar, route elsewhere):
5175
5199
 
5176
5200
  ## RENDER (quick)
5177
5201
 
5178
- 3-col table for delivered items (fit bar + company / why-fits \u226420 words /
5202
+ Identity pass (\`rows[]\`): one table Company / Website / LinkedIn, then the
5203
+ coverage line. Otherwise a 3-col table for delivered items (fit bar + company / why-fits \u226420 words /
5179
5204
  contact + channels) in returned order, then a compact Ref \u2192 Outcome table
5180
5205
  for skipped refs (not_in_universe, low_confidence_identity, ... in plain
5181
5206
  words), then the honest funnel line. Full algorithm below.
@@ -5190,6 +5215,14 @@ when asked \u2014 enriched with verified channels. Answers arrive per-item from
5190
5215
  job; this tool polls up to \`wait_seconds\` (default 45) and hands off to
5191
5216
  \`leadbay_lead_job_status\` when the batch needs longer.
5192
5217
 
5218
+ **A list of companies is ONE call, never a loop** of
5219
+ \`leadbay_research_lead_by_name_fuzzy\`: "for each of these companies give me
5220
+ X", a pasted list, a file. Identity only (in Leadbay or not, website,
5221
+ LinkedIn) \u2192 \`qualify: false\`: free, no \`confirm\`, returns \`rows[]\` and
5222
+ coverage counts. A fit question over the list ("which of these do
5223
+ factoring") keeps \`qualify: true\` and its quote. Over 500: one call per 500,
5224
+ each with its own \`request_id\`.
5225
+
5193
5226
  **Refs are flexible; outcomes are per-item.** \`lead_refs\` accepts any mix of
5194
5227
  \`lead_id\`, \`website\`, \`name\`(+\`location\`), or a stable \`contact_id\` from a
5195
5228
  prior result (enrichment then targets exactly that person, never a re-match).
@@ -5226,6 +5259,26 @@ Retry-After beyond \u2014 wait, don't hammer), 30-min job wall clock.
5226
5259
 
5227
5260
  ---
5228
5261
 
5262
+ ## RENDERING \u2014 identity pass (\`rows[]\`)
5263
+
5264
+ \`rows[]\` replaces \`leads[]\`: one row per company, in the user's order. One
5265
+ table: **Company** (\`input\`, plus \`name\` when it differs) \xB7 **Website** \xB7
5266
+ **LinkedIn**. Skipped rows stay in it: \`not_in_universe\` \u2192 "not in Leadbay",
5267
+ \`low_confidence_identity\` \u2192 "several matches \u2014 add a city or website".
5268
+
5269
+ Then ONE coverage line from \`summary\`: found \`resolved\` of \`rows_total\` \xB7
5270
+ \`with_website\` with a website \xB7 \`with_linkedin\` with a LinkedIn \xB7
5271
+ \`ambiguous\` unclear \xB7 \`not_found\` not found. When most rows lack what the
5272
+ user asked for, that line is the answer: say so, and do not look the
5273
+ missing ones up one by one.
5274
+
5275
+ Done job with \`next_poll.offset\`: say "showing N of \`rows_total\`" and offer
5276
+ the rest via \`leadbay_lead_job_status(job_id, compact: true, offset)\`.
5277
+
5278
+ \`file\` set (local install only): every row is saved there as a CSV. Give the
5279
+ user that path.
5280
+
5281
+
5229
5282
  ## RENDERING \u2014 delivery table + honest funnel line
5230
5283
 
5231
5284
  Render delivered leads (\`leads[]\`, i.e. items with status \`delivered\` or
@@ -5380,6 +5433,8 @@ Pick the 2-3 options that match what actually happened:
5380
5433
  | Items skipped \`low_confidence_identity\` | "Pick the right match" (show \`resolution.alternatives\`) | leadbay_qualify_leads with the chosen lead_id |
5381
5434
  | Contacts delivered without channels | "Get verified emails/phones for the keepers (uses quota \u2014 say so first)" | leadbay_qualify_leads(lead_refs with contact_id, channels) |
5382
5435
  | Disqualified with evidence | "Review why \u2014 adjust qualification questions if the criteria are off" | leadbay_get_qualification_questions |
5436
+ | Identity pass, list over 500 | "Run the next 500" | leadbay_qualify_leads(next 500, qualify: false) |
5437
+ | Identity pass, \`summary.without_website\` > 0. LAST option, never run unasked | "N have no website in Leadbay. Web research can look for one. It uses your plan's quota and may find none." | leadbay_qualify_leads(lead_refs: [{lead_id}] of rows without website, qualify: true) \u2192 quote first, confirm on the user's yes |
5383
5438
  `;
5384
5439
  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:
5385
5440
 
@@ -5858,9 +5913,9 @@ out?"\`
5858
5913
 
5859
5914
  Trigger phrases: "look up <Company>", "research <Company>", "what do we know about <Company>".
5860
5915
 
5861
- Do NOT use for: "picked row with leadId" \u2192 \`leadbay_research_lead_by_id\`; "draft outreach for <Contact>" \u2192 \`leadbay_prepare_outreach\`.
5916
+ Do NOT use for: "picked row with leadId" \u2192 \`leadbay_research_lead_by_id\`; "draft outreach for <Contact>" \u2192 \`leadbay_prepare_outreach\`; "for each of these companies\u2026" \u2192 \`leadbay_qualify_leads\`.
5862
5917
 
5863
- Prefer when: a company name or domain in prose, no Leadbay id yet \u2014 always pass \`website\` if a domain was mentioned
5918
+ Prefer when: ONE company name or domain in prose, no Leadbay id yet \u2014 always pass \`website\` if a domain was mentioned
5864
5919
 
5865
5920
  Examples that SHOULD invoke this tool:
5866
5921
  - "Look up Acme Corp for me."
@@ -5870,7 +5925,7 @@ Examples that SHOULD invoke this tool:
5870
5925
  Examples that should NOT invoke this tool (sound similar, route elsewhere):
5871
5926
  - "Tell me about that lead I just picked."
5872
5927
  - "Draft outreach to Acme's CTO."
5873
- - "Show me today's leads."
5928
+ - "Websites and LinkedIn for these 200 companies."
5874
5929
 
5875
5930
  ---
5876
5931
 
@@ -5884,15 +5939,13 @@ list" into an answer. With only a contact email, pass \`email\`: the company
5884
5939
  domain is derived from it, consumer mailboxes ignored.
5885
5940
 
5886
5941
  When the registry cannot pick one company it returns \`{resolution:
5887
- "ambiguous", query, candidates:[{leadId, name, website, location, \u2026}]}\`
5888
- instead of a card. Ask which one; never guess from \`score\`.
5942
+ "ambiguous", query, candidates:[\u2026]}\` instead of a card. Ask which one; never
5943
+ guess from \`score\`.
5889
5944
 
5890
5945
  \`LEAD_NOT_FOUND\` is not a dead end: its hint names the field that would have
5891
5946
  found it \u2014 \`website\` or \`registry_number\`, both params. Ask for it and call
5892
5947
  again. Do not offer an import before asking.
5893
5948
 
5894
- Offer \`_meta.match_candidates\` when present.
5895
-
5896
5949
  ---
5897
5950
 
5898
5951
  ## RENDERING \u2014 single-record research card, mode-adaptive
@@ -7199,7 +7252,7 @@ measured:
7199
7252
  \`employees_max: number\`. FLAT numbers \u2014 nested \`employees: {min, max}\`
7200
7253
  exists only in RESULT payloads. \`example_lead.employees\` does not filter.
7201
7254
  \`locations\` take city/state/region names ("Dallas, TX", "\xCEle-de-France");
7202
- a country name is refused in code \u2014 whole-country intent = omit it.
7255
+ never a country: this workspace's own is dropped, any other is refused.
7203
7256
  7. **Prefer \`example_lead\` over \`query\`.** Query matches topic *vocabulary*:
7204
7257
  "gyms that need durable flooring" surfaced flooring VENDORS, 0 delivered.
7205
7258
  Use \`query\` only for signal an example can't express.
@@ -7665,12 +7718,28 @@ var init_list_lenses = __esm({
7665
7718
  }
7666
7719
  });
7667
7720
 
7721
+ // ../core/dist/interactions.js
7722
+ function reportLeadInteractions(client, lensId, leadIds, types, logger) {
7723
+ if (leadIds.length === 0 || types.length === 0)
7724
+ return;
7725
+ const events = leadIds.flatMap((leadId) => types.map((type) => ({ type, lead_id: leadId, lens_id: String(lensId) })));
7726
+ void client.request("POST", "/interactions", events).catch((err) => {
7727
+ logger?.warn?.(`interactions: ${types.join("+")} for ${leadIds.length} lead(s) not recorded: ${err?.message ?? err?.code ?? err}`);
7728
+ });
7729
+ }
7730
+ var init_interactions = __esm({
7731
+ "../core/dist/interactions.js"() {
7732
+ "use strict";
7733
+ }
7734
+ });
7735
+
7668
7736
  // ../core/dist/tools/discover-leads.js
7669
7737
  var discoverLeads;
7670
7738
  var init_discover_leads = __esm({
7671
7739
  "../core/dist/tools/discover-leads.js"() {
7672
7740
  "use strict";
7673
7741
  init_tool_descriptions_generated();
7742
+ init_interactions();
7674
7743
  discoverLeads = {
7675
7744
  name: "leadbay_discover_leads",
7676
7745
  annotations: {
@@ -7699,11 +7768,12 @@ var init_discover_leads = __esm({
7699
7768
  },
7700
7769
  additionalProperties: false
7701
7770
  },
7702
- execute: async (client, params) => {
7771
+ execute: async (client, params, ctx) => {
7703
7772
  const lensId = params.lensId ?? await client.resolveDefaultLens();
7704
7773
  const page = params.page ?? 0;
7705
7774
  const count = Math.min(params.count ?? 20, 50);
7706
7775
  const res = await client.request("GET", `/lenses/${lensId}/leads/wishlist?count=${count}&page=${page}&contacts=true`);
7776
+ reportLeadInteractions(client, lensId, res.items.map((lead) => lead.id), ["LEAD_SEEN"], ctx?.logger);
7707
7777
  const totalPages = res.pagination?.pages ?? 0;
7708
7778
  const currentPage = res.pagination?.page ?? page;
7709
7779
  const hasMore = currentPage < totalPages - 1;
@@ -7752,6 +7822,7 @@ var init_get_lead_profile = __esm({
7752
7822
  "../core/dist/tools/get-lead-profile.js"() {
7753
7823
  "use strict";
7754
7824
  init_tool_descriptions_generated();
7825
+ init_interactions();
7755
7826
  getLeadProfile = {
7756
7827
  name: "leadbay_get_lead_profile",
7757
7828
  annotations: {
@@ -7799,13 +7870,9 @@ var init_get_lead_profile = __esm({
7799
7870
  },
7800
7871
  required: ["lead", "contacts"]
7801
7872
  },
7802
- execute: async (client, params) => {
7873
+ execute: async (client, params, ctx) => {
7803
7874
  const lensId = params.lensId ?? await client.resolveDefaultLens();
7804
- void client.request("POST", "/interactions", [
7805
- { type: "LEAD_SEEN", leadId: params.leadId, lensId: String(lensId) },
7806
- { type: "LEAD_CLICKED", leadId: params.leadId, lensId: String(lensId) }
7807
- ]).catch(() => {
7808
- });
7875
+ reportLeadInteractions(client, lensId, [params.leadId], ["LEAD_SEEN", "LEAD_CLICKED"], ctx?.logger);
7809
7876
  const [leadResult, qualResult, contactsResult, paidContactsResult, webFetchResult] = await Promise.allSettled([
7810
7877
  client.request("GET", `/lenses/${lensId}/leads/${params.leadId}`),
7811
7878
  client.request("GET", `/leads/${params.leadId}/ai_agent_responses`),
@@ -14238,6 +14305,7 @@ var init_pull_leads = __esm({
14238
14305
  "../core/dist/composite/pull-leads.js"() {
14239
14306
  "use strict";
14240
14307
  init_lead_order();
14308
+ init_interactions();
14241
14309
  init_empty_lens_reason();
14242
14310
  init_tool_descriptions_generated();
14243
14311
  pullLeads = {
@@ -14376,6 +14444,9 @@ var init_pull_leads = __esm({
14376
14444
  return resolvedOrder.error;
14377
14445
  const orderQs = resolvedOrder.order ? `&order=${encodeURIComponent(resolvedOrder.order)}` : "";
14378
14446
  const res = await client.request("GET", `/lenses/${lensId}/leads/wishlist?count=${count}&page=${page}&contacts=true${orderQs}`);
14447
+ if (params._reportSeen !== false) {
14448
+ reportLeadInteractions(client, lensId, res.items.map((lead) => lead.id), ["LEAD_SEEN"], ctx?.logger);
14449
+ }
14379
14450
  const summaries = await Promise.all(res.items.map(async (lead) => {
14380
14451
  try {
14381
14452
  const r = await client.request("GET", `/leads/${lead.id}/ai_agent_responses`);
@@ -14469,12 +14540,12 @@ var init_pull_leads = __esm({
14469
14540
  });
14470
14541
 
14471
14542
  // ../core/dist/composite/_geo-helpers.js
14472
- function expandAlias(text2) {
14473
- const key = text2.trim().toLowerCase();
14474
- return CITY_ALIASES[key] ?? text2;
14543
+ function expandAlias(text3) {
14544
+ const key = text3.trim().toLowerCase();
14545
+ return CITY_ALIASES[key] ?? text3;
14475
14546
  }
14476
- function scoreMatch(text2, match) {
14477
- const t = text2.trim().toLowerCase();
14547
+ function scoreMatch(text3, match) {
14548
+ const t = text3.trim().toLowerCase();
14478
14549
  const n = match.name.trim().toLowerCase();
14479
14550
  if (n === t)
14480
14551
  return 1;
@@ -14497,8 +14568,8 @@ async function resolveLocations(client, texts) {
14497
14568
  const resolved = [...direct];
14498
14569
  const ambiguities = [];
14499
14570
  for (const originalText of free) {
14500
- const text2 = expandAlias(originalText);
14501
- const path = `/geo/search?q=${encodeURIComponent(text2)}`;
14571
+ const text3 = expandAlias(originalText);
14572
+ const path = `/geo/search?q=${encodeURIComponent(text3)}`;
14502
14573
  let response;
14503
14574
  try {
14504
14575
  response = await client.request("GET", path);
@@ -14537,7 +14608,7 @@ async function resolveLocations(client, texts) {
14537
14608
  name: r.name,
14538
14609
  country: r.country,
14539
14610
  level: r.level,
14540
- score: scoreMatch(text2, r)
14611
+ score: scoreMatch(text3, r)
14541
14612
  })).sort((a, b) => {
14542
14613
  if (b.score !== a.score)
14543
14614
  return b.score - a.score;
@@ -14953,6 +15024,7 @@ var init_tour_plan = __esm({
14953
15024
  "use strict";
14954
15025
  init_pull_followups();
14955
15026
  init_pull_leads();
15027
+ init_interactions();
14956
15028
  init_country_guard();
14957
15029
  init_tool_descriptions_generated();
14958
15030
  DEFAULT_FOLLOWUPS_COUNT = 6;
@@ -15088,7 +15160,7 @@ var init_tour_plan = __esm({
15088
15160
  city_id: params.city_id,
15089
15161
  count: followupsCount
15090
15162
  }, ctx),
15091
- pullLeads.execute(client, { count: DISCOVER_OVER_PULL }, ctx)
15163
+ pullLeads.execute(client, { count: DISCOVER_OVER_PULL, _reportSeen: false }, ctx)
15092
15164
  ]);
15093
15165
  if (followupsResult.status === "fulfilled") {
15094
15166
  const r = followupsResult.value;
@@ -15124,6 +15196,10 @@ var init_tour_plan = __esm({
15124
15196
  }
15125
15197
  const filtered = rawDiscover.filter((l) => cityMatches(l, params.city));
15126
15198
  const discoverLeads2 = filtered.slice(0, discoverCount);
15199
+ const pulledLensId = leadsResult.status === "fulfilled" ? leadsResult.value?.lens?.id : null;
15200
+ if (pulledLensId != null) {
15201
+ reportLeadInteractions(client, pulledLensId, discoverLeads2.map((l) => l.id), ["LEAD_SEEN"], ctx?.logger);
15202
+ }
15127
15203
  const filterNote = params.city ? `Matched ${filtered.length}/${rawDiscover.length} Discover leads to '${params.city}'; returning top ${discoverLeads2.length}.` : `No city filter applied; returning top ${discoverLeads2.length} Discover leads.`;
15128
15204
  return {
15129
15205
  city: params.city ?? null,
@@ -15807,9 +15883,9 @@ ${firm.short_description}`);
15807
15883
  out.push(`### ${sec.section_emoji ?? ""} ${label}`.trim());
15808
15884
  const entries = Array.isArray(sec.entries) ? sec.entries : [];
15809
15885
  for (const e of entries.slice(0, 5)) {
15810
- const text2 = e.text ?? e.summary ?? JSON.stringify(e).slice(0, 200);
15886
+ const text3 = e.text ?? e.summary ?? JSON.stringify(e).slice(0, 200);
15811
15887
  const hot = e.hot === true ? " \u{1F525}" : "";
15812
- out.push(`- ${text2}${hot}`);
15888
+ out.push(`- ${text3}${hot}`);
15813
15889
  }
15814
15890
  if (entries.length > 5)
15815
15891
  out.push(`- _${entries.length - 5} more \u2026_`);
@@ -15890,6 +15966,7 @@ var init_research_lead_by_id = __esm({
15890
15966
  "../core/dist/composite/research-lead-by-id.js"() {
15891
15967
  "use strict";
15892
15968
  init_web_fetch_helpers();
15969
+ init_interactions();
15893
15970
  init_tool_descriptions_generated();
15894
15971
  researchLeadById = {
15895
15972
  name: "leadbay_research_lead_by_id",
@@ -16134,11 +16211,7 @@ var init_research_lead_by_id = __esm({
16134
16211
  }
16135
16212
  const lensId = params.lensId ?? await client.resolveDefaultLens();
16136
16213
  const leadId = params.leadId;
16137
- void client.request("POST", "/interactions", [
16138
- { type: "LEAD_SEEN", leadId, lensId: String(lensId) },
16139
- { type: "LEAD_CLICKED", leadId, lensId: String(lensId) }
16140
- ]).catch(() => {
16141
- });
16214
+ reportLeadInteractions(client, lensId, [leadId], ["LEAD_SEEN", "LEAD_CLICKED"], _ctx?.logger);
16142
16215
  const [profileR, qualR, contactsR, webFetchR, activitiesR, orgContactsR] = await Promise.allSettled([
16143
16216
  client.request("GET", `/lenses/${lensId}/leads/${leadId}`),
16144
16217
  client.request("GET", `/leads/${leadId}/ai_agent_responses`),
@@ -17181,6 +17254,7 @@ var init_get_lead_custom_fields = __esm({
17181
17254
  "../core/dist/composite/get-lead-custom-fields.js"() {
17182
17255
  "use strict";
17183
17256
  init_tool_descriptions_generated();
17257
+ init_interactions();
17184
17258
  getLeadCustomFields = {
17185
17259
  name: "leadbay_get_lead_custom_fields",
17186
17260
  annotations: {
@@ -17224,11 +17298,7 @@ var init_get_lead_custom_fields = __esm({
17224
17298
  },
17225
17299
  execute: async (client, params, ctx) => {
17226
17300
  const lensId = params.lensId ?? await client.resolveDefaultLens();
17227
- void client.request("POST", "/interactions", [
17228
- { type: "LEAD_SEEN", leadId: params.leadId, lensId: String(lensId) },
17229
- { type: "LEAD_CLICKED", leadId: params.leadId, lensId: String(lensId) }
17230
- ]).catch(() => {
17231
- });
17301
+ reportLeadInteractions(client, lensId, [params.leadId], ["LEAD_SEEN", "LEAD_CLICKED"], ctx?.logger);
17232
17302
  const lead = await client.request("GET", `/lenses/${lensId}/leads/${params.leadId}`);
17233
17303
  const entries = lead.custom_fields ?? [];
17234
17304
  const needsCatalog = entries.some((e) => !e?.field?.name);
@@ -20702,8 +20772,8 @@ function tokens(s) {
20702
20772
  return [];
20703
20773
  return s.toLowerCase().split(/[^\p{L}\p{N}]+/u).filter(Boolean);
20704
20774
  }
20705
- function bestMatches(text2, taxonomy) {
20706
- const want = new Set(tokens(text2));
20775
+ function bestMatches(text3, taxonomy) {
20776
+ const want = new Set(tokens(text3));
20707
20777
  if (want.size === 0)
20708
20778
  return [];
20709
20779
  const ranked = taxonomy.map((s) => {
@@ -20732,12 +20802,12 @@ async function resolveSectors(client, texts, ctx) {
20732
20802
  }
20733
20803
  const resolved = [...direct];
20734
20804
  const ambiguities = [];
20735
- for (const text2 of free) {
20736
- const matches = bestMatches(text2, taxonomy);
20805
+ for (const text3 of free) {
20806
+ const matches = bestMatches(text3, taxonomy);
20737
20807
  if (matches.length === 1 || matches.length >= 2 && matches[0].score >= 0.66 && matches[0].score - matches[1].score >= 0.34) {
20738
20808
  resolved.push(matches[0].id);
20739
20809
  } else {
20740
- ambiguities.push({ sector_text: text2, matches });
20810
+ ambiguities.push({ sector_text: text3, matches });
20741
20811
  }
20742
20812
  }
20743
20813
  return { resolved, ambiguities };
@@ -22066,6 +22136,7 @@ var init_find_new_leads = __esm({
22066
22136
  "../core/dist/composite/find-new-leads.js"() {
22067
22137
  "use strict";
22068
22138
  init_mcp_job_helpers();
22139
+ init_country_guard();
22069
22140
  init_tool_descriptions_generated();
22070
22141
  DEFAULT_WAIT_SECONDS = 45;
22071
22142
  findNewLeads = {
@@ -22190,6 +22261,21 @@ var init_find_new_leads = __esm({
22190
22261
  "channels",
22191
22262
  "exclude_lead_ids"
22192
22263
  ]);
22264
+ const homeCountry = detectCountryLocations(params.filters?.locations, "filters.locations", client.region).filter((hit) => hit.kind === "home_country");
22265
+ let countryNote;
22266
+ if (homeCountry.length > 0) {
22267
+ const dropped = new Set(homeCountry.map((hit) => hit.value));
22268
+ const raw = params.filters.locations;
22269
+ const rest = (Array.isArray(raw) ? raw : [raw]).filter((v) => !dropped.has(v));
22270
+ params = {
22271
+ ...params,
22272
+ filters: { ...params.filters, locations: rest.length > 0 ? rest : void 0 }
22273
+ };
22274
+ const removed = [...dropped].map((v) => `"${v}"`).join(", ");
22275
+ countryNote = {
22276
+ note: rest.length > 0 ? `Removed ${removed} from filters.locations: a country is never a location filter. The search covers ${rest.map((v) => `"${v}"`).join(", ")} only.` : `Removed ${removed} from filters.locations: this workspace holds ${homeCountry[0].country} companies only, so the search already covers all of it.`
22277
+ };
22278
+ }
22193
22279
  rejectCountryLocations(params.filters?.locations, client.region);
22194
22280
  rejectMalformedExclusions(params.exclude_lead_ids);
22195
22281
  rejectOversizedExclusions(params.exclude_lead_ids);
@@ -22267,6 +22353,7 @@ var init_find_new_leads = __esm({
22267
22353
  return {
22268
22354
  dry_run: true,
22269
22355
  ...forecast,
22356
+ ...countryNote,
22270
22357
  region: client.region
22271
22358
  };
22272
22359
  }
@@ -22287,6 +22374,7 @@ var init_find_new_leads = __esm({
22287
22374
  estimated_cost: forecast?.estimated_cost ?? null,
22288
22375
  items_requested: forecast?.items_requested ?? null,
22289
22376
  hint: vetoed ? "confirm:false vetoed the run \u2014 nothing was submitted. Re-call with confirm:true to proceed, or drop qualify/channels for a free search." : "Tell the user what will run and that it uses their plan's quota (no amounts, no money), get an explicit go-ahead, then re-call with confirm:true. For a free search instead: omit qualify and channels.",
22377
+ ...countryNote,
22290
22378
  region: client.region
22291
22379
  };
22292
22380
  }
@@ -22341,6 +22429,7 @@ var init_find_new_leads = __esm({
22341
22429
  since: snapshot.next_since ?? null,
22342
22430
  suggested_wait_seconds: done ? 0 : 60
22343
22431
  },
22432
+ ...countryNote,
22344
22433
  region: client.region
22345
22434
  };
22346
22435
  }
@@ -22348,6 +22437,144 @@ var init_find_new_leads = __esm({
22348
22437
  }
22349
22438
  });
22350
22439
 
22440
+ // ../core/dist/composite/_identity-rows.js
22441
+ function text(v) {
22442
+ return typeof v === "string" && v.trim() ? v.trim() : null;
22443
+ }
22444
+ function toRow(item) {
22445
+ const asked = item.ref?.requested_as ?? {};
22446
+ const row = {
22447
+ input_indexes: item.ref?.input_indexes ?? null,
22448
+ input: text(asked.name) ?? text(asked.website) ?? text(asked.lead_id) ?? text(asked.contact_id),
22449
+ status: item.status
22450
+ };
22451
+ if (item.status_reason)
22452
+ row.status_reason = item.status_reason;
22453
+ const lead = item.lead;
22454
+ if (!lead)
22455
+ return row;
22456
+ const company = lead.company ?? {};
22457
+ row.lead_id = lead.lead_id ?? item.ref?.lead_id ?? void 0;
22458
+ row.name = company.name;
22459
+ row.website = text(company.website);
22460
+ row.linkedin = text(company.socials?.linkedin);
22461
+ return row;
22462
+ }
22463
+ function identityRows(items) {
22464
+ const first = (i) => {
22465
+ const idx = i.ref?.input_indexes;
22466
+ return Array.isArray(idx) && idx.length > 0 ? Math.min(...idx) : Number.MAX_SAFE_INTEGER;
22467
+ };
22468
+ return [...items].sort((a, b) => first(a) - first(b) || a.seq - b.seq).map(toRow);
22469
+ }
22470
+ function identityCoverage(rows) {
22471
+ let resolved = 0;
22472
+ let ambiguous = 0;
22473
+ let notFound = 0;
22474
+ let otherSkipped = 0;
22475
+ let withWebsite = 0;
22476
+ let withLinkedin = 0;
22477
+ for (const r of rows) {
22478
+ if (r.status !== "skipped") {
22479
+ resolved += 1;
22480
+ if (r.website)
22481
+ withWebsite += 1;
22482
+ if (r.linkedin)
22483
+ withLinkedin += 1;
22484
+ } else if (r.status_reason === "low_confidence_identity")
22485
+ ambiguous += 1;
22486
+ else if (r.status_reason === "not_in_universe")
22487
+ notFound += 1;
22488
+ else
22489
+ otherSkipped += 1;
22490
+ }
22491
+ return {
22492
+ resolved,
22493
+ ambiguous,
22494
+ not_found: notFound,
22495
+ other_skipped: otherSkipped,
22496
+ with_website: withWebsite,
22497
+ with_linkedin: withLinkedin,
22498
+ without_website: resolved - withWebsite
22499
+ };
22500
+ }
22501
+ function identityCsv(rows) {
22502
+ return synthesizeCsv(CSV_HEADER, rows.map((r) => ({
22503
+ row: (r.input_indexes ?? []).map((i) => i + 1).join(";"),
22504
+ input: r.input ?? "",
22505
+ status: r.status,
22506
+ reason: r.status_reason ?? "",
22507
+ company: r.name ?? "",
22508
+ website: r.website ?? "",
22509
+ linkedin: r.linkedin ?? "",
22510
+ lead_id: r.lead_id ?? ""
22511
+ })));
22512
+ }
22513
+ async function saveIdentityFile(ctx, jobId, snapshot, items) {
22514
+ if (!ctx?.saveFile)
22515
+ return {};
22516
+ if (!TERMINAL_JOB_STATES.has(snapshot.job.state))
22517
+ return {};
22518
+ if (snapshot.items_truncated || items.length === 0)
22519
+ return {};
22520
+ try {
22521
+ const name = `leadbay-companies-${jobId.slice(0, 8)}.csv`;
22522
+ return { file: await ctx.saveFile(name, identityCsv(identityRows(items))) };
22523
+ } catch (e) {
22524
+ return { file_error: e instanceof Error ? e.message : String(e) };
22525
+ }
22526
+ }
22527
+ function readOffset(v) {
22528
+ const n = typeof v === "string" ? Number(v) : v;
22529
+ return typeof n === "number" && Number.isFinite(n) && n > 0 ? Math.floor(n) : 0;
22530
+ }
22531
+ function identityAnswer(jobId, snapshot, items, offset) {
22532
+ const all = identityRows(items);
22533
+ const rows = all.slice(offset, offset + IDENTITY_ROWS_PER_RESULT);
22534
+ const end = offset + rows.length;
22535
+ const done = TERMINAL_JOB_STATES.has(snapshot.job.state);
22536
+ const truncated = snapshot.items_truncated ?? false;
22537
+ const nextOffset = !done || truncated ? 0 : end < all.length ? end : null;
22538
+ return {
22539
+ summary: {
22540
+ ...identityCoverage(all),
22541
+ stop_reason: snapshot.funnel?.stop_reason ?? null
22542
+ },
22543
+ rows,
22544
+ rows_total: all.length,
22545
+ rows_offset: offset,
22546
+ items_truncated: truncated,
22547
+ scope_notes: snapshot.explain?.scope_notes ?? [],
22548
+ still_running: !done,
22549
+ next_poll: nextOffset === null ? null : {
22550
+ tool: "leadbay_lead_job_status",
22551
+ job_id: jobId,
22552
+ compact: true,
22553
+ offset: nextOffset,
22554
+ suggested_wait_seconds: done ? 0 : 60
22555
+ }
22556
+ };
22557
+ }
22558
+ var IDENTITY_ROWS_PER_RESULT, CSV_HEADER;
22559
+ var init_identity_rows = __esm({
22560
+ "../core/dist/composite/_identity-rows.js"() {
22561
+ "use strict";
22562
+ init_mcp_job_helpers();
22563
+ init_import_leads();
22564
+ IDENTITY_ROWS_PER_RESULT = 100;
22565
+ CSV_HEADER = [
22566
+ "row",
22567
+ "input",
22568
+ "status",
22569
+ "reason",
22570
+ "company",
22571
+ "website",
22572
+ "linkedin",
22573
+ "lead_id"
22574
+ ];
22575
+ }
22576
+ });
22577
+
22351
22578
  // ../core/dist/composite/qualify-leads.js
22352
22579
  function normalizeLeadRefs(refs) {
22353
22580
  if (!Array.isArray(refs))
@@ -22388,7 +22615,7 @@ function rejectMalformedLeadRefs(refs) {
22388
22615
  hint: "Each ref is an object whose fields are STRINGS \u2014 {lead_id} | {website} | {name, location?} | {contact_id}. A bare string is accepted and reshaped; null, numbers, arrays and non-string field values are not. Fix or drop those entries and re-call."
22389
22616
  };
22390
22617
  }
22391
- function text(value) {
22618
+ function text2(value) {
22392
22619
  if (typeof value !== "string")
22393
22620
  return null;
22394
22621
  const v = value.trim().toLowerCase();
@@ -22396,7 +22623,7 @@ function text(value) {
22396
22623
  }
22397
22624
  function derivedRequestId(params) {
22398
22625
  const refs = canonicalSet((params.lead_refs ?? []).map((r) => {
22399
- const website = text(r.website);
22626
+ const website = text2(r.website);
22400
22627
  return [
22401
22628
  // UUIDs are case-insensitive to the backend, so an uppercase id and
22402
22629
  // its lowercase form are the same lead and must share a key.
@@ -22407,8 +22634,8 @@ function derivedRequestId(params) {
22407
22634
  // AND to one key. Fall back to the trimmed/lowercased raw value when
22408
22635
  // it is not domain-shaped, rather than dropping the field.
22409
22636
  website ? normalizeDomain(website) ?? website : null,
22410
- text(r.name),
22411
- text(r.location)
22637
+ text2(r.name),
22638
+ text2(r.location)
22412
22639
  ];
22413
22640
  }));
22414
22641
  const shape = {
@@ -22452,6 +22679,7 @@ var init_qualify_leads = __esm({
22452
22679
  "use strict";
22453
22680
  init_mcp_job_helpers();
22454
22681
  init_import_leads();
22682
+ init_identity_rows();
22455
22683
  init_tool_descriptions_generated();
22456
22684
  DEFAULT_WAIT_SECONDS2 = 45;
22457
22685
  LEAD_REF_FIELDS = [
@@ -22569,6 +22797,7 @@ var init_qualify_leads = __esm({
22569
22797
  const buysChannels = (params.channels?.length ?? 0) > 0;
22570
22798
  const buysQualification = qualify !== false;
22571
22799
  const isPaid = buysQualification || buysChannels;
22800
+ const identityOnly = !isPaid && (params.contact_titles?.length ?? 0) === 0 && params.prior_deliveries == null;
22572
22801
  const vetoed = confirm === false;
22573
22802
  const consented = !vetoed && confirm === true;
22574
22803
  const requestId = presentRequestId(params.request_id) ?? (isPaid ? derivedRequestId(params) : void 0);
@@ -22617,6 +22846,25 @@ var init_qualify_leads = __esm({
22617
22846
  const done = TERMINAL_JOB_STATES.has(snapshot.job.state);
22618
22847
  const indexed = submit.duplicate ?? false ? remapInputIndexes(snapshot.items, params.lead_refs) : { items: snapshot.items, remapped: true };
22619
22848
  const view = { ...snapshot, items: indexed.items };
22849
+ if (identityOnly) {
22850
+ const answer = identityAnswer(submit.job_id, snapshot, view.items, 0);
22851
+ return {
22852
+ job_id: submit.job_id,
22853
+ request_id: requestId ?? null,
22854
+ duplicate_submit: submit.duplicate ?? false,
22855
+ state: snapshot.job.state,
22856
+ done,
22857
+ ...answer,
22858
+ summary: {
22859
+ refs_submitted: params.lead_refs?.length ?? 0,
22860
+ items_requested: submit.items_requested,
22861
+ ...answer.summary
22862
+ },
22863
+ input_indexes_remapped: submit.duplicate ?? false ? indexed.remapped : null,
22864
+ ...await saveIdentityFile(ctx, submit.job_id, snapshot, view.items),
22865
+ region: client.region
22866
+ };
22867
+ }
22620
22868
  return {
22621
22869
  job_id: submit.job_id,
22622
22870
  // Echo the key actually sent, so a retry can reuse it verbatim.
@@ -22685,6 +22933,7 @@ var init_lead_job_status = __esm({
22685
22933
  "../core/dist/composite/lead-job-status.js"() {
22686
22934
  "use strict";
22687
22935
  init_mcp_job_helpers();
22936
+ init_identity_rows();
22688
22937
  init_tool_descriptions_generated();
22689
22938
  leadJobStatus = {
22690
22939
  name: "leadbay_lead_job_status",
@@ -22714,6 +22963,14 @@ var init_lead_job_status = __esm({
22714
22963
  wait_seconds: {
22715
22964
  type: "number",
22716
22965
  description: "0 (default) = instant snapshot. >0 = keep polling up to this many seconds until the job is terminal \u2014 use ~60 when the user asked to wait for results."
22966
+ },
22967
+ compact: {
22968
+ type: "boolean",
22969
+ description: "true = one row per company (name, website, LinkedIn) in the order of the user's list, plus coverage counts, instead of full leads. Pass it when next_poll carries it (a leadbay_qualify_leads identity pass). Reads the whole job; since is ignored."
22970
+ },
22971
+ offset: {
22972
+ type: "number",
22973
+ description: "With compact: the first row to return (default 0). Each result carries at most 100 rows; take the next value from next_poll.offset."
22717
22974
  }
22718
22975
  },
22719
22976
  required: ["job_id"],
@@ -22721,8 +22978,19 @@ var init_lead_job_status = __esm({
22721
22978
  },
22722
22979
  execute: async (client, params, ctx) => {
22723
22980
  const waitSeconds = clampWaitSeconds(params.wait_seconds, 0);
22724
- const snapshot = waitSeconds > 0 ? await waitForJob(client, params.job_id, waitSeconds, ctx, void 0, params.since, params.limit) : await collectJobSnapshot(client, params.job_id, params.since, params.limit, ctx?.signal);
22981
+ const compact = params.compact === true || params.compact === "true";
22982
+ const since = compact ? void 0 : params.since;
22983
+ const snapshot = waitSeconds > 0 ? await waitForJob(client, params.job_id, waitSeconds, ctx, void 0, since, params.limit) : await collectJobSnapshot(client, params.job_id, since, params.limit, ctx?.signal);
22725
22984
  const done = TERMINAL_JOB_STATES.has(snapshot.job.state);
22985
+ if (compact) {
22986
+ return {
22987
+ job_id: params.job_id,
22988
+ state: snapshot.job.state,
22989
+ done,
22990
+ ...identityAnswer(params.job_id, snapshot, snapshot.items, readOffset(params.offset)),
22991
+ region: client.region
22992
+ };
22993
+ }
22726
22994
  const { leads, skipped } = splitItems(snapshot);
22727
22995
  return {
22728
22996
  job_id: params.job_id,
@@ -24164,8 +24432,8 @@ var init_send_feedback = __esm({
24164
24432
  }
24165
24433
  },
24166
24434
  execute: async (client, params, ctx) => {
24167
- const text2 = typeof params.message === "string" ? params.message.trim() : "";
24168
- if (!text2) {
24435
+ const text3 = typeof params.message === "string" ? params.message.trim() : "";
24436
+ if (!text3) {
24169
24437
  return {
24170
24438
  error: true,
24171
24439
  code: "BAD_INPUT",
@@ -24173,7 +24441,7 @@ var init_send_feedback = __esm({
24173
24441
  hint: "Ask the user what they'd like to tell the Leadbay team, then call again with their words in `message`."
24174
24442
  };
24175
24443
  }
24176
- const message = text2.length > MESSAGE_MAX2 ? `${text2.slice(0, MESSAGE_MAX2 - 1)}\u2026` : text2;
24444
+ const message = text3.length > MESSAGE_MAX2 ? `${text3.slice(0, MESSAGE_MAX2 - 1)}\u2026` : text3;
24177
24445
  if (!ctx?.sendFeedback) {
24178
24446
  return {
24179
24447
  sent: false,
@@ -24774,7 +25042,7 @@ var init_dist = __esm({
24774
25042
  init_dist();
24775
25043
  import { realpathSync } from "fs";
24776
25044
  import { fileURLToPath } from "url";
24777
- import { basename } from "path";
25045
+ import { basename as basename2 } from "path";
24778
25046
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
24779
25047
 
24780
25048
  // src/server.ts
@@ -27678,8 +27946,8 @@ var PROMPT_CATALOG_BULLETS = {
27678
27946
  };
27679
27947
 
27680
27948
  // src/prompts.ts
27681
- function userMessage(text2) {
27682
- return { role: "user", content: { type: "text", text: text2 } };
27949
+ function userMessage(text3) {
27950
+ return { role: "user", content: { type: "text", text: text3 } };
27683
27951
  }
27684
27952
  function substitutePlaceholders(body, substitutions) {
27685
27953
  let out = body;
@@ -29378,6 +29646,7 @@ ${url}
29378
29646
  // RAW value: the friction redaction above is an ANALYTICS control, and
29379
29647
  // must not change in-process tool behaviour.
29380
29648
  triggered_by: rawTriggeredBy,
29649
+ saveFile: opts.saveFile,
29381
29650
  // Route leadbay_send_feedback to Sentry's feedback inbox (same place
29382
29651
  // the web app's form lands). NOOP_TELEMETRY returns false, so the
29383
29652
  // tool reports honestly when telemetry is off.
@@ -29620,11 +29889,24 @@ ${url}
29620
29889
  return server;
29621
29890
  }
29622
29891
 
29892
+ // src/local-files.ts
29893
+ import { existsSync as existsSync2 } from "fs";
29894
+ import { writeFile } from "fs/promises";
29895
+ import { homedir } from "os";
29896
+ import { basename, join as join2 } from "path";
29897
+ async function saveToDownloads(name, content, home = homedir()) {
29898
+ const downloads = join2(home, "Downloads");
29899
+ const dir = existsSync2(downloads) ? downloads : home;
29900
+ const path = join2(dir, basename(name));
29901
+ await writeFile(path, content, "utf8");
29902
+ return path;
29903
+ }
29904
+
29623
29905
  // installer/install-shared.ts
29624
29906
  import { spawn } from "child_process";
29625
- import { existsSync as existsSync2, readFileSync as readFileSync2, readdirSync as readdirSync2 } from "fs";
29626
- import { join as join2 } from "path";
29627
- import { homedir } from "os";
29907
+ import { existsSync as existsSync3, readFileSync as readFileSync2, readdirSync as readdirSync2 } from "fs";
29908
+ import { join as join3 } from "path";
29909
+ import { homedir as homedir2 } from "os";
29628
29910
  var HOSTED_MCP_URL_CHATGPT = "https://mcp.leadbay.app/chatgpt/mcp";
29629
29911
  function formatInstallOsLabel(platform = process.platform, arch = process.arch) {
29630
29912
  const name = platform === "darwin" ? "macOS" : platform === "win32" ? "Windows" : platform === "linux" ? "Linux" : platform;
@@ -29632,15 +29914,15 @@ function formatInstallOsLabel(platform = process.platform, arch = process.arch)
29632
29914
  }
29633
29915
  function detectClaudeDesktopMode(claudeSupportDir) {
29634
29916
  const markers = [];
29635
- const legacy = existsSync2(join2(claudeSupportDir, "claude_desktop_config.json"));
29636
- if (existsSync2(join2(claudeSupportDir, "Claude Extensions"))) {
29917
+ const legacy = existsSync3(join3(claudeSupportDir, "claude_desktop_config.json"));
29918
+ if (existsSync3(join3(claudeSupportDir, "Claude Extensions"))) {
29637
29919
  markers.push("Claude Extensions/");
29638
29920
  }
29639
- if (existsSync2(join2(claudeSupportDir, "extensions-installations.json"))) {
29921
+ if (existsSync3(join3(claudeSupportDir, "extensions-installations.json"))) {
29640
29922
  markers.push("extensions-installations.json");
29641
29923
  }
29642
- const cfgPath = join2(claudeSupportDir, "config.json");
29643
- if (existsSync2(cfgPath)) {
29924
+ const cfgPath = join3(claudeSupportDir, "config.json");
29925
+ if (existsSync3(cfgPath)) {
29644
29926
  try {
29645
29927
  const raw = readFileSync2(cfgPath, "utf8");
29646
29928
  const parsed = JSON.parse(raw);
@@ -29680,8 +29962,8 @@ async function windowsStoreAppInstalled(packageName, appName) {
29680
29962
  });
29681
29963
  }
29682
29964
  function isClaudeStorePackagePresent(localAppData) {
29683
- const packagesDir = join2(localAppData, "Packages");
29684
- if (!existsSync2(packagesDir)) return false;
29965
+ const packagesDir = join3(localAppData, "Packages");
29966
+ if (!existsSync3(packagesDir)) return false;
29685
29967
  try {
29686
29968
  return readdirSync2(packagesDir).some((name) => /^Claude_/i.test(name));
29687
29969
  } catch {
@@ -29690,7 +29972,7 @@ function isClaudeStorePackagePresent(localAppData) {
29690
29972
  }
29691
29973
  async function isClaudeDesktopInstalled(home) {
29692
29974
  if (process.platform === "darwin") {
29693
- return existsSync2("/Applications/Claude.app") || existsSync2(home + "/Applications/Claude.app");
29975
+ return existsSync3("/Applications/Claude.app") || existsSync3(home + "/Applications/Claude.app");
29694
29976
  }
29695
29977
  if (process.platform === "win32") {
29696
29978
  const local = process.env.LOCALAPPDATA ?? home + "/AppData/Local";
@@ -29701,18 +29983,18 @@ async function isClaudeDesktopInstalled(home) {
29701
29983
  local + "/Claude/Claude.exe",
29702
29984
  programFiles ? programFiles + "/Claude/Claude.exe" : null,
29703
29985
  programFilesX86 ? programFilesX86 + "/Claude/Claude.exe" : null
29704
- ].some((candidate) => candidate !== null && existsSync2(candidate));
29986
+ ].some((candidate) => candidate !== null && existsSync3(candidate));
29705
29987
  if (exeInstalled) return true;
29706
29988
  if (isClaudeStorePackagePresent(local)) return true;
29707
29989
  return await windowsStoreAppInstalled("AnthropicPBC.Claude", "Claude");
29708
29990
  }
29709
29991
  const desktopBin = await findOnPath("claude-desktop");
29710
29992
  if (desktopBin) return true;
29711
- return existsSync2(home + "/.local/share/applications/claude-desktop.desktop") || existsSync2("/usr/share/applications/claude-desktop.desktop") || existsSync2("/opt/Claude/Claude") || existsSync2("/opt/Claude/claude") || existsSync2("/opt/claude/claude");
29993
+ return existsSync3(home + "/.local/share/applications/claude-desktop.desktop") || existsSync3("/usr/share/applications/claude-desktop.desktop") || existsSync3("/opt/Claude/Claude") || existsSync3("/opt/Claude/claude") || existsSync3("/opt/claude/claude");
29712
29994
  }
29713
29995
  async function isChatGptDesktopInstalled(home) {
29714
29996
  if (process.platform === "darwin") {
29715
- return existsSync2("/Applications/ChatGPT.app") || existsSync2(home + "/Applications/ChatGPT.app");
29997
+ return existsSync3("/Applications/ChatGPT.app") || existsSync3(home + "/Applications/ChatGPT.app");
29716
29998
  }
29717
29999
  if (process.platform === "win32") {
29718
30000
  const local = process.env.LOCALAPPDATA ?? home + "/AppData/Local";
@@ -29725,7 +30007,7 @@ async function isChatGptDesktopInstalled(home) {
29725
30007
  programFiles ? programFiles + "/ChatGPT/ChatGPT.exe" : null,
29726
30008
  programFilesX86 ? programFilesX86 + "/OpenAI/ChatGPT/ChatGPT.exe" : null,
29727
30009
  programFilesX86 ? programFilesX86 + "/ChatGPT/ChatGPT.exe" : null
29728
- ].some((candidate) => candidate !== null && existsSync2(candidate));
30010
+ ].some((candidate) => candidate !== null && existsSync3(candidate));
29729
30011
  return exeInstalled || await windowsStoreAppInstalled("OpenAI.ChatGPT-Desktop", "ChatGPT");
29730
30012
  }
29731
30013
  return false;
@@ -29733,16 +30015,16 @@ async function isChatGptDesktopInstalled(home) {
29733
30015
  async function isCursorInstalled(home) {
29734
30016
  const cursorBin = await findOnPath("cursor");
29735
30017
  if (cursorBin) return true;
29736
- if (process.platform === "darwin") return existsSync2("/Applications/Cursor.app");
30018
+ if (process.platform === "darwin") return existsSync3("/Applications/Cursor.app");
29737
30019
  if (process.platform === "win32") {
29738
30020
  const local = process.env.LOCALAPPDATA ?? `${home}\\AppData\\Local`;
29739
- return existsSync2(`${local}\\Programs\\Cursor\\Cursor.exe`);
30021
+ return existsSync3(`${local}\\Programs\\Cursor\\Cursor.exe`);
29740
30022
  }
29741
- return existsSync2("/usr/share/applications/cursor.desktop") || existsSync2("/opt/Cursor/cursor");
30023
+ return existsSync3("/usr/share/applications/cursor.desktop") || existsSync3("/opt/Cursor/cursor");
29742
30024
  }
29743
30025
  async function detectClients() {
29744
30026
  const out = [];
29745
- const home = homedir();
30027
+ const home = homedir2();
29746
30028
  const claudeBin = await findOnPath("claude");
29747
30029
  if (claudeBin) {
29748
30030
  out.push({ id: "claude-code", label: "Claude Code", detail: `${claudeBin} mcp add ...` });
@@ -29761,7 +30043,7 @@ async function detectClients() {
29761
30043
  out.push({
29762
30044
  id: "cursor",
29763
30045
  label: "Cursor",
29764
- detail: existsSync2(cursorPath) ? cursorPath : `${cursorPath} (will be created)`,
30046
+ detail: existsSync3(cursorPath) ? cursorPath : `${cursorPath} (will be created)`,
29765
30047
  configPath: cursorPath
29766
30048
  });
29767
30049
  }
@@ -29850,11 +30132,11 @@ async function uninstallFromClaudeCode() {
29850
30132
  // installer/install-json-config.ts
29851
30133
  async function installInJsonConfig(configPath, token, region, includeWrite, telemetryEnabled, localBinPath) {
29852
30134
  try {
29853
- const { readFileSync: readFileSync3, writeFileSync, existsSync: existsSync4, mkdirSync, statSync } = await import("fs");
30135
+ const { readFileSync: readFileSync3, writeFileSync, existsSync: existsSync5, mkdirSync, statSync } = await import("fs");
29854
30136
  const { dirname: dirname2 } = await import("path");
29855
30137
  let parsed = {};
29856
30138
  let preserved = {};
29857
- const existed = existsSync4(configPath);
30139
+ const existed = existsSync5(configPath);
29858
30140
  if (existed) {
29859
30141
  const raw = readFileSync3(configPath, "utf8");
29860
30142
  try {
@@ -29904,8 +30186,8 @@ function stripJsonMcpEntry(existing) {
29904
30186
  }
29905
30187
  async function uninstallFromJsonConfig(configPath) {
29906
30188
  try {
29907
- const { existsSync: existsSync4, readFileSync: readFileSync3, writeFileSync } = await import("fs");
29908
- if (!existsSync4(configPath)) return { ok: true, message: "config not found \u2014 nothing to do" };
30189
+ const { existsSync: existsSync5, readFileSync: readFileSync3, writeFileSync } = await import("fs");
30190
+ if (!existsSync5(configPath)) return { ok: true, message: "config not found \u2014 nothing to do" };
29909
30191
  const existing = readFileSync3(configPath, "utf8");
29910
30192
  const { content, changed } = stripJsonMcpEntry(existing);
29911
30193
  if (!changed) return { ok: true, message: "leadbay entry not present" };
@@ -29988,10 +30270,10 @@ function stripShellExportBlock(existing) {
29988
30270
  }
29989
30271
  async function installInCodexConfig(configPath, includeWrite, telemetryEnabled, localBinPath) {
29990
30272
  try {
29991
- const { readFileSync: readFileSync3, writeFileSync, existsSync: existsSync4, mkdirSync, statSync, renameSync, chmodSync } = await import("fs");
30273
+ const { readFileSync: readFileSync3, writeFileSync, existsSync: existsSync5, mkdirSync, statSync, renameSync, chmodSync } = await import("fs");
29992
30274
  const { dirname: dirname2 } = await import("path");
29993
30275
  let existing = "";
29994
- const existed = existsSync4(configPath);
30276
+ const existed = existsSync5(configPath);
29995
30277
  if (existed) {
29996
30278
  existing = readFileSync3(configPath, "utf8");
29997
30279
  } else {
@@ -30037,15 +30319,15 @@ async function appendShellExports(token, region, includeWrite, telemetryEnabled)
30037
30319
  }
30038
30320
  return { ok: true, message: "env exported with setx; restart Codex/terminal" };
30039
30321
  }
30040
- const { existsSync: existsSync4, readFileSync: readFileSync3, writeFileSync, renameSync } = await import("fs");
30322
+ const { existsSync: existsSync5, readFileSync: readFileSync3, writeFileSync, renameSync } = await import("fs");
30041
30323
  const os = await import("os");
30042
30324
  const home = os.homedir();
30043
- const preferred = [`${home}/.zshrc`, `${home}/.bashrc`].filter((path) => existsSync4(path));
30325
+ const preferred = [`${home}/.zshrc`, `${home}/.bashrc`].filter((path) => existsSync5(path));
30044
30326
  const paths = preferred.length ? preferred : [`${home}/.profile`];
30045
30327
  const block = buildShellExportBlock(token, region, includeWrite, telemetryEnabled);
30046
30328
  const updated = [];
30047
30329
  for (const path of paths) {
30048
- const existing = existsSync4(path) ? readFileSync3(path, "utf8") : "";
30330
+ const existing = existsSync5(path) ? readFileSync3(path, "utf8") : "";
30049
30331
  const merged = mergeShellExportBlock(existing, block);
30050
30332
  if (!merged.changed) continue;
30051
30333
  const tmp = `${path}.leadbay.tmp`;
@@ -30063,8 +30345,8 @@ async function appendShellExports(token, region, includeWrite, telemetryEnabled)
30063
30345
  }
30064
30346
  async function uninstallFromCodexConfig(configPath) {
30065
30347
  try {
30066
- const { existsSync: existsSync4, readFileSync: readFileSync3, writeFileSync } = await import("fs");
30067
- if (!existsSync4(configPath)) return { ok: true, message: "config not found \u2014 nothing to do" };
30348
+ const { existsSync: existsSync5, readFileSync: readFileSync3, writeFileSync } = await import("fs");
30349
+ if (!existsSync5(configPath)) return { ok: true, message: "config not found \u2014 nothing to do" };
30068
30350
  const existing = readFileSync3(configPath, "utf8");
30069
30351
  const { content, changed } = stripCodexBlock(existing);
30070
30352
  if (!changed) return { ok: true, message: "leadbay block not present" };
@@ -30076,11 +30358,11 @@ async function uninstallFromCodexConfig(configPath) {
30076
30358
  }
30077
30359
  async function uninstallShellExports() {
30078
30360
  try {
30079
- const { existsSync: existsSync4, readFileSync: readFileSync3, writeFileSync, renameSync } = await import("fs");
30361
+ const { existsSync: existsSync5, readFileSync: readFileSync3, writeFileSync, renameSync } = await import("fs");
30080
30362
  const os = await import("os");
30081
30363
  const home = os.homedir();
30082
30364
  const candidates = [`${home}/.zshrc`, `${home}/.bashrc`, `${home}/.profile`].filter(
30083
- (p) => existsSync4(p)
30365
+ (p) => existsSync5(p)
30084
30366
  );
30085
30367
  const updated = [];
30086
30368
  for (const p of candidates) {
@@ -30105,17 +30387,17 @@ async function uninstallShellExports() {
30105
30387
  var DXT_EXTENSION_ID = "local.dxt.leadbay.leadbay";
30106
30388
  async function removeDxtExtension(claudeSupportDir) {
30107
30389
  try {
30108
- const { existsSync: existsSync4, readFileSync: readFileSync3, writeFileSync, rmSync } = await import("fs");
30109
- const { join: join3 } = await import("path");
30110
- const extensionDir = join3(claudeSupportDir, "Claude Extensions", DXT_EXTENSION_ID);
30111
- const registryPath = join3(claudeSupportDir, "extensions-installations.json");
30390
+ const { existsSync: existsSync5, readFileSync: readFileSync3, writeFileSync, rmSync } = await import("fs");
30391
+ const { join: join4 } = await import("path");
30392
+ const extensionDir = join4(claudeSupportDir, "Claude Extensions", DXT_EXTENSION_ID);
30393
+ const registryPath = join4(claudeSupportDir, "extensions-installations.json");
30112
30394
  let removedDir = false;
30113
30395
  let removedEntry = false;
30114
- if (existsSync4(extensionDir)) {
30396
+ if (existsSync5(extensionDir)) {
30115
30397
  rmSync(extensionDir, { recursive: true, force: true });
30116
30398
  removedDir = true;
30117
30399
  }
30118
- if (existsSync4(registryPath)) {
30400
+ if (existsSync5(registryPath)) {
30119
30401
  try {
30120
30402
  const raw = readFileSync3(registryPath, "utf8");
30121
30403
  const parsed = JSON.parse(raw);
@@ -30142,8 +30424,8 @@ async function removeDxtExtension(claudeSupportDir) {
30142
30424
  }
30143
30425
 
30144
30426
  // installer/install-wizard.ts
30145
- function ansi(text2, code, enabled) {
30146
- return enabled ? `\x1B[${code}m${text2}\x1B[0m` : text2;
30427
+ function ansi(text3, code, enabled) {
30428
+ return enabled ? `\x1B[${code}m${text3}\x1B[0m` : text3;
30147
30429
  }
30148
30430
  function parseInstallSelection(input, clientCount) {
30149
30431
  const normalized = input.trim().toLowerCase();
@@ -30323,7 +30605,7 @@ import {
30323
30605
  } from "fs/promises";
30324
30606
  import { constants as fsConstants } from "fs";
30325
30607
  import { dirname, resolve as resolvePath } from "path";
30326
- import { homedir as homedir2 } from "os";
30608
+ import { homedir as homedir3 } from "os";
30327
30609
  function emptyState() {
30328
30610
  return {
30329
30611
  last_check_time: 0,
@@ -30359,7 +30641,7 @@ var UpdateStateStore = class {
30359
30641
  }
30360
30642
  validatePath(p) {
30361
30643
  if (this.allowUnsafePath) return;
30362
- const home = resolvePath(homedir2());
30644
+ const home = resolvePath(homedir3());
30363
30645
  if (p !== home && !p.startsWith(home + "/") && !p.startsWith(home + "\\")) {
30364
30646
  throw new Error(
30365
30647
  `UpdateStateStore: path ${p} is outside $HOME (${home}). Set LEADBAY_UPDATE_STATE_PATH_UNSAFE=1 to override.`
@@ -30487,7 +30769,7 @@ async function openTmpFileExclusive(path) {
30487
30769
  async function createDefaultUpdateStateStore(opts = {}) {
30488
30770
  const env = opts.env ?? process.env;
30489
30771
  const allowUnsafePath = env.LEADBAY_UPDATE_STATE_PATH_UNSAFE === "1";
30490
- const path = env.LEADBAY_UPDATE_STATE_PATH ?? resolvePath(homedir2(), ".leadbay", "update-state.json");
30772
+ const path = env.LEADBAY_UPDATE_STATE_PATH ?? resolvePath(homedir3(), ".leadbay", "update-state.json");
30491
30773
  try {
30492
30774
  const store = new UpdateStateStore({
30493
30775
  backend: "file",
@@ -30511,7 +30793,7 @@ import { createHash as createHash5, randomBytes } from "crypto";
30511
30793
  import { createServer } from "http";
30512
30794
  import { request as httpsRequestRaw } from "https";
30513
30795
  import { spawn as spawn3 } from "child_process";
30514
- import { readdirSync as readdirSync3, existsSync as existsSync3 } from "fs";
30796
+ import { readdirSync as readdirSync3, existsSync as existsSync4 } from "fs";
30515
30797
  var LEADBAY_LOOPBACK_PORTS = [51789, 51790, 51791, 51792];
30516
30798
  var BrowserOpenFailedError = class extends Error {
30517
30799
  authorizeUrl;
@@ -30844,11 +31126,11 @@ function browserLaunchEnv(debug) {
30844
31126
  const env = { ...process.env };
30845
31127
  if (process.platform !== "linux") return env;
30846
31128
  let runtimeDir = env.XDG_RUNTIME_DIR;
30847
- if (!runtimeDir || !existsSync3(runtimeDir)) {
31129
+ if (!runtimeDir || !existsSync4(runtimeDir)) {
30848
31130
  try {
30849
31131
  const uid = process.getuid?.();
30850
31132
  const candidate = uid !== void 0 ? `/run/user/${uid}` : void 0;
30851
- if (candidate && existsSync3(candidate)) {
31133
+ if (candidate && existsSync4(candidate)) {
30852
31134
  runtimeDir = candidate;
30853
31135
  env.XDG_RUNTIME_DIR = candidate;
30854
31136
  debug?.(`browserLaunchEnv: injected XDG_RUNTIME_DIR=${candidate}`);
@@ -30868,7 +31150,7 @@ function browserLaunchEnv(debug) {
30868
31150
  }
30869
31151
  if (!env.DBUS_SESSION_BUS_ADDRESS && runtimeDir) {
30870
31152
  const busPath = `${runtimeDir}/bus`;
30871
- if (existsSync3(busPath)) {
31153
+ if (existsSync4(busPath)) {
30872
31154
  env.DBUS_SESSION_BUS_ADDRESS = `unix:path=${busPath}`;
30873
31155
  debug?.(`browserLaunchEnv: injected DBUS_SESSION_BUS_ADDRESS=unix:path=${busPath}`);
30874
31156
  }
@@ -30891,7 +31173,7 @@ function browserLaunchEnv(debug) {
30891
31173
  );
30892
31174
  if (cookie) xauth = `${runtimeDir}/${cookie}`;
30893
31175
  }
30894
- if (!xauth && env.HOME && existsSync3(`${env.HOME}/.Xauthority`)) {
31176
+ if (!xauth && env.HOME && existsSync4(`${env.HOME}/.Xauthority`)) {
30895
31177
  xauth = `${env.HOME}/.Xauthority`;
30896
31178
  }
30897
31179
  if (xauth) {
@@ -31054,7 +31336,7 @@ var OAUTH_BASE_URLS = {
31054
31336
  fr: "https://staging.api.leadbay.app"
31055
31337
  }
31056
31338
  };
31057
- var VERSION = "0.38.2";
31339
+ var VERSION = "0.39.1";
31058
31340
  var HELP = `
31059
31341
  leadbay-mcp ${VERSION} \u2014 Leadbay Model Context Protocol server
31060
31342
 
@@ -31154,9 +31436,9 @@ function exitWithTokenError() {
31154
31436
  function hydrateEnvFromCredentialsFile() {
31155
31437
  if (process.env.LEADBAY_TOKEN) return false;
31156
31438
  try {
31157
- const { existsSync: existsSync4, readFileSync: readFileSync3 } = require_("node:fs");
31439
+ const { existsSync: existsSync5, readFileSync: readFileSync3 } = require_("node:fs");
31158
31440
  const { path } = resolveOAuthBootstrapCredentialsPath();
31159
- if (!existsSync4(path)) return false;
31441
+ if (!existsSync5(path)) return false;
31160
31442
  const parsed = JSON.parse(readFileSync3(path, "utf8"));
31161
31443
  const env = parsed?.mcpServers?.leadbay?.env;
31162
31444
  if (!env || typeof env !== "object") return false;
@@ -31177,9 +31459,9 @@ function hydrateEnvFromCredentialsFile() {
31177
31459
  function resolveOAuthBootstrapCredentialsPath() {
31178
31460
  const resolved = resolveDefaultCredentialsPath();
31179
31461
  if (process.env.LEADBAY_OAUTH_STAGING !== "1") return resolved;
31180
- const { dirname: dirname2, join: join3 } = require_("node:path");
31462
+ const { dirname: dirname2, join: join4 } = require_("node:path");
31181
31463
  return {
31182
- path: join3(dirname2(resolved.path), "credentials.staging.json"),
31464
+ path: join4(dirname2(resolved.path), "credentials.staging.json"),
31183
31465
  legacy: resolved.legacy
31184
31466
  };
31185
31467
  }
@@ -31189,20 +31471,20 @@ var bootstrapFailureMessage;
31189
31471
  function bootstrapDebug(msg) {
31190
31472
  try {
31191
31473
  const { appendFileSync, mkdirSync } = require_("node:fs");
31192
- const { join: join3 } = require_("node:path");
31193
- const { homedir: homedir3 } = require_("node:os");
31194
- const dir = join3(homedir3(), ".leadbay");
31474
+ const { join: join4 } = require_("node:path");
31475
+ const { homedir: homedir4 } = require_("node:os");
31476
+ const dir = join4(homedir4(), ".leadbay");
31195
31477
  mkdirSync(dir, { recursive: true });
31196
31478
  const ts = (/* @__PURE__ */ new Date()).toISOString();
31197
- appendFileSync(join3(dir, "oauth-bootstrap-debug.log"), `${ts} [pid ${process.pid}] ${msg}
31479
+ appendFileSync(join4(dir, "oauth-bootstrap-debug.log"), `${ts} [pid ${process.pid}] ${msg}
31198
31480
  `);
31199
31481
  } catch {
31200
31482
  }
31201
31483
  }
31202
31484
  function oauthClientCachePath() {
31203
- const { join: join3 } = require_("node:path");
31204
- const { homedir: homedir3 } = require_("node:os");
31205
- return join3(homedir3(), ".leadbay", "oauth-client.json");
31485
+ const { join: join4 } = require_("node:path");
31486
+ const { homedir: homedir4 } = require_("node:os");
31487
+ return join4(homedir4(), ".leadbay", "oauth-client.json");
31206
31488
  }
31207
31489
  function getCachedOAuthClientId(authServerBaseUrl, port) {
31208
31490
  try {
@@ -31529,8 +31811,8 @@ function parseFlag(args, name) {
31529
31811
  function hasFlag(args, name) {
31530
31812
  return args.some((a) => a === `--${name}`);
31531
31813
  }
31532
- function ansi2(text2, code, enabled) {
31533
- return enabled ? `\x1B[${code}m${text2}\x1B[0m` : text2;
31814
+ function ansi2(text3, code, enabled) {
31815
+ return enabled ? `\x1B[${code}m${text3}\x1B[0m` : text3;
31534
31816
  }
31535
31817
  function isInteractiveInstall() {
31536
31818
  return process.stdin.isTTY === true && process.stdout.isTTY === true;
@@ -31750,8 +32032,8 @@ Restart your MCP client to pick up the new server.
31750
32032
  usingLegacyPath = resolved.legacy;
31751
32033
  }
31752
32034
  try {
31753
- const { existsSync: existsSync4, readFileSync: readFileSync3 } = await import("fs");
31754
- if (existsSync4(targetPath) && !force) {
32035
+ const { existsSync: existsSync5, readFileSync: readFileSync3 } = await import("fs");
32036
+ if (existsSync5(targetPath) && !force) {
31755
32037
  let existing;
31756
32038
  try {
31757
32039
  existing = JSON.parse(readFileSync3(targetPath, "utf8"));
@@ -32279,6 +32561,9 @@ async function main() {
32279
32561
  version: VERSION,
32280
32562
  telemetry,
32281
32563
  updateStateStore,
32564
+ // This process runs on the user's machine, so a tool can save a file
32565
+ // they will find. The hosted server (http-server.ts) does not pass this.
32566
+ saveFile: saveToDownloads,
32282
32567
  // Non-blocking OAuth bootstrap gate. Read per tool call: once the
32283
32568
  // background flow lands the token (client.isAuthenticated → true) this
32284
32569
  // reports done and tools execute. While waiting it surfaces the live
@@ -32361,7 +32646,7 @@ var isEntrypoint = (() => {
32361
32646
  try {
32362
32647
  const entry = process.argv[1];
32363
32648
  if (!entry) return false;
32364
- const entryName = basename(entry).toLowerCase();
32649
+ const entryName = basename2(entry).toLowerCase();
32365
32650
  if (entryName !== "bin.js" && entryName !== "leadbay-mcp" && entryName !== "index.js") return false;
32366
32651
  const self = fileURLToPath(import.meta.url);
32367
32652
  return realpathSync(self) === realpathSync(entry);