1
0
Fork 0
docsearch/packages/docsearch-cli/src/commands.ts
Paul Jankowski ecd905d440
feat: promote DocSearch v5 to main (#2968)
* 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: 681cbfec03

Co-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: f68e52251c

Co-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: 0e41a78c44

Co-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>
2026-08-06 15:31:45 -04:00

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.`
);
}
}
}