diff --git a/docs/cli/schema.json b/docs/cli/schema.json index 7dc09cb1..14bb33e8 100644 --- a/docs/cli/schema.json +++ b/docs/cli/schema.json @@ -68059,11 +68059,25 @@ ], "name": "search", "parameters": [ + { + "role": "positional", + "name": "query", + "type": "string", + "required": false, + "summary": "Search terms" + }, + { + "role": "flag", + "name": "accept-experimental", + "type": "boolean", + "required": false, + "summary": "Acknowledge that this command is experimental and may be removed; suppresses the warning" + }, { "role": "flag", "name": "query", "type": "string", - "required": true, + "required": false, "summary": "Search terms" }, { @@ -68095,33 +68109,28 @@ "name": "ask", "parameters": [ { - "role": "flag", + "role": "positional", "name": "question", "type": "string", - "required": true, + "required": false, "summary": "Question to ask" - } - ], - "summary": "Ask a question about Elastic documentation using AI (single answer)", - "intent": { - "requiresAuth": false - } - }, - { - "path": [ - "docs" - ], - "name": "chat", - "parameters": [ + }, + { + "role": "flag", + "name": "accept-experimental", + "type": "boolean", + "required": false, + "summary": "Acknowledge that this command is experimental and may be removed; suppresses the warning" + }, { "role": "flag", "name": "question", "type": "string", - "required": true, - "summary": "Opening question" + "required": false, + "summary": "Question to ask" } ], - "summary": "Ask a question about Elastic documentation using AI, with follow-up conversation", + "summary": "Ask a question about Elastic documentation using AI (single answer)", "intent": { "requiresAuth": false } @@ -68132,11 +68141,18 @@ ], "name": "read", "parameters": [ + { + "role": "positional", + "name": "path", + "type": "string", + "required": false, + "summary": "Docs path, full elastic.co URL, or search query" + }, { "role": "flag", "name": "path", "type": "string", - "required": true, + "required": false, "summary": "Docs path, full elastic.co URL, or search query" }, { diff --git a/src/docs/ask.ts b/src/docs/ask.ts index 45b44010..a1007b17 100644 --- a/src/docs/ask.ts +++ b/src/docs/ask.ts @@ -23,18 +23,37 @@ const defaultDeps: AskDeps = { } const inputSchema = z.object({ - question: z.string().describe('Question to ask'), + question: z.string().optional().describe('Question to ask'), }) +function experimentalBanner (isTTY: boolean): string { + const text = + 'Warning: "docs ask" is experimental and in active development.\n' + + ' Not yet suited for scripts or automation. Pass --accept-experimental to suppress this warning.\n\n' + return isTTY ? `\x1b[33m${text}\x1b[0m` : text +} + export function createAskCommand (deps: AskDeps = defaultDeps): OpaqueCommandHandle { return defineCommand({ name: 'ask', description: 'Ask a question about Elastic documentation using AI (single answer)', input: inputSchema, + positionalArg: { name: 'question', description: 'Question to ask', required: false }, + options: [ + { + long: 'accept-experimental', + type: 'boolean', + description: 'Acknowledge that this command is experimental and may be removed; suppresses the warning', + }, + ], handler: async (parsed): Promise => { - const question = parsed.input!.question.trim() + const question = (parsed.arg ?? parsed.input?.question ?? '').trim() if (question === '') return { error: { code: 'missing_input', message: 'question is required' } } + if (parsed.options['accept-experimental'] !== true && parsed.options['json'] !== true) { + deps.stderr.write(experimentalBanner(process.stderr.isTTY === true)) + } + const conversationId = newUuid() const interactive = process.stderr.isTTY === true && parsed.options['json'] !== true const spinner = interactive ? startSpinner(deps.stderr, 'Thinking…') : undefined diff --git a/src/docs/read.ts b/src/docs/read.ts index 80d9299a..ee8105c1 100644 --- a/src/docs/read.ts +++ b/src/docs/read.ts @@ -22,7 +22,7 @@ const defaultDeps: ReadDeps = { } const inputSchema = z.object({ - path: z.string().describe('Docs path, full elastic.co URL, or search query'), + path: z.string().optional().describe('Docs path, full elastic.co URL, or search query'), raw: z.boolean().optional().describe('Output unrendered markdown instead of formatted output'), }) @@ -31,8 +31,9 @@ export function createReadCommand (deps: ReadDeps = defaultDeps): OpaqueCommandH name: 'read', description: 'Read an Elastic documentation page', input: inputSchema, + positionalArg: { name: 'path', description: 'Docs path, full elastic.co URL, or search query', required: false }, handler: async (parsed): Promise => { - const input = parsed.input!.path.trim() + const input = (parsed.arg ?? parsed.input?.path ?? '').trim() if (input === '') return { error: { code: 'missing_input', message: 'path is required' } } const raw = parsed.input!.raw === true diff --git a/src/docs/register.ts b/src/docs/register.ts index 5120f9a3..ab1a50d8 100644 --- a/src/docs/register.ts +++ b/src/docs/register.ts @@ -7,7 +7,6 @@ import { defineGroup } from '../factory.ts' import type { OpaqueCommandHandle } from '../factory.ts' import { createSearchCommand } from './search.ts' import { createAskCommand } from './ask.ts' -import { createChatCommand } from './chat.ts' import { createReadCommand } from './read.ts' export function registerDocsCommands (): OpaqueCommandHandle { @@ -15,7 +14,6 @@ export function registerDocsCommands (): OpaqueCommandHandle { { name: 'docs', description: 'Search, read, and ask questions about Elastic documentation' }, createSearchCommand(), createAskCommand(), - createChatCommand(), createReadCommand(), ) } diff --git a/src/docs/search.ts b/src/docs/search.ts index 5bbb9f75..2b813aa0 100644 --- a/src/docs/search.ts +++ b/src/docs/search.ts @@ -21,18 +21,38 @@ export interface SearchDeps { const defaultDeps: SearchDeps = { docsSearch, stderr: process.stderr } const inputSchema = z.object({ - query: z.string().describe('Search terms'), + query: z.string().optional().describe('Search terms'), page: z.number().default(1).describe('Page number'), size: z.number().default(5).describe('Results per page'), }) +function experimentalBanner (command: string, isTTY: boolean): string { + const text = + `Warning: "${command}" is experimental and in active development.\n` + + ` Not yet suited for scripts or automation. Pass --accept-experimental to suppress this warning.\n\n` + return isTTY ? `\x1b[33m${text}\x1b[0m` : text +} + export function createSearchCommand (deps: SearchDeps = defaultDeps): OpaqueCommandHandle { return defineCommand({ name: 'search', description: 'Search Elastic documentation', input: inputSchema, + positionalArg: { name: 'query', description: 'Search terms', required: false }, + options: [ + { + long: 'accept-experimental', + type: 'boolean', + description: 'Acknowledge that this command is experimental and may be removed; suppresses the warning', + }, + ], handler: async (parsed) => { - const { query, page, size } = parsed.input! + if (parsed.options['accept-experimental'] !== true && parsed.options['json'] !== true) { + deps.stderr.write(experimentalBanner('docs search', process.stderr.isTTY === true)) + } + const query = (parsed.arg ?? parsed.input?.query ?? '').trim() + if (query === '') return { error: { code: 'missing_input', message: 'query is required' } } + const { page, size } = parsed.input! try { const resp = await deps.docsSearch(query, page, size) diff --git a/test/docs/ask.test.ts b/test/docs/ask.test.ts index 3fa4ed41..3c39a96c 100644 --- a/test/docs/ask.test.ts +++ b/test/docs/ask.test.ts @@ -21,9 +21,9 @@ describe('createAskCommand', () => { assert.equal(cmd.name(), 'ask') }) - it('has a required --question option', () => { + it('accepts question as a positional argument or --question option', () => { const cmd = createAskCommand() - assert.equal(cmd.registeredArguments.length, 0) + assert.equal(cmd.registeredArguments.length, 1) const optNames = cmd.options.map((o) => o.long) assert.ok(optNames.includes('--question')) }) diff --git a/test/docs/read.test.ts b/test/docs/read.test.ts index c96c0d61..c040cedb 100644 --- a/test/docs/read.test.ts +++ b/test/docs/read.test.ts @@ -14,9 +14,9 @@ describe('createReadCommand', () => { assert.equal(cmd.name(), 'read') }) - it('has a required --path option', () => { + it('accepts path as a positional argument or --path option', () => { const cmd = createReadCommand() - assert.equal(cmd.registeredArguments.length, 0) + assert.equal(cmd.registeredArguments.length, 1) const optNames = cmd.options.map((o) => o.long) assert.ok(optNames.includes('--path')) }) diff --git a/test/docs/search.test.ts b/test/docs/search.test.ts index 66211394..c6605862 100644 --- a/test/docs/search.test.ts +++ b/test/docs/search.test.ts @@ -38,9 +38,9 @@ describe('createSearchCommand', () => { assert.equal(cmd.name(), 'search') }) - it('has a required --query option', () => { + it('accepts query as a positional argument or --query option', () => { const cmd = createSearchCommand() - assert.equal(cmd.registeredArguments.length, 0) + assert.equal(cmd.registeredArguments.length, 1) const optNames = cmd.options.map((o) => o.long) assert.ok(optNames.includes('--query')) })