Skip to content
Merged
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
56 changes: 36 additions & 20 deletions docs/cli/schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
},
{
Expand Down Expand Up @@ -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
}
Expand All @@ -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"
},
{
Expand Down
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