Skip to content

Commit 91c3069

Browse files
authored
feat: add CAPTCHA fallback detection and project selector (#203)
Add two features to the CLI/MCP/API: 1. CAPTCHA Fallback: Detect reCAPTCHA challenges during login via bframe visibility check, notify user via callback, extend timeout for manual solving, and report captchaDetected in login result. 2. Project Selector: List/select/current/clear Bloomreach projects from the /my-account project tree. Scrapes Angular accordion UI, persists selection in encrypted session metadata, supports CLI commands and MCP tools (bloomreach.projects.{list,select,current,clear}). New modules: - auth/captchaDetector.ts — CAPTCHA detection (detectCaptcha, isCaptchaVisible) - auth/projectSelectors.ts — DOM scraping (scrapeProjectTree, flattenProjects) - bloomreachProjects.ts — BloomreachProjectsService (list/select/current/clear) Modified: - bloomreachAuth.ts — CAPTCHA integration in openLogin poll loop - bloomreachSessionStore.ts — SelectedProjectMetadata, updateSessionMetadata - CLI bloomreach.ts — projects command group + CAPTCHA callback on login - MCP bloomreach-mcp.ts — 4 project tool definitions + handlers Tests: 5079 passing (core: 4985, mcp: 94)
1 parent dd6c68b commit 91c3069

16 files changed

+2585
-43
lines changed

packages/cli/src/bin/bloomreach.ts

Lines changed: 219 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ import {
5252
writeEnvFile,
5353
openBrowserUrl,
5454
BloomreachProfileManager,
55+
BloomreachProjectsService,
5556
BLOOMREACH_API_SETTINGS_URL,
5657
createAuthManager,
5758
} from '@bloomreach-buddy/core';
@@ -12329,6 +12330,11 @@ program
1232912330
profileName: options.profile,
1233012331
timeoutMs: Number(options.timeout),
1233112332
loginUrl: options.loginUrl,
12333+
onCaptchaDetected: () => {
12334+
if (!options.json) {
12335+
console.error(' CAPTCHA detected. Please solve it in the browser window.');
12336+
}
12337+
},
1233212338
});
1233312339

1233412340
if (options.json) {
@@ -12405,6 +12411,219 @@ program
1240512411
},
1240612412
);
1240712413

12414+
// --- Project management ---
12415+
const projects = program
12416+
.command('projects')
12417+
.description('Manage Bloomreach project selection');
12418+
12419+
projects
12420+
.command('list')
12421+
.description('List available Bloomreach projects')
12422+
.option('--profile <name>', 'Browser profile name', 'default')
12423+
.option('--json', 'Output as JSON')
12424+
.action(
12425+
async (options: { profile: string; json?: boolean }) => {
12426+
try {
12427+
const profilesDir = resolveProfilesDir();
12428+
const profileManager = new BloomreachProfileManager({ profilesDir });
12429+
const projectsService = new BloomreachProjectsService(profileManager, { profilesDir });
12430+
12431+
const result = await projectsService.listProjects({
12432+
profileName: options.profile,
12433+
});
12434+
12435+
if (options.json) {
12436+
console.log(JSON.stringify(result, null, 2));
12437+
} else {
12438+
console.log('');
12439+
console.log(' Available Bloomreach Projects');
12440+
console.log(' ============================');
12441+
console.log('');
12442+
for (const org of result.hierarchy.organizations) {
12443+
console.log(` Organization: ${org.name}`);
12444+
for (const ws of org.workspaces) {
12445+
console.log(` Workspace: ${ws.name}`);
12446+
for (const prod of ws.products) {
12447+
console.log(` ${prod.name} (${prod.projectCount} project${prod.projectCount !== 1 ? 's' : ''})`);
12448+
for (const projName of prod.projects) {
12449+
console.log(` - ${projName}`);
12450+
}
12451+
}
12452+
}
12453+
}
12454+
console.log('');
12455+
console.log(` Total: ${result.projects.length} project${result.projects.length !== 1 ? 's' : ''}`);
12456+
console.log('');
12457+
}
12458+
} catch (error) {
12459+
if (options.json) {
12460+
console.log(
12461+
JSON.stringify(
12462+
{ error: error instanceof Error ? error.message : String(error) },
12463+
null,
12464+
2,
12465+
),
12466+
);
12467+
} else {
12468+
console.error(
12469+
`Error: ${error instanceof Error ? error.message : String(error)}`,
12470+
);
12471+
}
12472+
process.exit(1);
12473+
}
12474+
},
12475+
);
12476+
12477+
projects
12478+
.command('select')
12479+
.description('Select a Bloomreach project to work in')
12480+
.argument('<name-or-slug>', 'Project name or URL slug')
12481+
.option('--profile <name>', 'Browser profile name', 'default')
12482+
.option('--json', 'Output as JSON')
12483+
.action(
12484+
async (
12485+
nameOrSlug: string,
12486+
options: { profile: string; json?: boolean },
12487+
) => {
12488+
try {
12489+
const profilesDir = resolveProfilesDir();
12490+
const profileManager = new BloomreachProfileManager({ profilesDir });
12491+
const projectsService = new BloomreachProjectsService(profileManager, { profilesDir });
12492+
12493+
const result = await projectsService.selectProject(nameOrSlug, {
12494+
profileName: options.profile,
12495+
});
12496+
12497+
if (options.json) {
12498+
console.log(JSON.stringify(result, null, 2));
12499+
} else {
12500+
console.log('');
12501+
console.log(` Project selected: ${result.project.name}`);
12502+
console.log(` URL: ${result.project.url}`);
12503+
console.log(` Organization: ${result.project.organization}`);
12504+
console.log(` Workspace: ${result.project.workspace}`);
12505+
console.log(` Product: ${result.project.product}`);
12506+
if (result.previousProject) {
12507+
console.log(` Previous: ${result.previousProject.name}`);
12508+
}
12509+
console.log('');
12510+
}
12511+
} catch (error) {
12512+
if (options.json) {
12513+
console.log(
12514+
JSON.stringify(
12515+
{ error: error instanceof Error ? error.message : String(error) },
12516+
null,
12517+
2,
12518+
),
12519+
);
12520+
} else {
12521+
console.error(
12522+
`Error: ${error instanceof Error ? error.message : String(error)}`,
12523+
);
12524+
}
12525+
process.exit(1);
12526+
}
12527+
},
12528+
);
12529+
12530+
projects
12531+
.command('current')
12532+
.description('Show the currently selected project')
12533+
.option('--profile <name>', 'Browser profile name', 'default')
12534+
.option('--json', 'Output as JSON')
12535+
.action(
12536+
async (options: { profile: string; json?: boolean }) => {
12537+
try {
12538+
const profilesDir = resolveProfilesDir();
12539+
const profileManager = new BloomreachProfileManager({ profilesDir });
12540+
const projectsService = new BloomreachProjectsService(profileManager, { profilesDir });
12541+
12542+
const result = await projectsService.currentProject({
12543+
profileName: options.profile,
12544+
});
12545+
12546+
if (options.json) {
12547+
console.log(JSON.stringify(result, null, 2));
12548+
} else if (result.project) {
12549+
console.log('');
12550+
console.log(` Current project: ${result.project.name}`);
12551+
console.log(` URL: ${result.project.url}`);
12552+
console.log(` Organization: ${result.project.organization}`);
12553+
console.log('');
12554+
} else {
12555+
console.log('');
12556+
console.log(' No project selected. Run "bloomreach projects select <name>" to choose one.');
12557+
console.log('');
12558+
}
12559+
} catch (error) {
12560+
if (options.json) {
12561+
console.log(
12562+
JSON.stringify(
12563+
{ error: error instanceof Error ? error.message : String(error) },
12564+
null,
12565+
2,
12566+
),
12567+
);
12568+
} else {
12569+
console.error(
12570+
`Error: ${error instanceof Error ? error.message : String(error)}`,
12571+
);
12572+
}
12573+
process.exit(1);
12574+
}
12575+
},
12576+
);
12577+
12578+
projects
12579+
.command('clear')
12580+
.description('Clear the current project selection')
12581+
.option('--profile <name>', 'Browser profile name', 'default')
12582+
.option('--json', 'Output as JSON')
12583+
.action(
12584+
async (options: { profile: string; json?: boolean }) => {
12585+
try {
12586+
const profilesDir = resolveProfilesDir();
12587+
const profileManager = new BloomreachProfileManager({ profilesDir });
12588+
const projectsService = new BloomreachProjectsService(profileManager, { profilesDir });
12589+
12590+
const result = await projectsService.clearProject({
12591+
profileName: options.profile,
12592+
});
12593+
12594+
if (options.json) {
12595+
console.log(JSON.stringify(result, null, 2));
12596+
} else if (result.cleared) {
12597+
console.log('');
12598+
console.log(' Project selection cleared.');
12599+
if (result.previousProject) {
12600+
console.log(` Previous: ${result.previousProject.name}`);
12601+
}
12602+
console.log('');
12603+
} else {
12604+
console.log('');
12605+
console.log(' No project was selected.');
12606+
console.log('');
12607+
}
12608+
} catch (error) {
12609+
if (options.json) {
12610+
console.log(
12611+
JSON.stringify(
12612+
{ error: error instanceof Error ? error.message : String(error) },
12613+
null,
12614+
2,
12615+
),
12616+
);
12617+
} else {
12618+
console.error(
12619+
`Error: ${error instanceof Error ? error.message : String(error)}`,
12620+
);
12621+
}
12622+
process.exit(1);
12623+
}
12624+
},
12625+
);
12626+
1240812627
const auth = program
1240912628
.command('auth')
1241012629
.description('Manage Bloomreach browser authentication');

0 commit comments

Comments
 (0)