@leadbay/mcp 0.38.2 → 0.39.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/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.
@@ -14469,12 +14522,12 @@ var init_pull_leads = __esm({
14469
14522
  });
14470
14523
 
14471
14524
  // ../core/dist/composite/_geo-helpers.js
14472
- function expandAlias(text2) {
14473
- const key = text2.trim().toLowerCase();
14474
- return CITY_ALIASES[key] ?? text2;
14525
+ function expandAlias(text3) {
14526
+ const key = text3.trim().toLowerCase();
14527
+ return CITY_ALIASES[key] ?? text3;
14475
14528
  }
14476
- function scoreMatch(text2, match) {
14477
- const t = text2.trim().toLowerCase();
14529
+ function scoreMatch(text3, match) {
14530
+ const t = text3.trim().toLowerCase();
14478
14531
  const n = match.name.trim().toLowerCase();
14479
14532
  if (n === t)
14480
14533
  return 1;
@@ -14497,8 +14550,8 @@ async function resolveLocations(client, texts) {
14497
14550
  const resolved = [...direct];
14498
14551
  const ambiguities = [];
14499
14552
  for (const originalText of free) {
14500
- const text2 = expandAlias(originalText);
14501
- const path = `/geo/search?q=${encodeURIComponent(text2)}`;
14553
+ const text3 = expandAlias(originalText);
14554
+ const path = `/geo/search?q=${encodeURIComponent(text3)}`;
14502
14555
  let response;
14503
14556
  try {
14504
14557
  response = await client.request("GET", path);
@@ -14537,7 +14590,7 @@ async function resolveLocations(client, texts) {
14537
14590
  name: r.name,
14538
14591
  country: r.country,
14539
14592
  level: r.level,
14540
- score: scoreMatch(text2, r)
14593
+ score: scoreMatch(text3, r)
14541
14594
  })).sort((a, b) => {
14542
14595
  if (b.score !== a.score)
14543
14596
  return b.score - a.score;
@@ -15807,9 +15860,9 @@ ${firm.short_description}`);
15807
15860
  out.push(`### ${sec.section_emoji ?? ""} ${label}`.trim());
15808
15861
  const entries = Array.isArray(sec.entries) ? sec.entries : [];
15809
15862
  for (const e of entries.slice(0, 5)) {
15810
- const text2 = e.text ?? e.summary ?? JSON.stringify(e).slice(0, 200);
15863
+ const text3 = e.text ?? e.summary ?? JSON.stringify(e).slice(0, 200);
15811
15864
  const hot = e.hot === true ? " \u{1F525}" : "";
15812
- out.push(`- ${text2}${hot}`);
15865
+ out.push(`- ${text3}${hot}`);
15813
15866
  }
15814
15867
  if (entries.length > 5)
15815
15868
  out.push(`- _${entries.length - 5} more \u2026_`);
@@ -20702,8 +20755,8 @@ function tokens(s) {
20702
20755
  return [];
20703
20756
  return s.toLowerCase().split(/[^\p{L}\p{N}]+/u).filter(Boolean);
20704
20757
  }
20705
- function bestMatches(text2, taxonomy) {
20706
- const want = new Set(tokens(text2));
20758
+ function bestMatches(text3, taxonomy) {
20759
+ const want = new Set(tokens(text3));
20707
20760
  if (want.size === 0)
20708
20761
  return [];
20709
20762
  const ranked = taxonomy.map((s) => {
@@ -20732,12 +20785,12 @@ async function resolveSectors(client, texts, ctx) {
20732
20785
  }
20733
20786
  const resolved = [...direct];
20734
20787
  const ambiguities = [];
20735
- for (const text2 of free) {
20736
- const matches = bestMatches(text2, taxonomy);
20788
+ for (const text3 of free) {
20789
+ const matches = bestMatches(text3, taxonomy);
20737
20790
  if (matches.length === 1 || matches.length >= 2 && matches[0].score >= 0.66 && matches[0].score - matches[1].score >= 0.34) {
20738
20791
  resolved.push(matches[0].id);
20739
20792
  } else {
20740
- ambiguities.push({ sector_text: text2, matches });
20793
+ ambiguities.push({ sector_text: text3, matches });
20741
20794
  }
20742
20795
  }
20743
20796
  return { resolved, ambiguities };
@@ -22066,6 +22119,7 @@ var init_find_new_leads = __esm({
22066
22119
  "../core/dist/composite/find-new-leads.js"() {
22067
22120
  "use strict";
22068
22121
  init_mcp_job_helpers();
22122
+ init_country_guard();
22069
22123
  init_tool_descriptions_generated();
22070
22124
  DEFAULT_WAIT_SECONDS = 45;
22071
22125
  findNewLeads = {
@@ -22190,6 +22244,21 @@ var init_find_new_leads = __esm({
22190
22244
  "channels",
22191
22245
  "exclude_lead_ids"
22192
22246
  ]);
22247
+ const homeCountry = detectCountryLocations(params.filters?.locations, "filters.locations", client.region).filter((hit) => hit.kind === "home_country");
22248
+ let countryNote;
22249
+ if (homeCountry.length > 0) {
22250
+ const dropped = new Set(homeCountry.map((hit) => hit.value));
22251
+ const raw = params.filters.locations;
22252
+ const rest = (Array.isArray(raw) ? raw : [raw]).filter((v) => !dropped.has(v));
22253
+ params = {
22254
+ ...params,
22255
+ filters: { ...params.filters, locations: rest.length > 0 ? rest : void 0 }
22256
+ };
22257
+ const removed = [...dropped].map((v) => `"${v}"`).join(", ");
22258
+ countryNote = {
22259
+ 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.`
22260
+ };
22261
+ }
22193
22262
  rejectCountryLocations(params.filters?.locations, client.region);
22194
22263
  rejectMalformedExclusions(params.exclude_lead_ids);
22195
22264
  rejectOversizedExclusions(params.exclude_lead_ids);
@@ -22267,6 +22336,7 @@ var init_find_new_leads = __esm({
22267
22336
  return {
22268
22337
  dry_run: true,
22269
22338
  ...forecast,
22339
+ ...countryNote,
22270
22340
  region: client.region
22271
22341
  };
22272
22342
  }
@@ -22287,6 +22357,7 @@ var init_find_new_leads = __esm({
22287
22357
  estimated_cost: forecast?.estimated_cost ?? null,
22288
22358
  items_requested: forecast?.items_requested ?? null,
22289
22359
  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.",
22360
+ ...countryNote,
22290
22361
  region: client.region
22291
22362
  };
22292
22363
  }
@@ -22341,6 +22412,7 @@ var init_find_new_leads = __esm({
22341
22412
  since: snapshot.next_since ?? null,
22342
22413
  suggested_wait_seconds: done ? 0 : 60
22343
22414
  },
22415
+ ...countryNote,
22344
22416
  region: client.region
22345
22417
  };
22346
22418
  }
@@ -22348,6 +22420,144 @@ var init_find_new_leads = __esm({
22348
22420
  }
22349
22421
  });
22350
22422
 
22423
+ // ../core/dist/composite/_identity-rows.js
22424
+ function text(v) {
22425
+ return typeof v === "string" && v.trim() ? v.trim() : null;
22426
+ }
22427
+ function toRow(item) {
22428
+ const asked = item.ref?.requested_as ?? {};
22429
+ const row = {
22430
+ input_indexes: item.ref?.input_indexes ?? null,
22431
+ input: text(asked.name) ?? text(asked.website) ?? text(asked.lead_id) ?? text(asked.contact_id),
22432
+ status: item.status
22433
+ };
22434
+ if (item.status_reason)
22435
+ row.status_reason = item.status_reason;
22436
+ const lead = item.lead;
22437
+ if (!lead)
22438
+ return row;
22439
+ const company = lead.company ?? {};
22440
+ row.lead_id = lead.lead_id ?? item.ref?.lead_id ?? void 0;
22441
+ row.name = company.name;
22442
+ row.website = text(company.website);
22443
+ row.linkedin = text(company.socials?.linkedin);
22444
+ return row;
22445
+ }
22446
+ function identityRows(items) {
22447
+ const first = (i) => {
22448
+ const idx = i.ref?.input_indexes;
22449
+ return Array.isArray(idx) && idx.length > 0 ? Math.min(...idx) : Number.MAX_SAFE_INTEGER;
22450
+ };
22451
+ return [...items].sort((a, b) => first(a) - first(b) || a.seq - b.seq).map(toRow);
22452
+ }
22453
+ function identityCoverage(rows) {
22454
+ let resolved = 0;
22455
+ let ambiguous = 0;
22456
+ let notFound = 0;
22457
+ let otherSkipped = 0;
22458
+ let withWebsite = 0;
22459
+ let withLinkedin = 0;
22460
+ for (const r of rows) {
22461
+ if (r.status !== "skipped") {
22462
+ resolved += 1;
22463
+ if (r.website)
22464
+ withWebsite += 1;
22465
+ if (r.linkedin)
22466
+ withLinkedin += 1;
22467
+ } else if (r.status_reason === "low_confidence_identity")
22468
+ ambiguous += 1;
22469
+ else if (r.status_reason === "not_in_universe")
22470
+ notFound += 1;
22471
+ else
22472
+ otherSkipped += 1;
22473
+ }
22474
+ return {
22475
+ resolved,
22476
+ ambiguous,
22477
+ not_found: notFound,
22478
+ other_skipped: otherSkipped,
22479
+ with_website: withWebsite,
22480
+ with_linkedin: withLinkedin,
22481
+ without_website: resolved - withWebsite
22482
+ };
22483
+ }
22484
+ function identityCsv(rows) {
22485
+ return synthesizeCsv(CSV_HEADER, rows.map((r) => ({
22486
+ row: (r.input_indexes ?? []).map((i) => i + 1).join(";"),
22487
+ input: r.input ?? "",
22488
+ status: r.status,
22489
+ reason: r.status_reason ?? "",
22490
+ company: r.name ?? "",
22491
+ website: r.website ?? "",
22492
+ linkedin: r.linkedin ?? "",
22493
+ lead_id: r.lead_id ?? ""
22494
+ })));
22495
+ }
22496
+ async function saveIdentityFile(ctx, jobId, snapshot, items) {
22497
+ if (!ctx?.saveFile)
22498
+ return {};
22499
+ if (!TERMINAL_JOB_STATES.has(snapshot.job.state))
22500
+ return {};
22501
+ if (snapshot.items_truncated || items.length === 0)
22502
+ return {};
22503
+ try {
22504
+ const name = `leadbay-companies-${jobId.slice(0, 8)}.csv`;
22505
+ return { file: await ctx.saveFile(name, identityCsv(identityRows(items))) };
22506
+ } catch (e) {
22507
+ return { file_error: e instanceof Error ? e.message : String(e) };
22508
+ }
22509
+ }
22510
+ function readOffset(v) {
22511
+ const n = typeof v === "string" ? Number(v) : v;
22512
+ return typeof n === "number" && Number.isFinite(n) && n > 0 ? Math.floor(n) : 0;
22513
+ }
22514
+ function identityAnswer(jobId, snapshot, items, offset) {
22515
+ const all = identityRows(items);
22516
+ const rows = all.slice(offset, offset + IDENTITY_ROWS_PER_RESULT);
22517
+ const end = offset + rows.length;
22518
+ const done = TERMINAL_JOB_STATES.has(snapshot.job.state);
22519
+ const truncated = snapshot.items_truncated ?? false;
22520
+ const nextOffset = !done || truncated ? 0 : end < all.length ? end : null;
22521
+ return {
22522
+ summary: {
22523
+ ...identityCoverage(all),
22524
+ stop_reason: snapshot.funnel?.stop_reason ?? null
22525
+ },
22526
+ rows,
22527
+ rows_total: all.length,
22528
+ rows_offset: offset,
22529
+ items_truncated: truncated,
22530
+ scope_notes: snapshot.explain?.scope_notes ?? [],
22531
+ still_running: !done,
22532
+ next_poll: nextOffset === null ? null : {
22533
+ tool: "leadbay_lead_job_status",
22534
+ job_id: jobId,
22535
+ compact: true,
22536
+ offset: nextOffset,
22537
+ suggested_wait_seconds: done ? 0 : 60
22538
+ }
22539
+ };
22540
+ }
22541
+ var IDENTITY_ROWS_PER_RESULT, CSV_HEADER;
22542
+ var init_identity_rows = __esm({
22543
+ "../core/dist/composite/_identity-rows.js"() {
22544
+ "use strict";
22545
+ init_mcp_job_helpers();
22546
+ init_import_leads();
22547
+ IDENTITY_ROWS_PER_RESULT = 100;
22548
+ CSV_HEADER = [
22549
+ "row",
22550
+ "input",
22551
+ "status",
22552
+ "reason",
22553
+ "company",
22554
+ "website",
22555
+ "linkedin",
22556
+ "lead_id"
22557
+ ];
22558
+ }
22559
+ });
22560
+
22351
22561
  // ../core/dist/composite/qualify-leads.js
22352
22562
  function normalizeLeadRefs(refs) {
22353
22563
  if (!Array.isArray(refs))
@@ -22388,7 +22598,7 @@ function rejectMalformedLeadRefs(refs) {
22388
22598
  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
22599
  };
22390
22600
  }
22391
- function text(value) {
22601
+ function text2(value) {
22392
22602
  if (typeof value !== "string")
22393
22603
  return null;
22394
22604
  const v = value.trim().toLowerCase();
@@ -22396,7 +22606,7 @@ function text(value) {
22396
22606
  }
22397
22607
  function derivedRequestId(params) {
22398
22608
  const refs = canonicalSet((params.lead_refs ?? []).map((r) => {
22399
- const website = text(r.website);
22609
+ const website = text2(r.website);
22400
22610
  return [
22401
22611
  // UUIDs are case-insensitive to the backend, so an uppercase id and
22402
22612
  // its lowercase form are the same lead and must share a key.
@@ -22407,8 +22617,8 @@ function derivedRequestId(params) {
22407
22617
  // AND to one key. Fall back to the trimmed/lowercased raw value when
22408
22618
  // it is not domain-shaped, rather than dropping the field.
22409
22619
  website ? normalizeDomain(website) ?? website : null,
22410
- text(r.name),
22411
- text(r.location)
22620
+ text2(r.name),
22621
+ text2(r.location)
22412
22622
  ];
22413
22623
  }));
22414
22624
  const shape = {
@@ -22452,6 +22662,7 @@ var init_qualify_leads = __esm({
22452
22662
  "use strict";
22453
22663
  init_mcp_job_helpers();
22454
22664
  init_import_leads();
22665
+ init_identity_rows();
22455
22666
  init_tool_descriptions_generated();
22456
22667
  DEFAULT_WAIT_SECONDS2 = 45;
22457
22668
  LEAD_REF_FIELDS = [
@@ -22569,6 +22780,7 @@ var init_qualify_leads = __esm({
22569
22780
  const buysChannels = (params.channels?.length ?? 0) > 0;
22570
22781
  const buysQualification = qualify !== false;
22571
22782
  const isPaid = buysQualification || buysChannels;
22783
+ const identityOnly = !isPaid && (params.contact_titles?.length ?? 0) === 0 && params.prior_deliveries == null;
22572
22784
  const vetoed = confirm === false;
22573
22785
  const consented = !vetoed && confirm === true;
22574
22786
  const requestId = presentRequestId(params.request_id) ?? (isPaid ? derivedRequestId(params) : void 0);
@@ -22617,6 +22829,25 @@ var init_qualify_leads = __esm({
22617
22829
  const done = TERMINAL_JOB_STATES.has(snapshot.job.state);
22618
22830
  const indexed = submit.duplicate ?? false ? remapInputIndexes(snapshot.items, params.lead_refs) : { items: snapshot.items, remapped: true };
22619
22831
  const view = { ...snapshot, items: indexed.items };
22832
+ if (identityOnly) {
22833
+ const answer = identityAnswer(submit.job_id, snapshot, view.items, 0);
22834
+ return {
22835
+ job_id: submit.job_id,
22836
+ request_id: requestId ?? null,
22837
+ duplicate_submit: submit.duplicate ?? false,
22838
+ state: snapshot.job.state,
22839
+ done,
22840
+ ...answer,
22841
+ summary: {
22842
+ refs_submitted: params.lead_refs?.length ?? 0,
22843
+ items_requested: submit.items_requested,
22844
+ ...answer.summary
22845
+ },
22846
+ input_indexes_remapped: submit.duplicate ?? false ? indexed.remapped : null,
22847
+ ...await saveIdentityFile(ctx, submit.job_id, snapshot, view.items),
22848
+ region: client.region
22849
+ };
22850
+ }
22620
22851
  return {
22621
22852
  job_id: submit.job_id,
22622
22853
  // Echo the key actually sent, so a retry can reuse it verbatim.
@@ -22685,6 +22916,7 @@ var init_lead_job_status = __esm({
22685
22916
  "../core/dist/composite/lead-job-status.js"() {
22686
22917
  "use strict";
22687
22918
  init_mcp_job_helpers();
22919
+ init_identity_rows();
22688
22920
  init_tool_descriptions_generated();
22689
22921
  leadJobStatus = {
22690
22922
  name: "leadbay_lead_job_status",
@@ -22714,6 +22946,14 @@ var init_lead_job_status = __esm({
22714
22946
  wait_seconds: {
22715
22947
  type: "number",
22716
22948
  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."
22949
+ },
22950
+ compact: {
22951
+ type: "boolean",
22952
+ 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."
22953
+ },
22954
+ offset: {
22955
+ type: "number",
22956
+ 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
22957
  }
22718
22958
  },
22719
22959
  required: ["job_id"],
@@ -22721,8 +22961,19 @@ var init_lead_job_status = __esm({
22721
22961
  },
22722
22962
  execute: async (client, params, ctx) => {
22723
22963
  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);
22964
+ const compact = params.compact === true || params.compact === "true";
22965
+ const since = compact ? void 0 : params.since;
22966
+ 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
22967
  const done = TERMINAL_JOB_STATES.has(snapshot.job.state);
22968
+ if (compact) {
22969
+ return {
22970
+ job_id: params.job_id,
22971
+ state: snapshot.job.state,
22972
+ done,
22973
+ ...identityAnswer(params.job_id, snapshot, snapshot.items, readOffset(params.offset)),
22974
+ region: client.region
22975
+ };
22976
+ }
22726
22977
  const { leads, skipped } = splitItems(snapshot);
22727
22978
  return {
22728
22979
  job_id: params.job_id,
@@ -24164,8 +24415,8 @@ var init_send_feedback = __esm({
24164
24415
  }
24165
24416
  },
24166
24417
  execute: async (client, params, ctx) => {
24167
- const text2 = typeof params.message === "string" ? params.message.trim() : "";
24168
- if (!text2) {
24418
+ const text3 = typeof params.message === "string" ? params.message.trim() : "";
24419
+ if (!text3) {
24169
24420
  return {
24170
24421
  error: true,
24171
24422
  code: "BAD_INPUT",
@@ -24173,7 +24424,7 @@ var init_send_feedback = __esm({
24173
24424
  hint: "Ask the user what they'd like to tell the Leadbay team, then call again with their words in `message`."
24174
24425
  };
24175
24426
  }
24176
- const message = text2.length > MESSAGE_MAX2 ? `${text2.slice(0, MESSAGE_MAX2 - 1)}\u2026` : text2;
24427
+ const message = text3.length > MESSAGE_MAX2 ? `${text3.slice(0, MESSAGE_MAX2 - 1)}\u2026` : text3;
24177
24428
  if (!ctx?.sendFeedback) {
24178
24429
  return {
24179
24430
  sent: false,
@@ -24774,7 +25025,7 @@ var init_dist = __esm({
24774
25025
  init_dist();
24775
25026
  import { realpathSync } from "fs";
24776
25027
  import { fileURLToPath } from "url";
24777
- import { basename } from "path";
25028
+ import { basename as basename2 } from "path";
24778
25029
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
24779
25030
 
24780
25031
  // src/server.ts
@@ -27678,8 +27929,8 @@ var PROMPT_CATALOG_BULLETS = {
27678
27929
  };
27679
27930
 
27680
27931
  // src/prompts.ts
27681
- function userMessage(text2) {
27682
- return { role: "user", content: { type: "text", text: text2 } };
27932
+ function userMessage(text3) {
27933
+ return { role: "user", content: { type: "text", text: text3 } };
27683
27934
  }
27684
27935
  function substitutePlaceholders(body, substitutions) {
27685
27936
  let out = body;
@@ -29378,6 +29629,7 @@ ${url}
29378
29629
  // RAW value: the friction redaction above is an ANALYTICS control, and
29379
29630
  // must not change in-process tool behaviour.
29380
29631
  triggered_by: rawTriggeredBy,
29632
+ saveFile: opts.saveFile,
29381
29633
  // Route leadbay_send_feedback to Sentry's feedback inbox (same place
29382
29634
  // the web app's form lands). NOOP_TELEMETRY returns false, so the
29383
29635
  // tool reports honestly when telemetry is off.
@@ -29620,11 +29872,24 @@ ${url}
29620
29872
  return server;
29621
29873
  }
29622
29874
 
29875
+ // src/local-files.ts
29876
+ import { existsSync as existsSync2 } from "fs";
29877
+ import { writeFile } from "fs/promises";
29878
+ import { homedir } from "os";
29879
+ import { basename, join as join2 } from "path";
29880
+ async function saveToDownloads(name, content, home = homedir()) {
29881
+ const downloads = join2(home, "Downloads");
29882
+ const dir = existsSync2(downloads) ? downloads : home;
29883
+ const path = join2(dir, basename(name));
29884
+ await writeFile(path, content, "utf8");
29885
+ return path;
29886
+ }
29887
+
29623
29888
  // installer/install-shared.ts
29624
29889
  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";
29890
+ import { existsSync as existsSync3, readFileSync as readFileSync2, readdirSync as readdirSync2 } from "fs";
29891
+ import { join as join3 } from "path";
29892
+ import { homedir as homedir2 } from "os";
29628
29893
  var HOSTED_MCP_URL_CHATGPT = "https://mcp.leadbay.app/chatgpt/mcp";
29629
29894
  function formatInstallOsLabel(platform = process.platform, arch = process.arch) {
29630
29895
  const name = platform === "darwin" ? "macOS" : platform === "win32" ? "Windows" : platform === "linux" ? "Linux" : platform;
@@ -29632,15 +29897,15 @@ function formatInstallOsLabel(platform = process.platform, arch = process.arch)
29632
29897
  }
29633
29898
  function detectClaudeDesktopMode(claudeSupportDir) {
29634
29899
  const markers = [];
29635
- const legacy = existsSync2(join2(claudeSupportDir, "claude_desktop_config.json"));
29636
- if (existsSync2(join2(claudeSupportDir, "Claude Extensions"))) {
29900
+ const legacy = existsSync3(join3(claudeSupportDir, "claude_desktop_config.json"));
29901
+ if (existsSync3(join3(claudeSupportDir, "Claude Extensions"))) {
29637
29902
  markers.push("Claude Extensions/");
29638
29903
  }
29639
- if (existsSync2(join2(claudeSupportDir, "extensions-installations.json"))) {
29904
+ if (existsSync3(join3(claudeSupportDir, "extensions-installations.json"))) {
29640
29905
  markers.push("extensions-installations.json");
29641
29906
  }
29642
- const cfgPath = join2(claudeSupportDir, "config.json");
29643
- if (existsSync2(cfgPath)) {
29907
+ const cfgPath = join3(claudeSupportDir, "config.json");
29908
+ if (existsSync3(cfgPath)) {
29644
29909
  try {
29645
29910
  const raw = readFileSync2(cfgPath, "utf8");
29646
29911
  const parsed = JSON.parse(raw);
@@ -29680,8 +29945,8 @@ async function windowsStoreAppInstalled(packageName, appName) {
29680
29945
  });
29681
29946
  }
29682
29947
  function isClaudeStorePackagePresent(localAppData) {
29683
- const packagesDir = join2(localAppData, "Packages");
29684
- if (!existsSync2(packagesDir)) return false;
29948
+ const packagesDir = join3(localAppData, "Packages");
29949
+ if (!existsSync3(packagesDir)) return false;
29685
29950
  try {
29686
29951
  return readdirSync2(packagesDir).some((name) => /^Claude_/i.test(name));
29687
29952
  } catch {
@@ -29690,7 +29955,7 @@ function isClaudeStorePackagePresent(localAppData) {
29690
29955
  }
29691
29956
  async function isClaudeDesktopInstalled(home) {
29692
29957
  if (process.platform === "darwin") {
29693
- return existsSync2("/Applications/Claude.app") || existsSync2(home + "/Applications/Claude.app");
29958
+ return existsSync3("/Applications/Claude.app") || existsSync3(home + "/Applications/Claude.app");
29694
29959
  }
29695
29960
  if (process.platform === "win32") {
29696
29961
  const local = process.env.LOCALAPPDATA ?? home + "/AppData/Local";
@@ -29701,18 +29966,18 @@ async function isClaudeDesktopInstalled(home) {
29701
29966
  local + "/Claude/Claude.exe",
29702
29967
  programFiles ? programFiles + "/Claude/Claude.exe" : null,
29703
29968
  programFilesX86 ? programFilesX86 + "/Claude/Claude.exe" : null
29704
- ].some((candidate) => candidate !== null && existsSync2(candidate));
29969
+ ].some((candidate) => candidate !== null && existsSync3(candidate));
29705
29970
  if (exeInstalled) return true;
29706
29971
  if (isClaudeStorePackagePresent(local)) return true;
29707
29972
  return await windowsStoreAppInstalled("AnthropicPBC.Claude", "Claude");
29708
29973
  }
29709
29974
  const desktopBin = await findOnPath("claude-desktop");
29710
29975
  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");
29976
+ 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
29977
  }
29713
29978
  async function isChatGptDesktopInstalled(home) {
29714
29979
  if (process.platform === "darwin") {
29715
- return existsSync2("/Applications/ChatGPT.app") || existsSync2(home + "/Applications/ChatGPT.app");
29980
+ return existsSync3("/Applications/ChatGPT.app") || existsSync3(home + "/Applications/ChatGPT.app");
29716
29981
  }
29717
29982
  if (process.platform === "win32") {
29718
29983
  const local = process.env.LOCALAPPDATA ?? home + "/AppData/Local";
@@ -29725,7 +29990,7 @@ async function isChatGptDesktopInstalled(home) {
29725
29990
  programFiles ? programFiles + "/ChatGPT/ChatGPT.exe" : null,
29726
29991
  programFilesX86 ? programFilesX86 + "/OpenAI/ChatGPT/ChatGPT.exe" : null,
29727
29992
  programFilesX86 ? programFilesX86 + "/ChatGPT/ChatGPT.exe" : null
29728
- ].some((candidate) => candidate !== null && existsSync2(candidate));
29993
+ ].some((candidate) => candidate !== null && existsSync3(candidate));
29729
29994
  return exeInstalled || await windowsStoreAppInstalled("OpenAI.ChatGPT-Desktop", "ChatGPT");
29730
29995
  }
29731
29996
  return false;
@@ -29733,16 +29998,16 @@ async function isChatGptDesktopInstalled(home) {
29733
29998
  async function isCursorInstalled(home) {
29734
29999
  const cursorBin = await findOnPath("cursor");
29735
30000
  if (cursorBin) return true;
29736
- if (process.platform === "darwin") return existsSync2("/Applications/Cursor.app");
30001
+ if (process.platform === "darwin") return existsSync3("/Applications/Cursor.app");
29737
30002
  if (process.platform === "win32") {
29738
30003
  const local = process.env.LOCALAPPDATA ?? `${home}\\AppData\\Local`;
29739
- return existsSync2(`${local}\\Programs\\Cursor\\Cursor.exe`);
30004
+ return existsSync3(`${local}\\Programs\\Cursor\\Cursor.exe`);
29740
30005
  }
29741
- return existsSync2("/usr/share/applications/cursor.desktop") || existsSync2("/opt/Cursor/cursor");
30006
+ return existsSync3("/usr/share/applications/cursor.desktop") || existsSync3("/opt/Cursor/cursor");
29742
30007
  }
29743
30008
  async function detectClients() {
29744
30009
  const out = [];
29745
- const home = homedir();
30010
+ const home = homedir2();
29746
30011
  const claudeBin = await findOnPath("claude");
29747
30012
  if (claudeBin) {
29748
30013
  out.push({ id: "claude-code", label: "Claude Code", detail: `${claudeBin} mcp add ...` });
@@ -29761,7 +30026,7 @@ async function detectClients() {
29761
30026
  out.push({
29762
30027
  id: "cursor",
29763
30028
  label: "Cursor",
29764
- detail: existsSync2(cursorPath) ? cursorPath : `${cursorPath} (will be created)`,
30029
+ detail: existsSync3(cursorPath) ? cursorPath : `${cursorPath} (will be created)`,
29765
30030
  configPath: cursorPath
29766
30031
  });
29767
30032
  }
@@ -29850,11 +30115,11 @@ async function uninstallFromClaudeCode() {
29850
30115
  // installer/install-json-config.ts
29851
30116
  async function installInJsonConfig(configPath, token, region, includeWrite, telemetryEnabled, localBinPath) {
29852
30117
  try {
29853
- const { readFileSync: readFileSync3, writeFileSync, existsSync: existsSync4, mkdirSync, statSync } = await import("fs");
30118
+ const { readFileSync: readFileSync3, writeFileSync, existsSync: existsSync5, mkdirSync, statSync } = await import("fs");
29854
30119
  const { dirname: dirname2 } = await import("path");
29855
30120
  let parsed = {};
29856
30121
  let preserved = {};
29857
- const existed = existsSync4(configPath);
30122
+ const existed = existsSync5(configPath);
29858
30123
  if (existed) {
29859
30124
  const raw = readFileSync3(configPath, "utf8");
29860
30125
  try {
@@ -29904,8 +30169,8 @@ function stripJsonMcpEntry(existing) {
29904
30169
  }
29905
30170
  async function uninstallFromJsonConfig(configPath) {
29906
30171
  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" };
30172
+ const { existsSync: existsSync5, readFileSync: readFileSync3, writeFileSync } = await import("fs");
30173
+ if (!existsSync5(configPath)) return { ok: true, message: "config not found \u2014 nothing to do" };
29909
30174
  const existing = readFileSync3(configPath, "utf8");
29910
30175
  const { content, changed } = stripJsonMcpEntry(existing);
29911
30176
  if (!changed) return { ok: true, message: "leadbay entry not present" };
@@ -29988,10 +30253,10 @@ function stripShellExportBlock(existing) {
29988
30253
  }
29989
30254
  async function installInCodexConfig(configPath, includeWrite, telemetryEnabled, localBinPath) {
29990
30255
  try {
29991
- const { readFileSync: readFileSync3, writeFileSync, existsSync: existsSync4, mkdirSync, statSync, renameSync, chmodSync } = await import("fs");
30256
+ const { readFileSync: readFileSync3, writeFileSync, existsSync: existsSync5, mkdirSync, statSync, renameSync, chmodSync } = await import("fs");
29992
30257
  const { dirname: dirname2 } = await import("path");
29993
30258
  let existing = "";
29994
- const existed = existsSync4(configPath);
30259
+ const existed = existsSync5(configPath);
29995
30260
  if (existed) {
29996
30261
  existing = readFileSync3(configPath, "utf8");
29997
30262
  } else {
@@ -30037,15 +30302,15 @@ async function appendShellExports(token, region, includeWrite, telemetryEnabled)
30037
30302
  }
30038
30303
  return { ok: true, message: "env exported with setx; restart Codex/terminal" };
30039
30304
  }
30040
- const { existsSync: existsSync4, readFileSync: readFileSync3, writeFileSync, renameSync } = await import("fs");
30305
+ const { existsSync: existsSync5, readFileSync: readFileSync3, writeFileSync, renameSync } = await import("fs");
30041
30306
  const os = await import("os");
30042
30307
  const home = os.homedir();
30043
- const preferred = [`${home}/.zshrc`, `${home}/.bashrc`].filter((path) => existsSync4(path));
30308
+ const preferred = [`${home}/.zshrc`, `${home}/.bashrc`].filter((path) => existsSync5(path));
30044
30309
  const paths = preferred.length ? preferred : [`${home}/.profile`];
30045
30310
  const block = buildShellExportBlock(token, region, includeWrite, telemetryEnabled);
30046
30311
  const updated = [];
30047
30312
  for (const path of paths) {
30048
- const existing = existsSync4(path) ? readFileSync3(path, "utf8") : "";
30313
+ const existing = existsSync5(path) ? readFileSync3(path, "utf8") : "";
30049
30314
  const merged = mergeShellExportBlock(existing, block);
30050
30315
  if (!merged.changed) continue;
30051
30316
  const tmp = `${path}.leadbay.tmp`;
@@ -30063,8 +30328,8 @@ async function appendShellExports(token, region, includeWrite, telemetryEnabled)
30063
30328
  }
30064
30329
  async function uninstallFromCodexConfig(configPath) {
30065
30330
  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" };
30331
+ const { existsSync: existsSync5, readFileSync: readFileSync3, writeFileSync } = await import("fs");
30332
+ if (!existsSync5(configPath)) return { ok: true, message: "config not found \u2014 nothing to do" };
30068
30333
  const existing = readFileSync3(configPath, "utf8");
30069
30334
  const { content, changed } = stripCodexBlock(existing);
30070
30335
  if (!changed) return { ok: true, message: "leadbay block not present" };
@@ -30076,11 +30341,11 @@ async function uninstallFromCodexConfig(configPath) {
30076
30341
  }
30077
30342
  async function uninstallShellExports() {
30078
30343
  try {
30079
- const { existsSync: existsSync4, readFileSync: readFileSync3, writeFileSync, renameSync } = await import("fs");
30344
+ const { existsSync: existsSync5, readFileSync: readFileSync3, writeFileSync, renameSync } = await import("fs");
30080
30345
  const os = await import("os");
30081
30346
  const home = os.homedir();
30082
30347
  const candidates = [`${home}/.zshrc`, `${home}/.bashrc`, `${home}/.profile`].filter(
30083
- (p) => existsSync4(p)
30348
+ (p) => existsSync5(p)
30084
30349
  );
30085
30350
  const updated = [];
30086
30351
  for (const p of candidates) {
@@ -30105,17 +30370,17 @@ async function uninstallShellExports() {
30105
30370
  var DXT_EXTENSION_ID = "local.dxt.leadbay.leadbay";
30106
30371
  async function removeDxtExtension(claudeSupportDir) {
30107
30372
  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");
30373
+ const { existsSync: existsSync5, readFileSync: readFileSync3, writeFileSync, rmSync } = await import("fs");
30374
+ const { join: join4 } = await import("path");
30375
+ const extensionDir = join4(claudeSupportDir, "Claude Extensions", DXT_EXTENSION_ID);
30376
+ const registryPath = join4(claudeSupportDir, "extensions-installations.json");
30112
30377
  let removedDir = false;
30113
30378
  let removedEntry = false;
30114
- if (existsSync4(extensionDir)) {
30379
+ if (existsSync5(extensionDir)) {
30115
30380
  rmSync(extensionDir, { recursive: true, force: true });
30116
30381
  removedDir = true;
30117
30382
  }
30118
- if (existsSync4(registryPath)) {
30383
+ if (existsSync5(registryPath)) {
30119
30384
  try {
30120
30385
  const raw = readFileSync3(registryPath, "utf8");
30121
30386
  const parsed = JSON.parse(raw);
@@ -30142,8 +30407,8 @@ async function removeDxtExtension(claudeSupportDir) {
30142
30407
  }
30143
30408
 
30144
30409
  // installer/install-wizard.ts
30145
- function ansi(text2, code, enabled) {
30146
- return enabled ? `\x1B[${code}m${text2}\x1B[0m` : text2;
30410
+ function ansi(text3, code, enabled) {
30411
+ return enabled ? `\x1B[${code}m${text3}\x1B[0m` : text3;
30147
30412
  }
30148
30413
  function parseInstallSelection(input, clientCount) {
30149
30414
  const normalized = input.trim().toLowerCase();
@@ -30323,7 +30588,7 @@ import {
30323
30588
  } from "fs/promises";
30324
30589
  import { constants as fsConstants } from "fs";
30325
30590
  import { dirname, resolve as resolvePath } from "path";
30326
- import { homedir as homedir2 } from "os";
30591
+ import { homedir as homedir3 } from "os";
30327
30592
  function emptyState() {
30328
30593
  return {
30329
30594
  last_check_time: 0,
@@ -30359,7 +30624,7 @@ var UpdateStateStore = class {
30359
30624
  }
30360
30625
  validatePath(p) {
30361
30626
  if (this.allowUnsafePath) return;
30362
- const home = resolvePath(homedir2());
30627
+ const home = resolvePath(homedir3());
30363
30628
  if (p !== home && !p.startsWith(home + "/") && !p.startsWith(home + "\\")) {
30364
30629
  throw new Error(
30365
30630
  `UpdateStateStore: path ${p} is outside $HOME (${home}). Set LEADBAY_UPDATE_STATE_PATH_UNSAFE=1 to override.`
@@ -30487,7 +30752,7 @@ async function openTmpFileExclusive(path) {
30487
30752
  async function createDefaultUpdateStateStore(opts = {}) {
30488
30753
  const env = opts.env ?? process.env;
30489
30754
  const allowUnsafePath = env.LEADBAY_UPDATE_STATE_PATH_UNSAFE === "1";
30490
- const path = env.LEADBAY_UPDATE_STATE_PATH ?? resolvePath(homedir2(), ".leadbay", "update-state.json");
30755
+ const path = env.LEADBAY_UPDATE_STATE_PATH ?? resolvePath(homedir3(), ".leadbay", "update-state.json");
30491
30756
  try {
30492
30757
  const store = new UpdateStateStore({
30493
30758
  backend: "file",
@@ -30511,7 +30776,7 @@ import { createHash as createHash5, randomBytes } from "crypto";
30511
30776
  import { createServer } from "http";
30512
30777
  import { request as httpsRequestRaw } from "https";
30513
30778
  import { spawn as spawn3 } from "child_process";
30514
- import { readdirSync as readdirSync3, existsSync as existsSync3 } from "fs";
30779
+ import { readdirSync as readdirSync3, existsSync as existsSync4 } from "fs";
30515
30780
  var LEADBAY_LOOPBACK_PORTS = [51789, 51790, 51791, 51792];
30516
30781
  var BrowserOpenFailedError = class extends Error {
30517
30782
  authorizeUrl;
@@ -30844,11 +31109,11 @@ function browserLaunchEnv(debug) {
30844
31109
  const env = { ...process.env };
30845
31110
  if (process.platform !== "linux") return env;
30846
31111
  let runtimeDir = env.XDG_RUNTIME_DIR;
30847
- if (!runtimeDir || !existsSync3(runtimeDir)) {
31112
+ if (!runtimeDir || !existsSync4(runtimeDir)) {
30848
31113
  try {
30849
31114
  const uid = process.getuid?.();
30850
31115
  const candidate = uid !== void 0 ? `/run/user/${uid}` : void 0;
30851
- if (candidate && existsSync3(candidate)) {
31116
+ if (candidate && existsSync4(candidate)) {
30852
31117
  runtimeDir = candidate;
30853
31118
  env.XDG_RUNTIME_DIR = candidate;
30854
31119
  debug?.(`browserLaunchEnv: injected XDG_RUNTIME_DIR=${candidate}`);
@@ -30868,7 +31133,7 @@ function browserLaunchEnv(debug) {
30868
31133
  }
30869
31134
  if (!env.DBUS_SESSION_BUS_ADDRESS && runtimeDir) {
30870
31135
  const busPath = `${runtimeDir}/bus`;
30871
- if (existsSync3(busPath)) {
31136
+ if (existsSync4(busPath)) {
30872
31137
  env.DBUS_SESSION_BUS_ADDRESS = `unix:path=${busPath}`;
30873
31138
  debug?.(`browserLaunchEnv: injected DBUS_SESSION_BUS_ADDRESS=unix:path=${busPath}`);
30874
31139
  }
@@ -30891,7 +31156,7 @@ function browserLaunchEnv(debug) {
30891
31156
  );
30892
31157
  if (cookie) xauth = `${runtimeDir}/${cookie}`;
30893
31158
  }
30894
- if (!xauth && env.HOME && existsSync3(`${env.HOME}/.Xauthority`)) {
31159
+ if (!xauth && env.HOME && existsSync4(`${env.HOME}/.Xauthority`)) {
30895
31160
  xauth = `${env.HOME}/.Xauthority`;
30896
31161
  }
30897
31162
  if (xauth) {
@@ -31054,7 +31319,7 @@ var OAUTH_BASE_URLS = {
31054
31319
  fr: "https://staging.api.leadbay.app"
31055
31320
  }
31056
31321
  };
31057
- var VERSION = "0.38.2";
31322
+ var VERSION = "0.39.0";
31058
31323
  var HELP = `
31059
31324
  leadbay-mcp ${VERSION} \u2014 Leadbay Model Context Protocol server
31060
31325
 
@@ -31154,9 +31419,9 @@ function exitWithTokenError() {
31154
31419
  function hydrateEnvFromCredentialsFile() {
31155
31420
  if (process.env.LEADBAY_TOKEN) return false;
31156
31421
  try {
31157
- const { existsSync: existsSync4, readFileSync: readFileSync3 } = require_("node:fs");
31422
+ const { existsSync: existsSync5, readFileSync: readFileSync3 } = require_("node:fs");
31158
31423
  const { path } = resolveOAuthBootstrapCredentialsPath();
31159
- if (!existsSync4(path)) return false;
31424
+ if (!existsSync5(path)) return false;
31160
31425
  const parsed = JSON.parse(readFileSync3(path, "utf8"));
31161
31426
  const env = parsed?.mcpServers?.leadbay?.env;
31162
31427
  if (!env || typeof env !== "object") return false;
@@ -31177,9 +31442,9 @@ function hydrateEnvFromCredentialsFile() {
31177
31442
  function resolveOAuthBootstrapCredentialsPath() {
31178
31443
  const resolved = resolveDefaultCredentialsPath();
31179
31444
  if (process.env.LEADBAY_OAUTH_STAGING !== "1") return resolved;
31180
- const { dirname: dirname2, join: join3 } = require_("node:path");
31445
+ const { dirname: dirname2, join: join4 } = require_("node:path");
31181
31446
  return {
31182
- path: join3(dirname2(resolved.path), "credentials.staging.json"),
31447
+ path: join4(dirname2(resolved.path), "credentials.staging.json"),
31183
31448
  legacy: resolved.legacy
31184
31449
  };
31185
31450
  }
@@ -31189,20 +31454,20 @@ var bootstrapFailureMessage;
31189
31454
  function bootstrapDebug(msg) {
31190
31455
  try {
31191
31456
  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");
31457
+ const { join: join4 } = require_("node:path");
31458
+ const { homedir: homedir4 } = require_("node:os");
31459
+ const dir = join4(homedir4(), ".leadbay");
31195
31460
  mkdirSync(dir, { recursive: true });
31196
31461
  const ts = (/* @__PURE__ */ new Date()).toISOString();
31197
- appendFileSync(join3(dir, "oauth-bootstrap-debug.log"), `${ts} [pid ${process.pid}] ${msg}
31462
+ appendFileSync(join4(dir, "oauth-bootstrap-debug.log"), `${ts} [pid ${process.pid}] ${msg}
31198
31463
  `);
31199
31464
  } catch {
31200
31465
  }
31201
31466
  }
31202
31467
  function oauthClientCachePath() {
31203
- const { join: join3 } = require_("node:path");
31204
- const { homedir: homedir3 } = require_("node:os");
31205
- return join3(homedir3(), ".leadbay", "oauth-client.json");
31468
+ const { join: join4 } = require_("node:path");
31469
+ const { homedir: homedir4 } = require_("node:os");
31470
+ return join4(homedir4(), ".leadbay", "oauth-client.json");
31206
31471
  }
31207
31472
  function getCachedOAuthClientId(authServerBaseUrl, port) {
31208
31473
  try {
@@ -31529,8 +31794,8 @@ function parseFlag(args, name) {
31529
31794
  function hasFlag(args, name) {
31530
31795
  return args.some((a) => a === `--${name}`);
31531
31796
  }
31532
- function ansi2(text2, code, enabled) {
31533
- return enabled ? `\x1B[${code}m${text2}\x1B[0m` : text2;
31797
+ function ansi2(text3, code, enabled) {
31798
+ return enabled ? `\x1B[${code}m${text3}\x1B[0m` : text3;
31534
31799
  }
31535
31800
  function isInteractiveInstall() {
31536
31801
  return process.stdin.isTTY === true && process.stdout.isTTY === true;
@@ -31750,8 +32015,8 @@ Restart your MCP client to pick up the new server.
31750
32015
  usingLegacyPath = resolved.legacy;
31751
32016
  }
31752
32017
  try {
31753
- const { existsSync: existsSync4, readFileSync: readFileSync3 } = await import("fs");
31754
- if (existsSync4(targetPath) && !force) {
32018
+ const { existsSync: existsSync5, readFileSync: readFileSync3 } = await import("fs");
32019
+ if (existsSync5(targetPath) && !force) {
31755
32020
  let existing;
31756
32021
  try {
31757
32022
  existing = JSON.parse(readFileSync3(targetPath, "utf8"));
@@ -32279,6 +32544,9 @@ async function main() {
32279
32544
  version: VERSION,
32280
32545
  telemetry,
32281
32546
  updateStateStore,
32547
+ // This process runs on the user's machine, so a tool can save a file
32548
+ // they will find. The hosted server (http-server.ts) does not pass this.
32549
+ saveFile: saveToDownloads,
32282
32550
  // Non-blocking OAuth bootstrap gate. Read per tool call: once the
32283
32551
  // background flow lands the token (client.isAuthenticated → true) this
32284
32552
  // reports done and tools execute. While waiting it surfaces the live
@@ -32361,7 +32629,7 @@ var isEntrypoint = (() => {
32361
32629
  try {
32362
32630
  const entry = process.argv[1];
32363
32631
  if (!entry) return false;
32364
- const entryName = basename(entry).toLowerCase();
32632
+ const entryName = basename2(entry).toLowerCase();
32365
32633
  if (entryName !== "bin.js" && entryName !== "leadbay-mcp" && entryName !== "index.js") return false;
32366
32634
  const self = fileURLToPath(import.meta.url);
32367
32635
  return realpathSync(self) === realpathSync(entry);