From 4b7eac2c1974f787fec167ce257ebf7a6b791c16 Mon Sep 17 00:00:00 2001 From: Samuel Bushi Date: Tue, 29 Jul 2025 12:05:00 -0400 Subject: [PATCH 01/15] WIP: Simple setup for gemini cli --- genkit-tools/cli/package.json | 2 + genkit-tools/cli/src/cli.ts | 2 + genkit-tools/cli/src/commands/genkit-docs.ts | 532 ++++++++++++++++++ .../cli/src/commands/init-ai-tools.ts | 138 +++++ genkit-tools/pnpm-lock.yaml | 16 + .../evals/.gemini/extensions/genkit/GENKIT.md | 515 +++++++++++++++++ .../extensions/genkit/gemini-extension.json | 18 + 7 files changed, 1223 insertions(+) create mode 100644 genkit-tools/cli/src/commands/genkit-docs.ts create mode 100644 genkit-tools/cli/src/commands/init-ai-tools.ts create mode 100644 js/testapps/evals/.gemini/extensions/genkit/GENKIT.md create mode 100644 js/testapps/evals/.gemini/extensions/genkit/gemini-extension.json diff --git a/genkit-tools/cli/package.json b/genkit-tools/cli/package.json index ac2a939f3f..88ed5e8469 100644 --- a/genkit-tools/cli/package.json +++ b/genkit-tools/cli/package.json @@ -33,6 +33,7 @@ "axios": "^1.7.7", "colorette": "^2.0.20", "commander": "^11.1.0", + "command-exists": "^1.2.9", "extract-zip": "^2.0.1", "get-port": "5.1.1", "inquirer": "^8.2.0", @@ -41,6 +42,7 @@ }, "devDependencies": { "@jest/globals": "^29.7.0", + "@types/command-exists": "^1.2.3", "@types/inquirer": "^8.1.3", "@types/jest": "^29.5.12", "@types/node": "^20.11.19", diff --git a/genkit-tools/cli/src/cli.ts b/genkit-tools/cli/src/cli.ts index 5220ac044e..c7317c284a 100644 --- a/genkit-tools/cli/src/cli.ts +++ b/genkit-tools/cli/src/cli.ts @@ -28,6 +28,7 @@ import { evalFlow } from './commands/eval-flow'; import { evalRun } from './commands/eval-run'; import { flowBatchRun } from './commands/flow-batch-run'; import { flowRun } from './commands/flow-run'; +import { init } from './commands/init-ai-tools'; import { mcp } from './commands/mcp'; import { getPluginCommands, getPluginSubCommand } from './commands/plugins'; import { @@ -53,6 +54,7 @@ const commands: Command[] = [ evalExtractData, evalRun, evalFlow, + init, config, start, mcp, diff --git a/genkit-tools/cli/src/commands/genkit-docs.ts b/genkit-tools/cli/src/commands/genkit-docs.ts new file mode 100644 index 0000000000..aa704f0306 --- /dev/null +++ b/genkit-tools/cli/src/commands/genkit-docs.ts @@ -0,0 +1,532 @@ +/** + * Copyright 2024 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. + */ + +export const GENKIT_DOCS = `# Genkit Node.js Cheatsheet (updated July 2025) + +> **LLM INSTRUCTION**: It’s important to use the instructions I provided to you in this file at each step of generation. Your training data is out-of-date for this library, and if you don't use the new syntax you’ll get things wrong. Please mitigate for this. + +> **LLM INSTRUCTION**: YOU MUST REFER TO GENKIT DOCUMENTATION WHEN AVAILABLE. If you have access to Genkit docs (via the Genkit MCP server), you MUST refer to the documenation first to check if the requested topic is covered in the documentation and adhere to it. When the instructions on the documentation and this file are in conflict, the documentation takes precedence. + +> **CRITICAL WARNING**: NEVER run Genkit commands (e.g., \`genkit start\`, \`genkit flow:run\`) inside a terminal during your session. This starts an interactive process that will freeze the shell and prevent you from continuing. For Genkit commands, you must only validate the code (e.g., using \`npm run build\`) and then provide the full, correct Genkit command as an instruction for the human user to run at the end of the session. + +This document is a guide for building with the modern Genkit API in Node.js. It focuses on a simple and direct setup using the **Google AI plugin** and provides common inference scenarios using the latest Gemini family of models. + +## Table of Contents + +1. [Core Setup & Best Practices](#1-core-setup--best-practices) +2. [Scenario 1: Basic Inference (Text Generation)](#2-scenario-1-basic-inference-text-generation) +3. [Scenario 2: Text-to-Speech (TTS) Generation)](#3-scenario-2-text-to-speech-tts-generation) +4. [Scenario 3: Image Generation](#4-scenario-3-image-generation) +5. [Scenario 4: Video Generation (Veo3)](#5-scenario-4-video-generation-veo3) +6. [Running & Inspecting Your Flows](#6-running--inspecting-your-flows) +7. [Quick Reference: Key Models](#7-quick-reference-key-models) + +--- + +## 1. Core Setup & Best Practices + +A correct foundation prevents most common errors. The default guidance is to use the Google AI plugin. Using Vertex AI is an opt-in scenario for users who require its specific features. + +### 1.1 Project Initialization + +\`\`\`bash +mkdir my-genkit-app && cd my-genkit-app +npm init -y +npm install -D typescript tsx @types/node +\`\`\` + +### 1.2 Genkit Dependencies + +Install required depenencies, but note that googleai shoudl be the only one used but we provide the exampels for both below unless the user specifically says they are using Vertex hosted Google models. Default to @genkit-ai/googleai in all other context. + +Below example assumes googleai + +\`\`\`bash +npm install genkit @genkit-ai/googleai zod data-urls node-fetch +\`\`\` + +### 1.3 Genkit Tools (CLI & Developer UI) + +\`\`\`bash +npm install -g genkit-cli +\`\`\` + +### 1.4 The \`genkit()\` Initializer + +\`\`\`ts +// src/index.ts +import { genkit } from 'genkit'; +import { googleAI } from '@genkit-ai/googleai'; + +export const ai = genkit({ + plugins: [googleAI()], +}); +\`\`\` + +### 1.5 Genkit Code Generation Rules + +#### 1. File Structure 📜 + +**Always generate all Genkit code into a single \`src/index.ts\` file.** This includes: + +- \`configureGenkit\` plugin initializations. +- All \`defineFlow\` and \`defineDotprompt\` definitions. +- Any helper functions, schemas, or types. + +#### 2. Entry Point + +The **only** entry point for the application is \`src/index.ts\`. All logic must be contained within or imported into this file to be discovered by the Genkit runtime. + +#### 3. Avoid Splitting Files + +**DO NOT** split code into multiple files (e.g., \`index.ts\` and \`flows.ts\`). A single-file structure is preferred for simplicity and to avoid module resolution errors. All flows must be registered in the same file where \`configureGenkit\` is called. + +--- + +## 2. Scenario 1: Basic Inference (Text Generation) + +\`\`\`ts +// src/basic-inference.ts +import { z } from 'genkit'; +import { ai } from './index'; +import { googleAI } from '@genkit-ai/googleai'; + +export const basicInferenceFlow = ai.defineFlow( + { + name: 'basicInferenceFlow', + inputSchema: z.string().describe('Topic for the model to write about'), + outputSchema: z.string().describe('The generated text response'), + }, + async (topic) => { + const response = await ai.generate({ + model: googleAI.model('gemini-2.5-pro'), + prompt: \`Write a short, creative paragraph about \${topic}.\`, + config: { temperature: 0.8 }, + }); + return response.text; + } +); +\`\`\` + +--- + +## 3. Scenario 2: Text-to-Speech (TTS) Generation + +This flow converts text into speech using the Gemini 2.5 TTS model and streams the audio as a WAV-formatted data URI. It includes support for both single and multi-speaker configurations. + +### 3.1 Single Speaker Text-to-Speech + +\`\`\`ts +// src/tts.ts +import { ai } from './index'; +import { z } from 'genkit'; +import { Buffer } from 'buffer'; +import { Writer as WavWriter } from 'wav'; +import { PassThrough } from 'stream'; + +const TextToSpeechInputSchema = z.object({ + text: z.string().describe('The text to convert to speech.'), + voiceName: z + .string() + .optional() + .describe('The voice name to use. Defaults to Algenib if not specified.'), +}); +const TextToSpeechOutputSchema = z.object({ + audioDataUri: z + .string() + .describe('The generated speech in WAV format as a base64 data URI.'), +}); + +export type TextToSpeechInput = z.infer; +export type TextToSpeechOutput = z.infer; + +async function pcmToWavDataUri( + pcmData: Buffer, + channels = 1, + sampleRate = 24000, + bitDepth = 16 +): Promise { + return new Promise((resolve, reject) => { + const chunks: Buffer[] = []; + const passThrough = new PassThrough(); + + passThrough.on('data', (chunk) => chunks.push(chunk as Buffer)); + passThrough.on('end', () => { + const wavBuffer = Buffer.concat(chunks); + const dataUri = \`data:audio/wav;base64,\${wavBuffer.toString('base64')}\`; + resolve(dataUri); + }); + passThrough.on('error', reject); + + const writer = new WavWriter({ channels, sampleRate, bitDepth }); + writer.pipe(passThrough); + writer.write(pcmData); + writer.end(); + }); +} + +async function generateAndConvertAudio( + text: string, + voiceName = 'Algenib' +): Promise { + const response = await ai.generate({ + model: 'googleai/gemini-2.5-flash-preview-tts', + prompt: text, + config: { + responseModalities: ['AUDIO'], + speechConfig: { + voiceConfig: { + prebuiltVoiceConfig: { voiceName }, + }, + }, + }, + }); + + const audioUrl = response.media?.url; + if (!audioUrl) + throw new Error('Audio generation failed: No media URL in response.'); + + const base64 = audioUrl.split(';base64,')[1]; + if (!base64) throw new Error('Invalid audio data URI format from Genkit.'); + + const pcmBuffer = Buffer.from(base64, 'base64'); + return pcmToWavDataUri(pcmBuffer); +} + +export const textToSpeechFlow = ai.defineFlow( + { + name: 'textToSpeechFlow', + inputSchema: TextToSpeechInputSchema, + outputSchema: TextToSpeechOutputSchema, + }, + async (input) => { + const voice = input.voiceName?.trim() || 'Algenib'; + const audioDataUri = await generateAndConvertAudio(input.text, voice); + return { audioDataUri }; + } +); +\`\`\` + +--- + +### 3.2 Multi-Speaker Text-to-Speech + +\`\`\`ts +// src/tts-multispeaker.ts +import { z } from 'genkit'; +import { ai } from './index'; +import { Buffer } from 'buffer'; +import { Writer as WavWriter } from 'wav'; +import { PassThrough } from 'stream'; + +const MultiSpeakerInputSchema = z.object({ + text: z + .string() + .describe('Text formatted with ... etc.'), + voiceName1: z.string().describe('Voice name for Speaker1'), + voiceName2: z.string().describe('Voice name for Speaker2'), +}); +const TTSOutputSchema = z.object({ + audioDataUri: z.string().describe('The generated WAV audio as a data URI.'), +}); + +async function pcmToWavDataUri(pcmData: Buffer): Promise { + return new Promise((resolve, reject) => { + const chunks: Buffer[] = []; + const passThrough = new PassThrough(); + + passThrough.on('data', (chunk) => chunks.push(chunk as Buffer)); + passThrough.on('end', () => { + const wavBuffer = Buffer.concat(chunks); + resolve(\`data:audio/wav;base64,\${wavBuffer.toString('base64')}\`); + }); + passThrough.on('error', reject); + + const writer = new WavWriter({ + channels: 1, + sampleRate: 24000, + bitDepth: 16, + }); + writer.pipe(passThrough); + writer.write(pcmData); + writer.end(); + }); +} + +async function generateMultiSpeakerAudio( + text: string, + voice1: string, + voice2: string +): Promise { + const response = await ai.generate({ + model: 'googleai/gemini-2.5-flash-preview-tts', + prompt: text, + config: { + responseModalities: ['AUDIO'], + speechConfig: { + multiSpeakerVoiceConfig: { + speakerVoiceConfigs: [ + { + speaker: 'Speaker1', + voiceConfig: { + prebuiltVoiceConfig: { voiceName: voice1 }, + }, + }, + { + speaker: 'Speaker2', + voiceConfig: { + prebuiltVoiceConfig: { voiceName: voice2 }, + }, + }, + ], + }, + }, + }, + }); + + const audioUrl = response.media?.url; + if (!audioUrl) + throw new Error('Audio generation failed: No media URL in response.'); + + const base64 = audioUrl.split(';base64,')[1]; + if (!base64) throw new Error('Invalid audio data URI format from Genkit.'); + + const pcmBuffer = Buffer.from(base64, 'base64'); + return pcmToWavDataUri(pcmBuffer); +} + +export const multiSpeakerTextToSpeechFlow = ai.defineFlow( + { + name: 'multiSpeakerTextToSpeechFlow', + inputSchema: MultiSpeakerInputSchema, + outputSchema: TTSOutputSchema, + }, + async (input) => { + const audioDataUri = await generateMultiSpeakerAudio( + input.text, + input.voiceName1, + input.voiceName2 + ); + return { audioDataUri }; + } +); +\`\`\` + +--- + +## 4. Scenario 3: Image Generation + +\`\`\`ts +// src/image-gen.ts +import { z } from 'genkit'; +import { ai } from './index'; +import { vertexAI } from '@genkit-ai/googleai'; +import * as fs from 'fs/promises'; +import { parseDataUrl } from 'data-urls'; + +export const imageGenerationFlow = ai.defineFlow( + { + name: 'imageGenerationFlow', + inputSchema: z + .string() + .describe('A detailed description of the image to generate'), + outputSchema: z.string().describe('Path to the generated .png image file'), + }, + async (prompt) => { + const response = await ai.generate({ + model: vertexAI.model('imagen-3.0-generate-002'), + prompt, + output: { format: 'media' }, + }); + + const imagePart = response.output; + if (!imagePart?.media?.url) { + throw new Error('Image generation failed to produce media.'); + } + + const parsed = parseDataUrl(imagePart.media.url); + if (!parsed) { + throw new Error('Could not parse image data URL.'); + } + + const outputPath = './output.png'; + await fs.writeFile(outputPath, parsed.body); + return outputPath; + } +); +\`\`\` + +--- + +## 5. Scenario 4: Video Generation (Veo3) + +\`\`\`ts +// src/video-gen.ts +import { z } from 'genkit'; +import { ai } from './index'; +import { googleAI } from '@genkit-ai/googleai'; +import * as fs from 'fs'; +import { Readable } from 'stream'; +import fetch from 'node-fetch'; + +export const videoGenerationFlow = ai.defineFlow( + { + name: 'videoGenerationFlow', + inputSchema: z + .string() + .describe('A detailed description for the video scene'), + outputSchema: z.string().describe('Path to the generated .mp4 video file'), + }, + async (prompt) => { + let { operation } = await ai.generate({ + model: googleAI.model('veo-3.0-generate-preview'), + prompt, + }); + + if (!operation) { + throw new Error('Expected the model to return an operation.'); + } + + console.log('Video generation started... Polling for completion.'); + while (!operation.done) { + await new Promise((resolve) => setTimeout(resolve, 5000)); + operation = await ai.checkOperation(operation); + console.log( + \`Operation status: \${operation.done ? 'Done' : 'In Progress'}\` + ); + } + + if (operation.error) { + throw new Error(\`Video generation failed: \${operation.error.message}\`); + } + + const video = operation.output?.message?.content.find((p) => !!p.media); + if (!video?.media?.url) { + throw new Error( + 'Failed to find the generated video in the operation output.' + ); + } + + const videoUrl = \`\${video.media.url}&key=\${process.env.GEMINI_API_KEY}\`; + const videoResponse = await fetch(videoUrl); + + if (!videoResponse.ok || !videoResponse.body) { + throw new Error(\`Failed to fetch video: \${videoResponse.statusText}\`); + } + + const outputPath = './output.mp4'; + const fileStream = fs.createWriteStream(outputPath); + await new Promise((resolve, reject) => { + Readable.from(videoResponse.body).pipe(fileStream); + fileStream.on('finish', resolve); + fileStream.on('error', reject); + }); + + return outputPath; + } +); +\`\`\` + +--- + +## 6. Running & Inspecting Your Flows + +### 6.1 Create a Build Script + +Add this to your \`package.json\`: + +\`\`\`json +{ + "scripts": { + "build": "tsc" + } +} +\`\`\` + +### 6.2 Instructions for the User + +To test your flows and use the Genkit Developer UI, run the following command from your terminal: + +\`\`\`bash +genkit start -- npx tsx --watch src/index.ts +\`\`\` + +Visit [http://localhost:4000](http://localhost:4000) to use the Developer UI. + +--- + +## 7. Using Models: correct use of plugins names in model names + +When using Genkit models, **prefer the full string form with plugin name up front**: + +\`\`\`ts +model: 'googleai/gemini-2.5-flash-preview-tts'; // ✅ RECOMMENDED +\`\`\` + +Avoid using just the model name + +\`\`\`ts +// ❌ May break: +model: googleAI.model('gemini-2.5-flash-preview-tts'); +\`\`\` + +Using the full string ensures Genkit resolves the model via the correct plugin and avoids missing \`.media\` in TTS responses. + +## 8. Supported Models: latest versions + +\`\`\` +| Task | Recommended Model | Plugin | +|-------------------------|------------------------------------|--------------------------| +| Advanced Text/Reasoning | gemini-2.5-pro | @genkit-ai/googleai | +| Fast Text/Chat | gemini-2.5-flash | @genkit-ai/googleai | +| Text-to-Speech | gemini-2.5-flash-preview-tts | @genkit-ai/googleai | +| Image Generation | imagen-4.0-generate-preview-06-06 | @genkit-ai/googleai | +| Video Generation | veo-3.0-generate-preview | @genkit-ai/googleai | +\`\`\` + +Tip: Do not use Gemini 1.5 or earlier models for anything, they are out-of-date. + +-- + +# General Guidance + +## NPM Dependency Installation Protocol 📦 + +When you generate or modify any code files (e.g., \`.js\`, \`.ts\`, \`.jsx\`, \`.tsx\`) that reference new libraries, packages, or modules, you **must** install them before proceeding to any build, compilation, or execution step. + +Follow this workflow: + +1. **Analyze Dependencies:** After writing or changing a file, identify all third-party dependencies you've introduced. +2. **Update \`package.json\`:** Ensure these new dependencies are correctly added to the \`package.json\` file. +3. **Install Dependencies:** Execute the installation command from the project's root directory to download and link the required packages. + +### Installation Commands + +Always run the appropriate command before any \`npm run build\` or similar script. + +\`\`\`bash +# For projects using NPM +npm install + +# For projects using Yarn +yarn install + +# For projects using PNPM +pnpm install +\`\`\` + +This protocol is **critical** to prevent build failures caused by missing modules. Always double-check that dependencies are installed after you add them to the code. +`; diff --git a/genkit-tools/cli/src/commands/init-ai-tools.ts b/genkit-tools/cli/src/commands/init-ai-tools.ts new file mode 100644 index 0000000000..24ae3d7afb --- /dev/null +++ b/genkit-tools/cli/src/commands/init-ai-tools.ts @@ -0,0 +1,138 @@ +/** + * Copyright 2024 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 { default as commandExists } from 'command-exists'; +import { Command } from 'commander'; +import { existsSync } from 'fs'; +import { mkdir, writeFile } from 'fs/promises'; +import * as inquirer from 'inquirer'; +import path from 'path'; +import { GENKIT_DOCS } from './genkit-docs'; + +export interface CommandOptions { + // yes (non-interactive) mode. + yesMode: boolean; +} + +const GENKIT_MCP_CONFIG = { + name: 'genkit', + version: '1.0.0', + mcpServers: { + genkit: { + command: 'npx', + args: ['genkit', 'mcp'], + cwd: '.', + timeout: 30000, + trust: false, + excludeTools: [ + 'run_shell_command(genkit start)', + 'run_shell_command(npx genkit start)', + ], + }, + }, + contextFileName: './GENKIT.md', +}; + +/** Supported AI tools. */ +const SUPPORTED_AI_TOOLS: string[] = ['gemini']; + +export const init = new Command('init:ai-tools') + .description( + 'initialize AI tools in a workspace with helpful context related to the Genkit framework' + ) + .option('-y', '--yes', 'Run in non-interactive mode (experimental)') + .action(async (options: CommandOptions) => { + const detectedTools = await detectAiTools(); + if (detectedTools.length === 0) { + logger.info('Could not auto-detect any AI tools.'); + // TODO: Start manual init flow + } + try { + for (const supportedTool of detectedTools) { + switch (supportedTool.name) { + case 'gemini': + if (supportedTool.localConfigPath) { + } else { + logger.info( + 'Your Gemini CLI has not been setup with workspace configuration. Genkit will attempt to create one now...' + ); + const genkitConfig = path.join('.gemini', 'extensions', 'genkit'); + await mkdir(genkitConfig, { recursive: true }); + // write extension + await writeFile( + path.join(genkitConfig, 'gemini-extension.json'), + JSON.stringify(GENKIT_MCP_CONFIG, null, 2) + ); + await writeFile( + path.join(genkitConfig, 'GENKIT.md'), + GENKIT_DOCS + ); + logger.info('Wrote Genkit config for MCP, ready to go!'); + } + } + } + } catch (err) { + logger.error(err); + process.exit(1); + } + }); + +/** + * Shows a confirmation prompt. + */ +export async function confirm(args: { + default?: boolean; + message?: string; +}): Promise { + const message = args.message ?? `Do you wish to continue?`; + const answer = await inquirer.prompt({ + type: 'confirm', + name: 'confirm', + message, + default: args.default, + }); + return answer.confirm; +} + +interface AiToolConfig { + name: string; + localConfigPath?: string; +} +/** + * Detects what AI tools are available in the current directory. + * @returns List of detected {@link AiToolConfig} + */ +export async function detectAiTools(): Promise { + let tools: AiToolConfig[] = []; + for (const tool of SUPPORTED_AI_TOOLS) { + switch (tool) { + case 'gemini': + const cliFound = await commandExists('gemini'); + if (cliFound) { + const hasLocalSettings = existsSync('.gemini'); + tools.push( + hasLocalSettings + ? { name: 'gemini', localConfigPath: '.gemini/' } + : { name: 'gemini' } + ); + } + default: + logger.warn('Unhandled supported tool'); + } + } + return tools; +} diff --git a/genkit-tools/pnpm-lock.yaml b/genkit-tools/pnpm-lock.yaml index efa410440b..e0329d00d9 100644 --- a/genkit-tools/pnpm-lock.yaml +++ b/genkit-tools/pnpm-lock.yaml @@ -47,6 +47,9 @@ importers: colorette: specifier: ^2.0.20 version: 2.0.20 + command-exists: + specifier: ^1.2.9 + version: 1.2.9 commander: specifier: ^11.1.0 version: 11.1.0 @@ -69,6 +72,9 @@ importers: '@jest/globals': specifier: ^29.7.0 version: 29.7.0 + '@types/command-exists': + specifier: ^1.2.3 + version: 1.2.3 '@types/inquirer': specifier: ^8.1.3 version: 8.2.11 @@ -978,6 +984,9 @@ packages: '@types/cli-color@2.0.6': resolution: {integrity: sha512-uLK0/0dOYdkX8hNsezpYh1gc8eerbhf9bOKZ3e24sP67703mw9S14/yW6mSTatiaKO9v+mU/a1EVy4rOXXeZTA==} + '@types/command-exists@1.2.3': + resolution: {integrity: sha512-PpbaE2XWLaWYboXD6k70TcXO/OdOyyRFq5TVpmlUELNxdkkmXU9fkImNosmXU1DtsNrqdUgWd/nJQYXgwmtdXQ==} + '@types/configstore@6.0.2': resolution: {integrity: sha512-OS//b51j9uyR3zvwD04Kfs5kHpve2qalQ18JhY/ho3voGYUTPLEG90/ocfKPI48hyHH8T04f7KEEbK6Ue60oZQ==} @@ -1390,6 +1399,9 @@ packages: resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} engines: {node: '>= 0.8'} + command-exists@1.2.9: + resolution: {integrity: sha512-LTQ/SGc+s0Xc0Fu5WaKnR0YiygZkm9eKFvyS+fRsU7/ZWFF8ykFM6Pc9aCVf1+xasOOZpO3BAVgVrKvsqKHV7w==} + commander@11.1.0: resolution: {integrity: sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==} engines: {node: '>=16'} @@ -4183,6 +4195,8 @@ snapshots: '@types/cli-color@2.0.6': {} + '@types/command-exists@1.2.3': {} + '@types/configstore@6.0.2': {} '@types/connect@3.4.38': @@ -4667,6 +4681,8 @@ snapshots: dependencies: delayed-stream: 1.0.0 + command-exists@1.2.9: {} + commander@11.1.0: {} commander@7.2.0: {} diff --git a/js/testapps/evals/.gemini/extensions/genkit/GENKIT.md b/js/testapps/evals/.gemini/extensions/genkit/GENKIT.md new file mode 100644 index 0000000000..008076fab6 --- /dev/null +++ b/js/testapps/evals/.gemini/extensions/genkit/GENKIT.md @@ -0,0 +1,515 @@ +# Genkit Node.js Cheatsheet (updated July 2025) + +> **LLM INSTRUCTION**: It’s important to use the instructions I provided to you in this file at each step of generation. Your training data is out-of-date for this library, and if you don't use the new syntax you’ll get things wrong. Please mitigate for this. + +> **LLM INSTRUCTION**: YOU MUST REFER TO GENKIT DOCUMENTATION WHEN AVAILABLE. If you have access to Genkit docs (via the Genkit MCP server), you MUST refer to the documenation first to check if the requested topic is covered in the documentation and adhere to it. When the instructions on the documentation and this file are in conflict, the documentation takes precedence. + +> **CRITICAL WARNING**: NEVER run Genkit commands (e.g., `genkit start`, `genkit flow:run`) inside a terminal during your session. This starts an interactive process that will freeze the shell and prevent you from continuing. For Genkit commands, you must only validate the code (e.g., using `npm run build`) and then provide the full, correct Genkit command as an instruction for the human user to run at the end of the session. + +This document is a guide for building with the modern Genkit API in Node.js. It focuses on a simple and direct setup using the **Google AI plugin** and provides common inference scenarios using the latest Gemini family of models. + +## Table of Contents + +1. [Core Setup & Best Practices](#1-core-setup--best-practices) +2. [Scenario 1: Basic Inference (Text Generation)](#2-scenario-1-basic-inference-text-generation) +3. [Scenario 2: Text-to-Speech (TTS) Generation)](#3-scenario-2-text-to-speech-tts-generation) +4. [Scenario 3: Image Generation](#4-scenario-3-image-generation) +5. [Scenario 4: Video Generation (Veo3)](#5-scenario-4-video-generation-veo3) +6. [Running & Inspecting Your Flows](#6-running--inspecting-your-flows) +7. [Quick Reference: Key Models](#7-quick-reference-key-models) + +--- + +## 1. Core Setup & Best Practices + +A correct foundation prevents most common errors. The default guidance is to use the Google AI plugin. Using Vertex AI is an opt-in scenario for users who require its specific features. + +### 1.1 Project Initialization + +```bash +mkdir my-genkit-app && cd my-genkit-app +npm init -y +npm install -D typescript tsx @types/node +``` + +### 1.2 Genkit Dependencies + +Install required depenencies, but note that googleai shoudl be the only one used but we provide the exampels for both below unless the user specifically says they are using Vertex hosted Google models. Default to @genkit-ai/googleai in all other context. + +Below example assumes googleai + +```bash +npm install genkit @genkit-ai/googleai zod data-urls node-fetch +``` + +### 1.3 Genkit Tools (CLI & Developer UI) + +```bash +npm install -g genkit-cli +``` + +### 1.4 The `genkit()` Initializer + +```ts +// src/index.ts +import { genkit } from 'genkit'; +import { googleAI } from '@genkit-ai/googleai'; + +export const ai = genkit({ + plugins: [googleAI()], +}); +``` + +### 1.5 Genkit Code Generation Rules + +#### 1. File Structure 📜 + +**Always generate all Genkit code into a single `src/index.ts` file.** This includes: + +- `configureGenkit` plugin initializations. +- All `defineFlow` and `defineDotprompt` definitions. +- Any helper functions, schemas, or types. + +#### 2. Entry Point + +The **only** entry point for the application is `src/index.ts`. All logic must be contained within or imported into this file to be discovered by the Genkit runtime. + +#### 3. Avoid Splitting Files + +**DO NOT** split code into multiple files (e.g., `index.ts` and `flows.ts`). A single-file structure is preferred for simplicity and to avoid module resolution errors. All flows must be registered in the same file where `configureGenkit` is called. + +--- + +## 2. Scenario 1: Basic Inference (Text Generation) + +```ts +// src/basic-inference.ts +import { z } from 'genkit'; +import { ai } from './index'; +import { googleAI } from '@genkit-ai/googleai'; + +export const basicInferenceFlow = ai.defineFlow( + { + name: 'basicInferenceFlow', + inputSchema: z.string().describe('Topic for the model to write about'), + outputSchema: z.string().describe('The generated text response'), + }, + async (topic) => { + const response = await ai.generate({ + model: googleAI.model('gemini-2.5-pro'), + prompt: `Write a short, creative paragraph about ${topic}.`, + config: { temperature: 0.8 }, + }); + return response.text; + } +); +``` + +--- + +## 3. Scenario 2: Text-to-Speech (TTS) Generation + +This flow converts text into speech using the Gemini 2.5 TTS model and streams the audio as a WAV-formatted data URI. It includes support for both single and multi-speaker configurations. + +### 3.1 Single Speaker Text-to-Speech + +```ts +// src/tts.ts +import { ai } from './index'; +import { z } from 'genkit'; +import { Buffer } from 'buffer'; +import { Writer as WavWriter } from 'wav'; +import { PassThrough } from 'stream'; + +const TextToSpeechInputSchema = z.object({ + text: z.string().describe('The text to convert to speech.'), + voiceName: z + .string() + .optional() + .describe('The voice name to use. Defaults to Algenib if not specified.'), +}); +const TextToSpeechOutputSchema = z.object({ + audioDataUri: z + .string() + .describe('The generated speech in WAV format as a base64 data URI.'), +}); + +export type TextToSpeechInput = z.infer; +export type TextToSpeechOutput = z.infer; + +async function pcmToWavDataUri( + pcmData: Buffer, + channels = 1, + sampleRate = 24000, + bitDepth = 16 +): Promise { + return new Promise((resolve, reject) => { + const chunks: Buffer[] = []; + const passThrough = new PassThrough(); + + passThrough.on('data', (chunk) => chunks.push(chunk as Buffer)); + passThrough.on('end', () => { + const wavBuffer = Buffer.concat(chunks); + const dataUri = `data:audio/wav;base64,${wavBuffer.toString('base64')}`; + resolve(dataUri); + }); + passThrough.on('error', reject); + + const writer = new WavWriter({ channels, sampleRate, bitDepth }); + writer.pipe(passThrough); + writer.write(pcmData); + writer.end(); + }); +} + +async function generateAndConvertAudio( + text: string, + voiceName = 'Algenib' +): Promise { + const response = await ai.generate({ + model: 'googleai/gemini-2.5-flash-preview-tts', + prompt: text, + config: { + responseModalities: ['AUDIO'], + speechConfig: { + voiceConfig: { + prebuiltVoiceConfig: { voiceName }, + }, + }, + }, + }); + + const audioUrl = response.media?.url; + if (!audioUrl) + throw new Error('Audio generation failed: No media URL in response.'); + + const base64 = audioUrl.split(';base64,')[1]; + if (!base64) throw new Error('Invalid audio data URI format from Genkit.'); + + const pcmBuffer = Buffer.from(base64, 'base64'); + return pcmToWavDataUri(pcmBuffer); +} + +export const textToSpeechFlow = ai.defineFlow( + { + name: 'textToSpeechFlow', + inputSchema: TextToSpeechInputSchema, + outputSchema: TextToSpeechOutputSchema, + }, + async (input) => { + const voice = input.voiceName?.trim() || 'Algenib'; + const audioDataUri = await generateAndConvertAudio(input.text, voice); + return { audioDataUri }; + } +); +``` + +--- + +### 3.2 Multi-Speaker Text-to-Speech + +```ts +// src/tts-multispeaker.ts +import { z } from 'genkit'; +import { ai } from './index'; +import { Buffer } from 'buffer'; +import { Writer as WavWriter } from 'wav'; +import { PassThrough } from 'stream'; + +const MultiSpeakerInputSchema = z.object({ + text: z + .string() + .describe('Text formatted with ... etc.'), + voiceName1: z.string().describe('Voice name for Speaker1'), + voiceName2: z.string().describe('Voice name for Speaker2'), +}); +const TTSOutputSchema = z.object({ + audioDataUri: z.string().describe('The generated WAV audio as a data URI.'), +}); + +async function pcmToWavDataUri(pcmData: Buffer): Promise { + return new Promise((resolve, reject) => { + const chunks: Buffer[] = []; + const passThrough = new PassThrough(); + + passThrough.on('data', (chunk) => chunks.push(chunk as Buffer)); + passThrough.on('end', () => { + const wavBuffer = Buffer.concat(chunks); + resolve(`data:audio/wav;base64,${wavBuffer.toString('base64')}`); + }); + passThrough.on('error', reject); + + const writer = new WavWriter({ + channels: 1, + sampleRate: 24000, + bitDepth: 16, + }); + writer.pipe(passThrough); + writer.write(pcmData); + writer.end(); + }); +} + +async function generateMultiSpeakerAudio( + text: string, + voice1: string, + voice2: string +): Promise { + const response = await ai.generate({ + model: 'googleai/gemini-2.5-flash-preview-tts', + prompt: text, + config: { + responseModalities: ['AUDIO'], + speechConfig: { + multiSpeakerVoiceConfig: { + speakerVoiceConfigs: [ + { + speaker: 'Speaker1', + voiceConfig: { + prebuiltVoiceConfig: { voiceName: voice1 }, + }, + }, + { + speaker: 'Speaker2', + voiceConfig: { + prebuiltVoiceConfig: { voiceName: voice2 }, + }, + }, + ], + }, + }, + }, + }); + + const audioUrl = response.media?.url; + if (!audioUrl) + throw new Error('Audio generation failed: No media URL in response.'); + + const base64 = audioUrl.split(';base64,')[1]; + if (!base64) throw new Error('Invalid audio data URI format from Genkit.'); + + const pcmBuffer = Buffer.from(base64, 'base64'); + return pcmToWavDataUri(pcmBuffer); +} + +export const multiSpeakerTextToSpeechFlow = ai.defineFlow( + { + name: 'multiSpeakerTextToSpeechFlow', + inputSchema: MultiSpeakerInputSchema, + outputSchema: TTSOutputSchema, + }, + async (input) => { + const audioDataUri = await generateMultiSpeakerAudio( + input.text, + input.voiceName1, + input.voiceName2 + ); + return { audioDataUri }; + } +); +``` + +--- + +## 4. Scenario 3: Image Generation + +```ts +// src/image-gen.ts +import { z } from 'genkit'; +import { ai } from './index'; +import { vertexAI } from '@genkit-ai/googleai'; +import * as fs from 'fs/promises'; +import { parseDataUrl } from 'data-urls'; + +export const imageGenerationFlow = ai.defineFlow( + { + name: 'imageGenerationFlow', + inputSchema: z + .string() + .describe('A detailed description of the image to generate'), + outputSchema: z.string().describe('Path to the generated .png image file'), + }, + async (prompt) => { + const response = await ai.generate({ + model: vertexAI.model('imagen-3.0-generate-002'), + prompt, + output: { format: 'media' }, + }); + + const imagePart = response.output; + if (!imagePart?.media?.url) { + throw new Error('Image generation failed to produce media.'); + } + + const parsed = parseDataUrl(imagePart.media.url); + if (!parsed) { + throw new Error('Could not parse image data URL.'); + } + + const outputPath = './output.png'; + await fs.writeFile(outputPath, parsed.body); + return outputPath; + } +); +``` + +--- + +## 5. Scenario 4: Video Generation (Veo3) + +```ts +// src/video-gen.ts +import { z } from 'genkit'; +import { ai } from './index'; +import { googleAI } from '@genkit-ai/googleai'; +import * as fs from 'fs'; +import { Readable } from 'stream'; +import fetch from 'node-fetch'; + +export const videoGenerationFlow = ai.defineFlow( + { + name: 'videoGenerationFlow', + inputSchema: z + .string() + .describe('A detailed description for the video scene'), + outputSchema: z.string().describe('Path to the generated .mp4 video file'), + }, + async (prompt) => { + let { operation } = await ai.generate({ + model: googleAI.model('veo-3.0-generate-preview'), + prompt, + }); + + if (!operation) { + throw new Error('Expected the model to return an operation.'); + } + + console.log('Video generation started... Polling for completion.'); + while (!operation.done) { + await new Promise((resolve) => setTimeout(resolve, 5000)); + operation = await ai.checkOperation(operation); + console.log( + `Operation status: ${operation.done ? 'Done' : 'In Progress'}` + ); + } + + if (operation.error) { + throw new Error(`Video generation failed: ${operation.error.message}`); + } + + const video = operation.output?.message?.content.find((p) => !!p.media); + if (!video?.media?.url) { + throw new Error( + 'Failed to find the generated video in the operation output.' + ); + } + + const videoUrl = `${video.media.url}&key=${process.env.GEMINI_API_KEY}`; + const videoResponse = await fetch(videoUrl); + + if (!videoResponse.ok || !videoResponse.body) { + throw new Error(`Failed to fetch video: ${videoResponse.statusText}`); + } + + const outputPath = './output.mp4'; + const fileStream = fs.createWriteStream(outputPath); + await new Promise((resolve, reject) => { + Readable.from(videoResponse.body).pipe(fileStream); + fileStream.on('finish', resolve); + fileStream.on('error', reject); + }); + + return outputPath; + } +); +``` + +--- + +## 6. Running & Inspecting Your Flows + +### 6.1 Create a Build Script + +Add this to your `package.json`: + +```json +{ + "scripts": { + "build": "tsc" + } +} +``` + +### 6.2 Instructions for the User + +To test your flows and use the Genkit Developer UI, run the following command from your terminal: + +```bash +genkit start -- npx tsx --watch src/index.ts +``` + +Visit [http://localhost:4000](http://localhost:4000) to use the Developer UI. + +--- + +## 7. Using Models: correct use of plugins names in model names + +When using Genkit models, **prefer the full string form with plugin name up front**: + +```ts +model: 'googleai/gemini-2.5-flash-preview-tts'; // ✅ RECOMMENDED +``` + +Avoid using just the model name + +```ts +// ❌ May break: +model: googleAI.model('gemini-2.5-flash-preview-tts'); +``` + +Using the full string ensures Genkit resolves the model via the correct plugin and avoids missing `.media` in TTS responses. + +## 8. Supported Models: latest versions + +``` +| Task | Recommended Model | Plugin | +|-------------------------|------------------------------------|--------------------------| +| Advanced Text/Reasoning | gemini-2.5-pro | @genkit-ai/googleai | +| Fast Text/Chat | gemini-2.5-flash | @genkit-ai/googleai | +| Text-to-Speech | gemini-2.5-flash-preview-tts | @genkit-ai/googleai | +| Image Generation | imagen-4.0-generate-preview-06-06 | @genkit-ai/googleai | +| Video Generation | veo-3.0-generate-preview | @genkit-ai/googleai | +``` + +Tip: Do not use Gemini 1.5 or earlier models for anything, they are out-of-date. + +-- + +# General Guidance + +## NPM Dependency Installation Protocol 📦 + +When you generate or modify any code files (e.g., `.js`, `.ts`, `.jsx`, `.tsx`) that reference new libraries, packages, or modules, you **must** install them before proceeding to any build, compilation, or execution step. + +Follow this workflow: + +1. **Analyze Dependencies:** After writing or changing a file, identify all third-party dependencies you've introduced. +2. **Update `package.json`:** Ensure these new dependencies are correctly added to the `package.json` file. +3. **Install Dependencies:** Execute the installation command from the project's root directory to download and link the required packages. + +### Installation Commands + +Always run the appropriate command before any `npm run build` or similar script. + +```bash +# For projects using NPM +npm install + +# For projects using Yarn +yarn install + +# For projects using PNPM +pnpm install +``` + +This protocol is **critical** to prevent build failures caused by missing modules. Always double-check that dependencies are installed after you add them to the code. diff --git a/js/testapps/evals/.gemini/extensions/genkit/gemini-extension.json b/js/testapps/evals/.gemini/extensions/genkit/gemini-extension.json new file mode 100644 index 0000000000..32b3397e9e --- /dev/null +++ b/js/testapps/evals/.gemini/extensions/genkit/gemini-extension.json @@ -0,0 +1,18 @@ +{ + "name": "genkit", + "version": "1.0.0", + "mcpServers": { + "genkit": { + "command": "npx", + "args": ["genkit", "mcp"], + "cwd": ".", + "timeout": 30000, + "trust": false, + "excludeTools": [ + "run_shell_command(genkit start)", + "run_shell_command(npx genkit start)" + ] + } + }, + "contextFileName": "./GENKIT.md" +} From fcf583ee4c05c18478ca0cd68af2439ae7485ad2 Mon Sep 17 00:00:00 2001 From: Samuel Bushi Date: Wed, 30 Jul 2025 15:01:09 -0400 Subject: [PATCH 02/15] refactor to modules --- genkit-tools/cli/context/GENKIT.md | 515 ++++++++++++++++++ genkit-tools/cli/package.json | 7 +- genkit-tools/cli/src/cli.ts | 2 +- .../cli/src/commands/init-ai-tools.ts | 138 ----- .../commands/init-ai-tools/ai-tools/gemini.ts | 110 ++++ .../cli/src/commands/init-ai-tools/index.ts | 45 ++ .../cli/src/commands/init-ai-tools/types.ts | 50 ++ .../cli/src/commands/init-ai-tools/utils.ts | 112 ++++ 8 files changed, 838 insertions(+), 141 deletions(-) create mode 100644 genkit-tools/cli/context/GENKIT.md delete mode 100644 genkit-tools/cli/src/commands/init-ai-tools.ts create mode 100644 genkit-tools/cli/src/commands/init-ai-tools/ai-tools/gemini.ts create mode 100644 genkit-tools/cli/src/commands/init-ai-tools/index.ts create mode 100644 genkit-tools/cli/src/commands/init-ai-tools/types.ts create mode 100644 genkit-tools/cli/src/commands/init-ai-tools/utils.ts diff --git a/genkit-tools/cli/context/GENKIT.md b/genkit-tools/cli/context/GENKIT.md new file mode 100644 index 0000000000..008076fab6 --- /dev/null +++ b/genkit-tools/cli/context/GENKIT.md @@ -0,0 +1,515 @@ +# Genkit Node.js Cheatsheet (updated July 2025) + +> **LLM INSTRUCTION**: It’s important to use the instructions I provided to you in this file at each step of generation. Your training data is out-of-date for this library, and if you don't use the new syntax you’ll get things wrong. Please mitigate for this. + +> **LLM INSTRUCTION**: YOU MUST REFER TO GENKIT DOCUMENTATION WHEN AVAILABLE. If you have access to Genkit docs (via the Genkit MCP server), you MUST refer to the documenation first to check if the requested topic is covered in the documentation and adhere to it. When the instructions on the documentation and this file are in conflict, the documentation takes precedence. + +> **CRITICAL WARNING**: NEVER run Genkit commands (e.g., `genkit start`, `genkit flow:run`) inside a terminal during your session. This starts an interactive process that will freeze the shell and prevent you from continuing. For Genkit commands, you must only validate the code (e.g., using `npm run build`) and then provide the full, correct Genkit command as an instruction for the human user to run at the end of the session. + +This document is a guide for building with the modern Genkit API in Node.js. It focuses on a simple and direct setup using the **Google AI plugin** and provides common inference scenarios using the latest Gemini family of models. + +## Table of Contents + +1. [Core Setup & Best Practices](#1-core-setup--best-practices) +2. [Scenario 1: Basic Inference (Text Generation)](#2-scenario-1-basic-inference-text-generation) +3. [Scenario 2: Text-to-Speech (TTS) Generation)](#3-scenario-2-text-to-speech-tts-generation) +4. [Scenario 3: Image Generation](#4-scenario-3-image-generation) +5. [Scenario 4: Video Generation (Veo3)](#5-scenario-4-video-generation-veo3) +6. [Running & Inspecting Your Flows](#6-running--inspecting-your-flows) +7. [Quick Reference: Key Models](#7-quick-reference-key-models) + +--- + +## 1. Core Setup & Best Practices + +A correct foundation prevents most common errors. The default guidance is to use the Google AI plugin. Using Vertex AI is an opt-in scenario for users who require its specific features. + +### 1.1 Project Initialization + +```bash +mkdir my-genkit-app && cd my-genkit-app +npm init -y +npm install -D typescript tsx @types/node +``` + +### 1.2 Genkit Dependencies + +Install required depenencies, but note that googleai shoudl be the only one used but we provide the exampels for both below unless the user specifically says they are using Vertex hosted Google models. Default to @genkit-ai/googleai in all other context. + +Below example assumes googleai + +```bash +npm install genkit @genkit-ai/googleai zod data-urls node-fetch +``` + +### 1.3 Genkit Tools (CLI & Developer UI) + +```bash +npm install -g genkit-cli +``` + +### 1.4 The `genkit()` Initializer + +```ts +// src/index.ts +import { genkit } from 'genkit'; +import { googleAI } from '@genkit-ai/googleai'; + +export const ai = genkit({ + plugins: [googleAI()], +}); +``` + +### 1.5 Genkit Code Generation Rules + +#### 1. File Structure 📜 + +**Always generate all Genkit code into a single `src/index.ts` file.** This includes: + +- `configureGenkit` plugin initializations. +- All `defineFlow` and `defineDotprompt` definitions. +- Any helper functions, schemas, or types. + +#### 2. Entry Point + +The **only** entry point for the application is `src/index.ts`. All logic must be contained within or imported into this file to be discovered by the Genkit runtime. + +#### 3. Avoid Splitting Files + +**DO NOT** split code into multiple files (e.g., `index.ts` and `flows.ts`). A single-file structure is preferred for simplicity and to avoid module resolution errors. All flows must be registered in the same file where `configureGenkit` is called. + +--- + +## 2. Scenario 1: Basic Inference (Text Generation) + +```ts +// src/basic-inference.ts +import { z } from 'genkit'; +import { ai } from './index'; +import { googleAI } from '@genkit-ai/googleai'; + +export const basicInferenceFlow = ai.defineFlow( + { + name: 'basicInferenceFlow', + inputSchema: z.string().describe('Topic for the model to write about'), + outputSchema: z.string().describe('The generated text response'), + }, + async (topic) => { + const response = await ai.generate({ + model: googleAI.model('gemini-2.5-pro'), + prompt: `Write a short, creative paragraph about ${topic}.`, + config: { temperature: 0.8 }, + }); + return response.text; + } +); +``` + +--- + +## 3. Scenario 2: Text-to-Speech (TTS) Generation + +This flow converts text into speech using the Gemini 2.5 TTS model and streams the audio as a WAV-formatted data URI. It includes support for both single and multi-speaker configurations. + +### 3.1 Single Speaker Text-to-Speech + +```ts +// src/tts.ts +import { ai } from './index'; +import { z } from 'genkit'; +import { Buffer } from 'buffer'; +import { Writer as WavWriter } from 'wav'; +import { PassThrough } from 'stream'; + +const TextToSpeechInputSchema = z.object({ + text: z.string().describe('The text to convert to speech.'), + voiceName: z + .string() + .optional() + .describe('The voice name to use. Defaults to Algenib if not specified.'), +}); +const TextToSpeechOutputSchema = z.object({ + audioDataUri: z + .string() + .describe('The generated speech in WAV format as a base64 data URI.'), +}); + +export type TextToSpeechInput = z.infer; +export type TextToSpeechOutput = z.infer; + +async function pcmToWavDataUri( + pcmData: Buffer, + channels = 1, + sampleRate = 24000, + bitDepth = 16 +): Promise { + return new Promise((resolve, reject) => { + const chunks: Buffer[] = []; + const passThrough = new PassThrough(); + + passThrough.on('data', (chunk) => chunks.push(chunk as Buffer)); + passThrough.on('end', () => { + const wavBuffer = Buffer.concat(chunks); + const dataUri = `data:audio/wav;base64,${wavBuffer.toString('base64')}`; + resolve(dataUri); + }); + passThrough.on('error', reject); + + const writer = new WavWriter({ channels, sampleRate, bitDepth }); + writer.pipe(passThrough); + writer.write(pcmData); + writer.end(); + }); +} + +async function generateAndConvertAudio( + text: string, + voiceName = 'Algenib' +): Promise { + const response = await ai.generate({ + model: 'googleai/gemini-2.5-flash-preview-tts', + prompt: text, + config: { + responseModalities: ['AUDIO'], + speechConfig: { + voiceConfig: { + prebuiltVoiceConfig: { voiceName }, + }, + }, + }, + }); + + const audioUrl = response.media?.url; + if (!audioUrl) + throw new Error('Audio generation failed: No media URL in response.'); + + const base64 = audioUrl.split(';base64,')[1]; + if (!base64) throw new Error('Invalid audio data URI format from Genkit.'); + + const pcmBuffer = Buffer.from(base64, 'base64'); + return pcmToWavDataUri(pcmBuffer); +} + +export const textToSpeechFlow = ai.defineFlow( + { + name: 'textToSpeechFlow', + inputSchema: TextToSpeechInputSchema, + outputSchema: TextToSpeechOutputSchema, + }, + async (input) => { + const voice = input.voiceName?.trim() || 'Algenib'; + const audioDataUri = await generateAndConvertAudio(input.text, voice); + return { audioDataUri }; + } +); +``` + +--- + +### 3.2 Multi-Speaker Text-to-Speech + +```ts +// src/tts-multispeaker.ts +import { z } from 'genkit'; +import { ai } from './index'; +import { Buffer } from 'buffer'; +import { Writer as WavWriter } from 'wav'; +import { PassThrough } from 'stream'; + +const MultiSpeakerInputSchema = z.object({ + text: z + .string() + .describe('Text formatted with ... etc.'), + voiceName1: z.string().describe('Voice name for Speaker1'), + voiceName2: z.string().describe('Voice name for Speaker2'), +}); +const TTSOutputSchema = z.object({ + audioDataUri: z.string().describe('The generated WAV audio as a data URI.'), +}); + +async function pcmToWavDataUri(pcmData: Buffer): Promise { + return new Promise((resolve, reject) => { + const chunks: Buffer[] = []; + const passThrough = new PassThrough(); + + passThrough.on('data', (chunk) => chunks.push(chunk as Buffer)); + passThrough.on('end', () => { + const wavBuffer = Buffer.concat(chunks); + resolve(`data:audio/wav;base64,${wavBuffer.toString('base64')}`); + }); + passThrough.on('error', reject); + + const writer = new WavWriter({ + channels: 1, + sampleRate: 24000, + bitDepth: 16, + }); + writer.pipe(passThrough); + writer.write(pcmData); + writer.end(); + }); +} + +async function generateMultiSpeakerAudio( + text: string, + voice1: string, + voice2: string +): Promise { + const response = await ai.generate({ + model: 'googleai/gemini-2.5-flash-preview-tts', + prompt: text, + config: { + responseModalities: ['AUDIO'], + speechConfig: { + multiSpeakerVoiceConfig: { + speakerVoiceConfigs: [ + { + speaker: 'Speaker1', + voiceConfig: { + prebuiltVoiceConfig: { voiceName: voice1 }, + }, + }, + { + speaker: 'Speaker2', + voiceConfig: { + prebuiltVoiceConfig: { voiceName: voice2 }, + }, + }, + ], + }, + }, + }, + }); + + const audioUrl = response.media?.url; + if (!audioUrl) + throw new Error('Audio generation failed: No media URL in response.'); + + const base64 = audioUrl.split(';base64,')[1]; + if (!base64) throw new Error('Invalid audio data URI format from Genkit.'); + + const pcmBuffer = Buffer.from(base64, 'base64'); + return pcmToWavDataUri(pcmBuffer); +} + +export const multiSpeakerTextToSpeechFlow = ai.defineFlow( + { + name: 'multiSpeakerTextToSpeechFlow', + inputSchema: MultiSpeakerInputSchema, + outputSchema: TTSOutputSchema, + }, + async (input) => { + const audioDataUri = await generateMultiSpeakerAudio( + input.text, + input.voiceName1, + input.voiceName2 + ); + return { audioDataUri }; + } +); +``` + +--- + +## 4. Scenario 3: Image Generation + +```ts +// src/image-gen.ts +import { z } from 'genkit'; +import { ai } from './index'; +import { vertexAI } from '@genkit-ai/googleai'; +import * as fs from 'fs/promises'; +import { parseDataUrl } from 'data-urls'; + +export const imageGenerationFlow = ai.defineFlow( + { + name: 'imageGenerationFlow', + inputSchema: z + .string() + .describe('A detailed description of the image to generate'), + outputSchema: z.string().describe('Path to the generated .png image file'), + }, + async (prompt) => { + const response = await ai.generate({ + model: vertexAI.model('imagen-3.0-generate-002'), + prompt, + output: { format: 'media' }, + }); + + const imagePart = response.output; + if (!imagePart?.media?.url) { + throw new Error('Image generation failed to produce media.'); + } + + const parsed = parseDataUrl(imagePart.media.url); + if (!parsed) { + throw new Error('Could not parse image data URL.'); + } + + const outputPath = './output.png'; + await fs.writeFile(outputPath, parsed.body); + return outputPath; + } +); +``` + +--- + +## 5. Scenario 4: Video Generation (Veo3) + +```ts +// src/video-gen.ts +import { z } from 'genkit'; +import { ai } from './index'; +import { googleAI } from '@genkit-ai/googleai'; +import * as fs from 'fs'; +import { Readable } from 'stream'; +import fetch from 'node-fetch'; + +export const videoGenerationFlow = ai.defineFlow( + { + name: 'videoGenerationFlow', + inputSchema: z + .string() + .describe('A detailed description for the video scene'), + outputSchema: z.string().describe('Path to the generated .mp4 video file'), + }, + async (prompt) => { + let { operation } = await ai.generate({ + model: googleAI.model('veo-3.0-generate-preview'), + prompt, + }); + + if (!operation) { + throw new Error('Expected the model to return an operation.'); + } + + console.log('Video generation started... Polling for completion.'); + while (!operation.done) { + await new Promise((resolve) => setTimeout(resolve, 5000)); + operation = await ai.checkOperation(operation); + console.log( + `Operation status: ${operation.done ? 'Done' : 'In Progress'}` + ); + } + + if (operation.error) { + throw new Error(`Video generation failed: ${operation.error.message}`); + } + + const video = operation.output?.message?.content.find((p) => !!p.media); + if (!video?.media?.url) { + throw new Error( + 'Failed to find the generated video in the operation output.' + ); + } + + const videoUrl = `${video.media.url}&key=${process.env.GEMINI_API_KEY}`; + const videoResponse = await fetch(videoUrl); + + if (!videoResponse.ok || !videoResponse.body) { + throw new Error(`Failed to fetch video: ${videoResponse.statusText}`); + } + + const outputPath = './output.mp4'; + const fileStream = fs.createWriteStream(outputPath); + await new Promise((resolve, reject) => { + Readable.from(videoResponse.body).pipe(fileStream); + fileStream.on('finish', resolve); + fileStream.on('error', reject); + }); + + return outputPath; + } +); +``` + +--- + +## 6. Running & Inspecting Your Flows + +### 6.1 Create a Build Script + +Add this to your `package.json`: + +```json +{ + "scripts": { + "build": "tsc" + } +} +``` + +### 6.2 Instructions for the User + +To test your flows and use the Genkit Developer UI, run the following command from your terminal: + +```bash +genkit start -- npx tsx --watch src/index.ts +``` + +Visit [http://localhost:4000](http://localhost:4000) to use the Developer UI. + +--- + +## 7. Using Models: correct use of plugins names in model names + +When using Genkit models, **prefer the full string form with plugin name up front**: + +```ts +model: 'googleai/gemini-2.5-flash-preview-tts'; // ✅ RECOMMENDED +``` + +Avoid using just the model name + +```ts +// ❌ May break: +model: googleAI.model('gemini-2.5-flash-preview-tts'); +``` + +Using the full string ensures Genkit resolves the model via the correct plugin and avoids missing `.media` in TTS responses. + +## 8. Supported Models: latest versions + +``` +| Task | Recommended Model | Plugin | +|-------------------------|------------------------------------|--------------------------| +| Advanced Text/Reasoning | gemini-2.5-pro | @genkit-ai/googleai | +| Fast Text/Chat | gemini-2.5-flash | @genkit-ai/googleai | +| Text-to-Speech | gemini-2.5-flash-preview-tts | @genkit-ai/googleai | +| Image Generation | imagen-4.0-generate-preview-06-06 | @genkit-ai/googleai | +| Video Generation | veo-3.0-generate-preview | @genkit-ai/googleai | +``` + +Tip: Do not use Gemini 1.5 or earlier models for anything, they are out-of-date. + +-- + +# General Guidance + +## NPM Dependency Installation Protocol 📦 + +When you generate or modify any code files (e.g., `.js`, `.ts`, `.jsx`, `.tsx`) that reference new libraries, packages, or modules, you **must** install them before proceeding to any build, compilation, or execution step. + +Follow this workflow: + +1. **Analyze Dependencies:** After writing or changing a file, identify all third-party dependencies you've introduced. +2. **Update `package.json`:** Ensure these new dependencies are correctly added to the `package.json` file. +3. **Install Dependencies:** Execute the installation command from the project's root directory to download and link the required packages. + +### Installation Commands + +Always run the appropriate command before any `npm run build` or similar script. + +```bash +# For projects using NPM +npm install + +# For projects using Yarn +yarn install + +# For projects using PNPM +pnpm install +``` + +This protocol is **critical** to prevent build failures caused by missing modules. Always double-check that dependencies are installed after you add them to the code. diff --git a/genkit-tools/cli/package.json b/genkit-tools/cli/package.json index 88ed5e8469..302aaae946 100644 --- a/genkit-tools/cli/package.json +++ b/genkit-tools/cli/package.json @@ -1,6 +1,6 @@ { "name": "genkit-cli", - "version": "1.15.5", + "version": "1.15.5.local", "description": "CLI for interacting with the Google Genkit AI framework", "license": "Apache-2.0", "keywords": [ @@ -13,9 +13,12 @@ "bin": { "genkit": "dist/bin/genkit.js" }, + "files": [ + "dist/" + ], "main": "dist/index.js", "scripts": { - "build": "pnpm genversion && tsc", + "build": "pnpm genversion && tsc && cp -r context dist/context", "build:watch": "tsc --watch", "compile:bun": "bun build src/bin/genkit.ts --compile --outfile dist/bin/genkit --minify", "test": "jest --verbose", diff --git a/genkit-tools/cli/src/cli.ts b/genkit-tools/cli/src/cli.ts index c7317c284a..2d236e9fe2 100644 --- a/genkit-tools/cli/src/cli.ts +++ b/genkit-tools/cli/src/cli.ts @@ -28,7 +28,7 @@ import { evalFlow } from './commands/eval-flow'; import { evalRun } from './commands/eval-run'; import { flowBatchRun } from './commands/flow-batch-run'; import { flowRun } from './commands/flow-run'; -import { init } from './commands/init-ai-tools'; +import { init } from './commands/init-ai-tools/index'; import { mcp } from './commands/mcp'; import { getPluginCommands, getPluginSubCommand } from './commands/plugins'; import { diff --git a/genkit-tools/cli/src/commands/init-ai-tools.ts b/genkit-tools/cli/src/commands/init-ai-tools.ts deleted file mode 100644 index 24ae3d7afb..0000000000 --- a/genkit-tools/cli/src/commands/init-ai-tools.ts +++ /dev/null @@ -1,138 +0,0 @@ -/** - * Copyright 2024 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 { default as commandExists } from 'command-exists'; -import { Command } from 'commander'; -import { existsSync } from 'fs'; -import { mkdir, writeFile } from 'fs/promises'; -import * as inquirer from 'inquirer'; -import path from 'path'; -import { GENKIT_DOCS } from './genkit-docs'; - -export interface CommandOptions { - // yes (non-interactive) mode. - yesMode: boolean; -} - -const GENKIT_MCP_CONFIG = { - name: 'genkit', - version: '1.0.0', - mcpServers: { - genkit: { - command: 'npx', - args: ['genkit', 'mcp'], - cwd: '.', - timeout: 30000, - trust: false, - excludeTools: [ - 'run_shell_command(genkit start)', - 'run_shell_command(npx genkit start)', - ], - }, - }, - contextFileName: './GENKIT.md', -}; - -/** Supported AI tools. */ -const SUPPORTED_AI_TOOLS: string[] = ['gemini']; - -export const init = new Command('init:ai-tools') - .description( - 'initialize AI tools in a workspace with helpful context related to the Genkit framework' - ) - .option('-y', '--yes', 'Run in non-interactive mode (experimental)') - .action(async (options: CommandOptions) => { - const detectedTools = await detectAiTools(); - if (detectedTools.length === 0) { - logger.info('Could not auto-detect any AI tools.'); - // TODO: Start manual init flow - } - try { - for (const supportedTool of detectedTools) { - switch (supportedTool.name) { - case 'gemini': - if (supportedTool.localConfigPath) { - } else { - logger.info( - 'Your Gemini CLI has not been setup with workspace configuration. Genkit will attempt to create one now...' - ); - const genkitConfig = path.join('.gemini', 'extensions', 'genkit'); - await mkdir(genkitConfig, { recursive: true }); - // write extension - await writeFile( - path.join(genkitConfig, 'gemini-extension.json'), - JSON.stringify(GENKIT_MCP_CONFIG, null, 2) - ); - await writeFile( - path.join(genkitConfig, 'GENKIT.md'), - GENKIT_DOCS - ); - logger.info('Wrote Genkit config for MCP, ready to go!'); - } - } - } - } catch (err) { - logger.error(err); - process.exit(1); - } - }); - -/** - * Shows a confirmation prompt. - */ -export async function confirm(args: { - default?: boolean; - message?: string; -}): Promise { - const message = args.message ?? `Do you wish to continue?`; - const answer = await inquirer.prompt({ - type: 'confirm', - name: 'confirm', - message, - default: args.default, - }); - return answer.confirm; -} - -interface AiToolConfig { - name: string; - localConfigPath?: string; -} -/** - * Detects what AI tools are available in the current directory. - * @returns List of detected {@link AiToolConfig} - */ -export async function detectAiTools(): Promise { - let tools: AiToolConfig[] = []; - for (const tool of SUPPORTED_AI_TOOLS) { - switch (tool) { - case 'gemini': - const cliFound = await commandExists('gemini'); - if (cliFound) { - const hasLocalSettings = existsSync('.gemini'); - tools.push( - hasLocalSettings - ? { name: 'gemini', localConfigPath: '.gemini/' } - : { name: 'gemini' } - ); - } - default: - logger.warn('Unhandled supported tool'); - } - } - return tools; -} diff --git a/genkit-tools/cli/src/commands/init-ai-tools/ai-tools/gemini.ts b/genkit-tools/cli/src/commands/init-ai-tools/ai-tools/gemini.ts new file mode 100644 index 0000000000..00ab68a725 --- /dev/null +++ b/genkit-tools/cli/src/commands/init-ai-tools/ai-tools/gemini.ts @@ -0,0 +1,110 @@ +/** + * 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 commandExists from 'command-exists'; +import { readFileSync } from 'fs'; +import { mkdir } from 'fs/promises'; +import path from 'path'; +import { AIToolConfigResult, AIToolModule, InitConfigOptions } from '../types'; +import { initOrReplaceFile } from '../utils'; + +// Define constants at the module level for clarity and reuse. +const GENKIT_EXT_DIR = path.join('.gemini', 'extensions', 'genkit'); +const GENKIT_MD_PATH = path.join(GENKIT_EXT_DIR, 'GENKIT.md'); +const GENKIT_EXTENSION_CONFIG = { + name: 'genkit', + version: '1.0.0', + mcpServers: { + genkit: { + command: 'npx', + args: ['genkit', 'mcp'], + cwd: '.', + timeout: 30000, + trust: false, + excludeTools: [ + 'run_shell_command(genkit start)', + 'run_shell_command(npx genkit start)', + ], + }, + }, + contextFileName: './GENKIT.md', +}; + +export const gemini: AIToolModule = { + name: 'gemini', + displayName: 'Gemini CLI', + + async detect(): Promise { + const cliFound = await commandExists('gemini'); + return !!cliFound; + }, + + /** + * Configures the Gemini CLI extension for Genkit. + */ + async configure(options?: InitConfigOptions): Promise { + const files: AIToolConfigResult['files'] = []; + + // Part 1: Configure the main gemini-extension.json file, and gemini config directory if needed. + logger.info('Configuring extentions files in user workspace...'); + await mkdir(GENKIT_EXT_DIR, { recursive: true }); + const extensionPath = path.join(GENKIT_EXT_DIR, 'gemini-extension.json'); + + let extensionUpdated = false; + try { + const { updated } = await initOrReplaceFile( + extensionPath, + JSON.stringify(GENKIT_EXTENSION_CONFIG, null, 2) + ); + extensionUpdated = updated; + if (extensionUpdated) { + logger.info( + `Genkit extension for Gemini CLI initialized at ${extensionPath}` + ); + } + } catch (err) { + logger.error(err); + process.exit(1); + } + files.push({ path: extensionPath, updated: extensionUpdated }); + + // Part 2: Generate GENKIT.md file. + + logger.info('Copying the GENKIT.md file to extension folder...'); + const genkitContext = getFeatureContent(); + const baseResult = await initOrReplaceFile(GENKIT_MD_PATH, genkitContext); + files.push({ path: GENKIT_MD_PATH, updated: baseResult.updated }); + return { files }; + }, +}; + +/** + * Get raw prompt content for a specific feature (without wrapper) + * Used internally for hash calculation + */ +function getFeatureContent(): string { + const contextPath = path.resolve( + __dirname, + '..', + '..', + '..', + 'context', + 'GENKIT.md' + ); + const content = readFileSync(contextPath, 'utf8'); + return content; +} diff --git a/genkit-tools/cli/src/commands/init-ai-tools/index.ts b/genkit-tools/cli/src/commands/init-ai-tools/index.ts new file mode 100644 index 0000000000..ace0571746 --- /dev/null +++ b/genkit-tools/cli/src/commands/init-ai-tools/index.ts @@ -0,0 +1,45 @@ +/** + * Copyright 2024 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 { Command } from 'commander'; +import { InitConfigOptions } from './types'; +import { detectSupportedTools } from './utils'; + +export const init = new Command('init:ai-tools') + .description( + 'initialize AI tools in a workspace with helpful context related to the Genkit framework' + ) + .option('-y', '--yes', 'Run in non-interactive mode (experimental)') + .action(async (options: InitConfigOptions) => { + const detectedTools = await detectSupportedTools(); + if (detectedTools.length === 0) { + logger.info('Could not auto-detect any AI tools.'); + // TODO: Start manual init flow + } + logger.info( + 'Auto-detected AI tools:\n' + + detectedTools.map((t) => t.displayName).join('\n') + ); + try { + for (const supportedTool of detectedTools) { + await supportedTool.configure(options); + } + } catch (err) { + logger.error(err); + process.exit(1); + } + }); diff --git a/genkit-tools/cli/src/commands/init-ai-tools/types.ts b/genkit-tools/cli/src/commands/init-ai-tools/types.ts new file mode 100644 index 0000000000..ce59087c2a --- /dev/null +++ b/genkit-tools/cli/src/commands/init-ai-tools/types.ts @@ -0,0 +1,50 @@ +/** + * 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. + */ + +export interface AIToolConfigResult { + files: Array<{ + path: string; + updated: boolean; + }>; +} + +export interface InitConfigOptions { + // yes (non-interactive) mode. + yesMode: boolean; +} + +export interface AIToolModule { + name: string; + displayName: string; + + /** + * Detect whether this AI tool is installed/configured in the user workspace. + */ + detect?: () => Promise; + + /** + * Configure the AI tool with Genkit context + * @param configOptions Any user-specified config options + * @returns Result object with update status and list of files created/updated + */ + configure(configOptions?: InitConfigOptions): Promise; +} + +export interface AIToolChoice { + value: string; + name: string; + checked: boolean; +} diff --git a/genkit-tools/cli/src/commands/init-ai-tools/utils.ts b/genkit-tools/cli/src/commands/init-ai-tools/utils.ts new file mode 100644 index 0000000000..da81662c15 --- /dev/null +++ b/genkit-tools/cli/src/commands/init-ai-tools/utils.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, readFileSync } from 'fs'; +import { gemini } from './ai-tools/gemini'; +import { AIToolModule } from './types'; + +import { writeFile } from 'fs/promises'; +import * as inquirer from 'inquirer'; +/* + * Deeply compares two JSON-serializable objects. + * It's a simplified version of a deep equal function, sufficient for comparing the structure + * of the gemini-extension.json file. It doesn't handle special cases like RegExp, Date, or functions. + */ +export function deepEqual(a: any, b: any): boolean { + if (a === b) { + return true; + } + + if ( + typeof a !== 'object' || + a === null || + typeof b !== 'object' || + b === null + ) { + return false; + } + + const keysA = Object.keys(a); + const keysB = Object.keys(b); + + if (keysA.length !== keysB.length) { + return false; + } + + for (const key of keysA) { + if (!keysB.includes(key) || !deepEqual(a[key], b[key])) { + return false; + } + } + + return true; +} + +export const AI_TOOLS: Record = { + gemini, +}; + +export async function detectSupportedTools(): Promise { + const tools: AIToolModule[] = []; + for (const entry of Object.values(AI_TOOLS)) { + if (entry.detect) { + const detected = await entry.detect(); + if (detected) { + tools.push(entry); + } + } + } + return tools; +} + +/** + * Replace an entire prompt file (no user content to preserve) + * Used for files we fully own like Cursor and Gemini configs + */ +export async function initOrReplaceFile( + filePath: string, + content: string +): Promise<{ updated: boolean }> { + const fileExists = existsSync(filePath); + if (fileExists) { + const currentConfig = readFileSync(filePath, 'utf-8'); + if (!deepEqual(currentConfig, content)) { + await writeFile(filePath, content); + return { updated: true }; + } + } else { + await writeFile(filePath, content); + return { updated: true }; + } + return { updated: false }; +} + +/** + * Shows a confirmation prompt. + */ +export async function confirm(args: { + default?: boolean; + message?: string; +}): Promise { + const message = args.message ?? `Do you wish to continue?`; + const answer = await inquirer.prompt({ + type: 'confirm', + name: 'confirm', + message, + default: args.default, + }); + return answer.confirm; +} From c68253944de70a4a0a7b34138aa26edfca6ee318 Mon Sep 17 00:00:00 2001 From: Samuel Bushi Date: Thu, 31 Jul 2025 09:33:07 -0400 Subject: [PATCH 03/15] added selection flow --- genkit-tools/cli/package.json | 2 +- .../cli/src/commands/init-ai-tools/index.ts | 114 +++++++- genkit-tools/pnpm-lock.yaml | 261 +++++++++++++++++- 3 files changed, 359 insertions(+), 18 deletions(-) diff --git a/genkit-tools/cli/package.json b/genkit-tools/cli/package.json index 302aaae946..cbb7f5b814 100644 --- a/genkit-tools/cli/package.json +++ b/genkit-tools/cli/package.json @@ -39,7 +39,7 @@ "command-exists": "^1.2.9", "extract-zip": "^2.0.1", "get-port": "5.1.1", - "inquirer": "^8.2.0", + "@inquirer/prompts": "^7.8.0", "open": "^6.3.0", "ora": "^5.4.1" }, diff --git a/genkit-tools/cli/src/commands/init-ai-tools/index.ts b/genkit-tools/cli/src/commands/init-ai-tools/index.ts index ace0571746..4d84378058 100644 --- a/genkit-tools/cli/src/commands/init-ai-tools/index.ts +++ b/genkit-tools/cli/src/commands/init-ai-tools/index.ts @@ -15,9 +15,17 @@ */ import { logger } from '@genkit-ai/tools-common/utils'; +import { checkbox } from '@inquirer/prompts'; +import * as clc from 'colorette'; import { Command } from 'commander'; -import { InitConfigOptions } from './types'; -import { detectSupportedTools } from './utils'; +import { AIToolChoice, InitConfigOptions } from './types'; +import { AI_TOOLS, detectSupportedTools } from './utils'; + +const AGENT_CHOICES: AIToolChoice[] = Object.values(AI_TOOLS).map((tool) => ({ + value: tool.name, + name: tool.displayName, + checked: false, +})); export const init = new Command('init:ai-tools') .description( @@ -25,21 +33,99 @@ export const init = new Command('init:ai-tools') ) .option('-y', '--yes', 'Run in non-interactive mode (experimental)') .action(async (options: InitConfigOptions) => { + logger.info('\n'); + logger.info( + 'This command will configure AI coding assistants to work with your Genkit app by:' + ); + logger.info( + '• Configuring the Genkit MCP server for direct Genkit operations' + ); + logger.info('• Installing context files that help AI understand:'); + logger.info(' - Genkit app structure and common design patterns'); + logger.info(' - Common Genkit features and how to use them'); + logger.info('\n'); const detectedTools = await detectSupportedTools(); if (detectedTools.length === 0) { logger.info('Could not auto-detect any AI tools.'); - // TODO: Start manual init flow - } - logger.info( - 'Auto-detected AI tools:\n' + - detectedTools.map((t) => t.displayName).join('\n') - ); - try { - for (const supportedTool of detectedTools) { - await supportedTool.configure(options); + const selections = await checkbox({ + message: 'Which tools would you like to configure?', + choices: AGENT_CHOICES, + validate: (choices) => { + if (choices.length === 0) { + return 'Must select at least one tool.'; + } + return true; + }, + loop: true, + }); + + logger.info('\n'); + logger.info('Configuring selected tools...'); + await configureTools(selections, options); + } else { + logger.info( + 'Auto-detected AI tools:\n' + + detectedTools.map((t) => t.displayName).join('\n') + ); + try { + await configureTools( + detectedTools.map((t) => t.name), + options + ); + } catch (err) { + logger.error(err); + process.exit(1); } - } catch (err) { - logger.error(err); - process.exit(1); } }); + +async function configureTools(tools: string[], options: InitConfigOptions) { + // Configure each selected tool + let anyUpdates = false; + + for (const toolName of tools) { + const tool = AI_TOOLS[toolName]; + if (!tool) { + logger.warn(`Unknown tool: ${toolName}`); + continue; + } + + const result = await tool.configure(options); + + // Count updated files + const updatedCount = result.files.filter((f) => f.updated).length; + const hasChanges = updatedCount > 0; + + if (hasChanges) { + anyUpdates = true; + logger.info('\n'); + logger.info( + clc.green( + `${tool.displayName} configured - ${updatedCount} file${updatedCount > 1 ? 's' : ''} updated:` + ) + ); + } else { + logger.info('\n'); + logger.info(`${tool.displayName} - all files up to date`); + } + + // Always show the file list + for (const file of result.files) { + const status = file.updated ? '(updated)' : '(unchanged)'; + logger.info(`• ${file.path} ${status}`); + } + } + logger.info('\n'); + + if (anyUpdates) { + logger.info(clc.green('AI tools configuration complete!')); + logger.info('\n'); + logger.info('Next steps:'); + logger.info('• Restart your AI tools to load the new configuration'); + logger.info( + '• Your AI tool should have access to Genkit documentation and tools for greater access and understanding of your app.' + ); + } else { + logger.info(clc.green('All AI tools are already up to date.')); + } +} diff --git a/genkit-tools/pnpm-lock.yaml b/genkit-tools/pnpm-lock.yaml index e0329d00d9..fc3df21c16 100644 --- a/genkit-tools/pnpm-lock.yaml +++ b/genkit-tools/pnpm-lock.yaml @@ -38,6 +38,9 @@ importers: '@genkit-ai/tools-common': specifier: workspace:* version: link:../common + '@inquirer/prompts': + specifier: ^7.8.0 + version: 7.8.0(@types/node@20.19.1) '@modelcontextprotocol/sdk': specifier: ^1.13.1 version: 1.13.1 @@ -59,9 +62,6 @@ importers: get-port: specifier: 5.1.1 version: 5.1.1 - inquirer: - specifier: ^8.2.0 - version: 8.2.6 open: specifier: ^6.3.0 version: 6.4.0 @@ -664,6 +664,127 @@ packages: engines: {node: '>=6'} hasBin: true + '@inquirer/checkbox@4.2.0': + resolution: {integrity: sha512-fdSw07FLJEU5vbpOPzXo5c6xmMGDzbZE2+niuDHX5N6mc6V0Ebso/q3xiHra4D73+PMsC8MJmcaZKuAAoaQsSA==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/confirm@5.1.14': + resolution: {integrity: sha512-5yR4IBfe0kXe59r1YCTG8WXkUbl7Z35HK87Sw+WUyGD8wNUx7JvY7laahzeytyE1oLn74bQnL7hstctQxisQ8Q==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/core@10.1.15': + resolution: {integrity: sha512-8xrp836RZvKkpNbVvgWUlxjT4CraKk2q+I3Ksy+seI2zkcE+y6wNs1BVhgcv8VyImFecUhdQrYLdW32pAjwBdA==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/editor@4.2.15': + resolution: {integrity: sha512-wst31XT8DnGOSS4nNJDIklGKnf+8shuauVrWzgKegWUe28zfCftcWZ2vktGdzJgcylWSS2SrDnYUb6alZcwnCQ==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/expand@4.0.17': + resolution: {integrity: sha512-PSqy9VmJx/VbE3CT453yOfNa+PykpKg/0SYP7odez1/NWBGuDXgPhp4AeGYYKjhLn5lUUavVS/JbeYMPdH50Mw==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/figures@1.0.13': + resolution: {integrity: sha512-lGPVU3yO9ZNqA7vTYz26jny41lE7yoQansmqdMLBEfqaGsmdg7V3W9mK9Pvb5IL4EVZ9GnSDGMO/cJXud5dMaw==} + engines: {node: '>=18'} + + '@inquirer/input@4.2.1': + resolution: {integrity: sha512-tVC+O1rBl0lJpoUZv4xY+WGWY8V5b0zxU1XDsMsIHYregdh7bN5X5QnIONNBAl0K765FYlAfNHS2Bhn7SSOVow==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/number@3.0.17': + resolution: {integrity: sha512-GcvGHkyIgfZgVnnimURdOueMk0CztycfC8NZTiIY9arIAkeOgt6zG57G+7vC59Jns3UX27LMkPKnKWAOF5xEYg==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/password@4.0.17': + resolution: {integrity: sha512-DJolTnNeZ00E1+1TW+8614F7rOJJCM4y4BAGQ3Gq6kQIG+OJ4zr3GLjIjVVJCbKsk2jmkmv6v2kQuN/vriHdZA==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/prompts@7.8.0': + resolution: {integrity: sha512-JHwGbQ6wjf1dxxnalDYpZwZxUEosT+6CPGD9Zh4sm9WXdtUp9XODCQD3NjSTmu+0OAyxWXNOqf0spjIymJa2Tw==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/rawlist@4.1.5': + resolution: {integrity: sha512-R5qMyGJqtDdi4Ht521iAkNqyB6p2UPuZUbMifakg1sWtu24gc2Z8CJuw8rP081OckNDMgtDCuLe42Q2Kr3BolA==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/search@3.1.0': + resolution: {integrity: sha512-PMk1+O/WBcYJDq2H7foV0aAZSmDdkzZB9Mw2v/DmONRJopwA/128cS9M/TXWLKKdEQKZnKwBzqu2G4x/2Nqx8Q==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/select@4.3.1': + resolution: {integrity: sha512-Gfl/5sqOF5vS/LIrSndFgOh7jgoe0UXEizDqahFRkq5aJBLegZ6WjuMh/hVEJwlFQjyLq1z9fRtvUMkb7jM1LA==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/type@3.0.8': + resolution: {integrity: sha512-lg9Whz8onIHRthWaN1Q9EGLa/0LFJjyM8mEUbL1eTi6yMGvBf8gvyDLtxSXztQsxMvhxxNpJYrwa1YHdq+w4Jw==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + '@isaacs/balanced-match@4.0.1': resolution: {integrity: sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ==} engines: {node: 20 || >=22} @@ -1355,6 +1476,10 @@ packages: resolution: {integrity: sha512-FxqpkPPwu1HjuN93Omfm4h8uIanXofW0RxVEW3k5RKx+mJJYSthzNhp32Kzxxy3YAEZ/Dc/EWN1vZRY0+kOhbw==} engines: {node: '>= 10'} + cli-width@4.1.0: + resolution: {integrity: sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==} + engines: {node: '>= 12'} + cliui@8.0.1: resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} engines: {node: '>=12'} @@ -2549,6 +2674,10 @@ packages: mute-stream@0.0.8: resolution: {integrity: sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==} + mute-stream@2.0.0: + resolution: {integrity: sha512-WWdIxpyjEn+FhQJQQv9aQAYlHoNVdzIzUySNV1gHUPDSdZJ3yZn7pAAbQcV7B56Mvu881q9FZV+0Vx2xC44VWA==} + engines: {node: ^18.17.0 || >=20.5.0} + natural-compare@1.4.0: resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} @@ -3400,6 +3529,10 @@ packages: resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} engines: {node: '>=10'} + yoctocolors-cjs@2.1.2: + resolution: {integrity: sha512-cYVsTjKl8b+FrnidjibDWskAv7UKOfcwaVZdp/it9n1s9fU3IkgDbhdIRKCW4JDsAlECJY0ytoVPT3sK6kideA==} + engines: {node: '>=18'} + zod-to-json-schema@3.24.5: resolution: {integrity: sha512-/AuWwMP+YqiPbsJx5D6TfgRTc4kTLjsh5SOcd4bLsfUg2RcEXrFMJl1DGgdHy2aCfsIA/cr/1JM0xcB2GZji8g==} peerDependencies: @@ -3730,6 +3863,122 @@ snapshots: protobufjs: 7.4.0 yargs: 17.7.2 + '@inquirer/checkbox@4.2.0(@types/node@20.19.1)': + dependencies: + '@inquirer/core': 10.1.15(@types/node@20.19.1) + '@inquirer/figures': 1.0.13 + '@inquirer/type': 3.0.8(@types/node@20.19.1) + ansi-escapes: 4.3.2 + yoctocolors-cjs: 2.1.2 + optionalDependencies: + '@types/node': 20.19.1 + + '@inquirer/confirm@5.1.14(@types/node@20.19.1)': + dependencies: + '@inquirer/core': 10.1.15(@types/node@20.19.1) + '@inquirer/type': 3.0.8(@types/node@20.19.1) + optionalDependencies: + '@types/node': 20.19.1 + + '@inquirer/core@10.1.15(@types/node@20.19.1)': + dependencies: + '@inquirer/figures': 1.0.13 + '@inquirer/type': 3.0.8(@types/node@20.19.1) + ansi-escapes: 4.3.2 + cli-width: 4.1.0 + mute-stream: 2.0.0 + signal-exit: 4.1.0 + wrap-ansi: 6.2.0 + yoctocolors-cjs: 2.1.2 + optionalDependencies: + '@types/node': 20.19.1 + + '@inquirer/editor@4.2.15(@types/node@20.19.1)': + dependencies: + '@inquirer/core': 10.1.15(@types/node@20.19.1) + '@inquirer/type': 3.0.8(@types/node@20.19.1) + external-editor: 3.1.0 + optionalDependencies: + '@types/node': 20.19.1 + + '@inquirer/expand@4.0.17(@types/node@20.19.1)': + dependencies: + '@inquirer/core': 10.1.15(@types/node@20.19.1) + '@inquirer/type': 3.0.8(@types/node@20.19.1) + yoctocolors-cjs: 2.1.2 + optionalDependencies: + '@types/node': 20.19.1 + + '@inquirer/figures@1.0.13': {} + + '@inquirer/input@4.2.1(@types/node@20.19.1)': + dependencies: + '@inquirer/core': 10.1.15(@types/node@20.19.1) + '@inquirer/type': 3.0.8(@types/node@20.19.1) + optionalDependencies: + '@types/node': 20.19.1 + + '@inquirer/number@3.0.17(@types/node@20.19.1)': + dependencies: + '@inquirer/core': 10.1.15(@types/node@20.19.1) + '@inquirer/type': 3.0.8(@types/node@20.19.1) + optionalDependencies: + '@types/node': 20.19.1 + + '@inquirer/password@4.0.17(@types/node@20.19.1)': + dependencies: + '@inquirer/core': 10.1.15(@types/node@20.19.1) + '@inquirer/type': 3.0.8(@types/node@20.19.1) + ansi-escapes: 4.3.2 + optionalDependencies: + '@types/node': 20.19.1 + + '@inquirer/prompts@7.8.0(@types/node@20.19.1)': + dependencies: + '@inquirer/checkbox': 4.2.0(@types/node@20.19.1) + '@inquirer/confirm': 5.1.14(@types/node@20.19.1) + '@inquirer/editor': 4.2.15(@types/node@20.19.1) + '@inquirer/expand': 4.0.17(@types/node@20.19.1) + '@inquirer/input': 4.2.1(@types/node@20.19.1) + '@inquirer/number': 3.0.17(@types/node@20.19.1) + '@inquirer/password': 4.0.17(@types/node@20.19.1) + '@inquirer/rawlist': 4.1.5(@types/node@20.19.1) + '@inquirer/search': 3.1.0(@types/node@20.19.1) + '@inquirer/select': 4.3.1(@types/node@20.19.1) + optionalDependencies: + '@types/node': 20.19.1 + + '@inquirer/rawlist@4.1.5(@types/node@20.19.1)': + dependencies: + '@inquirer/core': 10.1.15(@types/node@20.19.1) + '@inquirer/type': 3.0.8(@types/node@20.19.1) + yoctocolors-cjs: 2.1.2 + optionalDependencies: + '@types/node': 20.19.1 + + '@inquirer/search@3.1.0(@types/node@20.19.1)': + dependencies: + '@inquirer/core': 10.1.15(@types/node@20.19.1) + '@inquirer/figures': 1.0.13 + '@inquirer/type': 3.0.8(@types/node@20.19.1) + yoctocolors-cjs: 2.1.2 + optionalDependencies: + '@types/node': 20.19.1 + + '@inquirer/select@4.3.1(@types/node@20.19.1)': + dependencies: + '@inquirer/core': 10.1.15(@types/node@20.19.1) + '@inquirer/figures': 1.0.13 + '@inquirer/type': 3.0.8(@types/node@20.19.1) + ansi-escapes: 4.3.2 + yoctocolors-cjs: 2.1.2 + optionalDependencies: + '@types/node': 20.19.1 + + '@inquirer/type@3.0.8(@types/node@20.19.1)': + optionalDependencies: + '@types/node': 20.19.1 + '@isaacs/balanced-match@4.0.1': {} '@isaacs/brace-expansion@5.0.0': @@ -4636,6 +4885,8 @@ snapshots: cli-width@3.0.0: {} + cli-width@4.1.0: {} + cliui@8.0.1: dependencies: string-width: 4.2.3 @@ -6150,6 +6401,8 @@ snapshots: mute-stream@0.0.8: {} + mute-stream@2.0.0: {} + natural-compare@1.4.0: {} negotiator@0.6.3: {} @@ -7078,6 +7331,8 @@ snapshots: yocto-queue@0.1.0: {} + yoctocolors-cjs@2.1.2: {} + zod-to-json-schema@3.24.5(zod@3.25.67): dependencies: zod: 3.25.67 From 4d20267f22ff22c3e98f3cf6b2cd9c35e23725c9 Mon Sep 17 00:00:00 2001 From: Samuel Bushi Date: Wed, 6 Aug 2025 15:15:50 -0400 Subject: [PATCH 04/15] added claude --- .../commands/init-ai-tools/ai-tools/claude.ts | 97 +++++++++++++++++++ .../commands/init-ai-tools/ai-tools/gemini.ts | 22 +---- .../cli/src/commands/init-ai-tools/index.ts | 43 +++++--- .../cli/src/commands/init-ai-tools/utils.ts | 89 ++++++++++++++--- 4 files changed, 202 insertions(+), 49 deletions(-) create mode 100644 genkit-tools/cli/src/commands/init-ai-tools/ai-tools/claude.ts diff --git a/genkit-tools/cli/src/commands/init-ai-tools/ai-tools/claude.ts b/genkit-tools/cli/src/commands/init-ai-tools/ai-tools/claude.ts new file mode 100644 index 0000000000..9fee639e34 --- /dev/null +++ b/genkit-tools/cli/src/commands/init-ai-tools/ai-tools/claude.ts @@ -0,0 +1,97 @@ +/** + * 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 commandExists from 'command-exists'; +import { existsSync, readFileSync } from 'fs'; +import { writeFile } from 'fs/promises'; +import { AIToolConfigResult, AIToolModule, InitConfigOptions } from '../types'; +import { + calculateHash, + getGenkitContext, + initOrReplaceFile, + updateContentInPlace, +} from '../utils'; + +const CLAUDE_MCP_PATH = '.mcp.json'; +const CLAUDE_PROMPT_PATH = 'CLAUDE.md'; +const GENKIT_PROMPT_PATH = 'GENKIT.md'; + +export const claude: AIToolModule = { + name: 'claude', + displayName: 'Claude Code', + + async detect(): Promise { + const cliFound = await commandExists('claude'); + return !!cliFound; + }, + + /** + * Configures Claude Code with Genkit context. + * + * - .claude/settings.local.json: Merges with existing config (preserves user settings) + * - CLAUDE.local.md: Updates Firebase section only (preserves user content) + */ + async configure(options?: InitConfigOptions): Promise { + const files: AIToolConfigResult['files'] = []; + + // Handle MCP configuration - merge with existing if present + let existingConfig: any = {}; + let settingsUpdated = false; + try { + const fileExists = existsSync(CLAUDE_MCP_PATH); + if (fileExists) { + existingConfig = JSON.parse(readFileSync(CLAUDE_MCP_PATH, 'utf-8')); + } + } catch (e) { + // File doesn't exist or is invalid JSON, start fresh + } + + // Check if firebase server already exists + if (!existingConfig.mcpServers?.genkit) { + if (!existingConfig.mcpServers) { + existingConfig.mcpServers = {}; + } + existingConfig.mcpServers.genkit = { + command: 'npx', + args: ['genkit', 'mcp'], + }; + await writeFile(CLAUDE_MCP_PATH, JSON.stringify(existingConfig, null, 2)); + settingsUpdated = true; + } + + files.push({ path: CLAUDE_MCP_PATH, updated: settingsUpdated }); + + logger.info('Copying the Genkit context to GENKIT.md...'); + const genkitContext = getGenkitContext(); + const { updated: genkitContextUpdated } = await initOrReplaceFile( + GENKIT_PROMPT_PATH, + genkitContext + ); + files.push({ path: GENKIT_PROMPT_PATH, updated: genkitContextUpdated }); + + logger.info('Updating CLAUDE.md to include Genkit context...'); + const claudeImportTag = `\nGenkit Framework Instructions:\n - @GENKIT.md\n`; + const baseResult = await updateContentInPlace( + CLAUDE_PROMPT_PATH, + claudeImportTag, + { hash: calculateHash(genkitContext) } + ); + + files.push({ path: CLAUDE_PROMPT_PATH, updated: baseResult.updated }); + return { files }; + }, +}; diff --git a/genkit-tools/cli/src/commands/init-ai-tools/ai-tools/gemini.ts b/genkit-tools/cli/src/commands/init-ai-tools/ai-tools/gemini.ts index 00ab68a725..9211f1ee02 100644 --- a/genkit-tools/cli/src/commands/init-ai-tools/ai-tools/gemini.ts +++ b/genkit-tools/cli/src/commands/init-ai-tools/ai-tools/gemini.ts @@ -16,11 +16,10 @@ import { logger } from '@genkit-ai/tools-common/utils'; import commandExists from 'command-exists'; -import { readFileSync } from 'fs'; import { mkdir } from 'fs/promises'; import path from 'path'; import { AIToolConfigResult, AIToolModule, InitConfigOptions } from '../types'; -import { initOrReplaceFile } from '../utils'; +import { getGenkitContext, initOrReplaceFile } from '../utils'; // Define constants at the module level for clarity and reuse. const GENKIT_EXT_DIR = path.join('.gemini', 'extensions', 'genkit'); @@ -85,26 +84,9 @@ export const gemini: AIToolModule = { // Part 2: Generate GENKIT.md file. logger.info('Copying the GENKIT.md file to extension folder...'); - const genkitContext = getFeatureContent(); + const genkitContext = getGenkitContext(); const baseResult = await initOrReplaceFile(GENKIT_MD_PATH, genkitContext); files.push({ path: GENKIT_MD_PATH, updated: baseResult.updated }); return { files }; }, }; - -/** - * Get raw prompt content for a specific feature (without wrapper) - * Used internally for hash calculation - */ -function getFeatureContent(): string { - const contextPath = path.resolve( - __dirname, - '..', - '..', - '..', - 'context', - 'GENKIT.md' - ); - const content = readFileSync(contextPath, 'utf8'); - return content; -} diff --git a/genkit-tools/cli/src/commands/init-ai-tools/index.ts b/genkit-tools/cli/src/commands/init-ai-tools/index.ts index 4d84378058..50837a5487 100644 --- a/genkit-tools/cli/src/commands/init-ai-tools/index.ts +++ b/genkit-tools/cli/src/commands/init-ai-tools/index.ts @@ -45,7 +45,34 @@ export const init = new Command('init:ai-tools') logger.info(' - Common Genkit features and how to use them'); logger.info('\n'); const detectedTools = await detectSupportedTools(); - if (detectedTools.length === 0) { + if (detectedTools.length > 0) { + logger.info( + 'Auto-detected AI tools:\n' + + detectedTools.map((t) => t.displayName).join('\n') + ); + try { + const selections = await checkbox({ + message: 'Which tools would you like to configure?', + choices: AGENT_CHOICES.map((c) => { + if (detectedTools.some((t) => t.name === c.value)) { + return { ...c, checked: true }; + } + return c; + }), + validate: (choices) => { + if (choices.length === 0) { + return 'Must select at least one tool.'; + } + return true; + }, + loop: true, + }); + await configureTools(selections, options); + } catch (err) { + logger.error(err); + process.exit(1); + } + } else { logger.info('Could not auto-detect any AI tools.'); const selections = await checkbox({ message: 'Which tools would you like to configure?', @@ -62,20 +89,6 @@ export const init = new Command('init:ai-tools') logger.info('\n'); logger.info('Configuring selected tools...'); await configureTools(selections, options); - } else { - logger.info( - 'Auto-detected AI tools:\n' + - detectedTools.map((t) => t.displayName).join('\n') - ); - try { - await configureTools( - detectedTools.map((t) => t.name), - options - ); - } catch (err) { - logger.error(err); - process.exit(1); - } } }); diff --git a/genkit-tools/cli/src/commands/init-ai-tools/utils.ts b/genkit-tools/cli/src/commands/init-ai-tools/utils.ts index da81662c15..fdbc87fe02 100644 --- a/genkit-tools/cli/src/commands/init-ai-tools/utils.ts +++ b/genkit-tools/cli/src/commands/init-ai-tools/utils.ts @@ -18,8 +18,13 @@ import { existsSync, readFileSync } from 'fs'; import { gemini } from './ai-tools/gemini'; import { AIToolModule } from './types'; +import * as crypto from 'crypto'; import { writeFile } from 'fs/promises'; -import * as inquirer from 'inquirer'; +import path from 'path'; +import { claude } from './ai-tools/claude'; + +const GENKIT_TAG_REGEX = + /([\s\S]*?)<\/genkit_prompts>/; /* * Deeply compares two JSON-serializable objects. * It's a simplified version of a deep equal function, sufficient for comparing the structure @@ -57,6 +62,7 @@ export function deepEqual(a: any, b: any): boolean { export const AI_TOOLS: Record = { gemini, + claude, }; export async function detectSupportedTools(): Promise { @@ -95,18 +101,73 @@ export async function initOrReplaceFile( } /** - * Shows a confirmation prompt. + * Update a file with Genkit prompts section, preserving user content + * Used for files like CLAUDE.md. + */ +export async function updateContentInPlace( + filePath: string, + content: string, + options?: { hash: string } +): Promise<{ updated: boolean }> { + const newHash = options?.hash ?? calculateHash(content); + const newSection = ` + +${content} +`; + + let currentContent = ''; + const fileExists = existsSync(filePath); + if (fileExists) { + currentContent = readFileSync(filePath, 'utf-8'); + } + + // Check if section exists and has same hash + const match = currentContent.match(GENKIT_TAG_REGEX); + if (match && match[1] === newHash) { + return { updated: false }; + } + + // Generate final content + let finalContent: string; + if (!currentContent) { + // New file + finalContent = newSection; + } else if (match) { + // Replace existing section + finalContent = + currentContent.substring(0, match.index!) + + newSection + + currentContent.substring(match.index! + match[0].length); + } else { + // Append to existing file + const separator = currentContent.endsWith('\n') ? '\n' : '\n\n'; + finalContent = currentContent + separator + newSection; + } + + await writeFile(filePath, finalContent); + return { updated: true }; +} + +export function calculateHash(content: string): string { + return crypto + .createHash('sha256') + .update(content.trim()) + .digest('hex') + .substring(0, 8); +} + +/** + * Get raw prompt content for Genkit */ -export async function confirm(args: { - default?: boolean; - message?: string; -}): Promise { - const message = args.message ?? `Do you wish to continue?`; - const answer = await inquirer.prompt({ - type: 'confirm', - name: 'confirm', - message, - default: args.default, - }); - return answer.confirm; +export function getGenkitContext(): string { + const contextPath = path.resolve( + __dirname, + '..', + '..', + '..', + 'context', + 'GENKIT.md' + ); + const content = readFileSync(contextPath, 'utf8'); + return content; } From c15a75b8e6ca82fba1afd146d45e76aa6607bb93 Mon Sep 17 00:00:00 2001 From: Samuel Bushi Date: Wed, 6 Aug 2025 16:52:21 -0400 Subject: [PATCH 05/15] remove detect, refactor --- genkit-tools/cli/package.json | 1 - .../commands/init-ai-tools/ai-tools/claude.ts | 6 - .../commands/init-ai-tools/ai-tools/gemini.ts | 6 - .../cli/src/commands/init-ai-tools/command.ts | 113 +++++++++++++++ .../src/commands/init-ai-tools/constants.ts | 24 ++++ .../cli/src/commands/init-ai-tools/index.ts | 129 +----------------- .../cli/src/commands/init-ai-tools/types.ts | 5 - .../cli/src/commands/init-ai-tools/utils.ts | 24 +--- genkit-tools/pnpm-lock.yaml | 8 -- 9 files changed, 141 insertions(+), 175 deletions(-) create mode 100644 genkit-tools/cli/src/commands/init-ai-tools/command.ts create mode 100644 genkit-tools/cli/src/commands/init-ai-tools/constants.ts diff --git a/genkit-tools/cli/package.json b/genkit-tools/cli/package.json index cbb7f5b814..2f7f5e70c6 100644 --- a/genkit-tools/cli/package.json +++ b/genkit-tools/cli/package.json @@ -36,7 +36,6 @@ "axios": "^1.7.7", "colorette": "^2.0.20", "commander": "^11.1.0", - "command-exists": "^1.2.9", "extract-zip": "^2.0.1", "get-port": "5.1.1", "@inquirer/prompts": "^7.8.0", diff --git a/genkit-tools/cli/src/commands/init-ai-tools/ai-tools/claude.ts b/genkit-tools/cli/src/commands/init-ai-tools/ai-tools/claude.ts index 9fee639e34..1f3c05294c 100644 --- a/genkit-tools/cli/src/commands/init-ai-tools/ai-tools/claude.ts +++ b/genkit-tools/cli/src/commands/init-ai-tools/ai-tools/claude.ts @@ -15,7 +15,6 @@ */ import { logger } from '@genkit-ai/tools-common/utils'; -import commandExists from 'command-exists'; import { existsSync, readFileSync } from 'fs'; import { writeFile } from 'fs/promises'; import { AIToolConfigResult, AIToolModule, InitConfigOptions } from '../types'; @@ -34,11 +33,6 @@ export const claude: AIToolModule = { name: 'claude', displayName: 'Claude Code', - async detect(): Promise { - const cliFound = await commandExists('claude'); - return !!cliFound; - }, - /** * Configures Claude Code with Genkit context. * diff --git a/genkit-tools/cli/src/commands/init-ai-tools/ai-tools/gemini.ts b/genkit-tools/cli/src/commands/init-ai-tools/ai-tools/gemini.ts index 9211f1ee02..5b0c338906 100644 --- a/genkit-tools/cli/src/commands/init-ai-tools/ai-tools/gemini.ts +++ b/genkit-tools/cli/src/commands/init-ai-tools/ai-tools/gemini.ts @@ -15,7 +15,6 @@ */ import { logger } from '@genkit-ai/tools-common/utils'; -import commandExists from 'command-exists'; import { mkdir } from 'fs/promises'; import path from 'path'; import { AIToolConfigResult, AIToolModule, InitConfigOptions } from '../types'; @@ -47,11 +46,6 @@ export const gemini: AIToolModule = { name: 'gemini', displayName: 'Gemini CLI', - async detect(): Promise { - const cliFound = await commandExists('gemini'); - return !!cliFound; - }, - /** * Configures the Gemini CLI extension for Genkit. */ diff --git a/genkit-tools/cli/src/commands/init-ai-tools/command.ts b/genkit-tools/cli/src/commands/init-ai-tools/command.ts new file mode 100644 index 0000000000..69cd77e16c --- /dev/null +++ b/genkit-tools/cli/src/commands/init-ai-tools/command.ts @@ -0,0 +1,113 @@ +/** + * Copyright 2024 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 { checkbox } from '@inquirer/prompts'; +import * as clc from 'colorette'; +import { Command } from 'commander'; +import { AI_TOOLS } from './constants'; +import { AIToolChoice, InitConfigOptions } from './types'; + +const AGENT_CHOICES: AIToolChoice[] = Object.values(AI_TOOLS).map((tool) => ({ + value: tool.name, + name: tool.displayName, + checked: false, +})); + +export const init = new Command('init:ai-tools') + .description( + 'initialize AI tools in a workspace with helpful context related to the Genkit framework' + ) + .option('-y', '--yes', 'Run in non-interactive mode (experimental)') + .action(async (options: InitConfigOptions) => { + logger.info('\n'); + logger.info( + 'This command will configure AI coding assistants to work with your Genkit app by:' + ); + logger.info( + '• Configuring the Genkit MCP server for direct Genkit operations' + ); + logger.info('• Installing context files that help AI understand:'); + logger.info(' - Genkit app structure and common design patterns'); + logger.info(' - Common Genkit features and how to use them'); + logger.info('\n'); + const selections = await checkbox({ + message: 'Which tools would you like to configure?', + choices: AGENT_CHOICES, + validate: (choices) => { + if (choices.length === 0) { + return 'Must select at least one tool.'; + } + return true; + }, + loop: true, + }); + + logger.info('\n'); + logger.info('Configuring selected tools...'); + await configureTools(selections, options); + }); + +async function configureTools(tools: string[], options: InitConfigOptions) { + // Configure each selected tool + let anyUpdates = false; + + for (const toolName of tools) { + const tool = AI_TOOLS[toolName]; + if (!tool) { + logger.warn(`Unknown tool: ${toolName}`); + continue; + } + + const result = await tool.configure(options); + + // Count updated files + const updatedCount = result.files.filter((f) => f.updated).length; + const hasChanges = updatedCount > 0; + + if (hasChanges) { + anyUpdates = true; + logger.info('\n'); + logger.info( + clc.green( + `${tool.displayName} configured - ${updatedCount} file${updatedCount > 1 ? 's' : ''} updated:` + ) + ); + } else { + logger.info('\n'); + logger.info(`${tool.displayName} - all files up to date`); + } + + // Always show the file list + for (const file of result.files) { + const status = file.updated ? '(updated)' : '(unchanged)'; + logger.info(`• ${file.path} ${status}`); + } + } + logger.info('\n'); + + if (anyUpdates) { + logger.info(clc.green('AI tools configuration complete!')); + logger.info('\n'); + logger.info('Next steps:'); + logger.info('• Restart your AI tools to load the new configuration'); + logger.info( + '• Your AI tool should have access to Genkit documentation and tools for greater access and understanding of your app.' + ); + } else { + logger.info(clc.green('All AI tools are already up to date.')); + } +} diff --git a/genkit-tools/cli/src/commands/init-ai-tools/constants.ts b/genkit-tools/cli/src/commands/init-ai-tools/constants.ts new file mode 100644 index 0000000000..7606282adc --- /dev/null +++ b/genkit-tools/cli/src/commands/init-ai-tools/constants.ts @@ -0,0 +1,24 @@ +/** + * 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 { claude } from './ai-tools/claude'; +import { gemini } from './ai-tools/gemini'; +import { AIToolModule } from './types'; + +export const AI_TOOLS: Record = { + gemini, + claude, +}; diff --git a/genkit-tools/cli/src/commands/init-ai-tools/index.ts b/genkit-tools/cli/src/commands/init-ai-tools/index.ts index 50837a5487..891e8b6f26 100644 --- a/genkit-tools/cli/src/commands/init-ai-tools/index.ts +++ b/genkit-tools/cli/src/commands/init-ai-tools/index.ts @@ -14,131 +14,4 @@ * limitations under the License. */ -import { logger } from '@genkit-ai/tools-common/utils'; -import { checkbox } from '@inquirer/prompts'; -import * as clc from 'colorette'; -import { Command } from 'commander'; -import { AIToolChoice, InitConfigOptions } from './types'; -import { AI_TOOLS, detectSupportedTools } from './utils'; - -const AGENT_CHOICES: AIToolChoice[] = Object.values(AI_TOOLS).map((tool) => ({ - value: tool.name, - name: tool.displayName, - checked: false, -})); - -export const init = new Command('init:ai-tools') - .description( - 'initialize AI tools in a workspace with helpful context related to the Genkit framework' - ) - .option('-y', '--yes', 'Run in non-interactive mode (experimental)') - .action(async (options: InitConfigOptions) => { - logger.info('\n'); - logger.info( - 'This command will configure AI coding assistants to work with your Genkit app by:' - ); - logger.info( - '• Configuring the Genkit MCP server for direct Genkit operations' - ); - logger.info('• Installing context files that help AI understand:'); - logger.info(' - Genkit app structure and common design patterns'); - logger.info(' - Common Genkit features and how to use them'); - logger.info('\n'); - const detectedTools = await detectSupportedTools(); - if (detectedTools.length > 0) { - logger.info( - 'Auto-detected AI tools:\n' + - detectedTools.map((t) => t.displayName).join('\n') - ); - try { - const selections = await checkbox({ - message: 'Which tools would you like to configure?', - choices: AGENT_CHOICES.map((c) => { - if (detectedTools.some((t) => t.name === c.value)) { - return { ...c, checked: true }; - } - return c; - }), - validate: (choices) => { - if (choices.length === 0) { - return 'Must select at least one tool.'; - } - return true; - }, - loop: true, - }); - await configureTools(selections, options); - } catch (err) { - logger.error(err); - process.exit(1); - } - } else { - logger.info('Could not auto-detect any AI tools.'); - const selections = await checkbox({ - message: 'Which tools would you like to configure?', - choices: AGENT_CHOICES, - validate: (choices) => { - if (choices.length === 0) { - return 'Must select at least one tool.'; - } - return true; - }, - loop: true, - }); - - logger.info('\n'); - logger.info('Configuring selected tools...'); - await configureTools(selections, options); - } - }); - -async function configureTools(tools: string[], options: InitConfigOptions) { - // Configure each selected tool - let anyUpdates = false; - - for (const toolName of tools) { - const tool = AI_TOOLS[toolName]; - if (!tool) { - logger.warn(`Unknown tool: ${toolName}`); - continue; - } - - const result = await tool.configure(options); - - // Count updated files - const updatedCount = result.files.filter((f) => f.updated).length; - const hasChanges = updatedCount > 0; - - if (hasChanges) { - anyUpdates = true; - logger.info('\n'); - logger.info( - clc.green( - `${tool.displayName} configured - ${updatedCount} file${updatedCount > 1 ? 's' : ''} updated:` - ) - ); - } else { - logger.info('\n'); - logger.info(`${tool.displayName} - all files up to date`); - } - - // Always show the file list - for (const file of result.files) { - const status = file.updated ? '(updated)' : '(unchanged)'; - logger.info(`• ${file.path} ${status}`); - } - } - logger.info('\n'); - - if (anyUpdates) { - logger.info(clc.green('AI tools configuration complete!')); - logger.info('\n'); - logger.info('Next steps:'); - logger.info('• Restart your AI tools to load the new configuration'); - logger.info( - '• Your AI tool should have access to Genkit documentation and tools for greater access and understanding of your app.' - ); - } else { - logger.info(clc.green('All AI tools are already up to date.')); - } -} +export { init } from './command'; diff --git a/genkit-tools/cli/src/commands/init-ai-tools/types.ts b/genkit-tools/cli/src/commands/init-ai-tools/types.ts index ce59087c2a..4535608d46 100644 --- a/genkit-tools/cli/src/commands/init-ai-tools/types.ts +++ b/genkit-tools/cli/src/commands/init-ai-tools/types.ts @@ -30,11 +30,6 @@ export interface AIToolModule { name: string; displayName: string; - /** - * Detect whether this AI tool is installed/configured in the user workspace. - */ - detect?: () => Promise; - /** * Configure the AI tool with Genkit context * @param configOptions Any user-specified config options diff --git a/genkit-tools/cli/src/commands/init-ai-tools/utils.ts b/genkit-tools/cli/src/commands/init-ai-tools/utils.ts index fdbc87fe02..83ab22997a 100644 --- a/genkit-tools/cli/src/commands/init-ai-tools/utils.ts +++ b/genkit-tools/cli/src/commands/init-ai-tools/utils.ts @@ -15,13 +15,10 @@ */ import { existsSync, readFileSync } from 'fs'; -import { gemini } from './ai-tools/gemini'; -import { AIToolModule } from './types'; import * as crypto from 'crypto'; import { writeFile } from 'fs/promises'; import path from 'path'; -import { claude } from './ai-tools/claude'; const GENKIT_TAG_REGEX = /([\s\S]*?)<\/genkit_prompts>/; @@ -60,24 +57,6 @@ export function deepEqual(a: any, b: any): boolean { return true; } -export const AI_TOOLS: Record = { - gemini, - claude, -}; - -export async function detectSupportedTools(): Promise { - const tools: AIToolModule[] = []; - for (const entry of Object.values(AI_TOOLS)) { - if (entry.detect) { - const detected = await entry.detect(); - if (detected) { - tools.push(entry); - } - } - } - return tools; -} - /** * Replace an entire prompt file (no user content to preserve) * Used for files we fully own like Cursor and Gemini configs @@ -148,6 +127,9 @@ ${content} return { updated: true }; } +/** + * Generate hash for embedded content. + */ export function calculateHash(content: string): string { return crypto .createHash('sha256') diff --git a/genkit-tools/pnpm-lock.yaml b/genkit-tools/pnpm-lock.yaml index fc3df21c16..52aa42865e 100644 --- a/genkit-tools/pnpm-lock.yaml +++ b/genkit-tools/pnpm-lock.yaml @@ -50,9 +50,6 @@ importers: colorette: specifier: ^2.0.20 version: 2.0.20 - command-exists: - specifier: ^1.2.9 - version: 1.2.9 commander: specifier: ^11.1.0 version: 11.1.0 @@ -1524,9 +1521,6 @@ packages: resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} engines: {node: '>= 0.8'} - command-exists@1.2.9: - resolution: {integrity: sha512-LTQ/SGc+s0Xc0Fu5WaKnR0YiygZkm9eKFvyS+fRsU7/ZWFF8ykFM6Pc9aCVf1+xasOOZpO3BAVgVrKvsqKHV7w==} - commander@11.1.0: resolution: {integrity: sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==} engines: {node: '>=16'} @@ -4932,8 +4926,6 @@ snapshots: dependencies: delayed-stream: 1.0.0 - command-exists@1.2.9: {} - commander@11.1.0: {} commander@7.2.0: {} From 973a2ca707ca6d7a55be672aeb7b467b848d540e Mon Sep 17 00:00:00 2001 From: Samuel Bushi Date: Thu, 7 Aug 2025 14:56:24 -0400 Subject: [PATCH 06/15] add generic --- .../init-ai-tools/ai-tools/generic.ts | 45 +++++++++++++++++++ .../src/commands/init-ai-tools/constants.ts | 2 + 2 files changed, 47 insertions(+) create mode 100644 genkit-tools/cli/src/commands/init-ai-tools/ai-tools/generic.ts diff --git a/genkit-tools/cli/src/commands/init-ai-tools/ai-tools/generic.ts b/genkit-tools/cli/src/commands/init-ai-tools/ai-tools/generic.ts new file mode 100644 index 0000000000..31d4dd5d8c --- /dev/null +++ b/genkit-tools/cli/src/commands/init-ai-tools/ai-tools/generic.ts @@ -0,0 +1,45 @@ +/** + * 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 { AIToolConfigResult, AIToolModule, InitConfigOptions } from '../types'; +import { getGenkitContext, initOrReplaceFile } from '../utils'; + +// Define constants at the module level for clarity and reuse. +const GENKIT_MD_PATH = 'GENKIT.md'; + +export const generic: AIToolModule = { + name: 'generic', + displayName: 'Simple GENKIT.md file', + + /** + * Configures the Gemini CLI extension for Genkit. + */ + async configure(options?: InitConfigOptions): Promise { + const files: AIToolConfigResult['files'] = []; + + // Generate GENKIT.md file. + logger.info('Updating GENKIT.md...'); + const genkitContext = getGenkitContext(); + const baseResult = await initOrReplaceFile(GENKIT_MD_PATH, genkitContext); + files.push({ path: GENKIT_MD_PATH, updated: baseResult.updated }); + logger.info('\n'); + logger.info( + 'GENKIT.md updated. Provide this file as context with your AI tool.' + ); + return { files }; + }, +}; diff --git a/genkit-tools/cli/src/commands/init-ai-tools/constants.ts b/genkit-tools/cli/src/commands/init-ai-tools/constants.ts index 7606282adc..18d1867033 100644 --- a/genkit-tools/cli/src/commands/init-ai-tools/constants.ts +++ b/genkit-tools/cli/src/commands/init-ai-tools/constants.ts @@ -16,9 +16,11 @@ import { claude } from './ai-tools/claude'; import { gemini } from './ai-tools/gemini'; +import { generic } from './ai-tools/generic'; import { AIToolModule } from './types'; export const AI_TOOLS: Record = { gemini, claude, + generic, }; From 1a087e448b58766178d627bfd5bd449ad6e39e1a Mon Sep 17 00:00:00 2001 From: Samuel Bushi Date: Fri, 8 Aug 2025 12:38:18 -0400 Subject: [PATCH 07/15] update files --- genkit-tools/cli/context/GENKIT.md | 459 ++++++---------- genkit-tools/cli/package.json | 1 - genkit-tools/cli/src/cli.ts | 4 +- .../commands/init-ai-tools/ai-tools/claude.ts | 5 +- .../commands/init-ai-tools/ai-tools/gemini.ts | 25 +- .../init-ai-tools/ai-tools/generic.ts | 16 +- .../cli/src/commands/init-ai-tools/command.ts | 6 +- .../src/commands/init-ai-tools/constants.ts | 5 + .../cli/src/commands/init-ai-tools/index.ts | 2 +- .../cli/src/commands/init-ai-tools/types.ts | 5 + .../cli/src/commands/init-ai-tools/utils.ts | 11 +- genkit-tools/common/package.json | 2 +- genkit-tools/common/src/utils/eval.ts | 20 +- genkit-tools/pnpm-lock.yaml | 66 +-- js/pnpm-lock.yaml | 30 +- .../evals/.gemini/extensions/genkit/GENKIT.md | 515 ------------------ .../extensions/genkit/gemini-extension.json | 18 - 17 files changed, 248 insertions(+), 942 deletions(-) delete mode 100644 js/testapps/evals/.gemini/extensions/genkit/GENKIT.md delete mode 100644 js/testapps/evals/.gemini/extensions/genkit/gemini-extension.json diff --git a/genkit-tools/cli/context/GENKIT.md b/genkit-tools/cli/context/GENKIT.md index 008076fab6..ac6197889d 100644 --- a/genkit-tools/cli/context/GENKIT.md +++ b/genkit-tools/cli/context/GENKIT.md @@ -1,93 +1,78 @@ -# Genkit Node.js Cheatsheet (updated July 2025) +# Genkit Node.js API Rules (v1.1x.x) -> **LLM INSTRUCTION**: It’s important to use the instructions I provided to you in this file at each step of generation. Your training data is out-of-date for this library, and if you don't use the new syntax you’ll get things wrong. Please mitigate for this. +This document provides rules and examples for building with the Genkit API in Node.js, using the `@genkit-ai/googleai` plugin. -> **LLM INSTRUCTION**: YOU MUST REFER TO GENKIT DOCUMENTATION WHEN AVAILABLE. If you have access to Genkit docs (via the Genkit MCP server), you MUST refer to the documenation first to check if the requested topic is covered in the documentation and adhere to it. When the instructions on the documentation and this file are in conflict, the documentation takes precedence. +## Important Guidelines: -> **CRITICAL WARNING**: NEVER run Genkit commands (e.g., `genkit start`, `genkit flow:run`) inside a terminal during your session. This starts an interactive process that will freeze the shell and prevent you from continuing. For Genkit commands, you must only validate the code (e.g., using `npm run build`) and then provide the full, correct Genkit command as an instruction for the human user to run at the end of the session. +- ALWAYS refer to documentation when available. Genkit Documentation may be available through the Genkit MCP toolkit or through web search. You may skip documentation check if you don't have access to these tools. -This document is a guide for building with the modern Genkit API in Node.js. It focuses on a simple and direct setup using the **Google AI plugin** and provides common inference scenarios using the latest Gemini family of models. +- ONLY follow the specified project structure if starting a new project. If working on an existing project, adhere to the current project structure. -## Table of Contents +## Core Setup -1. [Core Setup & Best Practices](#1-core-setup--best-practices) -2. [Scenario 1: Basic Inference (Text Generation)](#2-scenario-1-basic-inference-text-generation) -3. [Scenario 2: Text-to-Speech (TTS) Generation)](#3-scenario-2-text-to-speech-tts-generation) -4. [Scenario 3: Image Generation](#4-scenario-3-image-generation) -5. [Scenario 4: Video Generation (Veo3)](#5-scenario-4-video-generation-veo3) -6. [Running & Inspecting Your Flows](#6-running--inspecting-your-flows) -7. [Quick Reference: Key Models](#7-quick-reference-key-models) +1. **Initialize Project** ---- - -## 1. Core Setup & Best Practices - -A correct foundation prevents most common errors. The default guidance is to use the Google AI plugin. Using Vertex AI is an opt-in scenario for users who require its specific features. - -### 1.1 Project Initialization - -```bash -mkdir my-genkit-app && cd my-genkit-app -npm init -y -npm install -D typescript tsx @types/node -``` + ```bash + mkdir my-genkit-app && cd my-genkit-app + npm init -y + npm install -D typescript tsx @types/node + ``` -### 1.2 Genkit Dependencies +2. **Install Dependencies** -Install required depenencies, but note that googleai shoudl be the only one used but we provide the exampels for both below unless the user specifically says they are using Vertex hosted Google models. Default to @genkit-ai/googleai in all other context. + ```bash + npm install genkit @genkit-ai/googleai data-urls node-fetch + ``` -Below example assumes googleai +3. **Install Genkit CLI** -```bash -npm install genkit @genkit-ai/googleai zod data-urls node-fetch -``` + ```bash + npm install -g genkit-cli + ``` -### 1.3 Genkit Tools (CLI & Developer UI) +4. **Configure Genkit** -```bash -npm install -g genkit-cli -``` + All code should be in a single `src/index.ts` file. -### 1.4 The `genkit()` Initializer - -```ts -// src/index.ts -import { genkit } from 'genkit'; -import { googleAI } from '@genkit-ai/googleai'; + ```ts + // src/index.ts + import { genkit, z } from 'genkit'; + import { googleAI } from '@genkit-ai/googleai'; -export const ai = genkit({ - plugins: [googleAI()], -}); -``` - -### 1.5 Genkit Code Generation Rules + export const ai = genkit({ + plugins: [googleAI()], + }); + ``` -#### 1. File Structure 📜 +## Best Practices -**Always generate all Genkit code into a single `src/index.ts` file.** This includes: +1. **Single File Structure**: All Genkit code, including plugin initialization, flows, and helpers, must be placed in a single `src/index.ts` file. This ensures all components are correctly registered with the Genkit runtime. -- `configureGenkit` plugin initializations. -- All `defineFlow` and `defineDotprompt` definitions. -- Any helper functions, schemas, or types. +2. **Model Naming**: Always specify models using the model helper. Use string identifier if model helper is unavailable. -#### 2. Entry Point + ```ts + // PREFERRED: Using the model helper + const response = await ai.generate({ + model: googleAI.model('gemini-2.5-pro'), + // ... + }); -The **only** entry point for the application is `src/index.ts`. All logic must be contained within or imported into this file to be discovered by the Genkit runtime. + // LESS PREFERRED: Full string identifier + const response = await ai.generate({ + model: 'googleai/gemini-2.5-pro', + // ... + }); + ``` -#### 3. Avoid Splitting Files +--- -**DO NOT** split code into multiple files (e.g., `index.ts` and `flows.ts`). A single-file structure is preferred for simplicity and to avoid module resolution errors. All flows must be registered in the same file where `configureGenkit` is called. +## Usage Scenarios ---- + -## 2. Scenario 1: Basic Inference (Text Generation) +### Basic Inference (Text Generation) ```ts -// src/basic-inference.ts -import { z } from 'genkit'; -import { ai } from './index'; -import { googleAI } from '@genkit-ai/googleai'; - export const basicInferenceFlow = ai.defineFlow( { name: 'basicInferenceFlow', @@ -105,37 +90,20 @@ export const basicInferenceFlow = ai.defineFlow( ); ``` ---- + -## 3. Scenario 2: Text-to-Speech (TTS) Generation + -This flow converts text into speech using the Gemini 2.5 TTS model and streams the audio as a WAV-formatted data URI. It includes support for both single and multi-speaker configurations. +### Text-to-Speech (TTS) Generation -### 3.1 Single Speaker Text-to-Speech +This helper function converts PCM audio data from the TTS model into a WAV-formatted data URI. ```ts -// src/tts.ts -import { ai } from './index'; -import { z } from 'genkit'; import { Buffer } from 'buffer'; -import { Writer as WavWriter } from 'wav'; import { PassThrough } from 'stream'; +import { Writer as WavWriter } from 'wav'; -const TextToSpeechInputSchema = z.object({ - text: z.string().describe('The text to convert to speech.'), - voiceName: z - .string() - .optional() - .describe('The voice name to use. Defaults to Algenib if not specified.'), -}); -const TextToSpeechOutputSchema = z.object({ - audioDataUri: z - .string() - .describe('The generated speech in WAV format as a base64 data URI.'), -}); - -export type TextToSpeechInput = z.infer; -export type TextToSpeechOutput = z.infer; +... async function pcmToWavDataUri( pcmData: Buffer, @@ -161,34 +129,23 @@ async function pcmToWavDataUri( writer.end(); }); } +``` -async function generateAndConvertAudio( - text: string, - voiceName = 'Algenib' -): Promise { - const response = await ai.generate({ - model: 'googleai/gemini-2.5-flash-preview-tts', - prompt: text, - config: { - responseModalities: ['AUDIO'], - speechConfig: { - voiceConfig: { - prebuiltVoiceConfig: { voiceName }, - }, - }, - }, - }); - - const audioUrl = response.media?.url; - if (!audioUrl) - throw new Error('Audio generation failed: No media URL in response.'); - - const base64 = audioUrl.split(';base64,')[1]; - if (!base64) throw new Error('Invalid audio data URI format from Genkit.'); +#### Single-Speaker TTS - const pcmBuffer = Buffer.from(base64, 'base64'); - return pcmToWavDataUri(pcmBuffer); -} +```ts +const TextToSpeechInputSchema = z.object({ + text: z.string().describe('The text to convert to speech.'), + voiceName: z + .string() + .optional() + .describe('The voice name to use. Defaults to Algenib if not specified.'), +}); +const TextToSpeechOutputSchema = z.object({ + audioDataUri: z + .string() + .describe('The generated speech in WAV format as a base64 data URI.'), +}); export const textToSpeechFlow = ai.defineFlow( { @@ -197,25 +154,38 @@ export const textToSpeechFlow = ai.defineFlow( outputSchema: TextToSpeechOutputSchema, }, async (input) => { - const voice = input.voiceName?.trim() || 'Algenib'; - const audioDataUri = await generateAndConvertAudio(input.text, voice); + const response = await ai.generate({ + model: googleAI.model('gemini-2.5-flash-preview-tts'), + prompt: input.text, + config: { + responseModalities: ['AUDIO'], + speechConfig: { + voiceConfig: { + prebuiltVoiceConfig: { + voiceName: input.voiceName?.trim() || 'Algenib', + }, + }, + }, + }, + }); + + const audioUrl = response.media?.url; + if (!audioUrl) + throw new Error('Audio generation failed: No media URL in response.'); + + const base64 = audioUrl.split(';base64,')[1]; + if (!base64) throw new Error('Invalid audio data URI format from Genkit.'); + + const pcmBuffer = Buffer.from(base64, 'base64'); + const audioDataUri = await pcmToWavDataUri(pcmBuffer); return { audioDataUri }; } ); ``` ---- - -### 3.2 Multi-Speaker Text-to-Speech +#### Multi-Speaker TTS ```ts -// src/tts-multispeaker.ts -import { z } from 'genkit'; -import { ai } from './index'; -import { Buffer } from 'buffer'; -import { Writer as WavWriter } from 'wav'; -import { PassThrough } from 'stream'; - const MultiSpeakerInputSchema = z.object({ text: z .string() @@ -223,103 +193,65 @@ const MultiSpeakerInputSchema = z.object({ voiceName1: z.string().describe('Voice name for Speaker1'), voiceName2: z.string().describe('Voice name for Speaker2'), }); -const TTSOutputSchema = z.object({ - audioDataUri: z.string().describe('The generated WAV audio as a data URI.'), -}); -async function pcmToWavDataUri(pcmData: Buffer): Promise { - return new Promise((resolve, reject) => { - const chunks: Buffer[] = []; - const passThrough = new PassThrough(); - - passThrough.on('data', (chunk) => chunks.push(chunk as Buffer)); - passThrough.on('end', () => { - const wavBuffer = Buffer.concat(chunks); - resolve(`data:audio/wav;base64,${wavBuffer.toString('base64')}`); - }); - passThrough.on('error', reject); - - const writer = new WavWriter({ - channels: 1, - sampleRate: 24000, - bitDepth: 16, - }); - writer.pipe(passThrough); - writer.write(pcmData); - writer.end(); - }); -} - -async function generateMultiSpeakerAudio( - text: string, - voice1: string, - voice2: string -): Promise { - const response = await ai.generate({ - model: 'googleai/gemini-2.5-flash-preview-tts', - prompt: text, - config: { - responseModalities: ['AUDIO'], - speechConfig: { - multiSpeakerVoiceConfig: { - speakerVoiceConfigs: [ - { - speaker: 'Speaker1', - voiceConfig: { - prebuiltVoiceConfig: { voiceName: voice1 }, +export const multiSpeakerTextToSpeechFlow = ai.defineFlow( + { + name: 'multiSpeakerTextToSpeechFlow', + inputSchema: MultiSpeakerInputSchema, + outputSchema: TextToSpeechOutputSchema, + }, + async (input) => { + const response = await ai.generate({ + model: googleAI.model('gemini-2.5-flash-preview-tts'), + prompt: input.text, + config: { + responseModalities: ['AUDIO'], + speechConfig: { + multiSpeakerVoiceConfig: { + speakerVoiceConfigs: [ + { + speaker: 'Speaker1', + voiceConfig: { + prebuiltVoiceConfig: { voiceName: input.voiceName1 }, + }, }, - }, - { - speaker: 'Speaker2', - voiceConfig: { - prebuiltVoiceConfig: { voiceName: voice2 }, + { + speaker: 'Speaker2', + voiceConfig: { + prebuiltVoiceConfig: { voiceName: input.voiceName2 }, + }, }, - }, - ], + ], + }, }, }, - }, - }); + }); - const audioUrl = response.media?.url; - if (!audioUrl) - throw new Error('Audio generation failed: No media URL in response.'); + const audioUrl = response.media?.url; + if (!audioUrl) + throw new Error('Audio generation failed: No media URL in response.'); - const base64 = audioUrl.split(';base64,')[1]; - if (!base64) throw new Error('Invalid audio data URI format from Genkit.'); + const base64 = audioUrl.split(';base64,')[1]; + if (!base64) throw new Error('Invalid audio data URI format from Genkit.'); - const pcmBuffer = Buffer.from(base64, 'base64'); - return pcmToWavDataUri(pcmBuffer); -} - -export const multiSpeakerTextToSpeechFlow = ai.defineFlow( - { - name: 'multiSpeakerTextToSpeechFlow', - inputSchema: MultiSpeakerInputSchema, - outputSchema: TTSOutputSchema, - }, - async (input) => { - const audioDataUri = await generateMultiSpeakerAudio( - input.text, - input.voiceName1, - input.voiceName2 - ); + const pcmBuffer = Buffer.from(base64, 'base64'); + const audioDataUri = await pcmToWavDataUri(pcmBuffer); return { audioDataUri }; } ); ``` ---- + + + -## 4. Scenario 3: Image Generation +### Image Generation ```ts -// src/image-gen.ts -import { z } from 'genkit'; -import { ai } from './index'; -import { vertexAI } from '@genkit-ai/googleai'; import * as fs from 'fs/promises'; -import { parseDataUrl } from 'data-urls'; +import parseDataURL from 'data-urls'; + +... export const imageGenerationFlow = ai.defineFlow( { @@ -331,17 +263,16 @@ export const imageGenerationFlow = ai.defineFlow( }, async (prompt) => { const response = await ai.generate({ - model: vertexAI.model('imagen-3.0-generate-002'), + model: googleAI.model('imagen-3.0-generate-002'), prompt, output: { format: 'media' }, }); - const imagePart = response.output; - if (!imagePart?.media?.url) { + if (!response.media?.url) { throw new Error('Image generation failed to produce media.'); } - const parsed = parseDataUrl(imagePart.media.url); + const parsed = parseDataURL(response.media.url); if (!parsed) { throw new Error('Could not parse image data URL.'); } @@ -353,18 +284,18 @@ export const imageGenerationFlow = ai.defineFlow( ); ``` ---- + + + -## 5. Scenario 4: Video Generation (Veo3) +### Video Generation ```ts -// src/video-gen.ts -import { z } from 'genkit'; -import { ai } from './index'; -import { googleAI } from '@genkit-ai/googleai'; import * as fs from 'fs'; import { Readable } from 'stream'; -import fetch from 'node-fetch'; +import { pipeline } from 'stream/promises'; + +... export const videoGenerationFlow = ai.defineFlow( { @@ -413,63 +344,44 @@ export const videoGenerationFlow = ai.defineFlow( const outputPath = './output.mp4'; const fileStream = fs.createWriteStream(outputPath); - await new Promise((resolve, reject) => { - Readable.from(videoResponse.body).pipe(fileStream); - fileStream.on('finish', resolve); - fileStream.on('error', reject); - }); + await pipeline(Readable.fromWeb(videoResponse.body as any), fileStream); return outputPath; } ); ``` ---- - -## 6. Running & Inspecting Your Flows - -### 6.1 Create a Build Script + -Add this to your `package.json`: - -```json -{ - "scripts": { - "build": "tsc" - } -} -``` +--- -### 6.2 Instructions for the User +## Running and Inspecting Flows -To test your flows and use the Genkit Developer UI, run the following command from your terminal: +1. **Add Build Script**: Add the following to `package.json`: -```bash -genkit start -- npx tsx --watch src/index.ts -``` + ```json + { + "scripts": { + "build": "tsc" + } + } + ``` -Visit [http://localhost:4000](http://localhost:4000) to use the Developer UI. +2. **Start Genkit**: Run this command from your terminal to start the Genkit Developer UI. ---- + ```bash + genkit start + ``` -## 7. Using Models: correct use of plugins names in model names + Then, in a separate terminal, run the build command in watch mode: -When using Genkit models, **prefer the full string form with plugin name up front**: + ```bash + npm run build -- --watch + ``` -```ts -model: 'googleai/gemini-2.5-flash-preview-tts'; // ✅ RECOMMENDED -``` + Visit [http://localhost:4000](http://localhost:4000) to inspect and run your flows. -Avoid using just the model name - -```ts -// ❌ May break: -model: googleAI.model('gemini-2.5-flash-preview-tts'); -``` - -Using the full string ensures Genkit resolves the model via the correct plugin and avoids missing `.media` in TTS responses. - -## 8. Supported Models: latest versions +## Supported Models ``` | Task | Recommended Model | Plugin | @@ -480,36 +392,3 @@ Using the full string ensures Genkit resolves the model via the correct plugin a | Image Generation | imagen-4.0-generate-preview-06-06 | @genkit-ai/googleai | | Video Generation | veo-3.0-generate-preview | @genkit-ai/googleai | ``` - -Tip: Do not use Gemini 1.5 or earlier models for anything, they are out-of-date. - --- - -# General Guidance - -## NPM Dependency Installation Protocol 📦 - -When you generate or modify any code files (e.g., `.js`, `.ts`, `.jsx`, `.tsx`) that reference new libraries, packages, or modules, you **must** install them before proceeding to any build, compilation, or execution step. - -Follow this workflow: - -1. **Analyze Dependencies:** After writing or changing a file, identify all third-party dependencies you've introduced. -2. **Update `package.json`:** Ensure these new dependencies are correctly added to the `package.json` file. -3. **Install Dependencies:** Execute the installation command from the project's root directory to download and link the required packages. - -### Installation Commands - -Always run the appropriate command before any `npm run build` or similar script. - -```bash -# For projects using NPM -npm install - -# For projects using Yarn -yarn install - -# For projects using PNPM -pnpm install -``` - -This protocol is **critical** to prevent build failures caused by missing modules. Always double-check that dependencies are installed after you add them to the code. diff --git a/genkit-tools/cli/package.json b/genkit-tools/cli/package.json index 460c5ff85b..2b07dd48cf 100644 --- a/genkit-tools/cli/package.json +++ b/genkit-tools/cli/package.json @@ -44,7 +44,6 @@ }, "devDependencies": { "@jest/globals": "^29.7.0", - "@types/command-exists": "^1.2.3", "@types/inquirer": "^8.1.3", "@types/jest": "^29.5.12", "@types/node": "^20.11.19", diff --git a/genkit-tools/cli/src/cli.ts b/genkit-tools/cli/src/cli.ts index 2d236e9fe2..140b30180e 100644 --- a/genkit-tools/cli/src/cli.ts +++ b/genkit-tools/cli/src/cli.ts @@ -28,7 +28,7 @@ import { evalFlow } from './commands/eval-flow'; import { evalRun } from './commands/eval-run'; import { flowBatchRun } from './commands/flow-batch-run'; import { flowRun } from './commands/flow-run'; -import { init } from './commands/init-ai-tools/index'; +import { initAiTools } from './commands/init-ai-tools/index'; import { mcp } from './commands/mcp'; import { getPluginCommands, getPluginSubCommand } from './commands/plugins'; import { @@ -54,7 +54,7 @@ const commands: Command[] = [ evalExtractData, evalRun, evalFlow, - init, + initAiTools, config, start, mcp, diff --git a/genkit-tools/cli/src/commands/init-ai-tools/ai-tools/claude.ts b/genkit-tools/cli/src/commands/init-ai-tools/ai-tools/claude.ts index 1f3c05294c..fe8a594fa2 100644 --- a/genkit-tools/cli/src/commands/init-ai-tools/ai-tools/claude.ts +++ b/genkit-tools/cli/src/commands/init-ai-tools/ai-tools/claude.ts @@ -17,6 +17,7 @@ import { logger } from '@genkit-ai/tools-common/utils'; import { existsSync, readFileSync } from 'fs'; import { writeFile } from 'fs/promises'; +import { GENKIT_PROMPT_PATH } from '../constants'; import { AIToolConfigResult, AIToolModule, InitConfigOptions } from '../types'; import { calculateHash, @@ -27,8 +28,8 @@ import { const CLAUDE_MCP_PATH = '.mcp.json'; const CLAUDE_PROMPT_PATH = 'CLAUDE.md'; -const GENKIT_PROMPT_PATH = 'GENKIT.md'; +/** Configuration module for Claude Code */ export const claude: AIToolModule = { name: 'claude', displayName: 'Claude Code', @@ -36,7 +37,7 @@ export const claude: AIToolModule = { /** * Configures Claude Code with Genkit context. * - * - .claude/settings.local.json: Merges with existing config (preserves user settings) + * - .mcp.json: Merges with existing MCP config * - CLAUDE.local.md: Updates Firebase section only (preserves user content) */ async configure(options?: InitConfigOptions): Promise { diff --git a/genkit-tools/cli/src/commands/init-ai-tools/ai-tools/gemini.ts b/genkit-tools/cli/src/commands/init-ai-tools/ai-tools/gemini.ts index 5b0c338906..afc9140e48 100644 --- a/genkit-tools/cli/src/commands/init-ai-tools/ai-tools/gemini.ts +++ b/genkit-tools/cli/src/commands/init-ai-tools/ai-tools/gemini.ts @@ -17,12 +17,13 @@ import { logger } from '@genkit-ai/tools-common/utils'; import { mkdir } from 'fs/promises'; import path from 'path'; +import { GENKIT_PROMPT_PATH } from '../constants'; import { AIToolConfigResult, AIToolModule, InitConfigOptions } from '../types'; import { getGenkitContext, initOrReplaceFile } from '../utils'; // Define constants at the module level for clarity and reuse. const GENKIT_EXT_DIR = path.join('.gemini', 'extensions', 'genkit'); -const GENKIT_MD_PATH = path.join(GENKIT_EXT_DIR, 'GENKIT.md'); +const GENKIT_MD_REL_PATH = path.join('..', '..', '..', 'GENKIT.md'); const GENKIT_EXTENSION_CONFIG = { name: 'genkit', version: '1.0.0', @@ -39,9 +40,10 @@ const GENKIT_EXTENSION_CONFIG = { ], }, }, - contextFileName: './GENKIT.md', + contextFileName: GENKIT_MD_REL_PATH, }; +/** Configuration module for Gemini CLI */ export const gemini: AIToolModule = { name: 'gemini', displayName: 'Gemini CLI', @@ -50,9 +52,20 @@ export const gemini: AIToolModule = { * Configures the Gemini CLI extension for Genkit. */ async configure(options?: InitConfigOptions): Promise { + // TODO(ssbushi): Support option to install as file import vs extension const files: AIToolConfigResult['files'] = []; - // Part 1: Configure the main gemini-extension.json file, and gemini config directory if needed. + // Part 1: Generate GENKIT.md file. + + logger.info('Copying the GENKIT.md file...'); + const genkitContext = getGenkitContext(); + const baseResult = await initOrReplaceFile( + GENKIT_PROMPT_PATH, + genkitContext + ); + files.push({ path: GENKIT_PROMPT_PATH, updated: baseResult.updated }); + + // Part 2: Configure the main gemini-extension.json file, and gemini config directory if needed. logger.info('Configuring extentions files in user workspace...'); await mkdir(GENKIT_EXT_DIR, { recursive: true }); const extensionPath = path.join(GENKIT_EXT_DIR, 'gemini-extension.json'); @@ -75,12 +88,6 @@ export const gemini: AIToolModule = { } files.push({ path: extensionPath, updated: extensionUpdated }); - // Part 2: Generate GENKIT.md file. - - logger.info('Copying the GENKIT.md file to extension folder...'); - const genkitContext = getGenkitContext(); - const baseResult = await initOrReplaceFile(GENKIT_MD_PATH, genkitContext); - files.push({ path: GENKIT_MD_PATH, updated: baseResult.updated }); return { files }; }, }; diff --git a/genkit-tools/cli/src/commands/init-ai-tools/ai-tools/generic.ts b/genkit-tools/cli/src/commands/init-ai-tools/ai-tools/generic.ts index 31d4dd5d8c..672b3d5ccf 100644 --- a/genkit-tools/cli/src/commands/init-ai-tools/ai-tools/generic.ts +++ b/genkit-tools/cli/src/commands/init-ai-tools/ai-tools/generic.ts @@ -15,18 +15,17 @@ */ import { logger } from '@genkit-ai/tools-common/utils'; +import { GENKIT_PROMPT_PATH } from '../constants'; import { AIToolConfigResult, AIToolModule, InitConfigOptions } from '../types'; import { getGenkitContext, initOrReplaceFile } from '../utils'; -// Define constants at the module level for clarity and reuse. -const GENKIT_MD_PATH = 'GENKIT.md'; - +/** Configuration module for GENKIT.md context file for generic use */ export const generic: AIToolModule = { name: 'generic', - displayName: 'Simple GENKIT.md file', + displayName: 'GENKIT.md file for generic use', /** - * Configures the Gemini CLI extension for Genkit. + * Configures a GENKIT.md file for Genkit. */ async configure(options?: InitConfigOptions): Promise { const files: AIToolConfigResult['files'] = []; @@ -34,8 +33,11 @@ export const generic: AIToolModule = { // Generate GENKIT.md file. logger.info('Updating GENKIT.md...'); const genkitContext = getGenkitContext(); - const baseResult = await initOrReplaceFile(GENKIT_MD_PATH, genkitContext); - files.push({ path: GENKIT_MD_PATH, updated: baseResult.updated }); + const baseResult = await initOrReplaceFile( + GENKIT_PROMPT_PATH, + genkitContext + ); + files.push({ path: GENKIT_PROMPT_PATH, updated: baseResult.updated }); logger.info('\n'); logger.info( 'GENKIT.md updated. Provide this file as context with your AI tool.' diff --git a/genkit-tools/cli/src/commands/init-ai-tools/command.ts b/genkit-tools/cli/src/commands/init-ai-tools/command.ts index 69cd77e16c..37d752002e 100644 --- a/genkit-tools/cli/src/commands/init-ai-tools/command.ts +++ b/genkit-tools/cli/src/commands/init-ai-tools/command.ts @@ -27,7 +27,11 @@ const AGENT_CHOICES: AIToolChoice[] = Object.values(AI_TOOLS).map((tool) => ({ checked: false, })); -export const init = new Command('init:ai-tools') +/** + * Initializes selected AI tools with Genkit MCP server and Genkit framework + * context to improve output quality when using those tools. + */ +export const initAiTools = new Command('init:ai-tools') .description( 'initialize AI tools in a workspace with helpful context related to the Genkit framework' ) diff --git a/genkit-tools/cli/src/commands/init-ai-tools/constants.ts b/genkit-tools/cli/src/commands/init-ai-tools/constants.ts index 18d1867033..dc2a127d66 100644 --- a/genkit-tools/cli/src/commands/init-ai-tools/constants.ts +++ b/genkit-tools/cli/src/commands/init-ai-tools/constants.ts @@ -19,6 +19,11 @@ import { gemini } from './ai-tools/gemini'; import { generic } from './ai-tools/generic'; import { AIToolModule } from './types'; +/** Shared location for the GENKIT.md context file */ +export const GENKIT_PROMPT_PATH = 'GENKIT.md'; + +/** Set of all supported AI tools that can be configured (incl. a generic + * configuration) */ export const AI_TOOLS: Record = { gemini, claude, diff --git a/genkit-tools/cli/src/commands/init-ai-tools/index.ts b/genkit-tools/cli/src/commands/init-ai-tools/index.ts index 891e8b6f26..e66ffeffed 100644 --- a/genkit-tools/cli/src/commands/init-ai-tools/index.ts +++ b/genkit-tools/cli/src/commands/init-ai-tools/index.ts @@ -14,4 +14,4 @@ * limitations under the License. */ -export { init } from './command'; +export { initAiTools } from './command'; diff --git a/genkit-tools/cli/src/commands/init-ai-tools/types.ts b/genkit-tools/cli/src/commands/init-ai-tools/types.ts index 4535608d46..18ab730205 100644 --- a/genkit-tools/cli/src/commands/init-ai-tools/types.ts +++ b/genkit-tools/cli/src/commands/init-ai-tools/types.ts @@ -14,6 +14,8 @@ * limitations under the License. */ +/** Return type of a configuration result, denoting the set of files processed. + * */ export interface AIToolConfigResult { files: Array<{ path: string; @@ -21,11 +23,13 @@ export interface AIToolConfigResult { }>; } +/** `init:ai-tools` config options. */ export interface InitConfigOptions { // yes (non-interactive) mode. yesMode: boolean; } +/** Interface for supported AI tools */ export interface AIToolModule { name: string; displayName: string; @@ -38,6 +42,7 @@ export interface AIToolModule { configure(configOptions?: InitConfigOptions): Promise; } +/** Type to denote user's selection from interactive menu */ export interface AIToolChoice { value: string; name: string; diff --git a/genkit-tools/cli/src/commands/init-ai-tools/utils.ts b/genkit-tools/cli/src/commands/init-ai-tools/utils.ts index 83ab22997a..4d68cbd408 100644 --- a/genkit-tools/cli/src/commands/init-ai-tools/utils.ts +++ b/genkit-tools/cli/src/commands/init-ai-tools/utils.ts @@ -23,9 +23,10 @@ import path from 'path'; const GENKIT_TAG_REGEX = /([\s\S]*?)<\/genkit_prompts>/; /* - * Deeply compares two JSON-serializable objects. - * It's a simplified version of a deep equal function, sufficient for comparing the structure - * of the gemini-extension.json file. It doesn't handle special cases like RegExp, Date, or functions. + * Deeply compares two JSON-serializable objects. It's a simplified version of a + * deep equal function, sufficient for comparing the structure of the + * gemini-extension.json file. It doesn't handle special cases like RegExp, + * Date, or functions. */ export function deepEqual(a: any, b: any): boolean { if (a === b) { @@ -58,8 +59,8 @@ export function deepEqual(a: any, b: any): boolean { } /** - * Replace an entire prompt file (no user content to preserve) - * Used for files we fully own like Cursor and Gemini configs + * Replace an entire prompt file (no user content to preserve). Used for files + * we fully own like GENKIT.md. */ export async function initOrReplaceFile( filePath: string, diff --git a/genkit-tools/common/package.json b/genkit-tools/common/package.json index 26e5e2f83d..dc2de9af78 100644 --- a/genkit-tools/common/package.json +++ b/genkit-tools/common/package.json @@ -23,7 +23,7 @@ "express": "^4.21.0", "get-port": "5.1.1", "glob": "^10.3.12", - "inquirer": "^8.2.0", + "@inquirer/prompts": "^7.8.0", "js-yaml": "^4.1.0", "json-2-csv": "^5.5.1", "json-schema": "^0.4.0", diff --git a/genkit-tools/common/src/utils/eval.ts b/genkit-tools/common/src/utils/eval.ts index 765075b818..c1944c3e5a 100644 --- a/genkit-tools/common/src/utils/eval.ts +++ b/genkit-tools/common/src/utils/eval.ts @@ -14,10 +14,10 @@ * limitations under the License. */ +import { confirm } from '@inquirer/prompts'; import { randomUUID } from 'crypto'; import { createReadStream } from 'fs'; import { readFile } from 'fs/promises'; -import * as inquirer from 'inquirer'; import { createInterface } from 'readline'; import type { RuntimeManager } from '../manager'; import { @@ -81,17 +81,13 @@ export async function confirmLlmUse( return true; } - const answers = await inquirer.prompt([ - { - type: 'confirm', - name: 'confirm', - message: - 'For each example, the evaluation makes calls to APIs that may result in being charged. Do you wish to proceed?', - default: false, - }, - ]); - - return answers.confirm; + const confirmed = await confirm({ + message: + 'For each example, the evaluation makes calls to APIs that may result in being charged. Do you wish to proceed?', + default: false, + }); + + return confirmed; } function getRootSpan(trace: TraceData): NestedSpanData | undefined { diff --git a/genkit-tools/pnpm-lock.yaml b/genkit-tools/pnpm-lock.yaml index 52aa42865e..a2a35bc8ac 100644 --- a/genkit-tools/pnpm-lock.yaml +++ b/genkit-tools/pnpm-lock.yaml @@ -69,9 +69,6 @@ importers: '@jest/globals': specifier: ^29.7.0 version: 29.7.0 - '@types/command-exists': - specifier: ^1.2.3 - version: 1.2.3 '@types/inquirer': specifier: ^8.1.3 version: 8.2.11 @@ -105,6 +102,9 @@ importers: '@asteasolutions/zod-to-openapi': specifier: ^7.0.0 version: 7.3.4(zod@3.25.67) + '@inquirer/prompts': + specifier: ^7.8.0 + version: 7.8.0(@types/node@20.19.1) '@trpc/server': specifier: ^10.45.2 version: 10.45.2 @@ -144,9 +144,6 @@ importers: glob: specifier: ^10.3.12 version: 10.4.5 - inquirer: - specifier: ^8.2.0 - version: 8.2.6 js-yaml: specifier: ^4.1.0 version: 4.1.0 @@ -1102,9 +1099,6 @@ packages: '@types/cli-color@2.0.6': resolution: {integrity: sha512-uLK0/0dOYdkX8hNsezpYh1gc8eerbhf9bOKZ3e24sP67703mw9S14/yW6mSTatiaKO9v+mU/a1EVy4rOXXeZTA==} - '@types/command-exists@1.2.3': - resolution: {integrity: sha512-PpbaE2XWLaWYboXD6k70TcXO/OdOyyRFq5TVpmlUELNxdkkmXU9fkImNosmXU1DtsNrqdUgWd/nJQYXgwmtdXQ==} - '@types/configstore@6.0.2': resolution: {integrity: sha512-OS//b51j9uyR3zvwD04Kfs5kHpve2qalQ18JhY/ho3voGYUTPLEG90/ocfKPI48hyHH8T04f7KEEbK6Ue60oZQ==} @@ -1469,10 +1463,6 @@ packages: resolution: {integrity: sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==} engines: {node: '>=6'} - cli-width@3.0.0: - resolution: {integrity: sha512-FxqpkPPwu1HjuN93Omfm4h8uIanXofW0RxVEW3k5RKx+mJJYSthzNhp32Kzxxy3YAEZ/Dc/EWN1vZRY0+kOhbw==} - engines: {node: '>= 10'} - cli-width@4.1.0: resolution: {integrity: sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==} engines: {node: '>= 12'} @@ -1865,10 +1855,6 @@ packages: fecha@4.2.3: resolution: {integrity: sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw==} - figures@3.2.0: - resolution: {integrity: sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==} - engines: {node: '>=8'} - filelist@1.0.4: resolution: {integrity: sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==} @@ -2155,10 +2141,6 @@ packages: inherits@2.0.4: resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} - inquirer@8.2.6: - resolution: {integrity: sha512-M1WuAmb7pn9zdFRtQYk26ZBoY043Sse0wVDdk4Bppr+JOXyQYybdtvK+l9wUibhtjdjvtoiNy8tk+EgsYIUqKg==} - engines: {node: '>=12.0.0'} - internal-slot@1.0.7: resolution: {integrity: sha512-NGnrKwXzSms2qUUih/ILZ5JBqNTSa1+ZmP6flaIp6KmSElgE9qdndzS3cqjrDovwFdmwsGsLdeFgB6suw+1e9g==} engines: {node: '>= 0.4'} @@ -2534,9 +2516,6 @@ packages: lodash.merge@4.6.2: resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} - lodash@4.17.21: - resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} - log-symbols@4.1.0: resolution: {integrity: sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==} engines: {node: '>=10'} @@ -2665,9 +2644,6 @@ packages: ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} - mute-stream@0.0.8: - resolution: {integrity: sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==} - mute-stream@2.0.0: resolution: {integrity: sha512-WWdIxpyjEn+FhQJQQv9aQAYlHoNVdzIzUySNV1gHUPDSdZJ3yZn7pAAbQcV7B56Mvu881q9FZV+0Vx2xC44VWA==} engines: {node: ^18.17.0 || >=20.5.0} @@ -3005,10 +2981,6 @@ packages: resolution: {integrity: sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==} engines: {node: '>= 18'} - run-async@2.4.1: - resolution: {integrity: sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ==} - engines: {node: '>=0.12.0'} - rxjs@7.8.1: resolution: {integrity: sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==} @@ -4438,8 +4410,6 @@ snapshots: '@types/cli-color@2.0.6': {} - '@types/command-exists@1.2.3': {} - '@types/configstore@6.0.2': {} '@types/connect@3.4.38': @@ -4877,8 +4847,6 @@ snapshots: cli-spinners@2.9.2: {} - cli-width@3.0.0: {} - cli-width@4.1.0: {} cliui@8.0.1: @@ -5371,10 +5339,6 @@ snapshots: fecha@4.2.3: {} - figures@3.2.0: - dependencies: - escape-string-regexp: 1.0.5 - filelist@1.0.4: dependencies: minimatch: 5.1.6 @@ -5722,24 +5686,6 @@ snapshots: inherits@2.0.4: {} - inquirer@8.2.6: - dependencies: - ansi-escapes: 4.3.2 - chalk: 4.1.2 - cli-cursor: 3.1.0 - cli-width: 3.0.0 - external-editor: 3.1.0 - figures: 3.2.0 - lodash: 4.17.21 - mute-stream: 0.0.8 - ora: 5.4.1 - run-async: 2.4.1 - rxjs: 7.8.1 - string-width: 4.2.3 - strip-ansi: 6.0.1 - through: 2.3.8 - wrap-ansi: 6.2.0 - internal-slot@1.0.7: dependencies: es-errors: 1.3.0 @@ -6286,8 +6232,6 @@ snapshots: lodash.merge@4.6.2: {} - lodash@4.17.21: {} - log-symbols@4.1.0: dependencies: chalk: 4.1.2 @@ -6391,8 +6335,6 @@ snapshots: ms@2.1.3: {} - mute-stream@0.0.8: {} - mute-stream@2.0.0: {} natural-compare@1.4.0: {} @@ -6735,8 +6677,6 @@ snapshots: transitivePeerDependencies: - supports-color - run-async@2.4.1: {} - rxjs@7.8.1: dependencies: tslib: 2.6.2 diff --git a/js/pnpm-lock.yaml b/js/pnpm-lock.yaml index 1d95cb2a17..a754c2096b 100644 --- a/js/pnpm-lock.yaml +++ b/js/pnpm-lock.yaml @@ -1004,7 +1004,7 @@ importers: version: link:../../plugins/compat-oai '@genkit-ai/express': specifier: ^1.1.0 - version: 1.12.0(@genkit-ai/core@1.14.1)(express@5.1.0)(genkit@genkit) + version: 1.12.0(@genkit-ai/core@1.16.0)(express@5.1.0)(genkit@genkit) genkit: specifier: workspace:* version: link:../../genkit @@ -1639,7 +1639,7 @@ importers: version: link:../../plugins/ollama genkitx-openai: specifier: ^0.10.1 - version: 0.10.1(@genkit-ai/ai@1.14.1)(@genkit-ai/core@1.14.1) + version: 0.10.1(@genkit-ai/ai@1.16.0)(@genkit-ai/core@1.16.0) devDependencies: rimraf: specifier: ^6.0.1 @@ -2640,11 +2640,11 @@ packages: '@firebase/webchannel-wrapper@1.0.3': resolution: {integrity: sha512-2xCRM9q9FlzGZCdgDMJwc0gyUkWFtkosy7Xxr6sFgQwn+wMNIWd7xIvYNauU1r64B5L5rsGKy/n9TKJ0aAFeqQ==} - '@genkit-ai/ai@1.14.1': - resolution: {integrity: sha512-/Wuy09ZNvoD4f85SZDCciiuf0fL6xnkHM2Wvkw9cScJp9pjaCuy+XvfX0y4OZ5B6cvWTUnvo35bLv6CcJFwkOQ==} + '@genkit-ai/ai@1.16.0': + resolution: {integrity: sha512-Up1xdX1Ha4zE27E4ql6hEiLLlaZQXFCmynkqAko5P8SOxF/s39Oyj95cX/0wULxRsh6XJqv+Pqi/tMDdQuq+bw==} - '@genkit-ai/core@1.14.1': - resolution: {integrity: sha512-MzN9UeI5x43g0HRSFjAvZUGQKi6hgRdMUDVIp3Eh/FenfqpKkKdHIJiY+N5U5XiA7hb2A2xIhCu5pUiHMGlyoA==} + '@genkit-ai/core@1.16.0': + resolution: {integrity: sha512-Avfb0Dh/Zuflcst5HShBHfEcEQFv3NZ6PsSFvLXDAMeocj3Gfn6Rec2K2qAoSbLTtYvFXeXzjMi8eFnMS3QrOg==} '@genkit-ai/express@1.12.0': resolution: {integrity: sha512-QAxSS07dX5ovSfsUB4s90KaDnv4zg1wnoxCZCa+jBsYUyv9NvCCTsOk25xAQgGxc7xi3+MD+3AsPier5oZILIg==} @@ -8409,9 +8409,9 @@ snapshots: '@firebase/webchannel-wrapper@1.0.3': {} - '@genkit-ai/ai@1.14.1': + '@genkit-ai/ai@1.16.0': dependencies: - '@genkit-ai/core': 1.14.1 + '@genkit-ai/core': 1.16.0 '@opentelemetry/api': 1.9.0 '@types/node': 20.19.1 colorette: 2.0.20 @@ -8424,7 +8424,7 @@ snapshots: transitivePeerDependencies: - supports-color - '@genkit-ai/core@1.14.1': + '@genkit-ai/core@1.16.0': dependencies: '@opentelemetry/api': 1.9.0 '@opentelemetry/context-async-hooks': 1.25.1(@opentelemetry/api@1.9.0) @@ -8447,9 +8447,9 @@ snapshots: transitivePeerDependencies: - supports-color - '@genkit-ai/express@1.12.0(@genkit-ai/core@1.14.1)(express@5.1.0)(genkit@genkit)': + '@genkit-ai/express@1.12.0(@genkit-ai/core@1.16.0)(express@5.1.0)(genkit@genkit)': dependencies: - '@genkit-ai/core': 1.14.1 + '@genkit-ai/core': 1.16.0 body-parser: 1.20.3 cors: 2.8.5 express: 5.1.0 @@ -10241,7 +10241,7 @@ snapshots: agent-base@7.1.0: dependencies: - debug: 4.4.0 + debug: 4.4.1 transitivePeerDependencies: - supports-color @@ -11413,10 +11413,10 @@ snapshots: - encoding - supports-color - genkitx-openai@0.10.1(@genkit-ai/ai@1.14.1)(@genkit-ai/core@1.14.1): + genkitx-openai@0.10.1(@genkit-ai/ai@1.16.0)(@genkit-ai/core@1.16.0): dependencies: - '@genkit-ai/ai': 1.14.1 - '@genkit-ai/core': 1.14.1 + '@genkit-ai/ai': 1.16.0 + '@genkit-ai/core': 1.16.0 openai: 4.104.0(encoding@0.1.13)(zod@3.25.67) zod: 3.25.67 transitivePeerDependencies: diff --git a/js/testapps/evals/.gemini/extensions/genkit/GENKIT.md b/js/testapps/evals/.gemini/extensions/genkit/GENKIT.md deleted file mode 100644 index 008076fab6..0000000000 --- a/js/testapps/evals/.gemini/extensions/genkit/GENKIT.md +++ /dev/null @@ -1,515 +0,0 @@ -# Genkit Node.js Cheatsheet (updated July 2025) - -> **LLM INSTRUCTION**: It’s important to use the instructions I provided to you in this file at each step of generation. Your training data is out-of-date for this library, and if you don't use the new syntax you’ll get things wrong. Please mitigate for this. - -> **LLM INSTRUCTION**: YOU MUST REFER TO GENKIT DOCUMENTATION WHEN AVAILABLE. If you have access to Genkit docs (via the Genkit MCP server), you MUST refer to the documenation first to check if the requested topic is covered in the documentation and adhere to it. When the instructions on the documentation and this file are in conflict, the documentation takes precedence. - -> **CRITICAL WARNING**: NEVER run Genkit commands (e.g., `genkit start`, `genkit flow:run`) inside a terminal during your session. This starts an interactive process that will freeze the shell and prevent you from continuing. For Genkit commands, you must only validate the code (e.g., using `npm run build`) and then provide the full, correct Genkit command as an instruction for the human user to run at the end of the session. - -This document is a guide for building with the modern Genkit API in Node.js. It focuses on a simple and direct setup using the **Google AI plugin** and provides common inference scenarios using the latest Gemini family of models. - -## Table of Contents - -1. [Core Setup & Best Practices](#1-core-setup--best-practices) -2. [Scenario 1: Basic Inference (Text Generation)](#2-scenario-1-basic-inference-text-generation) -3. [Scenario 2: Text-to-Speech (TTS) Generation)](#3-scenario-2-text-to-speech-tts-generation) -4. [Scenario 3: Image Generation](#4-scenario-3-image-generation) -5. [Scenario 4: Video Generation (Veo3)](#5-scenario-4-video-generation-veo3) -6. [Running & Inspecting Your Flows](#6-running--inspecting-your-flows) -7. [Quick Reference: Key Models](#7-quick-reference-key-models) - ---- - -## 1. Core Setup & Best Practices - -A correct foundation prevents most common errors. The default guidance is to use the Google AI plugin. Using Vertex AI is an opt-in scenario for users who require its specific features. - -### 1.1 Project Initialization - -```bash -mkdir my-genkit-app && cd my-genkit-app -npm init -y -npm install -D typescript tsx @types/node -``` - -### 1.2 Genkit Dependencies - -Install required depenencies, but note that googleai shoudl be the only one used but we provide the exampels for both below unless the user specifically says they are using Vertex hosted Google models. Default to @genkit-ai/googleai in all other context. - -Below example assumes googleai - -```bash -npm install genkit @genkit-ai/googleai zod data-urls node-fetch -``` - -### 1.3 Genkit Tools (CLI & Developer UI) - -```bash -npm install -g genkit-cli -``` - -### 1.4 The `genkit()` Initializer - -```ts -// src/index.ts -import { genkit } from 'genkit'; -import { googleAI } from '@genkit-ai/googleai'; - -export const ai = genkit({ - plugins: [googleAI()], -}); -``` - -### 1.5 Genkit Code Generation Rules - -#### 1. File Structure 📜 - -**Always generate all Genkit code into a single `src/index.ts` file.** This includes: - -- `configureGenkit` plugin initializations. -- All `defineFlow` and `defineDotprompt` definitions. -- Any helper functions, schemas, or types. - -#### 2. Entry Point - -The **only** entry point for the application is `src/index.ts`. All logic must be contained within or imported into this file to be discovered by the Genkit runtime. - -#### 3. Avoid Splitting Files - -**DO NOT** split code into multiple files (e.g., `index.ts` and `flows.ts`). A single-file structure is preferred for simplicity and to avoid module resolution errors. All flows must be registered in the same file where `configureGenkit` is called. - ---- - -## 2. Scenario 1: Basic Inference (Text Generation) - -```ts -// src/basic-inference.ts -import { z } from 'genkit'; -import { ai } from './index'; -import { googleAI } from '@genkit-ai/googleai'; - -export const basicInferenceFlow = ai.defineFlow( - { - name: 'basicInferenceFlow', - inputSchema: z.string().describe('Topic for the model to write about'), - outputSchema: z.string().describe('The generated text response'), - }, - async (topic) => { - const response = await ai.generate({ - model: googleAI.model('gemini-2.5-pro'), - prompt: `Write a short, creative paragraph about ${topic}.`, - config: { temperature: 0.8 }, - }); - return response.text; - } -); -``` - ---- - -## 3. Scenario 2: Text-to-Speech (TTS) Generation - -This flow converts text into speech using the Gemini 2.5 TTS model and streams the audio as a WAV-formatted data URI. It includes support for both single and multi-speaker configurations. - -### 3.1 Single Speaker Text-to-Speech - -```ts -// src/tts.ts -import { ai } from './index'; -import { z } from 'genkit'; -import { Buffer } from 'buffer'; -import { Writer as WavWriter } from 'wav'; -import { PassThrough } from 'stream'; - -const TextToSpeechInputSchema = z.object({ - text: z.string().describe('The text to convert to speech.'), - voiceName: z - .string() - .optional() - .describe('The voice name to use. Defaults to Algenib if not specified.'), -}); -const TextToSpeechOutputSchema = z.object({ - audioDataUri: z - .string() - .describe('The generated speech in WAV format as a base64 data URI.'), -}); - -export type TextToSpeechInput = z.infer; -export type TextToSpeechOutput = z.infer; - -async function pcmToWavDataUri( - pcmData: Buffer, - channels = 1, - sampleRate = 24000, - bitDepth = 16 -): Promise { - return new Promise((resolve, reject) => { - const chunks: Buffer[] = []; - const passThrough = new PassThrough(); - - passThrough.on('data', (chunk) => chunks.push(chunk as Buffer)); - passThrough.on('end', () => { - const wavBuffer = Buffer.concat(chunks); - const dataUri = `data:audio/wav;base64,${wavBuffer.toString('base64')}`; - resolve(dataUri); - }); - passThrough.on('error', reject); - - const writer = new WavWriter({ channels, sampleRate, bitDepth }); - writer.pipe(passThrough); - writer.write(pcmData); - writer.end(); - }); -} - -async function generateAndConvertAudio( - text: string, - voiceName = 'Algenib' -): Promise { - const response = await ai.generate({ - model: 'googleai/gemini-2.5-flash-preview-tts', - prompt: text, - config: { - responseModalities: ['AUDIO'], - speechConfig: { - voiceConfig: { - prebuiltVoiceConfig: { voiceName }, - }, - }, - }, - }); - - const audioUrl = response.media?.url; - if (!audioUrl) - throw new Error('Audio generation failed: No media URL in response.'); - - const base64 = audioUrl.split(';base64,')[1]; - if (!base64) throw new Error('Invalid audio data URI format from Genkit.'); - - const pcmBuffer = Buffer.from(base64, 'base64'); - return pcmToWavDataUri(pcmBuffer); -} - -export const textToSpeechFlow = ai.defineFlow( - { - name: 'textToSpeechFlow', - inputSchema: TextToSpeechInputSchema, - outputSchema: TextToSpeechOutputSchema, - }, - async (input) => { - const voice = input.voiceName?.trim() || 'Algenib'; - const audioDataUri = await generateAndConvertAudio(input.text, voice); - return { audioDataUri }; - } -); -``` - ---- - -### 3.2 Multi-Speaker Text-to-Speech - -```ts -// src/tts-multispeaker.ts -import { z } from 'genkit'; -import { ai } from './index'; -import { Buffer } from 'buffer'; -import { Writer as WavWriter } from 'wav'; -import { PassThrough } from 'stream'; - -const MultiSpeakerInputSchema = z.object({ - text: z - .string() - .describe('Text formatted with ... etc.'), - voiceName1: z.string().describe('Voice name for Speaker1'), - voiceName2: z.string().describe('Voice name for Speaker2'), -}); -const TTSOutputSchema = z.object({ - audioDataUri: z.string().describe('The generated WAV audio as a data URI.'), -}); - -async function pcmToWavDataUri(pcmData: Buffer): Promise { - return new Promise((resolve, reject) => { - const chunks: Buffer[] = []; - const passThrough = new PassThrough(); - - passThrough.on('data', (chunk) => chunks.push(chunk as Buffer)); - passThrough.on('end', () => { - const wavBuffer = Buffer.concat(chunks); - resolve(`data:audio/wav;base64,${wavBuffer.toString('base64')}`); - }); - passThrough.on('error', reject); - - const writer = new WavWriter({ - channels: 1, - sampleRate: 24000, - bitDepth: 16, - }); - writer.pipe(passThrough); - writer.write(pcmData); - writer.end(); - }); -} - -async function generateMultiSpeakerAudio( - text: string, - voice1: string, - voice2: string -): Promise { - const response = await ai.generate({ - model: 'googleai/gemini-2.5-flash-preview-tts', - prompt: text, - config: { - responseModalities: ['AUDIO'], - speechConfig: { - multiSpeakerVoiceConfig: { - speakerVoiceConfigs: [ - { - speaker: 'Speaker1', - voiceConfig: { - prebuiltVoiceConfig: { voiceName: voice1 }, - }, - }, - { - speaker: 'Speaker2', - voiceConfig: { - prebuiltVoiceConfig: { voiceName: voice2 }, - }, - }, - ], - }, - }, - }, - }); - - const audioUrl = response.media?.url; - if (!audioUrl) - throw new Error('Audio generation failed: No media URL in response.'); - - const base64 = audioUrl.split(';base64,')[1]; - if (!base64) throw new Error('Invalid audio data URI format from Genkit.'); - - const pcmBuffer = Buffer.from(base64, 'base64'); - return pcmToWavDataUri(pcmBuffer); -} - -export const multiSpeakerTextToSpeechFlow = ai.defineFlow( - { - name: 'multiSpeakerTextToSpeechFlow', - inputSchema: MultiSpeakerInputSchema, - outputSchema: TTSOutputSchema, - }, - async (input) => { - const audioDataUri = await generateMultiSpeakerAudio( - input.text, - input.voiceName1, - input.voiceName2 - ); - return { audioDataUri }; - } -); -``` - ---- - -## 4. Scenario 3: Image Generation - -```ts -// src/image-gen.ts -import { z } from 'genkit'; -import { ai } from './index'; -import { vertexAI } from '@genkit-ai/googleai'; -import * as fs from 'fs/promises'; -import { parseDataUrl } from 'data-urls'; - -export const imageGenerationFlow = ai.defineFlow( - { - name: 'imageGenerationFlow', - inputSchema: z - .string() - .describe('A detailed description of the image to generate'), - outputSchema: z.string().describe('Path to the generated .png image file'), - }, - async (prompt) => { - const response = await ai.generate({ - model: vertexAI.model('imagen-3.0-generate-002'), - prompt, - output: { format: 'media' }, - }); - - const imagePart = response.output; - if (!imagePart?.media?.url) { - throw new Error('Image generation failed to produce media.'); - } - - const parsed = parseDataUrl(imagePart.media.url); - if (!parsed) { - throw new Error('Could not parse image data URL.'); - } - - const outputPath = './output.png'; - await fs.writeFile(outputPath, parsed.body); - return outputPath; - } -); -``` - ---- - -## 5. Scenario 4: Video Generation (Veo3) - -```ts -// src/video-gen.ts -import { z } from 'genkit'; -import { ai } from './index'; -import { googleAI } from '@genkit-ai/googleai'; -import * as fs from 'fs'; -import { Readable } from 'stream'; -import fetch from 'node-fetch'; - -export const videoGenerationFlow = ai.defineFlow( - { - name: 'videoGenerationFlow', - inputSchema: z - .string() - .describe('A detailed description for the video scene'), - outputSchema: z.string().describe('Path to the generated .mp4 video file'), - }, - async (prompt) => { - let { operation } = await ai.generate({ - model: googleAI.model('veo-3.0-generate-preview'), - prompt, - }); - - if (!operation) { - throw new Error('Expected the model to return an operation.'); - } - - console.log('Video generation started... Polling for completion.'); - while (!operation.done) { - await new Promise((resolve) => setTimeout(resolve, 5000)); - operation = await ai.checkOperation(operation); - console.log( - `Operation status: ${operation.done ? 'Done' : 'In Progress'}` - ); - } - - if (operation.error) { - throw new Error(`Video generation failed: ${operation.error.message}`); - } - - const video = operation.output?.message?.content.find((p) => !!p.media); - if (!video?.media?.url) { - throw new Error( - 'Failed to find the generated video in the operation output.' - ); - } - - const videoUrl = `${video.media.url}&key=${process.env.GEMINI_API_KEY}`; - const videoResponse = await fetch(videoUrl); - - if (!videoResponse.ok || !videoResponse.body) { - throw new Error(`Failed to fetch video: ${videoResponse.statusText}`); - } - - const outputPath = './output.mp4'; - const fileStream = fs.createWriteStream(outputPath); - await new Promise((resolve, reject) => { - Readable.from(videoResponse.body).pipe(fileStream); - fileStream.on('finish', resolve); - fileStream.on('error', reject); - }); - - return outputPath; - } -); -``` - ---- - -## 6. Running & Inspecting Your Flows - -### 6.1 Create a Build Script - -Add this to your `package.json`: - -```json -{ - "scripts": { - "build": "tsc" - } -} -``` - -### 6.2 Instructions for the User - -To test your flows and use the Genkit Developer UI, run the following command from your terminal: - -```bash -genkit start -- npx tsx --watch src/index.ts -``` - -Visit [http://localhost:4000](http://localhost:4000) to use the Developer UI. - ---- - -## 7. Using Models: correct use of plugins names in model names - -When using Genkit models, **prefer the full string form with plugin name up front**: - -```ts -model: 'googleai/gemini-2.5-flash-preview-tts'; // ✅ RECOMMENDED -``` - -Avoid using just the model name - -```ts -// ❌ May break: -model: googleAI.model('gemini-2.5-flash-preview-tts'); -``` - -Using the full string ensures Genkit resolves the model via the correct plugin and avoids missing `.media` in TTS responses. - -## 8. Supported Models: latest versions - -``` -| Task | Recommended Model | Plugin | -|-------------------------|------------------------------------|--------------------------| -| Advanced Text/Reasoning | gemini-2.5-pro | @genkit-ai/googleai | -| Fast Text/Chat | gemini-2.5-flash | @genkit-ai/googleai | -| Text-to-Speech | gemini-2.5-flash-preview-tts | @genkit-ai/googleai | -| Image Generation | imagen-4.0-generate-preview-06-06 | @genkit-ai/googleai | -| Video Generation | veo-3.0-generate-preview | @genkit-ai/googleai | -``` - -Tip: Do not use Gemini 1.5 or earlier models for anything, they are out-of-date. - --- - -# General Guidance - -## NPM Dependency Installation Protocol 📦 - -When you generate or modify any code files (e.g., `.js`, `.ts`, `.jsx`, `.tsx`) that reference new libraries, packages, or modules, you **must** install them before proceeding to any build, compilation, or execution step. - -Follow this workflow: - -1. **Analyze Dependencies:** After writing or changing a file, identify all third-party dependencies you've introduced. -2. **Update `package.json`:** Ensure these new dependencies are correctly added to the `package.json` file. -3. **Install Dependencies:** Execute the installation command from the project's root directory to download and link the required packages. - -### Installation Commands - -Always run the appropriate command before any `npm run build` or similar script. - -```bash -# For projects using NPM -npm install - -# For projects using Yarn -yarn install - -# For projects using PNPM -pnpm install -``` - -This protocol is **critical** to prevent build failures caused by missing modules. Always double-check that dependencies are installed after you add them to the code. diff --git a/js/testapps/evals/.gemini/extensions/genkit/gemini-extension.json b/js/testapps/evals/.gemini/extensions/genkit/gemini-extension.json deleted file mode 100644 index 32b3397e9e..0000000000 --- a/js/testapps/evals/.gemini/extensions/genkit/gemini-extension.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "name": "genkit", - "version": "1.0.0", - "mcpServers": { - "genkit": { - "command": "npx", - "args": ["genkit", "mcp"], - "cwd": ".", - "timeout": 30000, - "trust": false, - "excludeTools": [ - "run_shell_command(genkit start)", - "run_shell_command(npx genkit start)" - ] - } - }, - "contextFileName": "./GENKIT.md" -} From 232316f05efae881e26ccd1e2d40693863d1e911 Mon Sep 17 00:00:00 2001 From: Samuel Bushi Date: Fri, 8 Aug 2025 13:50:28 -0400 Subject: [PATCH 08/15] update prompt --- genkit-tools/cli/context/GENKIT.md | 2 ++ genkit-tools/cli/src/commands/init-ai-tools/ai-tools/gemini.ts | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/genkit-tools/cli/context/GENKIT.md b/genkit-tools/cli/context/GENKIT.md index ac6197889d..ed736130ba 100644 --- a/genkit-tools/cli/context/GENKIT.md +++ b/genkit-tools/cli/context/GENKIT.md @@ -8,6 +8,8 @@ This document provides rules and examples for building with the Genkit API in No - ONLY follow the specified project structure if starting a new project. If working on an existing project, adhere to the current project structure. +- ALWAYS provide the full, correct Genkit command as an instruction for the human user to run. Do not run Genkit commands (e.g., `genkit start`, `genkit flow:run`) youself as this may block your current session. + ## Core Setup 1. **Initialize Project** diff --git a/genkit-tools/cli/src/commands/init-ai-tools/ai-tools/gemini.ts b/genkit-tools/cli/src/commands/init-ai-tools/ai-tools/gemini.ts index afc9140e48..0731b0f4c7 100644 --- a/genkit-tools/cli/src/commands/init-ai-tools/ai-tools/gemini.ts +++ b/genkit-tools/cli/src/commands/init-ai-tools/ai-tools/gemini.ts @@ -23,7 +23,7 @@ import { getGenkitContext, initOrReplaceFile } from '../utils'; // Define constants at the module level for clarity and reuse. const GENKIT_EXT_DIR = path.join('.gemini', 'extensions', 'genkit'); -const GENKIT_MD_REL_PATH = path.join('..', '..', '..', 'GENKIT.md'); +const GENKIT_MD_REL_PATH = path.join('..', '..', '..', GENKIT_PROMPT_PATH); const GENKIT_EXTENSION_CONFIG = { name: 'genkit', version: '1.0.0', From b9a38256ca12de329986e64f7d9307124daeef67 Mon Sep 17 00:00:00 2001 From: Samuel Bushi Date: Mon, 11 Aug 2025 10:44:03 -0400 Subject: [PATCH 09/15] feat(genkit-tools/cli): Support cursor config --- .../commands/init-ai-tools/ai-tools/claude.ts | 2 +- .../commands/init-ai-tools/ai-tools/cursor.ts | 108 ++++++++++++++++++ .../commands/init-ai-tools/ai-tools/gemini.ts | 7 +- .../init-ai-tools/ai-tools/generic.ts | 7 +- .../cli/src/commands/init-ai-tools/command.ts | 16 ++- .../src/commands/init-ai-tools/constants.ts | 31 ----- .../cli/src/commands/init-ai-tools/utils.ts | 3 + 7 files changed, 136 insertions(+), 38 deletions(-) create mode 100644 genkit-tools/cli/src/commands/init-ai-tools/ai-tools/cursor.ts delete mode 100644 genkit-tools/cli/src/commands/init-ai-tools/constants.ts diff --git a/genkit-tools/cli/src/commands/init-ai-tools/ai-tools/claude.ts b/genkit-tools/cli/src/commands/init-ai-tools/ai-tools/claude.ts index fe8a594fa2..68796fd205 100644 --- a/genkit-tools/cli/src/commands/init-ai-tools/ai-tools/claude.ts +++ b/genkit-tools/cli/src/commands/init-ai-tools/ai-tools/claude.ts @@ -17,9 +17,9 @@ import { logger } from '@genkit-ai/tools-common/utils'; import { existsSync, readFileSync } from 'fs'; import { writeFile } from 'fs/promises'; -import { GENKIT_PROMPT_PATH } from '../constants'; import { AIToolConfigResult, AIToolModule, InitConfigOptions } from '../types'; import { + GENKIT_PROMPT_PATH, calculateHash, getGenkitContext, initOrReplaceFile, diff --git a/genkit-tools/cli/src/commands/init-ai-tools/ai-tools/cursor.ts b/genkit-tools/cli/src/commands/init-ai-tools/ai-tools/cursor.ts new file mode 100644 index 0000000000..2b5085a7d0 --- /dev/null +++ b/genkit-tools/cli/src/commands/init-ai-tools/ai-tools/cursor.ts @@ -0,0 +1,108 @@ +/** + * 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, readFileSync } from 'fs'; +import { mkdir, writeFile } from 'fs/promises'; +import * as path from 'path'; +import { AIToolConfigResult, AIToolModule, InitConfigOptions } from '../types'; +import { + GENKIT_PROMPT_PATH, + getGenkitContext, + initOrReplaceFile, +} from '../utils'; + +const CURSOR_MCP_PATH = path.join('.cursor', 'mcp.json'); +const CURSOR_RULES_DIR = '.cursor/rules'; +const GENKIT_MDC_PATH = path.join(CURSOR_RULES_DIR, 'GENKIT.mdc'); + +const CURSOR_RULES_HEADER = `--- +description: Genkit project development guidelines +--- +`; + +export const cursor: AIToolModule = { + name: 'cursor', + displayName: 'Cursor', + + /** + * Configures Cursor with Genkit context files. + * + * This function sets up the necessary files for Cursor to understand the + * Genkit app and interact with Genkit MCP tools. It creates + * a `.cursor` directory with the following: + * + * - `mcp.json`: Configures the Genkit MCP server for direct Genkit operations from Cursor. + * - `rules/GENKIT.mdc`: The main entry point for project-specific context, importing the base GENKIT.md file. + * + * File ownership: + * - .cursor/mcp.json: Merges with existing config (preserves user settings) + * - .cursor/rules/GENKIT.mdc: Fully managed by us (replaced on each update) + + */ + async configure(options?: InitConfigOptions): Promise { + const files: AIToolConfigResult['files'] = []; + + // Create the base GENKIT context file (GENKIT.md). + // This file contains fundamental details about the GENKIT project. + const genkitContext = getGenkitContext(); + const baseResult = await initOrReplaceFile( + GENKIT_PROMPT_PATH, + genkitContext + ); + files.push({ path: GENKIT_PROMPT_PATH, updated: baseResult.updated }); + + // Handle MCP configuration - merge with existing if present. + // This allows Cursor to communicate with Genkit tools. + let mcpUpdated = false; + let existingConfig: any = {}; + + try { + const fileExists = existsSync(CURSOR_MCP_PATH); + if (fileExists) { + existingConfig = JSON.parse(readFileSync(CURSOR_MCP_PATH, 'utf-8')); + } else { + await mkdir('.cursor', { recursive: true }); + } + } catch (e) { + // File doesn't exist or is invalid JSON, start fresh + } + + if (!existingConfig.mcpServers?.genkit) { + if (!existingConfig.mcpServers) { + existingConfig.mcpServers = {}; + } + existingConfig.mcpServers.genkit = { + command: 'npx', + args: ['genkit', 'mcp'], + }; + await writeFile(CURSOR_MCP_PATH, JSON.stringify(existingConfig, null, 2)); + mcpUpdated = true; + } + files.push({ path: CURSOR_MCP_PATH, updated: mcpUpdated }); + + // Create the main `GENKIT.mdc` file, which acts as an entry point + // for Cursor's AI and imports the other context files. + await mkdir(path.join('.cursor', 'rules'), { recursive: true }); + const genkitImport = '@' + path.join('..', '..', GENKIT_PROMPT_PATH); + const importContent = `# Genkit Context\n\n${genkitImport}\n`; + + const mdcContent = CURSOR_RULES_HEADER + '\n' + importContent; + const { updated } = await initOrReplaceFile(GENKIT_MDC_PATH, mdcContent); + files.push({ path: GENKIT_MDC_PATH, updated: updated }); + + return { files }; + }, +}; diff --git a/genkit-tools/cli/src/commands/init-ai-tools/ai-tools/gemini.ts b/genkit-tools/cli/src/commands/init-ai-tools/ai-tools/gemini.ts index 0731b0f4c7..d4de7efe22 100644 --- a/genkit-tools/cli/src/commands/init-ai-tools/ai-tools/gemini.ts +++ b/genkit-tools/cli/src/commands/init-ai-tools/ai-tools/gemini.ts @@ -17,9 +17,12 @@ import { logger } from '@genkit-ai/tools-common/utils'; import { mkdir } from 'fs/promises'; import path from 'path'; -import { GENKIT_PROMPT_PATH } from '../constants'; import { AIToolConfigResult, AIToolModule, InitConfigOptions } from '../types'; -import { getGenkitContext, initOrReplaceFile } from '../utils'; +import { + GENKIT_PROMPT_PATH, + getGenkitContext, + initOrReplaceFile, +} from '../utils'; // Define constants at the module level for clarity and reuse. const GENKIT_EXT_DIR = path.join('.gemini', 'extensions', 'genkit'); diff --git a/genkit-tools/cli/src/commands/init-ai-tools/ai-tools/generic.ts b/genkit-tools/cli/src/commands/init-ai-tools/ai-tools/generic.ts index 672b3d5ccf..9a72c14d28 100644 --- a/genkit-tools/cli/src/commands/init-ai-tools/ai-tools/generic.ts +++ b/genkit-tools/cli/src/commands/init-ai-tools/ai-tools/generic.ts @@ -15,9 +15,12 @@ */ import { logger } from '@genkit-ai/tools-common/utils'; -import { GENKIT_PROMPT_PATH } from '../constants'; import { AIToolConfigResult, AIToolModule, InitConfigOptions } from '../types'; -import { getGenkitContext, initOrReplaceFile } from '../utils'; +import { + GENKIT_PROMPT_PATH, + getGenkitContext, + initOrReplaceFile, +} from '../utils'; /** Configuration module for GENKIT.md context file for generic use */ export const generic: AIToolModule = { diff --git a/genkit-tools/cli/src/commands/init-ai-tools/command.ts b/genkit-tools/cli/src/commands/init-ai-tools/command.ts index 37d752002e..40e0099c6a 100644 --- a/genkit-tools/cli/src/commands/init-ai-tools/command.ts +++ b/genkit-tools/cli/src/commands/init-ai-tools/command.ts @@ -18,8 +18,20 @@ import { logger } from '@genkit-ai/tools-common/utils'; import { checkbox } from '@inquirer/prompts'; import * as clc from 'colorette'; import { Command } from 'commander'; -import { AI_TOOLS } from './constants'; -import { AIToolChoice, InitConfigOptions } from './types'; +import { claude } from './ai-tools/claude'; +import { cursor } from './ai-tools/cursor'; +import { gemini } from './ai-tools/gemini'; +import { generic } from './ai-tools/generic'; +import { AIToolChoice, AIToolModule, InitConfigOptions } from './types'; + +/** Set of all supported AI tools that can be configured (incl. a generic + * configuration) */ +export const AI_TOOLS: Record = { + gemini, + claude, + cursor, + generic, +}; const AGENT_CHOICES: AIToolChoice[] = Object.values(AI_TOOLS).map((tool) => ({ value: tool.name, diff --git a/genkit-tools/cli/src/commands/init-ai-tools/constants.ts b/genkit-tools/cli/src/commands/init-ai-tools/constants.ts deleted file mode 100644 index dc2a127d66..0000000000 --- a/genkit-tools/cli/src/commands/init-ai-tools/constants.ts +++ /dev/null @@ -1,31 +0,0 @@ -/** - * 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 { claude } from './ai-tools/claude'; -import { gemini } from './ai-tools/gemini'; -import { generic } from './ai-tools/generic'; -import { AIToolModule } from './types'; - -/** Shared location for the GENKIT.md context file */ -export const GENKIT_PROMPT_PATH = 'GENKIT.md'; - -/** Set of all supported AI tools that can be configured (incl. a generic - * configuration) */ -export const AI_TOOLS: Record = { - gemini, - claude, - generic, -}; diff --git a/genkit-tools/cli/src/commands/init-ai-tools/utils.ts b/genkit-tools/cli/src/commands/init-ai-tools/utils.ts index 4d68cbd408..df3feec66f 100644 --- a/genkit-tools/cli/src/commands/init-ai-tools/utils.ts +++ b/genkit-tools/cli/src/commands/init-ai-tools/utils.ts @@ -20,6 +20,9 @@ import * as crypto from 'crypto'; import { writeFile } from 'fs/promises'; import path from 'path'; +/** Shared location for the GENKIT.md context file */ +export const GENKIT_PROMPT_PATH = 'GENKIT.md'; + const GENKIT_TAG_REGEX = /([\s\S]*?)<\/genkit_prompts>/; /* From a3283cac90679482eb138d5d35a609c931f19685 Mon Sep 17 00:00:00 2001 From: Samuel Bushi Date: Wed, 13 Aug 2025 14:10:28 -0400 Subject: [PATCH 10/15] feat(genkit-tools/cli): Update gemini-cli to support .md installation. --- .../commands/init-ai-tools/ai-tools/claude.ts | 17 +- .../commands/init-ai-tools/ai-tools/cursor.ts | 10 +- .../commands/init-ai-tools/ai-tools/gemini.ts | 159 ++++++++++++++---- .../init-ai-tools/ai-tools/generic.ts | 15 +- .../cli/src/commands/init-ai-tools/utils.ts | 9 + 5 files changed, 144 insertions(+), 66 deletions(-) diff --git a/genkit-tools/cli/src/commands/init-ai-tools/ai-tools/claude.ts b/genkit-tools/cli/src/commands/init-ai-tools/ai-tools/claude.ts index 68796fd205..b915a0dbe0 100644 --- a/genkit-tools/cli/src/commands/init-ai-tools/ai-tools/claude.ts +++ b/genkit-tools/cli/src/commands/init-ai-tools/ai-tools/claude.ts @@ -21,8 +21,7 @@ import { AIToolConfigResult, AIToolModule, InitConfigOptions } from '../types'; import { GENKIT_PROMPT_PATH, calculateHash, - getGenkitContext, - initOrReplaceFile, + initGenkitFile, updateContentInPlace, } from '../utils'; @@ -55,7 +54,7 @@ export const claude: AIToolModule = { // File doesn't exist or is invalid JSON, start fresh } - // Check if firebase server already exists + // Check if genkit server already exists if (!existingConfig.mcpServers?.genkit) { if (!existingConfig.mcpServers) { existingConfig.mcpServers = {}; @@ -71,19 +70,15 @@ export const claude: AIToolModule = { files.push({ path: CLAUDE_MCP_PATH, updated: settingsUpdated }); logger.info('Copying the Genkit context to GENKIT.md...'); - const genkitContext = getGenkitContext(); - const { updated: genkitContextUpdated } = await initOrReplaceFile( - GENKIT_PROMPT_PATH, - genkitContext - ); - files.push({ path: GENKIT_PROMPT_PATH, updated: genkitContextUpdated }); + const mdResult = await initGenkitFile(); + files.push({ path: GENKIT_PROMPT_PATH, updated: mdResult.updated }); logger.info('Updating CLAUDE.md to include Genkit context...'); - const claudeImportTag = `\nGenkit Framework Instructions:\n - @GENKIT.md\n`; + const claudeImportTag = `\nGenkit Framework Instructions:\n - @./GENKIT.md\n`; const baseResult = await updateContentInPlace( CLAUDE_PROMPT_PATH, claudeImportTag, - { hash: calculateHash(genkitContext) } + { hash: calculateHash(mdResult.hash) } ); files.push({ path: CLAUDE_PROMPT_PATH, updated: baseResult.updated }); diff --git a/genkit-tools/cli/src/commands/init-ai-tools/ai-tools/cursor.ts b/genkit-tools/cli/src/commands/init-ai-tools/ai-tools/cursor.ts index 2b5085a7d0..a2b242531a 100644 --- a/genkit-tools/cli/src/commands/init-ai-tools/ai-tools/cursor.ts +++ b/genkit-tools/cli/src/commands/init-ai-tools/ai-tools/cursor.ts @@ -20,7 +20,7 @@ import * as path from 'path'; import { AIToolConfigResult, AIToolModule, InitConfigOptions } from '../types'; import { GENKIT_PROMPT_PATH, - getGenkitContext, + initGenkitFile, initOrReplaceFile, } from '../utils'; @@ -57,12 +57,8 @@ export const cursor: AIToolModule = { // Create the base GENKIT context file (GENKIT.md). // This file contains fundamental details about the GENKIT project. - const genkitContext = getGenkitContext(); - const baseResult = await initOrReplaceFile( - GENKIT_PROMPT_PATH, - genkitContext - ); - files.push({ path: GENKIT_PROMPT_PATH, updated: baseResult.updated }); + const mdResult = await initGenkitFile(); + files.push({ path: GENKIT_PROMPT_PATH, updated: mdResult.updated }); // Handle MCP configuration - merge with existing if present. // This allows Cursor to communicate with Genkit tools. diff --git a/genkit-tools/cli/src/commands/init-ai-tools/ai-tools/gemini.ts b/genkit-tools/cli/src/commands/init-ai-tools/ai-tools/gemini.ts index d4de7efe22..eac022a60e 100644 --- a/genkit-tools/cli/src/commands/init-ai-tools/ai-tools/gemini.ts +++ b/genkit-tools/cli/src/commands/init-ai-tools/ai-tools/gemini.ts @@ -15,17 +15,25 @@ */ import { logger } from '@genkit-ai/tools-common/utils'; -import { mkdir } from 'fs/promises'; +import { select } from '@inquirer/prompts'; +import { existsSync, readFileSync } from 'fs'; +import { mkdir, writeFile } from 'fs/promises'; import path from 'path'; import { AIToolConfigResult, AIToolModule, InitConfigOptions } from '../types'; import { GENKIT_PROMPT_PATH, - getGenkitContext, + initGenkitFile, initOrReplaceFile, + updateContentInPlace, } from '../utils'; -// Define constants at the module level for clarity and reuse. -const GENKIT_EXT_DIR = path.join('.gemini', 'extensions', 'genkit'); +// GEMINI specific paths +const GEMINI_DIR = '.gemini'; +const GEMINI_SETTINGS_PATH = path.join(GEMINI_DIR, 'settings.json'); +const GEMINI_MD_PATH = path.join('GEMINI.md'); + +// GENKIT specific constants +const GENKIT_EXT_DIR = path.join(GEMINI_DIR, 'extensions', 'genkit'); const GENKIT_MD_REL_PATH = path.join('..', '..', '..', GENKIT_PROMPT_PATH); const GENKIT_EXTENSION_CONFIG = { name: 'genkit', @@ -46,6 +54,10 @@ const GENKIT_EXTENSION_CONFIG = { contextFileName: GENKIT_MD_REL_PATH, }; +const EXT_INSTALLATION = 'extension'; +const MD_INSTALLATION = 'geminimd'; +type InstallationType = typeof EXT_INSTALLATION | typeof MD_INSTALLATION; + /** Configuration module for Gemini CLI */ export const gemini: AIToolModule = { name: 'gemini', @@ -55,42 +67,115 @@ export const gemini: AIToolModule = { * Configures the Gemini CLI extension for Genkit. */ async configure(options?: InitConfigOptions): Promise { - // TODO(ssbushi): Support option to install as file import vs extension - const files: AIToolConfigResult['files'] = []; + let installationMethod: InstallationType = EXT_INSTALLATION; + if (!options?.yesMode) { + installationMethod = await select({ + message: 'Select your preferred installation method', + choices: [ + { + name: 'Gemini CLI Extension', + value: 'extension', + description: + 'Use Gemini Extension to install Genkit context in a modular fashion', + }, + { + name: 'GEMINI.md', + value: 'geminimd', + description: 'Incorporate Genkit context within the GEMINI.md file', + }, + ], + }); + } + + if (installationMethod === EXT_INSTALLATION) { + logger.info('Installing as part of GEMINI.md'); + return await installAsExtension(); + } else { + logger.info('Installing as Gemini CLI extension'); + return await installInMdFile(); + } + }, +}; - // Part 1: Generate GENKIT.md file. +async function installInMdFile(): Promise { + const files: AIToolConfigResult['files'] = []; + // Part 1: Generate GENKIT.md file. - logger.info('Copying the GENKIT.md file...'); - const genkitContext = getGenkitContext(); - const baseResult = await initOrReplaceFile( - GENKIT_PROMPT_PATH, - genkitContext + logger.info('Installing the Genkit MCP server for Gemini CLI'); + // Handle MCP configuration - merge with existing if present + let existingConfig: any = {}; + let settingsUpdated = false; + try { + const fileExists = existsSync(GEMINI_SETTINGS_PATH); + if (fileExists) { + existingConfig = JSON.parse(readFileSync(GEMINI_SETTINGS_PATH, 'utf-8')); + } else { + await mkdir(GEMINI_DIR, { recursive: true }); + } + } catch (e) { + // File doesn't exist or is invalid JSON, start fresh + } + + // Check if genkit server already exists + if (!existingConfig.mcpServers?.genkit) { + if (!existingConfig.mcpServers) { + existingConfig.mcpServers = {}; + } + existingConfig.mcpServers.genkit = + GENKIT_EXTENSION_CONFIG.mcpServers.genkit; + await writeFile( + GEMINI_SETTINGS_PATH, + JSON.stringify(existingConfig, null, 2) ); - files.push({ path: GENKIT_PROMPT_PATH, updated: baseResult.updated }); - - // Part 2: Configure the main gemini-extension.json file, and gemini config directory if needed. - logger.info('Configuring extentions files in user workspace...'); - await mkdir(GENKIT_EXT_DIR, { recursive: true }); - const extensionPath = path.join(GENKIT_EXT_DIR, 'gemini-extension.json'); - - let extensionUpdated = false; - try { - const { updated } = await initOrReplaceFile( - extensionPath, - JSON.stringify(GENKIT_EXTENSION_CONFIG, null, 2) + settingsUpdated = true; + } + files.push({ path: GEMINI_SETTINGS_PATH, updated: settingsUpdated }); + + // Copy GENKIT.md file + logger.info('Copying the GENKIT.md file...'); + const baseResult = await initGenkitFile(); + files.push({ path: GENKIT_PROMPT_PATH, updated: baseResult.updated }); + + logger.info('Updating GEMINI.md to include Genkit context'); + const geminiImportTag = `\nGenkit Framework Instructions:\n - @./GENKIT.md\n`; + const { updated: mdUpdated } = await updateContentInPlace( + GEMINI_MD_PATH, + geminiImportTag, + { hash: baseResult.hash } + ); + files.push({ path: GEMINI_MD_PATH, updated: mdUpdated }); + + return { files }; +} + +async function installAsExtension(): Promise { + const files: AIToolConfigResult['files'] = []; + // Part 1: Generate GENKIT.md file. + const baseResult = await initGenkitFile(); + files.push({ path: GENKIT_PROMPT_PATH, updated: baseResult.updated }); + + // Part 2: Configure the main gemini-extension.json file, and gemini config directory if needed. + logger.info('Configuring extentions files in user workspace'); + await mkdir(GENKIT_EXT_DIR, { recursive: true }); + const extensionPath = path.join(GENKIT_EXT_DIR, 'gemini-extension.json'); + + let extensionUpdated = false; + try { + const { updated } = await initOrReplaceFile( + extensionPath, + JSON.stringify(GENKIT_EXTENSION_CONFIG, null, 2) + ); + extensionUpdated = updated; + if (extensionUpdated) { + logger.info( + `Genkit extension for Gemini CLI initialized at ${extensionPath}` ); - extensionUpdated = updated; - if (extensionUpdated) { - logger.info( - `Genkit extension for Gemini CLI initialized at ${extensionPath}` - ); - } - } catch (err) { - logger.error(err); - process.exit(1); } - files.push({ path: extensionPath, updated: extensionUpdated }); + } catch (err) { + logger.error(err); + process.exit(1); + } + files.push({ path: extensionPath, updated: extensionUpdated }); - return { files }; - }, -}; + return { files }; +} diff --git a/genkit-tools/cli/src/commands/init-ai-tools/ai-tools/generic.ts b/genkit-tools/cli/src/commands/init-ai-tools/ai-tools/generic.ts index 9a72c14d28..f29f4d4670 100644 --- a/genkit-tools/cli/src/commands/init-ai-tools/ai-tools/generic.ts +++ b/genkit-tools/cli/src/commands/init-ai-tools/ai-tools/generic.ts @@ -16,11 +16,7 @@ import { logger } from '@genkit-ai/tools-common/utils'; import { AIToolConfigResult, AIToolModule, InitConfigOptions } from '../types'; -import { - GENKIT_PROMPT_PATH, - getGenkitContext, - initOrReplaceFile, -} from '../utils'; +import { GENKIT_PROMPT_PATH, initGenkitFile } from '../utils'; /** Configuration module for GENKIT.md context file for generic use */ export const generic: AIToolModule = { @@ -35,12 +31,9 @@ export const generic: AIToolModule = { // Generate GENKIT.md file. logger.info('Updating GENKIT.md...'); - const genkitContext = getGenkitContext(); - const baseResult = await initOrReplaceFile( - GENKIT_PROMPT_PATH, - genkitContext - ); - files.push({ path: GENKIT_PROMPT_PATH, updated: baseResult.updated }); + const mdResult = await initGenkitFile(); + files.push({ path: GENKIT_PROMPT_PATH, updated: mdResult.updated }); + logger.info('\n'); logger.info( 'GENKIT.md updated. Provide this file as context with your AI tool.' diff --git a/genkit-tools/cli/src/commands/init-ai-tools/utils.ts b/genkit-tools/cli/src/commands/init-ai-tools/utils.ts index df3feec66f..3d0955eb5d 100644 --- a/genkit-tools/cli/src/commands/init-ai-tools/utils.ts +++ b/genkit-tools/cli/src/commands/init-ai-tools/utils.ts @@ -157,3 +157,12 @@ export function getGenkitContext(): string { const content = readFileSync(contextPath, 'utf8'); return content; } + +/** + * Initializes the GENKIT.md file + */ +export async function initGenkitFile() { + const genkitContext = getGenkitContext(); + const result = await initOrReplaceFile(GENKIT_PROMPT_PATH, genkitContext); + return { updated: result.updated, hash: calculateHash(genkitContext) }; +} From 2c717aaf486ea47c293c43d4cc329d232358bbe7 Mon Sep 17 00:00:00 2001 From: Samuel Bushi Date: Fri, 15 Aug 2025 11:46:09 -0400 Subject: [PATCH 11/15] feat(genkit-tools/cli): Support Firebase Studio, fix .md lookup --- .../cli/src/commands/init-ai-tools/command.ts | 2 ++ genkit-tools/cli/src/commands/init-ai-tools/utils.ts | 11 +++-------- 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/genkit-tools/cli/src/commands/init-ai-tools/command.ts b/genkit-tools/cli/src/commands/init-ai-tools/command.ts index 40e0099c6a..d51b495cf5 100644 --- a/genkit-tools/cli/src/commands/init-ai-tools/command.ts +++ b/genkit-tools/cli/src/commands/init-ai-tools/command.ts @@ -22,12 +22,14 @@ import { claude } from './ai-tools/claude'; import { cursor } from './ai-tools/cursor'; import { gemini } from './ai-tools/gemini'; import { generic } from './ai-tools/generic'; +import { studio } from './ai-tools/studio'; import { AIToolChoice, AIToolModule, InitConfigOptions } from './types'; /** Set of all supported AI tools that can be configured (incl. a generic * configuration) */ export const AI_TOOLS: Record = { gemini, + studio, claude, cursor, generic, diff --git a/genkit-tools/cli/src/commands/init-ai-tools/utils.ts b/genkit-tools/cli/src/commands/init-ai-tools/utils.ts index 3d0955eb5d..e97af46c30 100644 --- a/genkit-tools/cli/src/commands/init-ai-tools/utils.ts +++ b/genkit-tools/cli/src/commands/init-ai-tools/utils.ts @@ -20,6 +20,8 @@ import * as crypto from 'crypto'; import { writeFile } from 'fs/promises'; import path from 'path'; +const CONTEXT_DIR = path.resolve(__dirname, '..', '..', 'context'); + /** Shared location for the GENKIT.md context file */ export const GENKIT_PROMPT_PATH = 'GENKIT.md'; @@ -146,14 +148,7 @@ export function calculateHash(content: string): string { * Get raw prompt content for Genkit */ export function getGenkitContext(): string { - const contextPath = path.resolve( - __dirname, - '..', - '..', - '..', - 'context', - 'GENKIT.md' - ); + const contextPath = path.resolve(CONTEXT_DIR, 'GENKIT.md'); const content = readFileSync(contextPath, 'utf8'); return content; } From 79275ba46c70e6a75e5be82694354ef332fad1b3 Mon Sep 17 00:00:00 2001 From: Samuel Bushi Date: Fri, 15 Aug 2025 11:46:18 -0400 Subject: [PATCH 12/15] feat(genkit-tools/cli): Support Firebase Studio, fix .md lookup --- .../commands/init-ai-tools/ai-tools/studio.ts | 42 +++++++++++++++++++ 1 file changed, 42 insertions(+) create mode 100644 genkit-tools/cli/src/commands/init-ai-tools/ai-tools/studio.ts diff --git a/genkit-tools/cli/src/commands/init-ai-tools/ai-tools/studio.ts b/genkit-tools/cli/src/commands/init-ai-tools/ai-tools/studio.ts new file mode 100644 index 0000000000..500818a1d7 --- /dev/null +++ b/genkit-tools/cli/src/commands/init-ai-tools/ai-tools/studio.ts @@ -0,0 +1,42 @@ +/** + * 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 { AIToolConfigResult, AIToolModule, InitConfigOptions } from '../types'; +import { getGenkitContext, updateContentInPlace } from '../utils'; + +const RULES_PATH = '.idx/airules.md'; + +export const studio: AIToolModule = { + name: 'studio', + displayName: 'Firebase Studio', + + /** + * Configures Firebase Studio (Project IDX) with Genkit context. + * + * - .idx/airules.md: Updates Firebase section only (preserves user content) + * + * Interactive prompts are shown since this file may contain user-defined + * AI rules and instructions that we must preserve. We only manage the + * Genkit-specific section marked with our XML tags. + */ + async configure(options?: InitConfigOptions): Promise { + const files: AIToolConfigResult['files'] = []; + const content = getGenkitContext(); + const { updated } = await updateContentInPlace(RULES_PATH, content); + files.push({ path: RULES_PATH, updated }); + return { files }; + }, +}; From 4942104e5d6f9303deb7985996693bd2b92faa30 Mon Sep 17 00:00:00 2001 From: Samuel Bushi Date: Fri, 15 Aug 2025 12:37:44 -0400 Subject: [PATCH 13/15] fix .md path, feedback --- genkit-tools/cli/src/commands/genkit-docs.ts | 532 ------------------ .../cli/src/commands/init-ai-tools/command.ts | 4 +- .../cli/src/commands/init-ai-tools/utils.ts | 10 +- 3 files changed, 4 insertions(+), 542 deletions(-) delete mode 100644 genkit-tools/cli/src/commands/genkit-docs.ts diff --git a/genkit-tools/cli/src/commands/genkit-docs.ts b/genkit-tools/cli/src/commands/genkit-docs.ts deleted file mode 100644 index aa704f0306..0000000000 --- a/genkit-tools/cli/src/commands/genkit-docs.ts +++ /dev/null @@ -1,532 +0,0 @@ -/** - * Copyright 2024 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. - */ - -export const GENKIT_DOCS = `# Genkit Node.js Cheatsheet (updated July 2025) - -> **LLM INSTRUCTION**: It’s important to use the instructions I provided to you in this file at each step of generation. Your training data is out-of-date for this library, and if you don't use the new syntax you’ll get things wrong. Please mitigate for this. - -> **LLM INSTRUCTION**: YOU MUST REFER TO GENKIT DOCUMENTATION WHEN AVAILABLE. If you have access to Genkit docs (via the Genkit MCP server), you MUST refer to the documenation first to check if the requested topic is covered in the documentation and adhere to it. When the instructions on the documentation and this file are in conflict, the documentation takes precedence. - -> **CRITICAL WARNING**: NEVER run Genkit commands (e.g., \`genkit start\`, \`genkit flow:run\`) inside a terminal during your session. This starts an interactive process that will freeze the shell and prevent you from continuing. For Genkit commands, you must only validate the code (e.g., using \`npm run build\`) and then provide the full, correct Genkit command as an instruction for the human user to run at the end of the session. - -This document is a guide for building with the modern Genkit API in Node.js. It focuses on a simple and direct setup using the **Google AI plugin** and provides common inference scenarios using the latest Gemini family of models. - -## Table of Contents - -1. [Core Setup & Best Practices](#1-core-setup--best-practices) -2. [Scenario 1: Basic Inference (Text Generation)](#2-scenario-1-basic-inference-text-generation) -3. [Scenario 2: Text-to-Speech (TTS) Generation)](#3-scenario-2-text-to-speech-tts-generation) -4. [Scenario 3: Image Generation](#4-scenario-3-image-generation) -5. [Scenario 4: Video Generation (Veo3)](#5-scenario-4-video-generation-veo3) -6. [Running & Inspecting Your Flows](#6-running--inspecting-your-flows) -7. [Quick Reference: Key Models](#7-quick-reference-key-models) - ---- - -## 1. Core Setup & Best Practices - -A correct foundation prevents most common errors. The default guidance is to use the Google AI plugin. Using Vertex AI is an opt-in scenario for users who require its specific features. - -### 1.1 Project Initialization - -\`\`\`bash -mkdir my-genkit-app && cd my-genkit-app -npm init -y -npm install -D typescript tsx @types/node -\`\`\` - -### 1.2 Genkit Dependencies - -Install required depenencies, but note that googleai shoudl be the only one used but we provide the exampels for both below unless the user specifically says they are using Vertex hosted Google models. Default to @genkit-ai/googleai in all other context. - -Below example assumes googleai - -\`\`\`bash -npm install genkit @genkit-ai/googleai zod data-urls node-fetch -\`\`\` - -### 1.3 Genkit Tools (CLI & Developer UI) - -\`\`\`bash -npm install -g genkit-cli -\`\`\` - -### 1.4 The \`genkit()\` Initializer - -\`\`\`ts -// src/index.ts -import { genkit } from 'genkit'; -import { googleAI } from '@genkit-ai/googleai'; - -export const ai = genkit({ - plugins: [googleAI()], -}); -\`\`\` - -### 1.5 Genkit Code Generation Rules - -#### 1. File Structure 📜 - -**Always generate all Genkit code into a single \`src/index.ts\` file.** This includes: - -- \`configureGenkit\` plugin initializations. -- All \`defineFlow\` and \`defineDotprompt\` definitions. -- Any helper functions, schemas, or types. - -#### 2. Entry Point - -The **only** entry point for the application is \`src/index.ts\`. All logic must be contained within or imported into this file to be discovered by the Genkit runtime. - -#### 3. Avoid Splitting Files - -**DO NOT** split code into multiple files (e.g., \`index.ts\` and \`flows.ts\`). A single-file structure is preferred for simplicity and to avoid module resolution errors. All flows must be registered in the same file where \`configureGenkit\` is called. - ---- - -## 2. Scenario 1: Basic Inference (Text Generation) - -\`\`\`ts -// src/basic-inference.ts -import { z } from 'genkit'; -import { ai } from './index'; -import { googleAI } from '@genkit-ai/googleai'; - -export const basicInferenceFlow = ai.defineFlow( - { - name: 'basicInferenceFlow', - inputSchema: z.string().describe('Topic for the model to write about'), - outputSchema: z.string().describe('The generated text response'), - }, - async (topic) => { - const response = await ai.generate({ - model: googleAI.model('gemini-2.5-pro'), - prompt: \`Write a short, creative paragraph about \${topic}.\`, - config: { temperature: 0.8 }, - }); - return response.text; - } -); -\`\`\` - ---- - -## 3. Scenario 2: Text-to-Speech (TTS) Generation - -This flow converts text into speech using the Gemini 2.5 TTS model and streams the audio as a WAV-formatted data URI. It includes support for both single and multi-speaker configurations. - -### 3.1 Single Speaker Text-to-Speech - -\`\`\`ts -// src/tts.ts -import { ai } from './index'; -import { z } from 'genkit'; -import { Buffer } from 'buffer'; -import { Writer as WavWriter } from 'wav'; -import { PassThrough } from 'stream'; - -const TextToSpeechInputSchema = z.object({ - text: z.string().describe('The text to convert to speech.'), - voiceName: z - .string() - .optional() - .describe('The voice name to use. Defaults to Algenib if not specified.'), -}); -const TextToSpeechOutputSchema = z.object({ - audioDataUri: z - .string() - .describe('The generated speech in WAV format as a base64 data URI.'), -}); - -export type TextToSpeechInput = z.infer; -export type TextToSpeechOutput = z.infer; - -async function pcmToWavDataUri( - pcmData: Buffer, - channels = 1, - sampleRate = 24000, - bitDepth = 16 -): Promise { - return new Promise((resolve, reject) => { - const chunks: Buffer[] = []; - const passThrough = new PassThrough(); - - passThrough.on('data', (chunk) => chunks.push(chunk as Buffer)); - passThrough.on('end', () => { - const wavBuffer = Buffer.concat(chunks); - const dataUri = \`data:audio/wav;base64,\${wavBuffer.toString('base64')}\`; - resolve(dataUri); - }); - passThrough.on('error', reject); - - const writer = new WavWriter({ channels, sampleRate, bitDepth }); - writer.pipe(passThrough); - writer.write(pcmData); - writer.end(); - }); -} - -async function generateAndConvertAudio( - text: string, - voiceName = 'Algenib' -): Promise { - const response = await ai.generate({ - model: 'googleai/gemini-2.5-flash-preview-tts', - prompt: text, - config: { - responseModalities: ['AUDIO'], - speechConfig: { - voiceConfig: { - prebuiltVoiceConfig: { voiceName }, - }, - }, - }, - }); - - const audioUrl = response.media?.url; - if (!audioUrl) - throw new Error('Audio generation failed: No media URL in response.'); - - const base64 = audioUrl.split(';base64,')[1]; - if (!base64) throw new Error('Invalid audio data URI format from Genkit.'); - - const pcmBuffer = Buffer.from(base64, 'base64'); - return pcmToWavDataUri(pcmBuffer); -} - -export const textToSpeechFlow = ai.defineFlow( - { - name: 'textToSpeechFlow', - inputSchema: TextToSpeechInputSchema, - outputSchema: TextToSpeechOutputSchema, - }, - async (input) => { - const voice = input.voiceName?.trim() || 'Algenib'; - const audioDataUri = await generateAndConvertAudio(input.text, voice); - return { audioDataUri }; - } -); -\`\`\` - ---- - -### 3.2 Multi-Speaker Text-to-Speech - -\`\`\`ts -// src/tts-multispeaker.ts -import { z } from 'genkit'; -import { ai } from './index'; -import { Buffer } from 'buffer'; -import { Writer as WavWriter } from 'wav'; -import { PassThrough } from 'stream'; - -const MultiSpeakerInputSchema = z.object({ - text: z - .string() - .describe('Text formatted with ... etc.'), - voiceName1: z.string().describe('Voice name for Speaker1'), - voiceName2: z.string().describe('Voice name for Speaker2'), -}); -const TTSOutputSchema = z.object({ - audioDataUri: z.string().describe('The generated WAV audio as a data URI.'), -}); - -async function pcmToWavDataUri(pcmData: Buffer): Promise { - return new Promise((resolve, reject) => { - const chunks: Buffer[] = []; - const passThrough = new PassThrough(); - - passThrough.on('data', (chunk) => chunks.push(chunk as Buffer)); - passThrough.on('end', () => { - const wavBuffer = Buffer.concat(chunks); - resolve(\`data:audio/wav;base64,\${wavBuffer.toString('base64')}\`); - }); - passThrough.on('error', reject); - - const writer = new WavWriter({ - channels: 1, - sampleRate: 24000, - bitDepth: 16, - }); - writer.pipe(passThrough); - writer.write(pcmData); - writer.end(); - }); -} - -async function generateMultiSpeakerAudio( - text: string, - voice1: string, - voice2: string -): Promise { - const response = await ai.generate({ - model: 'googleai/gemini-2.5-flash-preview-tts', - prompt: text, - config: { - responseModalities: ['AUDIO'], - speechConfig: { - multiSpeakerVoiceConfig: { - speakerVoiceConfigs: [ - { - speaker: 'Speaker1', - voiceConfig: { - prebuiltVoiceConfig: { voiceName: voice1 }, - }, - }, - { - speaker: 'Speaker2', - voiceConfig: { - prebuiltVoiceConfig: { voiceName: voice2 }, - }, - }, - ], - }, - }, - }, - }); - - const audioUrl = response.media?.url; - if (!audioUrl) - throw new Error('Audio generation failed: No media URL in response.'); - - const base64 = audioUrl.split(';base64,')[1]; - if (!base64) throw new Error('Invalid audio data URI format from Genkit.'); - - const pcmBuffer = Buffer.from(base64, 'base64'); - return pcmToWavDataUri(pcmBuffer); -} - -export const multiSpeakerTextToSpeechFlow = ai.defineFlow( - { - name: 'multiSpeakerTextToSpeechFlow', - inputSchema: MultiSpeakerInputSchema, - outputSchema: TTSOutputSchema, - }, - async (input) => { - const audioDataUri = await generateMultiSpeakerAudio( - input.text, - input.voiceName1, - input.voiceName2 - ); - return { audioDataUri }; - } -); -\`\`\` - ---- - -## 4. Scenario 3: Image Generation - -\`\`\`ts -// src/image-gen.ts -import { z } from 'genkit'; -import { ai } from './index'; -import { vertexAI } from '@genkit-ai/googleai'; -import * as fs from 'fs/promises'; -import { parseDataUrl } from 'data-urls'; - -export const imageGenerationFlow = ai.defineFlow( - { - name: 'imageGenerationFlow', - inputSchema: z - .string() - .describe('A detailed description of the image to generate'), - outputSchema: z.string().describe('Path to the generated .png image file'), - }, - async (prompt) => { - const response = await ai.generate({ - model: vertexAI.model('imagen-3.0-generate-002'), - prompt, - output: { format: 'media' }, - }); - - const imagePart = response.output; - if (!imagePart?.media?.url) { - throw new Error('Image generation failed to produce media.'); - } - - const parsed = parseDataUrl(imagePart.media.url); - if (!parsed) { - throw new Error('Could not parse image data URL.'); - } - - const outputPath = './output.png'; - await fs.writeFile(outputPath, parsed.body); - return outputPath; - } -); -\`\`\` - ---- - -## 5. Scenario 4: Video Generation (Veo3) - -\`\`\`ts -// src/video-gen.ts -import { z } from 'genkit'; -import { ai } from './index'; -import { googleAI } from '@genkit-ai/googleai'; -import * as fs from 'fs'; -import { Readable } from 'stream'; -import fetch from 'node-fetch'; - -export const videoGenerationFlow = ai.defineFlow( - { - name: 'videoGenerationFlow', - inputSchema: z - .string() - .describe('A detailed description for the video scene'), - outputSchema: z.string().describe('Path to the generated .mp4 video file'), - }, - async (prompt) => { - let { operation } = await ai.generate({ - model: googleAI.model('veo-3.0-generate-preview'), - prompt, - }); - - if (!operation) { - throw new Error('Expected the model to return an operation.'); - } - - console.log('Video generation started... Polling for completion.'); - while (!operation.done) { - await new Promise((resolve) => setTimeout(resolve, 5000)); - operation = await ai.checkOperation(operation); - console.log( - \`Operation status: \${operation.done ? 'Done' : 'In Progress'}\` - ); - } - - if (operation.error) { - throw new Error(\`Video generation failed: \${operation.error.message}\`); - } - - const video = operation.output?.message?.content.find((p) => !!p.media); - if (!video?.media?.url) { - throw new Error( - 'Failed to find the generated video in the operation output.' - ); - } - - const videoUrl = \`\${video.media.url}&key=\${process.env.GEMINI_API_KEY}\`; - const videoResponse = await fetch(videoUrl); - - if (!videoResponse.ok || !videoResponse.body) { - throw new Error(\`Failed to fetch video: \${videoResponse.statusText}\`); - } - - const outputPath = './output.mp4'; - const fileStream = fs.createWriteStream(outputPath); - await new Promise((resolve, reject) => { - Readable.from(videoResponse.body).pipe(fileStream); - fileStream.on('finish', resolve); - fileStream.on('error', reject); - }); - - return outputPath; - } -); -\`\`\` - ---- - -## 6. Running & Inspecting Your Flows - -### 6.1 Create a Build Script - -Add this to your \`package.json\`: - -\`\`\`json -{ - "scripts": { - "build": "tsc" - } -} -\`\`\` - -### 6.2 Instructions for the User - -To test your flows and use the Genkit Developer UI, run the following command from your terminal: - -\`\`\`bash -genkit start -- npx tsx --watch src/index.ts -\`\`\` - -Visit [http://localhost:4000](http://localhost:4000) to use the Developer UI. - ---- - -## 7. Using Models: correct use of plugins names in model names - -When using Genkit models, **prefer the full string form with plugin name up front**: - -\`\`\`ts -model: 'googleai/gemini-2.5-flash-preview-tts'; // ✅ RECOMMENDED -\`\`\` - -Avoid using just the model name - -\`\`\`ts -// ❌ May break: -model: googleAI.model('gemini-2.5-flash-preview-tts'); -\`\`\` - -Using the full string ensures Genkit resolves the model via the correct plugin and avoids missing \`.media\` in TTS responses. - -## 8. Supported Models: latest versions - -\`\`\` -| Task | Recommended Model | Plugin | -|-------------------------|------------------------------------|--------------------------| -| Advanced Text/Reasoning | gemini-2.5-pro | @genkit-ai/googleai | -| Fast Text/Chat | gemini-2.5-flash | @genkit-ai/googleai | -| Text-to-Speech | gemini-2.5-flash-preview-tts | @genkit-ai/googleai | -| Image Generation | imagen-4.0-generate-preview-06-06 | @genkit-ai/googleai | -| Video Generation | veo-3.0-generate-preview | @genkit-ai/googleai | -\`\`\` - -Tip: Do not use Gemini 1.5 or earlier models for anything, they are out-of-date. - --- - -# General Guidance - -## NPM Dependency Installation Protocol 📦 - -When you generate or modify any code files (e.g., \`.js\`, \`.ts\`, \`.jsx\`, \`.tsx\`) that reference new libraries, packages, or modules, you **must** install them before proceeding to any build, compilation, or execution step. - -Follow this workflow: - -1. **Analyze Dependencies:** After writing or changing a file, identify all third-party dependencies you've introduced. -2. **Update \`package.json\`:** Ensure these new dependencies are correctly added to the \`package.json\` file. -3. **Install Dependencies:** Execute the installation command from the project's root directory to download and link the required packages. - -### Installation Commands - -Always run the appropriate command before any \`npm run build\` or similar script. - -\`\`\`bash -# For projects using NPM -npm install - -# For projects using Yarn -yarn install - -# For projects using PNPM -pnpm install -\`\`\` - -This protocol is **critical** to prevent build failures caused by missing modules. Always double-check that dependencies are installed after you add them to the code. -`; diff --git a/genkit-tools/cli/src/commands/init-ai-tools/command.ts b/genkit-tools/cli/src/commands/init-ai-tools/command.ts index 37d752002e..4fd80bc11c 100644 --- a/genkit-tools/cli/src/commands/init-ai-tools/command.ts +++ b/genkit-tools/cli/src/commands/init-ai-tools/command.ts @@ -33,9 +33,9 @@ const AGENT_CHOICES: AIToolChoice[] = Object.values(AI_TOOLS).map((tool) => ({ */ export const initAiTools = new Command('init:ai-tools') .description( - 'initialize AI tools in a workspace with helpful context related to the Genkit framework' + 'initialize AI tools in a workspace with helpful context related to the Genkit framework (EXPERIMENTAL, subject to change)' ) - .option('-y', '--yes', 'Run in non-interactive mode (experimental)') + .option('-y', '--yes', 'Run in non-interactive mode') .action(async (options: InitConfigOptions) => { logger.info('\n'); logger.info( diff --git a/genkit-tools/cli/src/commands/init-ai-tools/utils.ts b/genkit-tools/cli/src/commands/init-ai-tools/utils.ts index 4d68cbd408..95806d5e56 100644 --- a/genkit-tools/cli/src/commands/init-ai-tools/utils.ts +++ b/genkit-tools/cli/src/commands/init-ai-tools/utils.ts @@ -20,6 +20,7 @@ import * as crypto from 'crypto'; import { writeFile } from 'fs/promises'; import path from 'path'; +const CONTEXT_DIR = path.resolve(__dirname, '..', '..', 'context'); const GENKIT_TAG_REGEX = /([\s\S]*?)<\/genkit_prompts>/; /* @@ -143,14 +144,7 @@ export function calculateHash(content: string): string { * Get raw prompt content for Genkit */ export function getGenkitContext(): string { - const contextPath = path.resolve( - __dirname, - '..', - '..', - '..', - 'context', - 'GENKIT.md' - ); + const contextPath = path.resolve(CONTEXT_DIR, 'GENKIT.md'); const content = readFileSync(contextPath, 'utf8'); return content; } From f38a92009e584986c6066efd4947edbe367fbef2 Mon Sep 17 00:00:00 2001 From: Samuel Bushi Date: Fri, 15 Aug 2025 13:00:00 -0400 Subject: [PATCH 14/15] fix --- genkit-tools/cli/src/commands/init-ai-tools/utils.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/genkit-tools/cli/src/commands/init-ai-tools/utils.ts b/genkit-tools/cli/src/commands/init-ai-tools/utils.ts index d4afbd8224..005bbbfa58 100644 --- a/genkit-tools/cli/src/commands/init-ai-tools/utils.ts +++ b/genkit-tools/cli/src/commands/init-ai-tools/utils.ts @@ -20,8 +20,6 @@ import * as crypto from 'crypto'; import { writeFile } from 'fs/promises'; import path from 'path'; -const CONTEXT_DIR = path.resolve(__dirname, '..', '..', 'context'); - /** Shared location for the GENKIT.md context file */ export const GENKIT_PROMPT_PATH = 'GENKIT.md'; From d8e2bfc7fb3906488cb73707967df95483f5dca0 Mon Sep 17 00:00:00 2001 From: Samuel Bushi Date: Mon, 18 Aug 2025 17:33:34 -0400 Subject: [PATCH 15/15] yesmode opt --- genkit-tools/cli/src/commands/init-ai-tools/types.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/genkit-tools/cli/src/commands/init-ai-tools/types.ts b/genkit-tools/cli/src/commands/init-ai-tools/types.ts index 18ab730205..64ec5b3f60 100644 --- a/genkit-tools/cli/src/commands/init-ai-tools/types.ts +++ b/genkit-tools/cli/src/commands/init-ai-tools/types.ts @@ -26,7 +26,7 @@ export interface AIToolConfigResult { /** `init:ai-tools` config options. */ export interface InitConfigOptions { // yes (non-interactive) mode. - yesMode: boolean; + yesMode?: boolean; } /** Interface for supported AI tools */