* feat(askai): add compatibility with algolia mcp search tool [DASH-2294] (#2862) ## Summary Fixes DASH-2294 Add compatibility with the Algolia MCP search tool (`algolia_search_index_${string}`) in AskAI. ## Changes - Add `AlgoliaMCPSearchTool` type to handle the Algolia MCP server search tool - Refactor how number of hits are retrieved in `ToolCall` into a `getNumberOfHits` helper ## Test plan - Added unit tests for modified code ✅ * chore: Update to use tsdown for build system (#2824) * chore: Update to use tsdown for build system * fix: docsearch-react build * fix: lint * fix: glob resolved to incorrect version * chore: migrate to from yarn, lerna and shipjs to bun & changesets (#2827) * chore: add tool-versions file for node and bun versions (#2866) * chore: watch in parallel (#2867) * feat: agent studio feedback integration (#2868) * feat(askai): Split Ask AI modal into own component (#2884) * feat(askai): Split Ask AI modal into own component * refactor(react): share modal utilities * refactor(react): share search box form * refactor(react): extract start screen sections * refactor(react): extract shared modal hooks * fix: lint adapter * refactor(react): reorganize modal files * fix: type error in examples * fix: remove ai modal from adapter for now, fix import paths of react package * feat(askai): Agent Studio core tools (#2886) * feat(askai): Implement dynamic tool calls * move ToolCall to components dir * Converge Agent Studio search tools to same definition, fix client side tools breaking UI state * add examples for custom tools * fix: lint & types * feat(askai): add Agent Studio memory support (#2888) * feat(askai): remove Ask AI transport layer (#2889) * feat(askai): add Agent Studio memory support * refactor(askai): remove Ask AI transport abstraction * feat(askai): Feedback notes and tags (#2890) * feat(askai): add Agent Studio memory support * refactor(askai): remove Ask AI transport abstraction * feat(askai): Feedback notes and tags * fix: bump css bundle size limit * move feedback actions to components * chore: fix deploys for v5 branch * feat(askai): Aggregate MCP search tool calls (#2891) * feat(askai): add Agent Studio memory support * refactor(askai): remove Ask AI transport abstraction * feat(askai): Feedback notes and tags * fix: bump css bundle size limit * move feedback actions to components * feat(askai): Aggregate MCP search tool calls * feat(askai): Allow dynamic indices for Agent Studio (#2893) * feat(v5): UI updates (#2896) * feat(v5): UI updates * fix: css file size * fix: e2e tests * fix: e2e tests * fix: e2e tests * chore: add theme toggle to react demo example * Update sources panel display, update dark theme * fix: lint * fix(askai): address ui review feedback * fix: pin icon positioning * fix(askai): improve a11y and dark-mode shimmer for thinking and error states - add role=alert/status and aria-hidden on error/thinking UI - support dark-mode shimmer gradients via CSS variables - respect prefers-reduced-motion for shimmer - handle null date in useRelativeFormattedDate with fallback translation * feat(v5): Add hit breadcrumbs (#2897) * feat(v5): UI updates * fix: css file size * fix: e2e tests * fix: e2e tests * fix: e2e tests * chore: add theme toggle to react demo example * Update sources panel display, update dark theme * fix: lint * fix(askai): address ui review feedback * fix: pin icon positioning * fix(askai): improve a11y and dark-mode shimmer for thinking and error states - add role=alert/status and aria-hidden on error/thinking UI - support dark-mode shimmer gradients via CSS variables - respect prefers-reduced-motion for shimmer - handle null date in useRelativeFormattedDate with fallback translation * feat(v5): Add hit breadcrumbs * fix: bump css bundle size limit * Fix after conflicts * chore: move CSS building to lightning css (#2898) * feat: Facet filters for search (#2899) * feat(v5): Initial facet filters work * Perf updates, dark theme, facet chips, a11y improvements * fix: bump css bundle size limit * Dedupe facet filters, refetch facets on searchParameters changes * Add chevron flourish * fix(askai): Fix new conversation causing thread depth errors (#2900) * feat(v5): Add hit result badge (#2901) * feat(v5): Add hit result badge * Add background to hit result badge * feat(v5): Add follow up prompt suggestions (#2902) * feat(v5): Add follow up prompt suggestions * fix: bump css bundle size limit * docs(agents): document Cursor Cloud dev environment setup for v5 (Bun) (#2903) Co-authored-by: Cursor Agent <cursoragent@cursor.com> * feat(mcp): setup mcp plugins (#2895) * feat(v5): Add prompt suggestions to keyword search (#2912) * feat(v5): Add prompt suggestions to keyword search * cleanup: Move consistent object to reusable constant * chore(v5): Split Ask AI related CSS into own bundle (#2913) * chore(v5): Split Ask AI related CSS into own bundle * move style.css to include modal and askai * fix: Ensure stage level and watch level scripts use bun runtime (#2915) * fix: Ensure stage level and watch level scripts use bun runtime * chore: move to node@24 update imports * fix: lint * feat(js): Document JS based hybrid mode, fix JS packages (#2916) * feat(js): Document JS based hybrid mode, fix JS packages * update: add model onOpen to docs * feat(cli): add @docsearch/cli for MCP setup and search (#2911) * chore(tsdown): Bump to latest tsdown version (#2918) * chore(tsdown): Bump to latest tsdown version * fix: bump nvmrc node version * fix: cli tsconfig * fix: website build * fix: example build * fix: circleci install bun * fix: lint * fix: circleci install bun * fix: circleci install bun * fix: circleci install bun * refactor(docusaurus-adapter): rework theme config for v5 and modularize SearchPage (#2904) Co-authored-by: Paul Jankowski <8BitTitan@gmail.com> * feat(askai): Move askai related props under root askai (#2919) * feat(askai): Move askai related props under root askai * fix: playwright test case * fix(docusaurus): validate Ask AI options * feat(js): Split JS bundles for search only (#2920) * chore: Move to oxlint and oxfmt (#2923) * chore: Get NPM OIDC token before publishing (#2924) * chore: Enter v5 beta (#2925) * chore: Enter pre release mode for v5 * chore: update release summary * chore: version bump * fix: Remove NPM_ID_TOKEN for release * fix: Try setting blank NPM_TOKEN * fix: Try blank NPM_AUTH_TOKEN * chore: bump node and npm for release job * docs(mcp): add service disclaimer (#2921) * fix: Agent Studio MCP search tool (#2927) * fix: Agent Studio MCP search tool * Add changeset * chore: Update stylelint (#2926) * chore: stylelint update * bun.lock * Add changeset * fix(website): use bare @import for tailwindcss (#2933) Tailwind's build-time `@import` cannot be written with `url()` notation, so `@import url('tailwindcss')` was passed through as a plain CSS import instead of being processed by Tailwind. Also syncs bun.lock with the 5.0.0-beta.0 versions already committed to package.json. * chore: version v5.0.0-beta.1 (#2932) * feat(react): remove deprecated index props (#2936) * feat(mcp): add ChatGPT and Codex DocSearch plugin package (#2938) * fix: cleanup claude * feat: website redesign (#2930) Co-authored-by: Cursor <cursoragent@cursor.com> * fix: lint * fix: crash on the demo (#2940) * feat(docs): Document v5 beta (#2935) * chore(docs): v5 documentation * Writing style clean up * fix: website after conflicts * fix: reported issues on mobile (#2944) * chore: Introduce a11y smoke tests (#2943) * chore: Add Lorris as codeowner (#2946) * fix(askai): Ask AI fixes for v5 (#2945) * fix: General v5 fixes (#2947) - Fix `ref` console error for a `FacetMenu` - Whitespace only search/conversation input does not trigger requests - Fix flash of no results page on search * feat: v5 general improvements (#2948) * feat(v5): General fixes and improvements * add changeset * fix: bundlesize * feat(v5): UI and DX improvements (#2949) * feat: Rename assistantId to agentId * feat: Allow reading default facet values from index searchParameters * feat: Remove indexName prop from Sidepanel, cleanup documentation pages * feat: Move appId and apiKey up into @docsearch/core * feat: Add back nested grouping of search results * add changeset * revert changes to example demo * fix: e2e tests * chore: push git tags on version release (#2951) * chore: release v5.0.0-beta.2 (#2950) * chore: Fix pushing git tags (#2953) * fix(askai): sanitize markdown HTML in v5 (#2954) Backport of #2929.\n\nOriginal commit:681cbfec03Co-authored-by: Vasco Bettencourt <32492444+vascobettencourt@users.noreply.github.com> * fix(v5): stop truncating mobile snippets (#2958) * fix(v5): stop truncating mobile snippets Backport of #2907.\n\nOriginal commit:9ad6d169fe* fix(v5): allow mobile hit text to wrap Completes the v5 adaptation of #2907 by overriding later v5 child-level truncation rules.\n\nOriginal commit:9ad6d169fe* chore(v5): account for mobile wrapping CSS Updates the CSS size budget for the v5 adaptation of #2907.\n\nOriginal commit:9ad6d169fe--------- Co-authored-by: Divyansh Singh <40380293+brc-dd@users.noreply.github.com> * feat(v5): Add new footerAction prop (#2952) * feat(v5): Add new footerAction prop * Resolve PR comments * fix(v5): recognize conversation depth errors (#2957) Backport of #2881.\n\nOriginal commit:f68e52251cCo-authored-by: Felipe Bermudez <felipeberm@gmail.com> * fix(v5): expose Sidepanel search parameter types (#2956) * fix(v5): expose Sidepanel search parameter types Backport of #2906.\n\nOriginal commit:4710d0ca77* Delete sidepanel.test.ts Had a pointless test case in it. --------- Co-authored-by: Divyansh Singh <40380293+brc-dd@users.noreply.github.com> * fix(v5): ignore slash shortcut on focused buttons (#2955) Backport of #2871.\n\nOriginal commit:0e41a78c44Co-authored-by: Sigmabro <122412346+Sigmabrogz@users.noreply.github.com> * fix(agentStudio): agents dynamic mode enabled (#2959) * fix(agentStudio): agents dynamic mode enabled * fix(askai): use string[] for dynamic agentStudio indices * feat(docs): add Ask AI to Agent Studio migration guide (#2931) * feat(docs): add Ask AI to Agent Studio migration guide * feat(docs): agentStudio migrating from askAI * feat(docs): renaming agentId * feat(agentStudio): dynamic mode indices updated * fix: Docusaurus adapter styling, DocSearch website fixes (#2960) * chore: release v5.0.0-beta.3 (#2961) * feat(website): Launch updates (#2964) * fix(website): Fix font loading (#2966) * feat(v5): Back port cost control errors (#2965) * chore: release v5.0.0-beta.4 (#2967) --------- Co-authored-by: Vincent Lemeunier <vincentlemeunier+git@gmail.com> Co-authored-by: Dylan Tientcheu <dylan.tientcheu@algolia.com> Co-authored-by: Lorris Saint-Genez <lorrissaintgenez@gmail.com> Co-authored-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: Dylan Tientcheu <dylantientcheu@gmail.com> Co-authored-by: Vasco Bettencourt <32492444+vascobettencourt@users.noreply.github.com> Co-authored-by: Divyansh Singh <40380293+brc-dd@users.noreply.github.com> Co-authored-by: Felipe Bermudez <felipeberm@gmail.com> Co-authored-by: Sigmabro <122412346+Sigmabrogz@users.noreply.github.com>
319 lines
7.9 KiB
TypeScript
319 lines
7.9 KiB
TypeScript
/* eslint-disable import/no-unresolved -- NodeNext source imports use runtime .js extensions. */
|
|
import {
|
|
DEFAULT_MCP_ENDPOINT,
|
|
TOOL_QUERY_DOCS,
|
|
TOOL_RESOLVE_DOCSET,
|
|
TOOL_SEARCH_DOCS,
|
|
} from './constants.js';
|
|
import { UsageError } from './errors.js';
|
|
import type { SetupAgent, SetupScope } from './setup/agents.js';
|
|
|
|
export type QueryCommandName = 'docs' | 'query' | 'resolve';
|
|
export type CommandName = QueryCommandName | 'help' | 'setup' | 'version';
|
|
|
|
export interface ParsedArgs {
|
|
options: Record<string, boolean | string>;
|
|
positionals: string[];
|
|
}
|
|
|
|
export interface ToolRequest {
|
|
endpoint: string;
|
|
json: boolean;
|
|
toolName: string;
|
|
toolArguments: Record<string, unknown>;
|
|
}
|
|
|
|
export interface SetupRequest {
|
|
all: boolean;
|
|
agents: SetupAgent[];
|
|
endpoint: string;
|
|
scope?: SetupScope;
|
|
yes: boolean;
|
|
}
|
|
|
|
const AGENT_FLAGS: Record<string, SetupAgent> = {
|
|
'--claude': 'claude',
|
|
'--codex': 'codex',
|
|
'--cursor': 'cursor',
|
|
'--gemini': 'gemini',
|
|
'--opencode': 'opencode',
|
|
};
|
|
|
|
const BOOLEAN_OPTIONS = new Set([
|
|
'--all',
|
|
'--claude',
|
|
'--codex',
|
|
'--cursor',
|
|
'--gemini',
|
|
'--global',
|
|
'--help',
|
|
'--json',
|
|
'--opencode',
|
|
'--project',
|
|
'--yes',
|
|
'-h',
|
|
'-y',
|
|
]);
|
|
|
|
const VALUE_OPTIONS = new Set([
|
|
'--endpoint',
|
|
'--max-docsets',
|
|
'--max-results',
|
|
'--top-n',
|
|
]);
|
|
const HELP_OPTIONS = new Set(['--help', '-h']);
|
|
const QUERY_COMMON_OPTIONS = new Set(['--endpoint', '--help', '--json', '-h']);
|
|
const SETUP_OPTIONS = new Set([
|
|
'--all',
|
|
'--claude',
|
|
'--codex',
|
|
'--cursor',
|
|
'--endpoint',
|
|
'--gemini',
|
|
'--global',
|
|
'--help',
|
|
'--opencode',
|
|
'--project',
|
|
'--yes',
|
|
'-h',
|
|
'-y',
|
|
]);
|
|
|
|
export function parseArgs(argv: string[]): ParsedArgs {
|
|
const options: Record<string, boolean | string> = {};
|
|
const positionals: string[] = [];
|
|
|
|
for (let idx = 0; idx < argv.length; idx++) {
|
|
const token = argv[idx];
|
|
const equalsIdx = token.startsWith('--') ? token.indexOf('=') : -1;
|
|
const option = equalsIdx > 0 ? token.slice(0, equalsIdx) : token;
|
|
const inlineValue = equalsIdx > 0 ? token.slice(equalsIdx + 1) : undefined;
|
|
|
|
if (VALUE_OPTIONS.has(option)) {
|
|
const value = inlineValue ?? argv[idx + 1];
|
|
if (!value || value.startsWith('-')) {
|
|
throw new UsageError(`Missing value for ${option}.`);
|
|
}
|
|
options[option] = value;
|
|
if (inlineValue === undefined) {
|
|
idx++;
|
|
}
|
|
} else if (BOOLEAN_OPTIONS.has(option)) {
|
|
if (inlineValue !== undefined) {
|
|
throw new UsageError(`${option} does not accept a value.`);
|
|
}
|
|
options[option] = true;
|
|
} else if (option.startsWith('-')) {
|
|
throw new UsageError(`Unknown option: ${option}.`);
|
|
} else {
|
|
positionals.push(token);
|
|
}
|
|
}
|
|
|
|
return { options, positionals };
|
|
}
|
|
|
|
export function buildToolRequest(
|
|
commandName: QueryCommandName,
|
|
args: ParsedArgs
|
|
): ToolRequest {
|
|
validateToolOptions(commandName, args.options);
|
|
const endpoint = readEndpoint(args.options);
|
|
const json = Boolean(args.options['--json']);
|
|
const maxResults = readNumberOption(args.options, '--max-results');
|
|
|
|
switch (commandName) {
|
|
case 'docs': {
|
|
const [library, ...queryParts] = args.positionals;
|
|
const query = queryParts.join(' ').trim();
|
|
if (!library || !query) {
|
|
throw new UsageError(
|
|
'Usage: docsearch docs <library> <query> [--json]'
|
|
);
|
|
}
|
|
|
|
return {
|
|
endpoint,
|
|
json,
|
|
toolName: TOOL_SEARCH_DOCS,
|
|
toolArguments: removeUndefined({
|
|
library,
|
|
query,
|
|
maxResults,
|
|
maxDocsets: readNumberOption(args.options, '--max-docsets'),
|
|
}),
|
|
};
|
|
}
|
|
|
|
case 'resolve': {
|
|
const query = args.positionals.join(' ').trim();
|
|
if (!query) {
|
|
throw new UsageError(
|
|
'Usage: docsearch resolve <library-or-product> [--json]'
|
|
);
|
|
}
|
|
|
|
return {
|
|
endpoint,
|
|
json,
|
|
toolName: TOOL_RESOLVE_DOCSET,
|
|
toolArguments: removeUndefined({
|
|
query,
|
|
topN: readNumberOption(args.options, '--top-n'),
|
|
}),
|
|
};
|
|
}
|
|
|
|
case 'query': {
|
|
const [docsetId, ...queryParts] = args.positionals;
|
|
const query = queryParts.join(' ').trim();
|
|
if (!docsetId || !query) {
|
|
throw new UsageError(
|
|
'Usage: docsearch query <docset-id[,docset-id...]> <query> [--json]'
|
|
);
|
|
}
|
|
|
|
const docsetIds = docsetId
|
|
.split(',')
|
|
.map((value) => value.trim())
|
|
.filter(Boolean);
|
|
if (docsetIds.length === 0) {
|
|
throw new UsageError('At least one non-empty docset ID is required.');
|
|
}
|
|
|
|
return {
|
|
endpoint,
|
|
json,
|
|
toolName: TOOL_QUERY_DOCS,
|
|
toolArguments: removeUndefined({
|
|
docsetIds,
|
|
query,
|
|
maxResults,
|
|
}),
|
|
};
|
|
}
|
|
|
|
default: {
|
|
const exhaustive: never = commandName;
|
|
return exhaustive;
|
|
}
|
|
}
|
|
}
|
|
|
|
export function buildSetupRequest(args: ParsedArgs): SetupRequest {
|
|
validateOptions(args.options, SETUP_OPTIONS, 'setup');
|
|
if (args.positionals.length > 0) {
|
|
throw new UsageError(`Unexpected setup argument: ${args.positionals[0]}.`);
|
|
}
|
|
|
|
const agents = Object.entries(AGENT_FLAGS)
|
|
.filter(([flag]) => Boolean(args.options[flag]))
|
|
.map(([, agent]) => agent);
|
|
|
|
if (args.options['--project'] && args.options['--global']) {
|
|
throw new UsageError('Use either --project or --global, not both.');
|
|
}
|
|
|
|
return {
|
|
agents: uniqueAgents(agents),
|
|
all: Boolean(args.options['--all']),
|
|
endpoint: readEndpoint(args.options),
|
|
scope: resolveScopeFlag(args.options),
|
|
yes: Boolean(args.options['--yes'] || args.options['-y']),
|
|
};
|
|
}
|
|
|
|
function resolveScopeFlag(
|
|
options: Record<string, boolean | string>
|
|
): SetupScope | undefined {
|
|
if (options['--project']) {
|
|
return 'project';
|
|
}
|
|
|
|
if (options['--global']) {
|
|
return 'global';
|
|
}
|
|
|
|
return undefined;
|
|
}
|
|
|
|
function readStringOption(
|
|
options: Record<string, boolean | string>,
|
|
key: string
|
|
): string | undefined {
|
|
const value = options[key];
|
|
return typeof value === 'string' ? value : undefined;
|
|
}
|
|
|
|
function readEndpoint(options: Record<string, boolean | string>): string {
|
|
const value = readStringOption(options, '--endpoint') ?? DEFAULT_MCP_ENDPOINT;
|
|
let url: URL;
|
|
try {
|
|
url = new URL(value);
|
|
} catch {
|
|
throw new UsageError(`--endpoint must be a valid HTTP(S) URL.`);
|
|
}
|
|
if (url.protocol !== 'http:' && url.protocol !== 'https:') {
|
|
throw new UsageError(`--endpoint must use HTTP or HTTPS.`);
|
|
}
|
|
return url.toString();
|
|
}
|
|
|
|
function readNumberOption(
|
|
options: Record<string, boolean | string>,
|
|
key: string
|
|
): number | undefined {
|
|
const value = readStringOption(options, key);
|
|
if (value === undefined) {
|
|
return undefined;
|
|
}
|
|
|
|
const parsed = Number(value);
|
|
if (!Number.isInteger(parsed) || parsed < 1) {
|
|
throw new UsageError(`${key} must be a positive integer.`);
|
|
}
|
|
|
|
return parsed;
|
|
}
|
|
|
|
function removeUndefined(
|
|
values: Record<string, unknown>
|
|
): Record<string, unknown> {
|
|
return Object.fromEntries(
|
|
Object.entries(values).filter(([, value]) => value !== undefined)
|
|
);
|
|
}
|
|
|
|
function uniqueAgents(agents: SetupAgent[]): SetupAgent[] {
|
|
return [...new Set(agents)];
|
|
}
|
|
|
|
function validateToolOptions(
|
|
commandName: QueryCommandName,
|
|
options: Record<string, boolean | string>
|
|
): void {
|
|
const allowed = new Set(QUERY_COMMON_OPTIONS);
|
|
if (commandName === 'docs') {
|
|
allowed.add('--max-docsets');
|
|
allowed.add('--max-results');
|
|
} else if (commandName === 'resolve') {
|
|
allowed.add('--top-n');
|
|
} else {
|
|
allowed.add('--max-results');
|
|
}
|
|
validateOptions(options, allowed, commandName);
|
|
}
|
|
|
|
function validateOptions(
|
|
options: Record<string, boolean | string>,
|
|
allowed: ReadonlySet<string>,
|
|
commandName: string
|
|
): void {
|
|
for (const option of Object.keys(options)) {
|
|
if (!allowed.has(option) && !HELP_OPTIONS.has(option)) {
|
|
throw new UsageError(
|
|
`${option} is not valid for the ${commandName} command.`
|
|
);
|
|
}
|
|
}
|
|
}
|