Skip to content

Commit e7cd2bb

Browse files
authored
fix(cli/init-ai-tools): remove Gemini CLI extension installation (#3595)
1 parent ef73f51 commit e7cd2bb

File tree

3 files changed

+191
-99
lines changed

3 files changed

+191
-99
lines changed

genkit-tools/cli/src/commands/init-ai-tools/ai-tools/gemini.ts

Lines changed: 128 additions & 98 deletions
Original file line numberDiff line numberDiff line change
@@ -16,49 +16,37 @@
1616

1717
import { Runtime } from '@genkit-ai/tools-common/manager';
1818
import { logger } from '@genkit-ai/tools-common/utils';
19-
import { select } from '@inquirer/prompts';
2019
import { existsSync, readFileSync } from 'fs';
21-
import { mkdir, writeFile } from 'fs/promises';
22-
import path from 'path';
23-
import { AIToolConfigResult, AIToolModule, InitConfigOptions } from '../types';
2420
import {
25-
GENKIT_PROMPT_PATH,
26-
initGenkitFile,
27-
initOrReplaceFile,
28-
updateContentInPlace,
29-
} from '../utils';
21+
copyFile,
22+
lstat,
23+
mkdir,
24+
readlink,
25+
symlink,
26+
writeFile,
27+
} from 'fs/promises';
28+
import * as path from 'path';
29+
import { AIToolConfigResult, AIToolModule, InitConfigOptions } from '../types';
30+
import { GENKIT_PROMPT_PATH, initGenkitFile } from '../utils';
3031

3132
// GEMINI specific paths
3233
const GEMINI_DIR = '.gemini';
3334
const GEMINI_SETTINGS_PATH = path.join(GEMINI_DIR, 'settings.json');
34-
const GEMINI_MD_PATH = path.join('GEMINI.md');
35+
const GENKIT_MD_SYMLINK_PATH = path.join(GEMINI_DIR, GENKIT_PROMPT_PATH);
3536

3637
// GENKIT specific constants
37-
const GENKIT_EXT_DIR = path.join(GEMINI_DIR, 'extensions', 'genkit');
38-
const GENKIT_MD_REL_PATH = path.join('..', '..', '..', GENKIT_PROMPT_PATH);
39-
const GENKIT_EXTENSION_CONFIG = {
40-
name: 'genkit',
41-
version: '1.0.0',
42-
mcpServers: {
43-
genkit: {
44-
command: 'genkit',
45-
args: ['mcp', '--no-update-notification'],
46-
cwd: '.',
47-
timeout: 30000,
48-
trust: false,
49-
excludeTools: [
50-
'run_shell_command(genkit start)',
51-
'run_shell_command(npx genkit start)',
52-
],
53-
},
54-
},
55-
contextFileName: GENKIT_MD_REL_PATH,
38+
const GENKIT_MCP_CONFIG = {
39+
command: 'genkit',
40+
args: ['mcp', '--no-update-notification'],
41+
cwd: '.',
42+
timeout: 30000,
43+
trust: false,
44+
excludeTools: [
45+
'run_shell_command(genkit start)',
46+
'run_shell_command(npx genkit start)',
47+
],
5648
};
5749

58-
const EXT_INSTALLATION = 'extension';
59-
const MD_INSTALLATION = 'geminimd';
60-
type InstallationType = typeof EXT_INSTALLATION | typeof MD_INSTALLATION;
61-
6250
/** Configuration module for Gemini CLI */
6351
export const gemini: AIToolModule = {
6452
name: 'gemini',
@@ -71,33 +59,8 @@ export const gemini: AIToolModule = {
7159
runtime: Runtime,
7260
options?: InitConfigOptions
7361
): Promise<AIToolConfigResult> {
74-
let installationMethod: InstallationType = EXT_INSTALLATION;
75-
if (!options?.yesMode) {
76-
installationMethod = await select({
77-
message: 'Select your preferred installation method',
78-
choices: [
79-
{
80-
name: 'Gemini CLI Extension',
81-
value: 'extension',
82-
description:
83-
'Use Gemini Extension to install Genkit context in a modular fashion',
84-
},
85-
{
86-
name: 'GEMINI.md',
87-
value: 'geminimd',
88-
description: 'Incorporate Genkit context within the GEMINI.md file',
89-
},
90-
],
91-
});
92-
}
93-
94-
if (installationMethod === EXT_INSTALLATION) {
95-
logger.info('Installing as part of GEMINI.md');
96-
return await installAsExtension(runtime);
97-
} else {
98-
logger.info('Installing as Gemini CLI extension');
99-
return await installInMdFile(runtime);
100-
}
62+
logger.info('Installing as part of GEMINI.md');
63+
return await installInMdFile(runtime);
10164
},
10265
};
10366

@@ -125,8 +88,19 @@ async function installInMdFile(runtime: Runtime): Promise<AIToolConfigResult> {
12588
if (!existingConfig.mcpServers) {
12689
existingConfig.mcpServers = {};
12790
}
128-
existingConfig.mcpServers.genkit =
129-
GENKIT_EXTENSION_CONFIG.mcpServers.genkit;
91+
existingConfig.mcpServers.genkit = GENKIT_MCP_CONFIG;
92+
93+
if (existingConfig.contextFileName) {
94+
const contextFiles = Array.isArray(existingConfig.contextFileName)
95+
? [...existingConfig.contextFileName]
96+
: [existingConfig.contextFileName];
97+
if (!contextFiles.includes('GENKIT.md')) {
98+
contextFiles.push('GENKIT.md');
99+
}
100+
existingConfig.contextFileName = contextFiles;
101+
} else {
102+
existingConfig.contextFileName = 'GENKIT.md';
103+
}
130104
await writeFile(
131105
GEMINI_SETTINGS_PATH,
132106
JSON.stringify(existingConfig, null, 2)
@@ -140,48 +114,104 @@ async function installInMdFile(runtime: Runtime): Promise<AIToolConfigResult> {
140114
const baseResult = await initGenkitFile(runtime);
141115
files.push({ path: GENKIT_PROMPT_PATH, updated: baseResult.updated });
142116

143-
logger.info('Updating GEMINI.md to include Genkit context');
144-
const geminiImportTag = `\nGenkit Framework Instructions:\n - @./GENKIT.md\n`;
145-
const { updated: mdUpdated } = await updateContentInPlace(
146-
GEMINI_MD_PATH,
147-
geminiImportTag,
148-
{ hash: baseResult.hash }
149-
);
150-
files.push({ path: GEMINI_MD_PATH, updated: mdUpdated });
117+
// Create link/copy of GENKIT.md in .gemini folder
118+
const linkResult = await createGenkitMdLink();
119+
files.push(linkResult);
151120

152121
return { files };
153122
}
154123

155-
async function installAsExtension(
156-
runtime: Runtime
157-
): Promise<AIToolConfigResult> {
158-
const files: AIToolConfigResult['files'] = [];
159-
// Part 1: Generate GENKIT.md file.
160-
const baseResult = await initGenkitFile(runtime);
161-
files.push({ path: GENKIT_PROMPT_PATH, updated: baseResult.updated });
124+
/**
125+
* Creates a link to GENKIT.md in the .gemini folder.
126+
* On Windows, copies the file instead of creating a symlink.
127+
* On Unix systems, creates a symlink.
128+
*/
129+
async function createGenkitMdLink(): Promise<{
130+
path: string;
131+
updated: boolean;
132+
}> {
133+
const sourcePath = GENKIT_PROMPT_PATH;
134+
const targetPath = GENKIT_MD_SYMLINK_PATH;
135+
const isWindows = process.platform === 'win32';
136+
137+
if (isWindows) {
138+
logger.info(
139+
'Copying GENKIT.md to .gemini folder (Windows compatibility mode)'
140+
);
162141

163-
// Part 2: Configure the main gemini-extension.json file, and gemini config directory if needed.
164-
logger.info('Configuring extentions files in user workspace');
165-
await mkdir(GENKIT_EXT_DIR, { recursive: true });
166-
const extensionPath = path.join(GENKIT_EXT_DIR, 'gemini-extension.json');
142+
// Check if file already exists
143+
try {
144+
const stats = await lstat(targetPath);
145+
if (stats.isFile()) {
146+
logger.info('GENKIT.md copy already exists in .gemini folder');
147+
return { path: targetPath, updated: false };
148+
}
149+
} catch (error: any) {
150+
if (error.code !== 'ENOENT') {
151+
logger.error('Error checking GENKIT.md copy:', error);
152+
throw error;
153+
}
154+
// File doesn't exist, proceed with copying
155+
}
167156

168-
let extensionUpdated = false;
169-
try {
170-
const { updated } = await initOrReplaceFile(
171-
extensionPath,
172-
JSON.stringify(GENKIT_EXTENSION_CONFIG, null, 2)
173-
);
174-
extensionUpdated = updated;
175-
if (extensionUpdated) {
176-
logger.info(
177-
`Genkit extension for Gemini CLI initialized at ${extensionPath}`
178-
);
157+
// Copy the file
158+
try {
159+
await copyFile(sourcePath, targetPath);
160+
logger.info('Successfully copied GENKIT.md to .gemini folder');
161+
return { path: targetPath, updated: true };
162+
} catch (error) {
163+
logger.error('Error copying GENKIT.md:', error);
164+
throw error;
165+
}
166+
} else {
167+
// Unix-like systems: create symlink
168+
logger.info('Adding a symlink for GENKIT.md in the .gemini folder');
169+
170+
// Check if symlink already exists
171+
try {
172+
const stats = await lstat(targetPath);
173+
if (stats.isSymbolicLink()) {
174+
// Verify the symlink points to the correct target
175+
const linkTarget = await readlink(targetPath);
176+
177+
// Resolve the link target to absolute path
178+
const resolvedLinkTarget = path.resolve(
179+
path.dirname(targetPath),
180+
linkTarget
181+
);
182+
// Resolve the source path to absolute
183+
const resolvedSourcePath = path.resolve(sourcePath);
184+
185+
// Compare absolute paths
186+
if (resolvedLinkTarget === resolvedSourcePath) {
187+
logger.info(
188+
'Symlink already exists and points to the correct location'
189+
);
190+
return { path: targetPath, updated: false };
191+
} else {
192+
logger.warn(
193+
`Symlink exists but points to wrong location. Expected: ${resolvedSourcePath}, Found: ${resolvedLinkTarget}. Please remove this file and try again if this was not intentional.`
194+
);
195+
return { path: targetPath, updated: false };
196+
}
197+
} else {
198+
// File exists but is not a symlink
199+
logger.warn(
200+
`${targetPath} exists but is not a symlink, skipping symlink creation`
201+
);
202+
return { path: targetPath, updated: false };
203+
}
204+
} catch (error: any) {
205+
if (error.code === 'ENOENT') {
206+
// Symlink doesn't exist, create it
207+
await symlink(path.relative(GEMINI_DIR, sourcePath), targetPath);
208+
logger.info('Successfully created symlink for GENKIT.md');
209+
return { path: targetPath, updated: true };
210+
} else {
211+
// Some other error occurred
212+
logger.error('Error checking symlink:', error);
213+
throw error;
214+
}
179215
}
180-
} catch (err) {
181-
logger.error(err);
182-
process.exit(1);
183216
}
184-
files.push({ path: extensionPath, updated: extensionUpdated });
185-
186-
return { files };
187217
}

genkit-tools/cli/src/mcp/server.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,14 +21,16 @@ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
2121
import { defineDocsTool } from '../mcp/docs';
2222
import { defineFlowTools } from './flows';
2323
import { defineTraceTools } from './trace';
24+
import { defineUsageGuideTool } from './usage';
2425

2526
export async function startMcpServer(manager: RuntimeManager) {
2627
const server = new McpServer({
2728
name: 'Genkit MCP',
28-
version: '0.0.1',
29+
version: '0.0.2',
2930
});
3031

3132
await defineDocsTool(server);
33+
await defineUsageGuideTool(server);
3234
defineFlowTools(server, manager);
3335
defineTraceTools(server, manager);
3436

genkit-tools/cli/src/mcp/usage.ts

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
/**
2+
* Copyright 2025 Google LLC
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
import { record } from '@genkit-ai/tools-common/utils';
18+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp';
19+
import { ContentBlock } from '@modelcontextprotocol/sdk/types';
20+
import z from 'zod';
21+
import { McpRunToolEvent } from './analytics.js';
22+
23+
import { GENKIT_CONTEXT as GoContext } from '../commands/init-ai-tools/context/go.js';
24+
import { GENKIT_CONTEXT as JsContext } from '../commands/init-ai-tools/context/nodejs.js';
25+
26+
export async function defineUsageGuideTool(server: McpServer) {
27+
server.registerTool(
28+
'get_usage_guide',
29+
{
30+
title: 'Genkit Instructions',
31+
description:
32+
'Use this tool to look up the Genkit usage guide before implementing any AI feature',
33+
inputSchema: {
34+
language: z
35+
.enum(['js', 'go'])
36+
.describe('which language this usage guide is for')
37+
.default('js')
38+
.optional(),
39+
},
40+
},
41+
async ({ language }) => {
42+
await record(new McpRunToolEvent('get_usage_guide'));
43+
44+
const content = [] as ContentBlock[];
45+
if (!language) {
46+
language = 'js';
47+
}
48+
let text = JsContext;
49+
if (language === 'go') {
50+
text = GoContext;
51+
}
52+
content.push({
53+
type: 'text',
54+
text,
55+
});
56+
57+
return { content };
58+
}
59+
);
60+
}

0 commit comments

Comments
 (0)