feat: add Gemini CLI, OpenCode, and Windsurf IDE integrations

Gemini CLI: platform adapter mapping 6 of 11 hooks, settings.json
deep-merge installer, GEMINI.md context injection.

OpenCode: plugin with tool.execute.after interceptor, bus events for
session lifecycle, claude_mem_search custom tool, AGENTS.md context.

Windsurf: platform adapter for tool_info envelope format, hooks.json
installer for 5 post-action hooks, .windsurf/rules context injection.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Alex Newman
2026-02-27 00:03:29 -05:00
parent 3a09c1bb1a
commit f2cc33b494
11 changed files with 1936 additions and 10 deletions
+85
View File
@@ -0,0 +1,85 @@
import type { PlatformAdapter, NormalizedHookInput, HookResult } from '../types.js';
/**
* Gemini CLI Platform Adapter
*
* Normalizes Gemini CLI's hook JSON to NormalizedHookInput.
* Gemini CLI has 11 lifecycle hooks; we map 6 of them:
* SessionStart → session-init
* BeforeAgent → user-message (captures prompt)
* AfterAgent → observation (full response)
* AfterTool → observation (tool result)
* PreCompress → summarize
* SessionEnd → session-complete
*
* Base fields (all events): session_id, transcript_path, cwd, hook_event_name, timestamp
*
* Output format: { continue, stopReason, suppressOutput, systemMessage, decision, reason }
* Advisory hooks (SessionStart, SessionEnd, PreCompress) ignore `continue` and `decision`.
*/
export const geminiCliAdapter: PlatformAdapter = {
normalizeInput(raw) {
const r = (raw ?? {}) as any;
// Use GEMINI_CWD, GEMINI_PROJECT_DIR, or the JSON cwd field
const cwd = r.cwd
?? process.env.GEMINI_CWD
?? process.env.GEMINI_PROJECT_DIR
?? process.env.CLAUDE_PROJECT_DIR
?? process.cwd();
const sessionId = r.session_id
?? process.env.GEMINI_SESSION_ID
?? undefined;
// Map event-specific fields into normalized shape
// AfterTool provides tool_name, tool_input, tool_response
// BeforeAgent/AfterAgent provide prompt (and prompt_response for AfterAgent)
const hookEventName: string | undefined = r.hook_event_name;
// For AfterAgent, treat the full response as an observation by packing it
// into toolResponse so the observation handler can process it
let toolName: string | undefined = r.tool_name;
let toolInput: unknown = r.tool_input;
let toolResponse: unknown = r.tool_response;
if (hookEventName === 'AfterAgent' && r.prompt_response) {
toolName = toolName ?? 'GeminiAgent';
toolInput = toolInput ?? { prompt: r.prompt };
toolResponse = toolResponse ?? { response: r.prompt_response };
}
return {
sessionId,
cwd,
prompt: r.prompt,
toolName,
toolInput,
toolResponse,
transcriptPath: r.transcript_path,
};
},
formatOutput(result) {
// Gemini CLI expects: { continue, stopReason, suppressOutput, systemMessage, decision, reason }
const output: Record<string, unknown> = {};
// Always include continue — controls whether the agent proceeds
output.continue = result.continue ?? true;
if (result.suppressOutput !== undefined) {
output.suppressOutput = result.suppressOutput;
}
if (result.systemMessage) {
output.systemMessage = result.systemMessage;
}
// hookSpecificOutput carries context injection data
if (result.hookSpecificOutput) {
output.systemMessage = result.hookSpecificOutput.additionalContext || output.systemMessage;
}
return output;
}
};
+5 -1
View File
@@ -1,16 +1,20 @@
import type { PlatformAdapter } from '../types.js';
import { claudeCodeAdapter } from './claude-code.js';
import { cursorAdapter } from './cursor.js';
import { geminiCliAdapter } from './gemini-cli.js';
import { rawAdapter } from './raw.js';
import { windsurfAdapter } from './windsurf.js';
export function getPlatformAdapter(platform: string): PlatformAdapter {
switch (platform) {
case 'claude-code': return claudeCodeAdapter;
case 'cursor': return cursorAdapter;
case 'gemini-cli': return geminiCliAdapter;
case 'windsurf': return windsurfAdapter;
case 'raw': return rawAdapter;
// Codex CLI and other compatible platforms use the raw adapter (accepts both camelCase and snake_case fields)
default: return rawAdapter;
}
}
export { claudeCodeAdapter, cursorAdapter, rawAdapter };
export { claudeCodeAdapter, cursorAdapter, geminiCliAdapter, rawAdapter, windsurfAdapter };
+79
View File
@@ -0,0 +1,79 @@
import type { PlatformAdapter, NormalizedHookInput, HookResult } from '../types.js';
// Maps Windsurf stdin format — JSON envelope with agent_action_name + tool_info payload
//
// Common envelope (all hooks):
// { agent_action_name, trajectory_id, execution_id, timestamp, tool_info: { ... } }
//
// Event-specific tool_info payloads:
// pre_user_prompt: { user_prompt: string }
// post_write_code: { file_path, edits: [{ old_string, new_string }] }
// post_run_command: { command_line, cwd }
// post_mcp_tool_use: { mcp_server_name, mcp_tool_name, mcp_tool_arguments, mcp_result }
// post_cascade_response: { response }
export const windsurfAdapter: PlatformAdapter = {
normalizeInput(raw) {
const r = (raw ?? {}) as any;
const toolInfo = r.tool_info ?? {};
const actionName: string = r.agent_action_name ?? '';
const base: NormalizedHookInput = {
sessionId: r.trajectory_id ?? r.execution_id,
cwd: toolInfo.cwd ?? process.cwd(),
platform: 'windsurf',
};
switch (actionName) {
case 'pre_user_prompt':
return {
...base,
prompt: toolInfo.user_prompt,
};
case 'post_write_code':
return {
...base,
toolName: 'Write',
filePath: toolInfo.file_path,
edits: toolInfo.edits,
toolInput: {
file_path: toolInfo.file_path,
edits: toolInfo.edits,
},
};
case 'post_run_command':
return {
...base,
cwd: toolInfo.cwd ?? base.cwd,
toolName: 'Bash',
toolInput: { command: toolInfo.command_line },
};
case 'post_mcp_tool_use':
return {
...base,
toolName: toolInfo.mcp_tool_name ?? 'mcp_tool',
toolInput: toolInfo.mcp_tool_arguments,
toolResponse: toolInfo.mcp_result,
};
case 'post_cascade_response':
return {
...base,
toolName: 'cascade_response',
toolResponse: toolInfo.response,
};
default:
// Unknown action — pass through what we can
return base;
}
},
formatOutput(result) {
// Windsurf exit codes: 0 = success, 2 = block (pre-hooks only)
// The CLI layer handles exit codes; here we just return a simple continue flag
return { continue: result.continue ?? true };
},
};