Saving uncommitted changes before archiving
This commit is contained in:
File diff suppressed because one or more lines are too long
@@ -1,27 +1,39 @@
|
|||||||
import type { PlatformAdapter, NormalizedHookInput, HookResult } from '../types.js';
|
import type { PlatformAdapter } from '../types.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gemini CLI Platform Adapter
|
* Gemini CLI Platform Adapter
|
||||||
*
|
*
|
||||||
* Normalizes Gemini CLI's hook JSON to NormalizedHookInput.
|
* Normalizes Gemini CLI's hook JSON to NormalizedHookInput.
|
||||||
* Gemini CLI has 11 lifecycle hooks; we map 6 of them:
|
* Gemini CLI supports 11 lifecycle hooks; we register 8:
|
||||||
* SessionStart → session-init
|
*
|
||||||
* BeforeAgent → user-message (captures prompt)
|
* Lifecycle:
|
||||||
* AfterAgent → observation (full response)
|
* SessionStart → context (inject memory context)
|
||||||
* AfterTool → observation (tool result)
|
|
||||||
* PreCompress → summarize
|
|
||||||
* SessionEnd → session-complete
|
* SessionEnd → session-complete
|
||||||
|
* PreCompress → summarize
|
||||||
|
* Notification → observation (system events like ToolPermission)
|
||||||
|
*
|
||||||
|
* Agent:
|
||||||
|
* BeforeAgent → user-message (captures user prompt)
|
||||||
|
* AfterAgent → observation (full agent response)
|
||||||
|
*
|
||||||
|
* Tool:
|
||||||
|
* BeforeTool → observation (tool intent before execution)
|
||||||
|
* AfterTool → observation (tool result after execution)
|
||||||
|
*
|
||||||
|
* Unmapped (not useful for memory):
|
||||||
|
* BeforeModel, AfterModel, BeforeToolSelection — model-level events
|
||||||
|
* that fire per-LLM-call, too chatty for observation capture.
|
||||||
*
|
*
|
||||||
* Base fields (all events): session_id, transcript_path, cwd, hook_event_name, timestamp
|
* Base fields (all events): session_id, transcript_path, cwd, hook_event_name, timestamp
|
||||||
*
|
*
|
||||||
* Output format: { continue, stopReason, suppressOutput, systemMessage, decision, reason }
|
* Output format: { continue, stopReason, suppressOutput, systemMessage, decision, reason, hookSpecificOutput }
|
||||||
* Advisory hooks (SessionStart, SessionEnd, PreCompress) ignore `continue` and `decision`.
|
* Advisory hooks (SessionStart, SessionEnd, PreCompress, Notification) ignore flow-control fields.
|
||||||
*/
|
*/
|
||||||
export const geminiCliAdapter: PlatformAdapter = {
|
export const geminiCliAdapter: PlatformAdapter = {
|
||||||
normalizeInput(raw) {
|
normalizeInput(raw) {
|
||||||
const r = (raw ?? {}) as any;
|
const r = (raw ?? {}) as any;
|
||||||
|
|
||||||
// Use GEMINI_CWD, GEMINI_PROJECT_DIR, or the JSON cwd field
|
// CWD resolution chain: JSON field → env vars → process.cwd()
|
||||||
const cwd = r.cwd
|
const cwd = r.cwd
|
||||||
?? process.env.GEMINI_CWD
|
?? process.env.GEMINI_CWD
|
||||||
?? process.env.GEMINI_PROJECT_DIR
|
?? process.env.GEMINI_PROJECT_DIR
|
||||||
@@ -32,23 +44,47 @@ export const geminiCliAdapter: PlatformAdapter = {
|
|||||||
?? process.env.GEMINI_SESSION_ID
|
?? process.env.GEMINI_SESSION_ID
|
||||||
?? undefined;
|
?? 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;
|
const hookEventName: string | undefined = r.hook_event_name;
|
||||||
|
|
||||||
// For AfterAgent, treat the full response as an observation by packing it
|
// Tool fields — present in BeforeTool, AfterTool
|
||||||
// into toolResponse so the observation handler can process it
|
|
||||||
let toolName: string | undefined = r.tool_name;
|
let toolName: string | undefined = r.tool_name;
|
||||||
let toolInput: unknown = r.tool_input;
|
let toolInput: unknown = r.tool_input;
|
||||||
let toolResponse: unknown = r.tool_response;
|
let toolResponse: unknown = r.tool_response;
|
||||||
|
|
||||||
|
// AfterAgent: synthesize observation shape from the full agent response
|
||||||
if (hookEventName === 'AfterAgent' && r.prompt_response) {
|
if (hookEventName === 'AfterAgent' && r.prompt_response) {
|
||||||
toolName = toolName ?? 'GeminiAgent';
|
toolName = toolName ?? 'GeminiAgent';
|
||||||
toolInput = toolInput ?? { prompt: r.prompt };
|
toolInput = toolInput ?? { prompt: r.prompt };
|
||||||
toolResponse = toolResponse ?? { response: r.prompt_response };
|
toolResponse = toolResponse ?? { response: r.prompt_response };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// BeforeTool: has tool_name and tool_input but no tool_response yet
|
||||||
|
// Synthesize a marker so observation handler knows this is pre-execution
|
||||||
|
if (hookEventName === 'BeforeTool' && toolName && !toolResponse) {
|
||||||
|
toolResponse = { _preExecution: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Notification: capture as an observation with notification details
|
||||||
|
if (hookEventName === 'Notification') {
|
||||||
|
toolName = toolName ?? 'GeminiNotification';
|
||||||
|
toolInput = toolInput ?? {
|
||||||
|
notification_type: r.notification_type,
|
||||||
|
message: r.message,
|
||||||
|
};
|
||||||
|
toolResponse = toolResponse ?? { details: r.details };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Collect platform-specific metadata
|
||||||
|
const metadata: Record<string, unknown> = {};
|
||||||
|
if (r.source) metadata.source = r.source; // SessionStart: startup|resume|clear
|
||||||
|
if (r.reason) metadata.reason = r.reason; // SessionEnd: exit|clear|logout|...
|
||||||
|
if (r.trigger) metadata.trigger = r.trigger; // PreCompress: auto|manual
|
||||||
|
if (r.mcp_context) metadata.mcp_context = r.mcp_context; // Tool hooks: MCP server context
|
||||||
|
if (r.notification_type) metadata.notification_type = r.notification_type;
|
||||||
|
if (r.stop_hook_active !== undefined) metadata.stop_hook_active = r.stop_hook_active;
|
||||||
|
if (r.original_request_name) metadata.original_request_name = r.original_request_name;
|
||||||
|
if (hookEventName) metadata.hook_event_name = hookEventName;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
sessionId,
|
sessionId,
|
||||||
cwd,
|
cwd,
|
||||||
@@ -57,14 +93,15 @@ export const geminiCliAdapter: PlatformAdapter = {
|
|||||||
toolInput,
|
toolInput,
|
||||||
toolResponse,
|
toolResponse,
|
||||||
transcriptPath: r.transcript_path,
|
transcriptPath: r.transcript_path,
|
||||||
|
metadata: Object.keys(metadata).length > 0 ? metadata : undefined,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
formatOutput(result) {
|
formatOutput(result) {
|
||||||
// Gemini CLI expects: { continue, stopReason, suppressOutput, systemMessage, decision, reason }
|
// Gemini CLI expects: { continue, stopReason, suppressOutput, systemMessage, decision, reason, hookSpecificOutput }
|
||||||
const output: Record<string, unknown> = {};
|
const output: Record<string, unknown> = {};
|
||||||
|
|
||||||
// Always include continue — controls whether the agent proceeds
|
// Flow control — always include `continue` to prevent accidental agent termination
|
||||||
output.continue = result.continue ?? true;
|
output.continue = result.continue ?? true;
|
||||||
|
|
||||||
if (result.suppressOutput !== undefined) {
|
if (result.suppressOutput !== undefined) {
|
||||||
@@ -75,9 +112,12 @@ export const geminiCliAdapter: PlatformAdapter = {
|
|||||||
output.systemMessage = result.systemMessage;
|
output.systemMessage = result.systemMessage;
|
||||||
}
|
}
|
||||||
|
|
||||||
// hookSpecificOutput carries context injection data
|
// hookSpecificOutput is a first-class Gemini CLI field — pass through directly
|
||||||
|
// This includes additionalContext for context injection in SessionStart, BeforeAgent, AfterTool
|
||||||
if (result.hookSpecificOutput) {
|
if (result.hookSpecificOutput) {
|
||||||
output.systemMessage = result.hookSpecificOutput.additionalContext || output.systemMessage;
|
output.hookSpecificOutput = {
|
||||||
|
additionalContext: result.hookSpecificOutput.additionalContext,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
return output;
|
return output;
|
||||||
|
|||||||
+3
-1
@@ -1,7 +1,7 @@
|
|||||||
export interface NormalizedHookInput {
|
export interface NormalizedHookInput {
|
||||||
sessionId: string;
|
sessionId: string;
|
||||||
cwd: string;
|
cwd: string;
|
||||||
platform?: string; // 'claude-code' or 'cursor'
|
platform?: string; // 'claude-code', 'cursor', 'gemini-cli', etc.
|
||||||
prompt?: string;
|
prompt?: string;
|
||||||
toolName?: string;
|
toolName?: string;
|
||||||
toolInput?: unknown;
|
toolInput?: unknown;
|
||||||
@@ -10,6 +10,8 @@ export interface NormalizedHookInput {
|
|||||||
// Cursor-specific fields
|
// Cursor-specific fields
|
||||||
filePath?: string; // afterFileEdit
|
filePath?: string; // afterFileEdit
|
||||||
edits?: unknown[]; // afterFileEdit
|
edits?: unknown[]; // afterFileEdit
|
||||||
|
// Platform-specific metadata (source, reason, trigger, mcp_context, etc.)
|
||||||
|
metadata?: Record<string, unknown>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface HookResult {
|
export interface HookResult {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/**
|
/**
|
||||||
* GeminiCliHooksInstaller - Gemini CLI integration for claude-mem
|
* GeminiCliHooksInstaller - First-class Gemini CLI integration for claude-mem
|
||||||
*
|
*
|
||||||
* Installs claude-mem hooks into ~/.gemini/settings.json using deep merge
|
* Installs claude-mem hooks into ~/.gemini/settings.json using deep merge
|
||||||
* to preserve any existing user configuration.
|
* to preserve any existing user configuration.
|
||||||
@@ -9,18 +9,23 @@
|
|||||||
* "hooks": {
|
* "hooks": {
|
||||||
* "AfterTool": [{
|
* "AfterTool": [{
|
||||||
* "matcher": "*",
|
* "matcher": "*",
|
||||||
* "hooks": [{ "name": "claude-mem", "type": "command", "command": "...", "timeout": 5000 }]
|
* "hooks": [{ "name": "claude-mem", "type": "command", "command": "...", "timeout": 5000, "description": "..." }]
|
||||||
* }]
|
* }]
|
||||||
* }
|
* }
|
||||||
* }
|
* }
|
||||||
*
|
*
|
||||||
* Events registered:
|
* Registers 8 of 11 Gemini CLI hooks:
|
||||||
* SessionStart — session init
|
* SessionStart — inject memory context (via hookSpecificOutput.additionalContext)
|
||||||
* BeforeAgent — capture user prompt
|
* BeforeAgent — capture user prompt
|
||||||
* AfterAgent — capture full response
|
* AfterAgent — capture full agent response
|
||||||
|
* BeforeTool — capture tool intent before execution
|
||||||
* AfterTool — capture all tool results (matcher: "*")
|
* AfterTool — capture all tool results (matcher: "*")
|
||||||
* PreCompress — trigger summary
|
* Notification — capture system events (ToolPermission, etc.)
|
||||||
|
* PreCompress — trigger summary generation
|
||||||
* SessionEnd — finalize session
|
* SessionEnd — finalize session
|
||||||
|
*
|
||||||
|
* Skipped (model-level, too chatty):
|
||||||
|
* BeforeModel, AfterModel, BeforeToolSelection
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
@@ -39,6 +44,7 @@ interface GeminiHookEntry {
|
|||||||
type: 'command';
|
type: 'command';
|
||||||
command: string;
|
command: string;
|
||||||
timeout: number;
|
timeout: number;
|
||||||
|
description?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface GeminiHookMatcher {
|
interface GeminiHookMatcher {
|
||||||
@@ -62,15 +68,25 @@ const HOOK_NAME = 'claude-mem';
|
|||||||
const HOOK_TIMEOUT_MS = 5000;
|
const HOOK_TIMEOUT_MS = 5000;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The Gemini CLI events we register hooks for, mapped to our internal event names.
|
* Gemini CLI events → claude-mem internal events.
|
||||||
|
*
|
||||||
|
* We register 8 of 11 hooks. Skipped: BeforeModel, AfterModel, BeforeToolSelection
|
||||||
|
* (model-level events fire per-LLM-call — too chatty for observation capture).
|
||||||
*/
|
*/
|
||||||
const GEMINI_EVENT_TO_CLAUDE_MEM_EVENT: Record<string, string> = {
|
interface GeminiEventConfig {
|
||||||
'SessionStart': 'session-init',
|
claudeMemEvent: string;
|
||||||
'BeforeAgent': 'user-message',
|
description: string;
|
||||||
'AfterAgent': 'observation',
|
}
|
||||||
'AfterTool': 'observation',
|
|
||||||
'PreCompress': 'summarize',
|
const GEMINI_EVENTS: Record<string, GeminiEventConfig> = {
|
||||||
'SessionEnd': 'session-complete',
|
'SessionStart': { claudeMemEvent: 'context', description: 'Inject memory context from past sessions' },
|
||||||
|
'BeforeAgent': { claudeMemEvent: 'session-init', description: 'Initialize session and capture user prompt' },
|
||||||
|
'AfterAgent': { claudeMemEvent: 'observation', description: 'Capture full agent response' },
|
||||||
|
'BeforeTool': { claudeMemEvent: 'observation', description: 'Capture tool intent before execution' },
|
||||||
|
'AfterTool': { claudeMemEvent: 'observation', description: 'Capture tool results after execution' },
|
||||||
|
'Notification': { claudeMemEvent: 'observation', description: 'Capture system events (permissions, etc.)' },
|
||||||
|
'PreCompress': { claudeMemEvent: 'summarize', description: 'Generate session summary before compression' },
|
||||||
|
'SessionEnd': { claudeMemEvent: 'session-complete', description: 'Finalize session and persist memory' },
|
||||||
};
|
};
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
@@ -170,19 +186,17 @@ export async function installGeminiCliHooks(): Promise<number> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Register each event
|
// Register each event
|
||||||
for (const [geminiEvent, claudeMemEvent] of Object.entries(GEMINI_EVENT_TO_CLAUDE_MEM_EVENT)) {
|
for (const [geminiEvent, config] of Object.entries(GEMINI_EVENTS)) {
|
||||||
const command = buildHookCommand(bunPath, workerServicePath, claudeMemEvent);
|
const command = buildHookCommand(bunPath, workerServicePath, config.claudeMemEvent);
|
||||||
|
|
||||||
// AfterTool uses matcher: "*" to capture all tool results
|
|
||||||
const matcherValue = geminiEvent === 'AfterTool' ? '*' : '*';
|
|
||||||
|
|
||||||
const newMatcher: GeminiHookMatcher = {
|
const newMatcher: GeminiHookMatcher = {
|
||||||
matcher: matcherValue,
|
matcher: '*',
|
||||||
hooks: [{
|
hooks: [{
|
||||||
name: HOOK_NAME,
|
name: HOOK_NAME,
|
||||||
type: 'command',
|
type: 'command',
|
||||||
command,
|
command,
|
||||||
timeout: HOOK_TIMEOUT_MS,
|
timeout: HOOK_TIMEOUT_MS,
|
||||||
|
description: config.description,
|
||||||
}],
|
}],
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -193,25 +207,35 @@ export async function installGeminiCliHooks(): Promise<number> {
|
|||||||
// Write merged settings
|
// Write merged settings
|
||||||
writeFileSync(GEMINI_SETTINGS_PATH, JSON.stringify(settings, null, 2) + '\n');
|
writeFileSync(GEMINI_SETTINGS_PATH, JSON.stringify(settings, null, 2) + '\n');
|
||||||
console.log(` Updated ${GEMINI_SETTINGS_PATH}`);
|
console.log(` Updated ${GEMINI_SETTINGS_PATH}`);
|
||||||
console.log(` Registered hooks for: ${Object.keys(GEMINI_EVENT_TO_CLAUDE_MEM_EVENT).join(', ')}`);
|
console.log(` Registered hooks for: ${Object.keys(GEMINI_EVENTS).join(', ')}`);
|
||||||
|
|
||||||
// Inject context into GEMINI.md
|
// Inject context into GEMINI.md
|
||||||
injectGeminiMdContext();
|
injectGeminiMdContext();
|
||||||
|
|
||||||
console.log(`
|
console.log(`
|
||||||
Installation complete!
|
Installation complete! (8 hooks registered)
|
||||||
|
|
||||||
Hooks installed to: ${GEMINI_SETTINGS_PATH}
|
Hooks installed to: ${GEMINI_SETTINGS_PATH}
|
||||||
Using unified CLI: bun worker-service.cjs hook gemini-cli <event>
|
Using unified CLI: bun worker-service.cjs hook gemini-cli <event>
|
||||||
|
|
||||||
|
Registered hooks:
|
||||||
|
SessionStart → Inject memory context from past sessions
|
||||||
|
BeforeAgent → Capture user prompt for memory
|
||||||
|
AfterAgent → Capture full agent response
|
||||||
|
BeforeTool → Capture tool intent before execution
|
||||||
|
AfterTool → Capture tool results after execution
|
||||||
|
Notification → Capture system events (permissions, etc.)
|
||||||
|
PreCompress → Generate session summary before compression
|
||||||
|
SessionEnd → Finalize session and persist memory
|
||||||
|
|
||||||
Next steps:
|
Next steps:
|
||||||
1. Start claude-mem worker: claude-mem start
|
1. Start claude-mem worker: npx claude-mem start
|
||||||
2. Restart Gemini CLI to load the hooks
|
2. Restart Gemini CLI to load the hooks
|
||||||
3. Memory capture is now automatic!
|
3. Memory capture is now automatic!
|
||||||
|
|
||||||
Context Injection:
|
Context Injection:
|
||||||
Context from past sessions is injected via ${GEMINI_MD_PATH}
|
Memory from past sessions is injected via hookSpecificOutput.additionalContext
|
||||||
and automatically included in every Gemini CLI session.
|
on SessionStart, and persisted in ${GEMINI_MD_PATH} for static context.
|
||||||
`);
|
`);
|
||||||
|
|
||||||
return 0;
|
return 0;
|
||||||
@@ -429,7 +453,7 @@ export function checkGeminiCliHooksStatus(): number {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Check expected vs actual events
|
// Check expected vs actual events
|
||||||
const expectedEvents = Object.keys(GEMINI_EVENT_TO_CLAUDE_MEM_EVENT);
|
const expectedEvents = Object.keys(GEMINI_EVENTS);
|
||||||
const missingEvents = expectedEvents.filter((e) => !installedEvents.includes(e));
|
const missingEvents = expectedEvents.filter((e) => !installedEvents.includes(e));
|
||||||
if (missingEvents.length > 0) {
|
if (missingEvents.length > 0) {
|
||||||
console.log(` Warning: Missing events: ${missingEvents.join(', ')}`);
|
console.log(` Warning: Missing events: ${missingEvents.join(', ')}`);
|
||||||
|
|||||||
Reference in New Issue
Block a user