Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15,840 changes: 15,241 additions & 599 deletions docs/cli/schema.json

Large diffs are not rendered by default.

23 changes: 21 additions & 2 deletions src/docs/ask.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<JsonValue> => {
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
Expand Down
5 changes: 3 additions & 2 deletions src/docs/read.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'),
})

Expand All @@ -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<JsonValue> => {
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
Expand Down
2 changes: 0 additions & 2 deletions src/docs/register.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,13 @@ 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 {
return defineGroup(
{ name: 'docs', description: 'Search, read, and ask questions about Elastic documentation' },
createSearchCommand(),
createAskCommand(),
createChatCommand(),
createReadCommand(),
)
}
24 changes: 22 additions & 2 deletions src/docs/search.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
4 changes: 2 additions & 2 deletions test/docs/ask.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'))
})
Expand Down
4 changes: 2 additions & 2 deletions test/docs/read.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'))
})
Expand Down
4 changes: 2 additions & 2 deletions test/docs/search.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'))
})
Expand Down
Loading