@leadbay/mcp 0.21.0 → 0.21.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +16 -0
- package/dist/bin.js +449 -65
- package/dist/http-server.js +130 -7
- package/dist/installer-electron.js +351 -144
- package/dist/installer-gui.js +349 -142
- package/package.json +1 -1
package/dist/bin.js
CHANGED
|
@@ -10828,6 +10828,13 @@ function coerceCell(client, v, path) {
|
|
|
10828
10828
|
return String(v);
|
|
10829
10829
|
throw client.makeError("IMPORT_INVALID_CELL_TYPE", `Cell at ${path} is ${Array.isArray(v) ? "an array" : typeof v}, expected string|number|boolean|null`, `Convert the value to a string before passing.`, "POST /imports");
|
|
10830
10830
|
}
|
|
10831
|
+
function enforceLeadStatus(client, raw, path) {
|
|
10832
|
+
const canonical = String(raw).trim().toUpperCase();
|
|
10833
|
+
if (!LEAD_STATUS_SET.has(canonical)) {
|
|
10834
|
+
throw client.makeError("IMPORT_INVALID_STATUS", `${path} ${JSON.stringify(raw)} is not a valid lead status`, `Use one of ${LEAD_STATUSES.join(", ")} (case-insensitive), or omit it.`, "POST /imports");
|
|
10835
|
+
}
|
|
10836
|
+
return canonical;
|
|
10837
|
+
}
|
|
10831
10838
|
function prepareDomainsMode(client, inputs) {
|
|
10832
10839
|
const validInputs = [];
|
|
10833
10840
|
const malformedDomains = [];
|
|
@@ -10949,6 +10956,12 @@ function prepareRecordsMode(client, records, mappings, customFieldCatalog) {
|
|
|
10949
10956
|
if (normDomain && !byDomain.has(normDomain))
|
|
10950
10957
|
byDomain.set(normDomain, idx);
|
|
10951
10958
|
});
|
|
10959
|
+
const statuses = {};
|
|
10960
|
+
for (const [cell, status] of Object.entries(mappings.statuses ?? {})) {
|
|
10961
|
+
statuses[cell] = enforceLeadStatus(client, status, `mappings.statuses[${JSON.stringify(cell)}]`);
|
|
10962
|
+
}
|
|
10963
|
+
const rawDefault = mappings.default_status;
|
|
10964
|
+
const default_status = rawDefault == null || String(rawDefault).trim() === "" ? null : enforceLeadStatus(client, rawDefault, "mappings.default_status");
|
|
10952
10965
|
return {
|
|
10953
10966
|
mode: "records",
|
|
10954
10967
|
validInputs,
|
|
@@ -10958,8 +10971,8 @@ function prepareRecordsMode(client, records, mappings, customFieldCatalog) {
|
|
|
10958
10971
|
header,
|
|
10959
10972
|
mappings: {
|
|
10960
10973
|
fields: { ...normalizedFields },
|
|
10961
|
-
statuses
|
|
10962
|
-
default_status
|
|
10974
|
+
statuses,
|
|
10975
|
+
default_status
|
|
10963
10976
|
}
|
|
10964
10977
|
};
|
|
10965
10978
|
}
|
|
@@ -11334,7 +11347,7 @@ async function runImportInBackground(client, prep, uploadedChunks, opts, ctx, ha
|
|
|
11334
11347
|
})();
|
|
11335
11348
|
}, 0);
|
|
11336
11349
|
}
|
|
11337
|
-
var CHUNK_SIZE, POLL_INTERVAL_MS2, DEFAULT_PER_PHASE_BUDGET_MS, DEFAULT_TOTAL_BUDGET_MS, STABILIZATION_POLLS, MAX_COLUMN_NAME_LEN, RESERVED_COLUMN_RE, CUSTOM_FIELD_RE, IMPORT_RESOLVER_FIELDS, PUBLIC_MAILBOX_DOMAINS, importLeads;
|
|
11350
|
+
var CHUNK_SIZE, POLL_INTERVAL_MS2, DEFAULT_PER_PHASE_BUDGET_MS, DEFAULT_TOTAL_BUDGET_MS, STABILIZATION_POLLS, MAX_COLUMN_NAME_LEN, RESERVED_COLUMN_RE, CUSTOM_FIELD_RE, IMPORT_RESOLVER_FIELDS, PUBLIC_MAILBOX_DOMAINS, LEAD_STATUSES, LEAD_STATUS_SET, importLeads;
|
|
11338
11351
|
var init_import_leads = __esm({
|
|
11339
11352
|
"../core/dist/composite/import-leads.js"() {
|
|
11340
11353
|
"use strict";
|
|
@@ -11379,6 +11392,15 @@ var init_import_leads = __esm({
|
|
|
11379
11392
|
"163.com",
|
|
11380
11393
|
"126.com"
|
|
11381
11394
|
]);
|
|
11395
|
+
LEAD_STATUSES = [
|
|
11396
|
+
"DEFAULT",
|
|
11397
|
+
"INBOUND",
|
|
11398
|
+
"UNWANTED",
|
|
11399
|
+
"WANTED",
|
|
11400
|
+
"LOST",
|
|
11401
|
+
"WON"
|
|
11402
|
+
];
|
|
11403
|
+
LEAD_STATUS_SET = new Set(LEAD_STATUSES);
|
|
11382
11404
|
importLeads = {
|
|
11383
11405
|
name: "leadbay_import_leads",
|
|
11384
11406
|
annotations: {
|
|
@@ -11437,11 +11459,12 @@ var init_import_leads = __esm({
|
|
|
11437
11459
|
},
|
|
11438
11460
|
statuses: {
|
|
11439
11461
|
type: "object",
|
|
11440
|
-
description:
|
|
11462
|
+
description: `Optional map of raw CSV status-cell text \u2192 lead status (rarely needed). Keys are the verbatim cell strings; values must be one of ${LEAD_STATUSES.join(", ")} (case-insensitive). Defaults to {}.`
|
|
11441
11463
|
},
|
|
11442
11464
|
default_status: {
|
|
11443
11465
|
type: ["string", "null"],
|
|
11444
|
-
|
|
11466
|
+
enum: [...LEAD_STATUSES, null],
|
|
11467
|
+
description: `Optional default lead status applied to rows without an explicit status. One of ${LEAD_STATUSES.join(", ")} (case-insensitive). Defaults to null.`
|
|
11445
11468
|
}
|
|
11446
11469
|
},
|
|
11447
11470
|
required: ["fields"]
|
|
@@ -17499,8 +17522,15 @@ var init_import_and_qualify = __esm({
|
|
|
17499
17522
|
type: "object",
|
|
17500
17523
|
description: "Ergonomic shorthand: `{CsvColumn: <number-id>}` or `{CsvColumn: '<field-name>'}` for custom-field mappings. Resolved against /crm/custom_fields catalog."
|
|
17501
17524
|
},
|
|
17502
|
-
statuses: {
|
|
17503
|
-
|
|
17525
|
+
statuses: {
|
|
17526
|
+
type: "object",
|
|
17527
|
+
description: `Optional map of raw CSV status-cell text \u2192 lead status. Values must be one of ${LEAD_STATUSES.join(", ")} (case-insensitive); keys are the verbatim cell strings.`
|
|
17528
|
+
},
|
|
17529
|
+
default_status: {
|
|
17530
|
+
type: ["string", "null"],
|
|
17531
|
+
enum: [...LEAD_STATUSES, null],
|
|
17532
|
+
description: `Optional default lead status. One of ${LEAD_STATUSES.join(", ")} (case-insensitive).`
|
|
17533
|
+
}
|
|
17504
17534
|
},
|
|
17505
17535
|
// mappings has a closed shape (fields/custom_fields/statuses/default_status).
|
|
17506
17536
|
// Inner objects (fields, custom_fields, statuses) keep open shapes
|
|
@@ -22461,6 +22491,49 @@ After your first \`leadbay_pull_leads\` call, capture \`response.lens.id\` (the
|
|
|
22461
22491
|
|
|
22462
22492
|
You don't need to memorize every tool here \u2014 each tool's own description carries a RENDERING block (how to present the response) and a NEXT STEPS block (observation \u2192 suggestion table). Read the relevant tool's description in full when the user picks an entry point. This overview just gets you to the right starting tool.
|
|
22463
22493
|
|
|
22494
|
+
## Proposing a next step \u2014 only when it genuinely helps
|
|
22495
|
+
|
|
22496
|
+
After reporting account state, you MAY propose a concrete next step \u2014 but only when one is genuinely useful, not by reflex. A reflexive "want me to also\u2026?" on every turn is noise; the user notices and it erodes trust.
|
|
22497
|
+
|
|
22498
|
+
**Propose a next step when** the overview surfaced an obvious unfinished thread or a blocker the user would want resolved \u2014 a fresh discovery batch waiting, follow-ups due today, or a quota/auth blocker with a specific unblock action. In those cases the next move is real and worth offering.
|
|
22499
|
+
|
|
22500
|
+
**Skip it when** there's no clear unfinished thread, the user only wanted the status (a bare "where do I stand?"), or the work they asked for is plainly done. A status read that ends cleanly is a complete answer \u2014 don't manufacture a next step just to have one.
|
|
22501
|
+
|
|
22502
|
+
**Lean on memory.** Check the \`_meta.agent_memory.summary\` for prior signal on how this user reacts to next-step offers. If the memory shows they routinely dismiss them, default to NOT proposing (let them ask). If they routinely act on them, lean toward proposing. When the user dismisses or accepts a proposal this turn, that's a material signal \u2014 call \`leadbay_agent_memory_capture\` (\`source:"inferred"\`, low confidence) so the preference compounds across sessions.
|
|
22503
|
+
|
|
22504
|
+
**When you do propose, the proposal IS a native choice dialog \u2014 never a prose "let me know if\u2026".** Route 2\u20134 mutually-exclusive next moves into your host's next-step widget (\`ask_user_input_v0\` on Claude chat / ChatGPT, \`AskUserQuestion\` on Claude cowork / Claude Code). The widget is the question; do not also list the same options as prose.
|
|
22505
|
+
|
|
22506
|
+
**ALWAYS render NEXT STEPS via your host's next-step widget.** Use whichever is in your tool set \u2014 the NAME and SCHEMA differ: **\`ask_user_input_v0\`** (Claude chat / ChatGPT) takes plain-string options with \`type:"single_select"\`; **\`AskUserQuestion\`** (Claude cowork / Claude Code) takes object options \`{label, description}\` plus a required short \`header\` (\u226412 chars) and \`multiSelect\`, NO \`type\` field, and never add an "Other" option (the host adds it). Match the schema to the tool you actually have \u2014 the wrong schema fails silently and you fall back to prose. Prose bullets are the fallback ONLY when NEITHER widget exists. Any turn that would end with a choice must be the widget \u2014 the widget IS the question.
|
|
22507
|
+
|
|
22508
|
+
**If the tool result carries a \`next_steps\` object, that is the source of truth \u2014 use it directly.** Each option has a short \`.label\` (\u22645 words) and a full \`.description\`. Map \`next_steps.options[]\` into your host widget VERBATIM and in order: for \`AskUserQuestion\` (cowork / Claude Code) pass each as \`{label, description}\`; for \`ask_user_input_v0\` (Claude chat / ChatGPT, string options only) pass each option's \`.description\` as the string (it's the full sentence). Do NOT reword, reorder, drop, or prose-ify them \u2014 they're built deterministically by the server so the offer (incl. the artifact option at position 0) fires every time. Fall back to the table below only when there is NO \`next_steps\` field.
|
|
22509
|
+
|
|
22510
|
+
**One exception \u2014 skip the widget** when the user's original message contained a complete sequential instruction chain ("show me X and then do Y") AND all stated steps have been completed. In that case, end with STOP directly \u2014 the user stated their full plan and does not need a "what next?" prompt.
|
|
22511
|
+
- Skip example: "Show me today's leads and then research the top one for me." \u2192 after research completes, emit STOP without the widget.
|
|
22512
|
+
- Do NOT skip for: plain requests ("show me today's leads", "run my check-in"), recurring-language requests ("I do this every day"), or requests where only one action was stated.
|
|
22513
|
+
|
|
22514
|
+
Pick 2\u20134 rows from the (Observation, Suggest, Calls) table below most relevant to the response, then call your host's widget with ITS schema (per the schema rules above \u2014 wrong schema fails silently):
|
|
22515
|
+
- \`ask_user_input_v0\`: \`{questions:[{question,type:"single_select",options:["<Suggest 1>","<Suggest 2>"]}]}\`
|
|
22516
|
+
- \`AskUserQuestion\`: \`{questions:[{question,header:"Next step",multiSelect:false,options:[{label:"<\u22645 words>",description:"<Suggest 1>"}]}]}\`
|
|
22517
|
+
|
|
22518
|
+
User picks \u2192 call the matching \`Calls\` tool. Constraints: 2\u20134 mutually-exclusive options, AskUserQuestion labels \u22645 words (full text in \`description\`), max 3 questions. Table stays internal; never recite it.
|
|
22519
|
+
|
|
22520
|
+
---
|
|
22521
|
+
|
|
22522
|
+
|
|
22523
|
+
|
|
22524
|
+
The overview itself returns no \`next_steps\` object, so when you DO propose, build the options from this table \u2014 pick the 2\u20134 rows that match what the account state actually showed. If none apply cleanly, propose none (the status read was complete) rather than inventing an option.
|
|
22525
|
+
|
|
22526
|
+
All \`Calls\` below are agent-callable \`leadbay_*\` tools (never an MCP prompt name like \`leadbay_daily_check_in\` \u2014 the agent cannot invoke a prompt from a turn; route to the underlying tool instead).
|
|
22527
|
+
|
|
22528
|
+
| Observation | Suggest | Calls |
|
|
22529
|
+
|---------------------------------------------------------------------|--------------------------------------------------------|--------------------------------------------------------------|
|
|
22530
|
+
| Fresh discovery batch waiting / user wants new leads | "See today's best new leads" | leadbay_pull_leads(lensId = pinned) |
|
|
22531
|
+
| Follow-ups due / known leads to re-engage | "Show follow-ups due now" | leadbay_pull_followups |
|
|
22532
|
+
| Quota/credit read shows low or exhausted balance | "Review what's eating your quota" | leadbay_account_status (deeper read) |
|
|
22533
|
+
| Auth/connection blocker (e.g. 401 / AUTH_EXPIRED on a read) | "Reconnect Leadbay to unblock actions" | (guide the user to re-authenticate \u2014 no tool call) |
|
|
22534
|
+
| Lens audience looks mismatched (batch is off-ICP) | "Adjust the lens audience to match your ICP" | ASK first \u2014 collect the target sectors / sizes / exclusions, THEN leadbay_adjust_audience(...) with those params. NEVER call it with no args (an empty call writes the current filter / may clone the default lens \u2014 a no-op or unwanted change). |
|
|
22535
|
+
| Status is healthy and nothing is pending | propose nothing \u2014 the overview is a complete answer | \u2014 |
|
|
22536
|
+
|
|
22464
22537
|
GATE \u2014 DEFER TO TOOL RENDERING. When you call a Leadbay composite that ships its own RENDERING block (every composite in 0.9.0+ does), render the response using that block's recipe verbatim \u2014 score bars, glyph palette, column order, hide-list, link priorities, all of it. Do NOT substitute prose, a numbered list, or a different column structure even when an orchestrating prompt's body suggests alternate framing. Prompt-specific commentary (motivational nudges, summaries, next-action recommendations) belongs ABOVE or BELOW the canonical table, never in place of it.
|
|
22465
22538
|
|
|
22466
22539
|
If the prompt's body and the tool's RENDERING appear to conflict, the tool's RENDERING wins for the structural layout; the prompt's voice wins for the commentary that surrounds it.
|
|
@@ -24436,6 +24509,56 @@ function buildServer(client, opts = {}) {
|
|
|
24436
24509
|
};
|
|
24437
24510
|
};
|
|
24438
24511
|
try {
|
|
24512
|
+
const bootstrapState = opts.bootstrapStatus?.() ?? { done: true };
|
|
24513
|
+
if (!bootstrapState.done) {
|
|
24514
|
+
const url = bootstrapState.signInUrl;
|
|
24515
|
+
const envelope = bootstrapState.failureMessage ? {
|
|
24516
|
+
error: true,
|
|
24517
|
+
code: "AUTH_FAILED",
|
|
24518
|
+
message: "Couldn't sign you in to Leadbay.",
|
|
24519
|
+
hint: `Sign-in failed: ${bootstrapState.failureMessage}
|
|
24520
|
+
|
|
24521
|
+
Restart the Leadbay extension in Claude Desktop to retry. If it keeps failing, check your network/region and that Leadbay is reachable.`
|
|
24522
|
+
} : url ? {
|
|
24523
|
+
// Prefer surfacing the live sign-in URL — the spawned MCP process
|
|
24524
|
+
// often can't open a GUI browser itself (no DISPLAY / sanitized
|
|
24525
|
+
// env), so a clickable link the agent renders is the reliable path.
|
|
24526
|
+
error: true,
|
|
24527
|
+
code: "AUTH_REQUIRED",
|
|
24528
|
+
message: "Sign in to Leadbay to finish connecting.",
|
|
24529
|
+
hint: `Open this link to authorize Leadbay, then re-run this tool:
|
|
24530
|
+
|
|
24531
|
+
${url}
|
|
24532
|
+
|
|
24533
|
+
` + (bootstrapState.openFailed ? "(The extension couldn't open your browser automatically.)" : "(A browser may have opened automatically \u2014 if not, use the link above.)")
|
|
24534
|
+
} : {
|
|
24535
|
+
error: true,
|
|
24536
|
+
code: "AUTH_PENDING",
|
|
24537
|
+
message: "Signing you in to Leadbay \u2014 a browser window should have opened. Authorize there, then try again.",
|
|
24538
|
+
hint: "Complete the Leadbay sign-in in your browser, then re-run this tool."
|
|
24539
|
+
};
|
|
24540
|
+
const pendingText = formatErrorForLLM(envelope);
|
|
24541
|
+
const pendingDur = Date.now() - callStart;
|
|
24542
|
+
telemetry.captureToolCall({
|
|
24543
|
+
tool: name,
|
|
24544
|
+
ok: false,
|
|
24545
|
+
duration_ms: pendingDur,
|
|
24546
|
+
format: "error-envelope",
|
|
24547
|
+
bytes: pendingText.length,
|
|
24548
|
+
error_code: envelope.code,
|
|
24549
|
+
triggered_by
|
|
24550
|
+
});
|
|
24551
|
+
if (DEBUG_ON) {
|
|
24552
|
+
process.stderr.write(
|
|
24553
|
+
`[leadbay-mcp debug] tool=${name} dur=${pendingDur}ms ok=false code=${envelope.code} (auth-bootstrap, no-sentry)
|
|
24554
|
+
`
|
|
24555
|
+
);
|
|
24556
|
+
}
|
|
24557
|
+
return {
|
|
24558
|
+
content: [{ type: "text", text: pendingText }],
|
|
24559
|
+
isError: true
|
|
24560
|
+
};
|
|
24561
|
+
}
|
|
24439
24562
|
if (COMPOSITE_FILE_TOOL_NAMES.has(name) && !triggered_by) {
|
|
24440
24563
|
const envelope = {
|
|
24441
24564
|
error: true,
|
|
@@ -25567,6 +25690,18 @@ import { createHash as createHash5, randomBytes } from "crypto";
|
|
|
25567
25690
|
import { createServer } from "http";
|
|
25568
25691
|
import { request as httpsRequestRaw } from "https";
|
|
25569
25692
|
import { spawn as spawn3 } from "child_process";
|
|
25693
|
+
import { readdirSync as readdirSync2 } from "fs";
|
|
25694
|
+
var LEADBAY_LOOPBACK_PORTS = [51789, 51790, 51791, 51792];
|
|
25695
|
+
var BrowserOpenFailedError = class extends Error {
|
|
25696
|
+
authorizeUrl;
|
|
25697
|
+
constructor(authorizeUrl, cause) {
|
|
25698
|
+
super(
|
|
25699
|
+
`Could not open a browser automatically: ${cause?.message ?? cause}`
|
|
25700
|
+
);
|
|
25701
|
+
this.name = "BrowserOpenFailedError";
|
|
25702
|
+
this.authorizeUrl = authorizeUrl;
|
|
25703
|
+
}
|
|
25704
|
+
};
|
|
25570
25705
|
var STARGATE_URLS = {
|
|
25571
25706
|
prod: "https://stargate.leadbay.app/1.0/user_info",
|
|
25572
25707
|
staging: "https://staging.stargate.leadbay.app/1.0/user_info"
|
|
@@ -25759,13 +25894,24 @@ async function startLoopbackListener(opts) {
|
|
|
25759
25894
|
res.end(renderHtml("You're signed in", "You can close this tab and return to the terminal."));
|
|
25760
25895
|
resolveCallback({ code, state });
|
|
25761
25896
|
});
|
|
25762
|
-
|
|
25763
|
-
|
|
25764
|
-
server.
|
|
25765
|
-
|
|
25897
|
+
const bindPort = async (port) => new Promise((resolve, reject) => {
|
|
25898
|
+
const onErr = (e) => reject(e);
|
|
25899
|
+
server.once("error", onErr);
|
|
25900
|
+
server.listen(port, "127.0.0.1", () => {
|
|
25901
|
+
server.off("error", onErr);
|
|
25766
25902
|
resolve();
|
|
25767
25903
|
});
|
|
25768
25904
|
});
|
|
25905
|
+
let bound = false;
|
|
25906
|
+
for (const port of opts.preferredPorts ?? []) {
|
|
25907
|
+
try {
|
|
25908
|
+
await bindPort(port);
|
|
25909
|
+
bound = true;
|
|
25910
|
+
break;
|
|
25911
|
+
} catch {
|
|
25912
|
+
}
|
|
25913
|
+
}
|
|
25914
|
+
if (!bound) await bindPort(0);
|
|
25769
25915
|
const addr = server.address();
|
|
25770
25916
|
const redirectUri = `http://127.0.0.1:${addr.port}/callback`;
|
|
25771
25917
|
const timer = setTimeout(() => {
|
|
@@ -25773,6 +25919,7 @@ async function startLoopbackListener(opts) {
|
|
|
25773
25919
|
}, opts.timeoutMs);
|
|
25774
25920
|
return {
|
|
25775
25921
|
redirectUri,
|
|
25922
|
+
port: addr.port,
|
|
25776
25923
|
waitForCallback: () => callbackPromise.finally(() => {
|
|
25777
25924
|
clearTimeout(timer);
|
|
25778
25925
|
}),
|
|
@@ -25838,28 +25985,79 @@ async function exchangeCodeForToken(opts) {
|
|
|
25838
25985
|
}
|
|
25839
25986
|
return { accessToken: parsed.access_token };
|
|
25840
25987
|
}
|
|
25841
|
-
|
|
25988
|
+
function browserOpenCandidates(url) {
|
|
25842
25989
|
const platform2 = process.platform;
|
|
25843
|
-
let cmd;
|
|
25844
|
-
let args;
|
|
25845
25990
|
if (platform2 === "darwin") {
|
|
25846
|
-
|
|
25847
|
-
|
|
25848
|
-
|
|
25849
|
-
|
|
25850
|
-
args = ["/c", "start", '""', url];
|
|
25851
|
-
} else {
|
|
25852
|
-
cmd = "xdg-open";
|
|
25853
|
-
args = [url];
|
|
25991
|
+
return [
|
|
25992
|
+
{ cmd: "/usr/bin/open", args: [url] },
|
|
25993
|
+
{ cmd: "open", args: [url] }
|
|
25994
|
+
];
|
|
25854
25995
|
}
|
|
25855
|
-
|
|
25856
|
-
const
|
|
25857
|
-
|
|
25858
|
-
|
|
25859
|
-
|
|
25860
|
-
|
|
25861
|
-
|
|
25862
|
-
}
|
|
25996
|
+
if (platform2 === "win32") {
|
|
25997
|
+
const sysRoot = process.env.SystemRoot || process.env.windir || "C:\\Windows";
|
|
25998
|
+
const cmdExe = `${sysRoot}\\System32\\cmd.exe`;
|
|
25999
|
+
return [
|
|
26000
|
+
{ cmd: cmdExe, args: ["/c", "start", '""', url] },
|
|
26001
|
+
{ cmd: "cmd", args: ["/c", "start", '""', url] }
|
|
26002
|
+
];
|
|
26003
|
+
}
|
|
26004
|
+
return [
|
|
26005
|
+
{ cmd: "/usr/bin/xdg-open", args: [url] },
|
|
26006
|
+
{ cmd: "/usr/local/bin/xdg-open", args: [url] },
|
|
26007
|
+
{ cmd: "xdg-open", args: [url] }
|
|
26008
|
+
];
|
|
26009
|
+
}
|
|
26010
|
+
function browserLaunchEnv(debug) {
|
|
26011
|
+
const env = { ...process.env };
|
|
26012
|
+
if (process.platform !== "linux") return env;
|
|
26013
|
+
const runtimeDir = env.XDG_RUNTIME_DIR;
|
|
26014
|
+
if (!env.WAYLAND_DISPLAY && runtimeDir) {
|
|
26015
|
+
try {
|
|
26016
|
+
const sock = readdirSync2(runtimeDir).find((f) => /^wayland-\d+$/.test(f));
|
|
26017
|
+
if (sock) {
|
|
26018
|
+
env.WAYLAND_DISPLAY = sock;
|
|
26019
|
+
debug?.(`browserLaunchEnv: injected WAYLAND_DISPLAY=${sock}`);
|
|
26020
|
+
}
|
|
26021
|
+
} catch {
|
|
26022
|
+
}
|
|
26023
|
+
}
|
|
26024
|
+
if (!env.DISPLAY) {
|
|
26025
|
+
try {
|
|
26026
|
+
const x = readdirSync2("/tmp/.X11-unix").map((f) => f.match(/^X(\d+)$/)?.[1]).filter((n) => !!n).sort((a, b) => Number(a) - Number(b))[0];
|
|
26027
|
+
env.DISPLAY = x !== void 0 ? `:${x}` : ":0";
|
|
26028
|
+
} catch {
|
|
26029
|
+
env.DISPLAY = ":0";
|
|
26030
|
+
}
|
|
26031
|
+
debug?.(`browserLaunchEnv: injected DISPLAY=${env.DISPLAY}`);
|
|
26032
|
+
}
|
|
26033
|
+
return env;
|
|
26034
|
+
}
|
|
26035
|
+
async function openInBrowser(url, debug) {
|
|
26036
|
+
const candidates = browserOpenCandidates(url);
|
|
26037
|
+
const launchEnv = browserLaunchEnv(debug);
|
|
26038
|
+
debug?.(
|
|
26039
|
+
`openInBrowser: platform=${process.platform} DISPLAY=${launchEnv.DISPLAY ?? "<unset>"} WAYLAND=${launchEnv.WAYLAND_DISPLAY ?? "<unset>"} DBUS=${launchEnv.DBUS_SESSION_BUS_ADDRESS ? "set" : "<unset>"} candidates=[${candidates.map((c) => c.cmd).join(", ")}]`
|
|
26040
|
+
);
|
|
26041
|
+
let lastErr;
|
|
26042
|
+
for (const { cmd, args } of candidates) {
|
|
26043
|
+
try {
|
|
26044
|
+
await new Promise((resolve, reject) => {
|
|
26045
|
+
const child = spawn3(cmd, args, { stdio: "ignore", detached: true, env: launchEnv });
|
|
26046
|
+
child.on("error", reject);
|
|
26047
|
+
child.on("spawn", () => {
|
|
26048
|
+
debug?.(`spawn OK: ${cmd} (pid=${child.pid})`);
|
|
26049
|
+
child.unref();
|
|
26050
|
+
resolve();
|
|
26051
|
+
});
|
|
26052
|
+
});
|
|
26053
|
+
return;
|
|
26054
|
+
} catch (err) {
|
|
26055
|
+
lastErr = err;
|
|
26056
|
+
debug?.(`spawn FAILED: ${cmd} \u2192 ${err?.code ?? err?.message ?? err}`);
|
|
26057
|
+
}
|
|
26058
|
+
}
|
|
26059
|
+
debug?.(`openInBrowser: ALL candidates failed (lastErr=${lastErr?.message ?? lastErr})`);
|
|
26060
|
+
throw lastErr ?? new Error("no browser launcher available");
|
|
25863
26061
|
}
|
|
25864
26062
|
async function oauthLogin(opts) {
|
|
25865
26063
|
const log = opts.log ?? (() => {
|
|
@@ -25872,22 +26070,45 @@ async function oauthLogin(opts) {
|
|
|
25872
26070
|
const state = base64UrlEncode(randomBytes(16));
|
|
25873
26071
|
const pkce = generatePkce();
|
|
25874
26072
|
log("Starting loopback listener on 127.0.0.1\u2026\n");
|
|
25875
|
-
const listener = await startLoopbackListener({
|
|
26073
|
+
const listener = await startLoopbackListener({
|
|
26074
|
+
expectedState: state,
|
|
26075
|
+
timeoutMs,
|
|
26076
|
+
preferredPorts: LEADBAY_LOOPBACK_PORTS
|
|
26077
|
+
});
|
|
25876
26078
|
try {
|
|
25877
|
-
|
|
26079
|
+
const boundPort = listener.port;
|
|
26080
|
+
let clientId = opts.getCachedClientId?.(boundPort);
|
|
26081
|
+
if (clientId) {
|
|
26082
|
+
log(`Reusing cached OAuth client_id (${clientId}) for port ${boundPort} \u2014 skipping registration.
|
|
25878
26083
|
`);
|
|
25879
|
-
|
|
25880
|
-
|
|
25881
|
-
|
|
25882
|
-
|
|
25883
|
-
|
|
26084
|
+
} else {
|
|
26085
|
+
log(`Registering client at ${doc.registration_endpoint} (redirect ${listener.redirectUri})\u2026
|
|
26086
|
+
`);
|
|
26087
|
+
const registered = await registerClient(doc.registration_endpoint, {
|
|
26088
|
+
clientName: opts.clientName,
|
|
26089
|
+
redirectUri: listener.redirectUri,
|
|
26090
|
+
// exact bound-port redirect
|
|
26091
|
+
logoUri: opts.logoUri
|
|
26092
|
+
});
|
|
26093
|
+
clientId = registered.client_id;
|
|
26094
|
+
try {
|
|
26095
|
+
opts.onClientRegistered?.(clientId, boundPort);
|
|
26096
|
+
} catch {
|
|
26097
|
+
}
|
|
26098
|
+
}
|
|
25884
26099
|
const authorizeUrl = new URL(doc.authorization_endpoint);
|
|
25885
26100
|
authorizeUrl.searchParams.set("response_type", "code");
|
|
25886
|
-
authorizeUrl.searchParams.set("client_id",
|
|
26101
|
+
authorizeUrl.searchParams.set("client_id", clientId);
|
|
25887
26102
|
authorizeUrl.searchParams.set("redirect_uri", listener.redirectUri);
|
|
25888
26103
|
authorizeUrl.searchParams.set("state", state);
|
|
25889
26104
|
authorizeUrl.searchParams.set("code_challenge", pkce.challenge);
|
|
25890
26105
|
authorizeUrl.searchParams.set("code_challenge_method", pkce.method);
|
|
26106
|
+
if (opts.onAuthorizeUrl) {
|
|
26107
|
+
try {
|
|
26108
|
+
opts.onAuthorizeUrl(authorizeUrl.toString());
|
|
26109
|
+
} catch {
|
|
26110
|
+
}
|
|
26111
|
+
}
|
|
25891
26112
|
log(`Opening browser to authorize\u2026
|
|
25892
26113
|
${authorizeUrl.toString()}
|
|
25893
26114
|
`);
|
|
@@ -25899,6 +26120,9 @@ async function oauthLogin(opts) {
|
|
|
25899
26120
|
${authorizeUrl.toString()}
|
|
25900
26121
|
`
|
|
25901
26122
|
);
|
|
26123
|
+
if (opts.failFastOnOpenError) {
|
|
26124
|
+
throw new BrowserOpenFailedError(authorizeUrl.toString(), err);
|
|
26125
|
+
}
|
|
25902
26126
|
}
|
|
25903
26127
|
log("Waiting for authorization (5 min timeout)\u2026\n");
|
|
25904
26128
|
const { code } = await listener.waitForCallback();
|
|
@@ -25907,7 +26131,7 @@ async function oauthLogin(opts) {
|
|
|
25907
26131
|
tokenEndpoint: doc.token_endpoint,
|
|
25908
26132
|
code,
|
|
25909
26133
|
codeVerifier: pkce.verifier,
|
|
25910
|
-
clientId
|
|
26134
|
+
clientId,
|
|
25911
26135
|
redirectUri: listener.redirectUri
|
|
25912
26136
|
});
|
|
25913
26137
|
return { accessToken };
|
|
@@ -25928,7 +26152,7 @@ var OAUTH_BASE_URLS = {
|
|
|
25928
26152
|
fr: "https://staging.api.leadbay.app"
|
|
25929
26153
|
}
|
|
25930
26154
|
};
|
|
25931
|
-
var VERSION = "0.21.
|
|
26155
|
+
var VERSION = "0.21.2";
|
|
25932
26156
|
var HELP = `
|
|
25933
26157
|
leadbay-mcp ${VERSION} \u2014 Leadbay Model Context Protocol server
|
|
25934
26158
|
|
|
@@ -26057,8 +26281,65 @@ function resolveOAuthBootstrapCredentialsPath() {
|
|
|
26057
26281
|
legacy: resolved.legacy
|
|
26058
26282
|
};
|
|
26059
26283
|
}
|
|
26284
|
+
var pendingSignInUrl;
|
|
26285
|
+
var browserOpenFailedAtBootstrap = false;
|
|
26286
|
+
var bootstrapFailureMessage;
|
|
26287
|
+
function bootstrapDebug(msg) {
|
|
26288
|
+
try {
|
|
26289
|
+
const { appendFileSync, mkdirSync } = require_("node:fs");
|
|
26290
|
+
const { join: join4 } = require_("node:path");
|
|
26291
|
+
const { homedir: homedir5 } = require_("node:os");
|
|
26292
|
+
const dir = join4(homedir5(), ".leadbay");
|
|
26293
|
+
mkdirSync(dir, { recursive: true });
|
|
26294
|
+
const ts = (/* @__PURE__ */ new Date()).toISOString();
|
|
26295
|
+
appendFileSync(join4(dir, "oauth-bootstrap-debug.log"), `${ts} [pid ${process.pid}] ${msg}
|
|
26296
|
+
`);
|
|
26297
|
+
} catch {
|
|
26298
|
+
}
|
|
26299
|
+
}
|
|
26300
|
+
function oauthClientCachePath() {
|
|
26301
|
+
const { join: join4 } = require_("node:path");
|
|
26302
|
+
const { homedir: homedir5 } = require_("node:os");
|
|
26303
|
+
return join4(homedir5(), ".leadbay", "oauth-client.json");
|
|
26304
|
+
}
|
|
26305
|
+
function getCachedOAuthClientId(authServerBaseUrl, port) {
|
|
26306
|
+
try {
|
|
26307
|
+
const { readFileSync: readFileSync3 } = require_("node:fs");
|
|
26308
|
+
const parsed = JSON.parse(readFileSync3(oauthClientCachePath(), "utf8"));
|
|
26309
|
+
const byPort = parsed?.clients?.[authServerBaseUrl]?.byPort;
|
|
26310
|
+
const id = byPort?.[String(port)];
|
|
26311
|
+
return typeof id === "string" && id.length > 0 ? id : void 0;
|
|
26312
|
+
} catch {
|
|
26313
|
+
return void 0;
|
|
26314
|
+
}
|
|
26315
|
+
}
|
|
26316
|
+
function cacheOAuthClientId(authServerBaseUrl, clientId, port) {
|
|
26317
|
+
try {
|
|
26318
|
+
const { readFileSync: readFileSync3, writeFileSync, mkdirSync } = require_("node:fs");
|
|
26319
|
+
const { dirname: dirname3 } = require_("node:path");
|
|
26320
|
+
const path = oauthClientCachePath();
|
|
26321
|
+
let data = { clients: {} };
|
|
26322
|
+
try {
|
|
26323
|
+
data = JSON.parse(readFileSync3(path, "utf8"));
|
|
26324
|
+
if (!data || typeof data !== "object" || typeof data.clients !== "object") data = { clients: {} };
|
|
26325
|
+
} catch {
|
|
26326
|
+
}
|
|
26327
|
+
const server = data.clients[authServerBaseUrl];
|
|
26328
|
+
const byPort = server && typeof server === "object" && server.byPort && typeof server.byPort === "object" ? server.byPort : {};
|
|
26329
|
+
byPort[String(port)] = clientId;
|
|
26330
|
+
data.clients[authServerBaseUrl] = { byPort };
|
|
26331
|
+
mkdirSync(dirname3(path), { recursive: true });
|
|
26332
|
+
writeFileSync(path, JSON.stringify(data, null, 2) + "\n", { mode: 384 });
|
|
26333
|
+
} catch {
|
|
26334
|
+
}
|
|
26335
|
+
}
|
|
26336
|
+
var browserOpenInFlight = null;
|
|
26337
|
+
var bootstrapInFlight = null;
|
|
26060
26338
|
async function bootstrapOAuthIfMissing(logger) {
|
|
26061
26339
|
if (process.env.LEADBAY_TOKEN) return false;
|
|
26340
|
+
bootstrapDebug(
|
|
26341
|
+
`bootstrap START \u2014 clientName=Leadbay MCP, REGION=${process.env.LEADBAY_REGION ?? "<unset>"} BASE_URL=${process.env.LEADBAY_BASE_URL ?? "<unset>"}`
|
|
26342
|
+
);
|
|
26062
26343
|
const { hostname } = await import("os");
|
|
26063
26344
|
process.stderr.write(
|
|
26064
26345
|
`
|
|
@@ -26094,7 +26375,49 @@ async function bootstrapOAuthIfMissing(logger) {
|
|
|
26094
26375
|
const { accessToken } = await oauthLogin({
|
|
26095
26376
|
authServerBaseUrl,
|
|
26096
26377
|
clientName: `Leadbay MCP @ ${hostname()}`,
|
|
26097
|
-
log: (m) => process.stderr.write(m)
|
|
26378
|
+
log: (m) => process.stderr.write(m),
|
|
26379
|
+
// The moment the URL is known: (1) stash it so the AUTH_PENDING envelope
|
|
26380
|
+
// surfaces a clickable link (reliable fallback), and (2) fire the browser
|
|
26381
|
+
// auto-open OURSELVES, tracked in browserOpenInFlight so shutdown waits
|
|
26382
|
+
// for the spawn to dispatch. This wins the install-time race: Claude
|
|
26383
|
+
// Desktop probes a fresh extension with <100ms connect→shutdown cycles,
|
|
26384
|
+
// and tracking the open lets us finish dispatching the detached child
|
|
26385
|
+
// even if SIGTERM/stdin-end arrives first. We pass our own opener as
|
|
26386
|
+
// `openBrowser` so oauthLogin doesn't ALSO open (no double tab).
|
|
26387
|
+
onAuthorizeUrl: (url) => {
|
|
26388
|
+
pendingSignInUrl = url;
|
|
26389
|
+
bootstrapDebug(`authorize URL ready \u2014 spawning browser open`);
|
|
26390
|
+
browserOpenInFlight = openInBrowser(url, bootstrapDebug).then(() => {
|
|
26391
|
+
bootstrapDebug(`auto-open dispatched OK`);
|
|
26392
|
+
}).catch((err) => {
|
|
26393
|
+
browserOpenFailedAtBootstrap = true;
|
|
26394
|
+
bootstrapDebug(`auto-open FAILED: ${err?.message ?? err}`);
|
|
26395
|
+
process.stderr.write(
|
|
26396
|
+
`[leadbay-mcp] auto-open browser failed (${err?.message ?? err}); user has the sign-in link.
|
|
26397
|
+
`
|
|
26398
|
+
);
|
|
26399
|
+
}).finally(() => {
|
|
26400
|
+
browserOpenInFlight = null;
|
|
26401
|
+
});
|
|
26402
|
+
},
|
|
26403
|
+
// No-op: we drive the open from onAuthorizeUrl (tracked) instead, so the
|
|
26404
|
+
// shutdown race can be handled. Returning resolved means oauthLogin won't
|
|
26405
|
+
// try its own open or hit the fail-fast path.
|
|
26406
|
+
openBrowser: async () => {
|
|
26407
|
+
},
|
|
26408
|
+
// Reuse a cached client_id (keyed by auth server + loopback port) so we
|
|
26409
|
+
// register at most once — avoids the 429 from re-registering on every
|
|
26410
|
+
// probe-restart. Port is part of the key because the backend pins the
|
|
26411
|
+
// exact redirect_uri (port included).
|
|
26412
|
+
getCachedClientId: (port) => {
|
|
26413
|
+
const id = getCachedOAuthClientId(authServerBaseUrl, port);
|
|
26414
|
+
if (id) bootstrapDebug(`reusing cached client_id=${id} (port ${port}) for ${authServerBaseUrl}`);
|
|
26415
|
+
return id;
|
|
26416
|
+
},
|
|
26417
|
+
onClientRegistered: (id, port) => {
|
|
26418
|
+
bootstrapDebug(`registered new client_id=${id} (port ${port}) \u2014 caching for ${authServerBaseUrl}`);
|
|
26419
|
+
cacheOAuthClientId(authServerBaseUrl, id, port);
|
|
26420
|
+
}
|
|
26098
26421
|
});
|
|
26099
26422
|
try {
|
|
26100
26423
|
const { writeFileSync, mkdirSync, chmodSync } = require_("node:fs");
|
|
@@ -26131,12 +26454,27 @@ async function bootstrapOAuthIfMissing(logger) {
|
|
|
26131
26454
|
process.env.LEADBAY_TOKEN = accessToken;
|
|
26132
26455
|
process.env.LEADBAY_REGION = region;
|
|
26133
26456
|
if (isStaging || envBaseUrl) process.env.LEADBAY_BASE_URL = authServerBaseUrl;
|
|
26457
|
+
pendingSignInUrl = void 0;
|
|
26458
|
+
bootstrapDebug(`bootstrap COMPLETE \u2014 token acquired, region=${region}`);
|
|
26134
26459
|
logger.info?.(`OAuth bootstrap complete \u2014 region=${region}`);
|
|
26135
26460
|
return true;
|
|
26136
26461
|
} catch (err) {
|
|
26462
|
+
if (err instanceof BrowserOpenFailedError) {
|
|
26463
|
+
browserOpenFailedAtBootstrap = true;
|
|
26464
|
+
process.stderr.write(
|
|
26465
|
+
`[leadbay-mcp] Could not open a browser automatically: ${err.message}
|
|
26466
|
+
The sign-in link is surfaced to the user via the AUTH_PENDING envelope.
|
|
26467
|
+
`
|
|
26468
|
+
);
|
|
26469
|
+
return false;
|
|
26470
|
+
}
|
|
26471
|
+
const message = err?.message ?? String(err);
|
|
26472
|
+
bootstrapFailureMessage = message;
|
|
26473
|
+
pendingSignInUrl = void 0;
|
|
26474
|
+
bootstrapDebug(`bootstrap FAILED (non-open): ${message}`);
|
|
26137
26475
|
process.stderr.write(
|
|
26138
|
-
`[leadbay-mcp] OAuth bootstrap failed: ${
|
|
26139
|
-
|
|
26476
|
+
`[leadbay-mcp] OAuth bootstrap failed: ${message}
|
|
26477
|
+
Tools will return AUTH_FAILED until you restart the extension to retry.
|
|
26140
26478
|
`
|
|
26141
26479
|
);
|
|
26142
26480
|
return false;
|
|
@@ -26146,30 +26484,16 @@ async function resolveClientFromEnv(logger) {
|
|
|
26146
26484
|
if (process.env.LEADBAY_OAUTH_BOOTSTRAP === "1") {
|
|
26147
26485
|
hydrateEnvFromCredentialsFile();
|
|
26148
26486
|
if (!process.env.LEADBAY_TOKEN) {
|
|
26149
|
-
|
|
26487
|
+
const regionEnv2 = process.env.LEADBAY_REGION;
|
|
26488
|
+
const region = regionEnv2 === "fr" ? "fr" : "us";
|
|
26489
|
+
const config = { region };
|
|
26490
|
+
if (process.env.LEADBAY_BASE_URL) config.baseUrl = process.env.LEADBAY_BASE_URL;
|
|
26491
|
+
logger.info?.("OAuth bootstrap pending \u2014 server will come up unauthenticated, OAuth runs in background");
|
|
26492
|
+
return { client: createClient(config), authState: "pending" };
|
|
26150
26493
|
}
|
|
26151
26494
|
}
|
|
26152
26495
|
const token = process.env.LEADBAY_TOKEN;
|
|
26153
26496
|
if (!token) {
|
|
26154
|
-
if (process.env.LEADBAY_OAUTH_BOOTSTRAP === "1") {
|
|
26155
|
-
process.stderr.write(
|
|
26156
|
-
"leadbay-mcp: OAuth authorization is required but no token is available.\n Restart the Claude Desktop extension to authorize Leadbay in your browser.\n\nRun `leadbay-mcp --help` for the full config template.\n"
|
|
26157
|
-
);
|
|
26158
|
-
const regionEnv3 = process.env.LEADBAY_REGION;
|
|
26159
|
-
const region2 = regionEnv3 === "fr" ? "fr" : "us";
|
|
26160
|
-
return {
|
|
26161
|
-
client: makeBrokenClient(
|
|
26162
|
-
{
|
|
26163
|
-
error: true,
|
|
26164
|
-
code: "AUTH_MISSING",
|
|
26165
|
-
message: "Leadbay OAuth authorization has not completed.",
|
|
26166
|
-
hint: "Restart the Claude Desktop extension and complete the Leadbay OAuth browser authorization."
|
|
26167
|
-
},
|
|
26168
|
-
region2
|
|
26169
|
-
),
|
|
26170
|
-
authState: "missing"
|
|
26171
|
-
};
|
|
26172
|
-
}
|
|
26173
26497
|
process.stderr.write(
|
|
26174
26498
|
"leadbay-mcp: LEADBAY_TOKEN environment variable is required.\n 1. Run: npx -y @leadbay/mcp install --oauth\n 2. Set it in your MCP client config (e.g. claude_desktop_config.json).\n\nRun `leadbay-mcp --help` for the full config template.\n"
|
|
26175
26499
|
);
|
|
@@ -27000,7 +27324,8 @@ async function main() {
|
|
|
27000
27324
|
installStartupSafetyNets(logger);
|
|
27001
27325
|
const telemetry = initTelemetry({ version: VERSION, logger });
|
|
27002
27326
|
const { client, authState } = await resolveClientFromEnv(logger);
|
|
27003
|
-
|
|
27327
|
+
const bootstrapPending = authState === "pending";
|
|
27328
|
+
if (!bootstrapPending) telemetry.identify(client);
|
|
27004
27329
|
telemetry.captureStartup({
|
|
27005
27330
|
auth_state: authState,
|
|
27006
27331
|
region: client.region
|
|
@@ -27048,14 +27373,71 @@ async function main() {
|
|
|
27048
27373
|
notificationsInbox,
|
|
27049
27374
|
version: VERSION,
|
|
27050
27375
|
telemetry,
|
|
27051
|
-
updateStateStore
|
|
27376
|
+
updateStateStore,
|
|
27377
|
+
// Non-blocking OAuth bootstrap gate. Read per tool call: once the
|
|
27378
|
+
// background flow lands the token (client.isAuthenticated → true) this
|
|
27379
|
+
// reports done and tools execute. While waiting it surfaces the live
|
|
27380
|
+
// sign-in URL (captured via onAuthorizeUrl) so the agent can render a
|
|
27381
|
+
// clickable link — the reliable path when the spawned process can't open
|
|
27382
|
+
// a browser itself.
|
|
27383
|
+
bootstrapStatus: bootstrapPending ? () => client.isAuthenticated ? { done: true } : {
|
|
27384
|
+
done: false,
|
|
27385
|
+
signInUrl: pendingSignInUrl,
|
|
27386
|
+
openFailed: browserOpenFailedAtBootstrap,
|
|
27387
|
+
failureMessage: bootstrapFailureMessage
|
|
27388
|
+
} : void 0
|
|
27052
27389
|
});
|
|
27053
27390
|
const transport = new StdioServerTransport();
|
|
27054
27391
|
logger.info?.(
|
|
27055
27392
|
`Starting MCP server v${VERSION} (advanced=${includeAdvanced}, write=${includeWrite}, baseUrl=${client.baseUrl}, bulk_store=${bulkTracker.durability}, notifications_ws=${WS_DISABLED ? "disabled" : "enabled"}, auth_state=${authState})`
|
|
27056
27393
|
);
|
|
27057
27394
|
await server.connect(transport);
|
|
27395
|
+
if (bootstrapPending) {
|
|
27396
|
+
bootstrapDebug(`server connected; launching background OAuth bootstrap`);
|
|
27397
|
+
bootstrapInFlight = (async () => {
|
|
27398
|
+
const ok = await bootstrapOAuthIfMissing(logger);
|
|
27399
|
+
if (!ok) return;
|
|
27400
|
+
const region = process.env.LEADBAY_REGION === "fr" ? "fr" : "us";
|
|
27401
|
+
const apiBaseUrl = process.env.LEADBAY_BASE_URL ?? REGIONS[region];
|
|
27402
|
+
client.setBaseUrl(apiBaseUrl, region);
|
|
27403
|
+
client.setToken(process.env.LEADBAY_TOKEN);
|
|
27404
|
+
logger.info?.(`OAuth bootstrap landed \u2014 client authenticated (region=${region})`);
|
|
27405
|
+
telemetry.identify(client);
|
|
27406
|
+
if (process.env.LEADBAY_NOTIFICATIONS_WS_DISABLED !== "1" && !notificationsWs) {
|
|
27407
|
+
notificationsWs = new NotificationsWsClient({
|
|
27408
|
+
client,
|
|
27409
|
+
inbox: notificationsInbox,
|
|
27410
|
+
logger
|
|
27411
|
+
});
|
|
27412
|
+
void notificationsWs.start().catch((err) => {
|
|
27413
|
+
logger.warn?.(`notifications.ws start_failed (post-oauth): ${err?.message ?? err}`);
|
|
27414
|
+
});
|
|
27415
|
+
}
|
|
27416
|
+
})().catch((err) => {
|
|
27417
|
+
logger.warn?.(`oauth.bootstrap_bg_failed ${err?.message ?? err}`);
|
|
27418
|
+
});
|
|
27419
|
+
}
|
|
27058
27420
|
const shutdown = async (code) => {
|
|
27421
|
+
if (bootstrapInFlight && !client.isAuthenticated) {
|
|
27422
|
+
bootstrapDebug(`shutdown(code=${code}) while bootstrap in flight \u2014 waiting up to 4s for URL/open`);
|
|
27423
|
+
try {
|
|
27424
|
+
await Promise.race([
|
|
27425
|
+
bootstrapInFlight,
|
|
27426
|
+
new Promise((r) => setTimeout(r, 4e3))
|
|
27427
|
+
]);
|
|
27428
|
+
} catch {
|
|
27429
|
+
}
|
|
27430
|
+
}
|
|
27431
|
+
if (browserOpenInFlight) {
|
|
27432
|
+
bootstrapDebug(`shutdown(code=${code}) browser-open still in flight \u2014 waiting up to 1.5s`);
|
|
27433
|
+
try {
|
|
27434
|
+
await Promise.race([
|
|
27435
|
+
browserOpenInFlight,
|
|
27436
|
+
new Promise((r) => setTimeout(r, 1500))
|
|
27437
|
+
]);
|
|
27438
|
+
} catch {
|
|
27439
|
+
}
|
|
27440
|
+
}
|
|
27059
27441
|
try {
|
|
27060
27442
|
notificationsWs?.stop();
|
|
27061
27443
|
} catch {
|
|
@@ -27101,10 +27483,12 @@ export {
|
|
|
27101
27483
|
buildClaudeCodeRemoveArgs,
|
|
27102
27484
|
buildCodexConfigBlock,
|
|
27103
27485
|
buildShellExportBlock,
|
|
27486
|
+
cacheOAuthClientId,
|
|
27104
27487
|
checkLoginCollision,
|
|
27105
27488
|
computeFreshDefaultPath,
|
|
27106
27489
|
detectClaudeDesktopMode,
|
|
27107
27490
|
formatInstallOsLabel,
|
|
27491
|
+
getCachedOAuthClientId,
|
|
27108
27492
|
installInClaudeCode,
|
|
27109
27493
|
installInCodexConfig,
|
|
27110
27494
|
installInJsonConfig,
|