Skip to content

Commit 151601e

Browse files
konardclaude
andcommitted
Merge main branch - resolve conflicts between Qwen and Gemini implementations
Both Qwen Code CLI and Gemini CLI are now supported: - JavaScript: qwen.mjs and gemini.mjs tools - Rust: qwen.rs and gemini.rs modules - Updated tool registries to include both tools - Test files updated to include both tools Co-Authored-By: Claude Sonnet 4.5 <[email protected]>
2 parents 1defe1b + ac40b85 commit 151601e

File tree

17 files changed

+2046
-358
lines changed

17 files changed

+2046
-358
lines changed

.github/workflows/e2e-gemini.yml

Lines changed: 431 additions & 0 deletions
Large diffs are not rendered by default.

js/src/tools/gemini.mjs

Lines changed: 317 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,317 @@
1+
/**
2+
* Gemini CLI tool configuration
3+
* Based on Google's official gemini-cli: https://github.com/google-gemini/gemini-cli
4+
*/
5+
6+
/**
7+
* Available Gemini model configurations
8+
* Maps aliases to full model IDs
9+
*/
10+
export const modelMap = {
11+
// Gemini 2.5 models (current stable)
12+
flash: 'gemini-2.5-flash',
13+
'2.5-flash': 'gemini-2.5-flash',
14+
pro: 'gemini-2.5-pro',
15+
'2.5-pro': 'gemini-2.5-pro',
16+
lite: 'gemini-2.5-flash-lite',
17+
'2.5-lite': 'gemini-2.5-flash-lite',
18+
// Gemini 3 models (latest generation)
19+
'3-flash': 'gemini-3-flash-preview',
20+
'3-pro': 'gemini-3-pro-preview',
21+
// Legacy aliases
22+
'gemini-flash': 'gemini-2.5-flash',
23+
'gemini-pro': 'gemini-2.5-pro',
24+
};
25+
26+
/**
27+
* Map model alias to full model ID
28+
* @param {Object} options - Options
29+
* @param {string} options.model - Model alias or full ID
30+
* @returns {string} Full model ID
31+
*/
32+
export function mapModelToId(options) {
33+
const { model } = options;
34+
return modelMap[model] || model;
35+
}
36+
37+
/**
38+
* Build command line arguments for Gemini CLI
39+
* @param {Object} options - Options
40+
* @param {string} [options.prompt] - User prompt (for non-interactive mode)
41+
* @param {string} [options.systemPrompt] - System prompt (combined with user prompt)
42+
* @param {string} [options.model] - Model to use
43+
* @param {boolean} [options.json] - JSON output mode (stream-json format)
44+
* @param {boolean} [options.yolo] - Auto-approve all tool calls (autonomous mode)
45+
* @param {boolean} [options.sandbox] - Run tools in secure sandbox
46+
* @param {boolean} [options.debug] - Enable debug output
47+
* @param {boolean} [options.checkpointing] - Save project snapshot before file modifications
48+
* @param {boolean} [options.interactive] - Start interactive session with initial prompt
49+
* @returns {string[]} Array of CLI arguments
50+
*/
51+
export function buildArgs(options) {
52+
const {
53+
prompt,
54+
model,
55+
json = false,
56+
yolo = true, // Enable autonomous mode by default for agent use
57+
sandbox = false,
58+
debug = false,
59+
checkpointing = false,
60+
interactive = false,
61+
} = options;
62+
63+
const args = [];
64+
65+
if (model) {
66+
const mappedModel = mapModelToId({ model });
67+
args.push('-m', mappedModel);
68+
}
69+
70+
// Enable yolo mode for autonomous execution (auto-approve all tool calls)
71+
if (yolo) {
72+
args.push('--yolo');
73+
}
74+
75+
// Sandbox mode for secure execution
76+
if (sandbox) {
77+
args.push('--sandbox');
78+
}
79+
80+
// Debug output
81+
if (debug) {
82+
args.push('-d');
83+
}
84+
85+
// Checkpointing for file modifications
86+
if (checkpointing) {
87+
args.push('--checkpointing');
88+
}
89+
90+
// JSON output mode - use stream-json for streaming events
91+
if (json) {
92+
args.push('--output-format', 'stream-json');
93+
}
94+
95+
// Add prompt for non-interactive mode
96+
if (prompt) {
97+
if (interactive) {
98+
args.push('-i', prompt);
99+
} else {
100+
args.push('-p', prompt);
101+
}
102+
}
103+
104+
return args;
105+
}
106+
107+
/**
108+
* Build complete command string for Gemini CLI
109+
* @param {Object} options - Options
110+
* @param {string} options.workingDirectory - Working directory
111+
* @param {string} [options.prompt] - User prompt
112+
* @param {string} [options.systemPrompt] - System prompt
113+
* @param {string} [options.model] - Model to use
114+
* @param {boolean} [options.json] - JSON output mode
115+
* @param {boolean} [options.yolo] - Auto-approve all tool calls
116+
* @param {boolean} [options.sandbox] - Run tools in secure sandbox
117+
* @param {boolean} [options.debug] - Enable debug output
118+
* @param {boolean} [options.checkpointing] - Save project snapshot
119+
* @param {boolean} [options.interactive] - Start interactive session
120+
* @returns {string} Complete command string
121+
*/
122+
export function buildCommand(options) {
123+
// eslint-disable-next-line no-unused-vars
124+
const { workingDirectory, systemPrompt, prompt, ...argOptions } = options;
125+
126+
// Gemini CLI supports system prompt via GEMINI_SYSTEM_PROMPT env var
127+
// or via .gemini/system.md file. For now, combine with user prompt.
128+
const combinedPrompt = systemPrompt
129+
? `${systemPrompt}\n\n${prompt || ''}`
130+
: prompt || '';
131+
132+
const args = buildArgs({ ...argOptions, prompt: combinedPrompt });
133+
return `gemini ${args.map(escapeArg).join(' ')}`.trim();
134+
}
135+
136+
/**
137+
* Escape an argument for shell usage
138+
* @param {string} arg - Argument to escape
139+
* @returns {string} Escaped argument
140+
*/
141+
function escapeArg(arg) {
142+
// If argument contains spaces, quotes, or special chars, wrap in quotes
143+
if (/["\s$`\\]/.test(arg)) {
144+
return `"${arg.replace(/"/g, '\\"').replace(/\$/g, '\\$').replace(/`/g, '\\`').replace(/\\/g, '\\\\')}"`;
145+
}
146+
return arg;
147+
}
148+
149+
/**
150+
* Parse JSON messages from Gemini CLI output
151+
* Gemini CLI outputs NDJSON (newline-delimited JSON) in stream-json mode
152+
* @param {Object} options - Options
153+
* @param {string} options.output - Raw output string
154+
* @returns {Object[]} Array of parsed JSON messages
155+
*/
156+
export function parseOutput(options) {
157+
const { output } = options;
158+
const messages = [];
159+
const lines = output.split('\n');
160+
161+
for (const line of lines) {
162+
const trimmed = line.trim();
163+
if (!trimmed || !trimmed.startsWith('{')) {
164+
continue;
165+
}
166+
167+
try {
168+
const parsed = JSON.parse(trimmed);
169+
messages.push(parsed);
170+
} catch {
171+
// Skip lines that aren't valid JSON
172+
}
173+
}
174+
175+
return messages;
176+
}
177+
178+
/**
179+
* Extract session ID from Gemini CLI output
180+
* Gemini CLI may include session information in its output
181+
* @param {Object} options - Options
182+
* @param {string} options.output - Raw output string
183+
* @returns {string|null} Session ID or null
184+
*/
185+
export function extractSessionId(options) {
186+
const { output } = options;
187+
const messages = parseOutput({ output });
188+
189+
for (const msg of messages) {
190+
if (msg.session_id) {
191+
return msg.session_id;
192+
}
193+
// Gemini might use different session identifier
194+
if (msg.conversation_id) {
195+
return msg.conversation_id;
196+
}
197+
}
198+
199+
return null;
200+
}
201+
202+
/**
203+
* Extract usage statistics from Gemini CLI output
204+
* @param {Object} options - Options
205+
* @param {string} options.output - Raw output string
206+
* @returns {Object} Usage statistics
207+
*/
208+
export function extractUsage(options) {
209+
const { output } = options;
210+
const messages = parseOutput({ output });
211+
212+
const usage = {
213+
inputTokens: 0,
214+
outputTokens: 0,
215+
totalTokens: 0,
216+
};
217+
218+
for (const msg of messages) {
219+
// Check for usage metadata in different possible formats
220+
if (msg.usage) {
221+
const u = msg.usage;
222+
if (u.input_tokens !== undefined) {
223+
usage.inputTokens += u.input_tokens;
224+
}
225+
if (u.output_tokens !== undefined) {
226+
usage.outputTokens += u.output_tokens;
227+
}
228+
if (u.total_tokens !== undefined) {
229+
usage.totalTokens += u.total_tokens;
230+
}
231+
// Also check camelCase variants
232+
if (u.inputTokens !== undefined) {
233+
usage.inputTokens += u.inputTokens;
234+
}
235+
if (u.outputTokens !== undefined) {
236+
usage.outputTokens += u.outputTokens;
237+
}
238+
if (u.totalTokens !== undefined) {
239+
usage.totalTokens += u.totalTokens;
240+
}
241+
}
242+
243+
// Also check for Gemini-specific token metrics
244+
if (msg.usageMetadata) {
245+
const u = msg.usageMetadata;
246+
if (u.promptTokenCount !== undefined) {
247+
usage.inputTokens += u.promptTokenCount;
248+
}
249+
if (u.candidatesTokenCount !== undefined) {
250+
usage.outputTokens += u.candidatesTokenCount;
251+
}
252+
if (u.totalTokenCount !== undefined) {
253+
usage.totalTokens += u.totalTokenCount;
254+
}
255+
}
256+
}
257+
258+
// Calculate total if not provided
259+
if (
260+
usage.totalTokens === 0 &&
261+
(usage.inputTokens > 0 || usage.outputTokens > 0)
262+
) {
263+
usage.totalTokens = usage.inputTokens + usage.outputTokens;
264+
}
265+
266+
return usage;
267+
}
268+
269+
/**
270+
* Detect errors in Gemini CLI output
271+
* @param {Object} options - Options
272+
* @param {string} options.output - Raw output string
273+
* @returns {Object} Error detection result
274+
*/
275+
export function detectErrors(options) {
276+
const { output } = options;
277+
const messages = parseOutput({ output });
278+
279+
for (const msg of messages) {
280+
// Check for explicit error message types
281+
if (msg.type === 'error' || msg.error) {
282+
return {
283+
hasError: true,
284+
errorType: msg.type || 'error',
285+
message: msg.message || msg.error || 'Unknown error',
286+
};
287+
}
288+
}
289+
290+
return { hasError: false };
291+
}
292+
293+
/**
294+
* Gemini CLI tool configuration
295+
*/
296+
export const geminiTool = {
297+
name: 'gemini',
298+
displayName: 'Gemini CLI',
299+
executable: 'gemini',
300+
supportsJsonOutput: true,
301+
supportsJsonInput: false, // Gemini CLI uses -p flag for prompts, not stdin JSON
302+
supportsSystemPrompt: false, // System prompt via env var or file, combined with user prompt
303+
supportsResume: true, // Via /chat resume command in interactive mode
304+
supportsYolo: true, // Supports --yolo for autonomous execution
305+
supportsSandbox: true, // Supports --sandbox for secure execution
306+
supportsCheckpointing: true, // Supports --checkpointing
307+
supportsDebug: true, // Supports -d for debug output
308+
defaultModel: 'gemini-2.5-flash',
309+
modelMap,
310+
mapModelToId,
311+
buildArgs,
312+
buildCommand,
313+
parseOutput,
314+
extractSessionId,
315+
extractUsage,
316+
detectErrors,
317+
};

js/src/tools/index.mjs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
/**
22
* Tool configurations and utilities
3-
* Provides configuration for different CLI agents: claude, codex, opencode, agent, qwen
3+
* Provides configuration for different CLI agents: claude, codex, opencode, agent, gemini, qwen
44
*/
55

66
import { claudeTool } from './claude.mjs';
77
import { codexTool } from './codex.mjs';
88
import { opencodeTool } from './opencode.mjs';
99
import { agentTool } from './agent.mjs';
10+
import { geminiTool } from './gemini.mjs';
1011
import { qwenTool } from './qwen.mjs';
1112

1213
/**
@@ -17,6 +18,7 @@ export const tools = {
1718
codex: codexTool,
1819
opencode: opencodeTool,
1920
agent: agentTool,
21+
gemini: geminiTool,
2022
qwen: qwenTool,
2123
};
2224

@@ -56,4 +58,4 @@ export function isToolSupported(options) {
5658
return toolName in tools;
5759
}
5860

59-
export { claudeTool, codexTool, opencodeTool, agentTool, qwenTool };
61+
export { claudeTool, codexTool, opencodeTool, agentTool, geminiTool, qwenTool };

0 commit comments

Comments
 (0)