From a26c410dda0894fd92064b2d3f3bf1875304a4c8 Mon Sep 17 00:00:00 2001 From: Samuel Bushi Date: Thu, 5 Feb 2026 11:52:41 -0500 Subject: [PATCH 1/2] feat(genkit-tools/cli): Add docs discovery tools to the CLI --- genkit-tools/cli/src/cli.ts | 4 ++ genkit-tools/cli/src/mcp/docs.ts | 77 ++------------------------------ 2 files changed, 7 insertions(+), 74 deletions(-) diff --git a/genkit-tools/cli/src/cli.ts b/genkit-tools/cli/src/cli.ts index 614aba79e7..09ee0262cc 100644 --- a/genkit-tools/cli/src/cli.ts +++ b/genkit-tools/cli/src/cli.ts @@ -24,6 +24,7 @@ import { import { Command, program } from 'commander'; import { config } from './commands/config'; import { devTestModel } from './commands/dev-test-model'; +import { docsList, docsRead, docsSearch } from './commands/docs'; import { evalExtractData } from './commands/eval-extract-data'; import { evalFlow } from './commands/eval-flow'; import { evalRun } from './commands/eval-run'; @@ -62,6 +63,9 @@ const commands: Command[] = [ start, devTestModel, mcp, + docsList, + docsRead, + docsSearch, ]; /** Main entry point for CLI. */ diff --git a/genkit-tools/cli/src/mcp/docs.ts b/genkit-tools/cli/src/mcp/docs.ts index ef50464f13..33fc49f82e 100644 --- a/genkit-tools/cli/src/mcp/docs.ts +++ b/genkit-tools/cli/src/mcp/docs.ts @@ -17,59 +17,12 @@ import { record } from '@genkit-ai/tools-common/utils'; import { McpServer } from '@modelcontextprotocol/sdk/server/mcp'; import { ContentBlock } from '@modelcontextprotocol/sdk/types'; -import { existsSync, mkdirSync, readFileSync, renameSync } from 'node:fs'; -import { writeFile } from 'node:fs/promises'; -import { Readable } from 'node:stream'; -import os from 'os'; -import path from 'path'; import z from 'zod'; -import { version } from '../utils/version'; +import { loadDocs, searchDocs } from '../utils/docs'; import { McpRunToolEvent } from './analytics.js'; -const DOCS_URL = - process.env.GENKIT_DOCS_BUNDLE_URL ?? - 'http://genkit.dev/docs-bundle-experimental.json'; - -const DOCS_BUNDLE_FILE_PATH = path.resolve( - os.homedir(), - '.genkit', - 'docs', - version, - 'bundle.json' -); - -async function maybeDownloadDocsBundle() { - if (existsSync(DOCS_BUNDLE_FILE_PATH)) { - return; - } - const response = await fetch(DOCS_URL); - if (response.status !== 200) { - throw new Error( - 'Failed to download genkit docs bundle. Try again later or/and report the issue.\n\n' + - DOCS_URL - ); - } - const stream = Readable.fromWeb(response.body as any); - - mkdirSync(path.dirname(DOCS_BUNDLE_FILE_PATH), { recursive: true }); - - await writeFile(DOCS_BUNDLE_FILE_PATH + '.pending', stream); - renameSync(DOCS_BUNDLE_FILE_PATH + '.pending', DOCS_BUNDLE_FILE_PATH); -} - -interface Doc { - title: string; - description?: string; - text: string; - lang: string; - headers: string; -} - export async function defineDocsTool(server: McpServer) { - await maybeDownloadDocsBundle(); - const documents = JSON.parse( - readFileSync(DOCS_BUNDLE_FILE_PATH, { encoding: 'utf8' }) - ) as Record; + const documents = await loadDocs(); server.registerTool( 'list_genkit_docs', @@ -138,32 +91,8 @@ export async function defineDocsTool(server: McpServer) { async ({ query, language }) => { await record(new McpRunToolEvent('search_genkit_docs')); const lang = language || 'js'; - const terms = query - .toLowerCase() - .split(/\s+/) - .filter((t) => t.length > 2); // Filter out short words to reduce noise - - const results = Object.keys(documents) - .filter((file) => file.startsWith(lang)) - .map((file) => { - const doc = documents[file]; - let score = 0; - const title = doc.title.toLowerCase(); - const desc = (doc.description || '').toLowerCase(); - const headers = (doc.headers || '').toLowerCase(); - - terms.forEach((term) => { - if (title.includes(term)) score += 10; - if (desc.includes(term)) score += 5; - if (headers.includes(term)) score += 3; - if (file.includes(term)) score += 5; - }); - return { file, doc, score }; - }) - .filter((r) => r.score > 0) - .sort((a, b) => b.score - a.score) - .slice(0, 10); // Top 10 + const results = searchDocs(documents, query, lang).slice(0, 10); // Top 10 if (results.length === 0) { return { From f771dd8f1a6c71e46c7e5a08ee7e79be3891712b Mon Sep 17 00:00:00 2001 From: Samuel Bushi Date: Thu, 5 Feb 2026 11:53:23 -0500 Subject: [PATCH 2/2] feat(genkit-tools/cli): Add docs discovery tools to the CLI --- genkit-tools/cli/src/commands/docs.ts | 107 ++++++++++++++++++++++++ genkit-tools/cli/src/utils/docs.ts | 112 ++++++++++++++++++++++++++ 2 files changed, 219 insertions(+) create mode 100644 genkit-tools/cli/src/commands/docs.ts create mode 100644 genkit-tools/cli/src/utils/docs.ts diff --git a/genkit-tools/cli/src/commands/docs.ts b/genkit-tools/cli/src/commands/docs.ts new file mode 100644 index 0000000000..aa44504e9a --- /dev/null +++ b/genkit-tools/cli/src/commands/docs.ts @@ -0,0 +1,107 @@ +/** + * Copyright 2025 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { logger } from '@genkit-ai/tools-common/utils'; +import * as clc from 'colorette'; +import { Command } from 'commander'; +import { loadDocs, searchDocs } from '../utils/docs'; + +export const docsList = new Command('docs:list') + .description('list available Genkit documentation files') + .argument('[language]', 'language to list docs for (js, go, python)', 'js') + .action(async (language) => { + try { + const documents = await loadDocs(); + const lang = language || 'js'; + const fileList = Object.keys(documents) + .filter((file) => file.startsWith(lang)) + .sort(); + + if (fileList.length === 0) { + logger.info(`No documentation found for language: ${lang}`); + return; + } + + logger.info(`Genkit Documentation Index (${lang}):\n`); + fileList.forEach((file) => { + const doc = documents[file]; + logger.info(`${clc.bold(doc.title)}`); + logger.info(` Path: ${file}`); + if (doc.description) { + logger.info(` ${clc.italic(doc.description)}`); + } + logger.info(''); + }); + logger.info(`Use 'genkit docs:read ' to read a document.`); + } catch (e: any) { + logger.error(`Failed to load documentation: ${e.message}`); + } + }); + +export const docsSearch = new Command('docs:search') + .description('search Genkit documentation') + .argument( + '', + 'keywords to search for. For multiple keywords, enclose in quotes. E.g. "stream flows"' + ) + .argument('[language]', 'language to search docs for (js, go, python)', 'js') + .action(async (query, language) => { + try { + const documents = await loadDocs(); + const lang = language || 'js'; + const results = searchDocs(documents, query, lang).slice(0, 10); + + if (results.length === 0) { + logger.info(`No results found for "${query}" in ${lang} docs.`); + return; + } + + logger.info( + `Found ${results.length} matching documents for "${query}":\n` + ); + results.forEach((r) => { + logger.info(`${clc.bold(r.doc.title)}`); + logger.info(` Path: ${r.file}`); + if (r.doc.description) { + logger.info(` ${clc.italic(r.doc.description)}`); + } + logger.info(''); + }); + } catch (e: any) { + logger.error(`Failed to load documentation: ${e.message}`); + } + }); + +export const docsRead = new Command('docs:read') + .description('read a Genkit documentation file') + .argument('', 'path of the document to read') + .action(async (filePath) => { + try { + const documents = await loadDocs(); + const doc = documents[filePath]; + if (!doc) { + logger.error(`Document not found: ${filePath}`); + return; + } + + logger.info(clc.bold(doc.title)); + logger.info('='.repeat(doc.title.length)); + logger.info(''); + logger.info(doc.text); + } catch (e: any) { + logger.error(`Failed to load documentation: ${e.message}`); + } + }); diff --git a/genkit-tools/cli/src/utils/docs.ts b/genkit-tools/cli/src/utils/docs.ts new file mode 100644 index 0000000000..08b1dc2415 --- /dev/null +++ b/genkit-tools/cli/src/utils/docs.ts @@ -0,0 +1,112 @@ +/** + * Copyright 2025 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { + existsSync, + mkdirSync, + readFileSync, + renameSync, + statSync, +} from 'node:fs'; +import { writeFile } from 'node:fs/promises'; +import { Readable } from 'node:stream'; +import os from 'os'; +import path from 'path'; +import { version } from './version'; + +export const DOCS_URL = + process.env.GENKIT_DOCS_BUNDLE_URL ?? + 'http://genkit.dev/docs-bundle-experimental.json'; + +export const DOCS_BUNDLE_FILE_PATH = path.resolve( + os.homedir(), + '.genkit', + 'docs', + version, + 'bundle.json' +); + +export async function maybeDownloadDocsBundle() { + if (existsSync(DOCS_BUNDLE_FILE_PATH)) { + const stats = statSync(DOCS_BUNDLE_FILE_PATH); + const DOCS_TTL = 1000 * 60 * 60 * 24 * 7; // 1 week + if (Date.now() - stats.mtimeMs < DOCS_TTL) { + return; + } + } + const response = await fetch(DOCS_URL); + if (response.status !== 200) { + throw new Error( + 'Failed to download genkit docs bundle. Try again later or/and report the issue.\n\n' + + DOCS_URL + ); + } + const stream = Readable.fromWeb(response.body as any); + + mkdirSync(path.dirname(DOCS_BUNDLE_FILE_PATH), { recursive: true }); + + await writeFile(DOCS_BUNDLE_FILE_PATH + '.pending', stream); + renameSync(DOCS_BUNDLE_FILE_PATH + '.pending', DOCS_BUNDLE_FILE_PATH); +} + +export interface Doc { + title: string; + description?: string; + text: string; + lang: string; + headers: string; +} + +export async function loadDocs(): Promise> { + await maybeDownloadDocsBundle(); + return JSON.parse( + readFileSync(DOCS_BUNDLE_FILE_PATH, { encoding: 'utf8' }) + ) as Record; +} + +export function searchDocs( + documents: Record, + query: string, + lang: string +) { + const terms = query + .toLowerCase() + .split(/\s+/) + .filter((t) => t.length > 2); // Filter out short words to reduce noise + + const results = Object.keys(documents) + .filter((file) => file.startsWith(lang)) + .map((file) => { + const doc = documents[file]; + let score = 0; + const title = doc.title.toLowerCase(); + const desc = (doc.description || '').toLowerCase(); + const headers = (doc.headers || '').toLowerCase(); + + terms.forEach((term) => { + if (title.includes(term)) score += 10; + if (desc.includes(term)) score += 5; + if (headers.includes(term)) score += 3; + if (file.includes(term)) score += 5; + }); + + return { file, doc, score }; + }) + .filter((r) => r.score > 0) + .sort((a, b) => b.score - a.score); + + return results; +}