feat(cursor): Enhance context injection and project registry management
- Updated `context-inject.sh` to refresh context before prompt submission and ensure worker is running. - Added functionality to register and unregister projects for automatic context updates in `worker-service.ts`. - Implemented methods to read and write the cursor project registry, allowing for better management of installed hooks. - Integrated context updates into the `GeminiAgent`, `OpenRouterAgent`, and `SDKAgent` to ensure the latest context is available during sessions. This update improves the integration of Claude-Mem with Cursor, ensuring that context is consistently updated and accessible across sessions.
This commit is contained in:
@@ -23,6 +23,35 @@ This:
|
|||||||
2. Creates `hooks.json` configuration
|
2. Creates `hooks.json` configuration
|
||||||
3. Fetches existing context from claude-mem and writes to `.cursor/rules/claude-mem-context.mdc`
|
3. Fetches existing context from claude-mem and writes to `.cursor/rules/claude-mem-context.mdc`
|
||||||
|
|
||||||
|
### Context Updates at Three Points
|
||||||
|
|
||||||
|
Context is refreshed **three times** per session for maximum freshness:
|
||||||
|
|
||||||
|
1. **Before prompt submission** (`context-inject.sh`): Ensures you start with the latest context from previous sessions
|
||||||
|
2. **After summary completes** (worker auto-update): Immediately after the summary is saved, worker updates the context file
|
||||||
|
3. **After session ends** (`session-summary.sh`): Fallback update in case worker update was missed
|
||||||
|
|
||||||
|
### Before Prompt Hook Updates Context
|
||||||
|
|
||||||
|
When you submit a prompt, `context-inject.sh`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. Ensure worker is running
|
||||||
|
ensure_worker_running "$worker_port"
|
||||||
|
|
||||||
|
# 2. Fetch fresh context
|
||||||
|
context=$(curl -s ".../api/context/inject?project=...")
|
||||||
|
|
||||||
|
# 3. Write to rules file (used immediately by Cursor)
|
||||||
|
cat > .cursor/rules/claude-mem-context.mdc << EOF
|
||||||
|
---
|
||||||
|
alwaysApply: true
|
||||||
|
---
|
||||||
|
# Memory Context
|
||||||
|
${context}
|
||||||
|
EOF
|
||||||
|
```
|
||||||
|
|
||||||
### Stop Hook Updates Context
|
### Stop Hook Updates Context
|
||||||
|
|
||||||
After each session ends, `session-summary.sh`:
|
After each session ends, `session-summary.sh`:
|
||||||
@@ -64,19 +93,41 @@ description: "Claude-mem context from past sessions (auto-updated)"
|
|||||||
|
|
||||||
### Update Flow
|
### Update Flow
|
||||||
|
|
||||||
Context updates **after each session ends**:
|
Context updates at **three points**:
|
||||||
1. User has a conversation
|
|
||||||
2. Agent completes (loop ends)
|
**Before each prompt:**
|
||||||
3. `stop` hook runs `session-summary.sh`
|
1. User submits a prompt
|
||||||
4. Summary generated + context file updated
|
2. `beforeSubmitPrompt` hook runs `context-inject.sh`
|
||||||
5. **Next session** sees the updated context
|
3. Context file refreshed with latest observations from previous sessions
|
||||||
|
4. Cursor reads the updated rules file
|
||||||
|
|
||||||
|
**After summary completes (worker auto-update):**
|
||||||
|
1. Summary is saved to database
|
||||||
|
2. Worker checks if project is registered for Cursor
|
||||||
|
3. If yes, immediately writes updated context file with new observations
|
||||||
|
4. No hook involved - happens in the worker process
|
||||||
|
|
||||||
|
**After session ends (fallback):**
|
||||||
|
1. Agent completes (loop ends)
|
||||||
|
2. `stop` hook runs `session-summary.sh`
|
||||||
|
3. Context file updated (ensures nothing was missed)
|
||||||
|
4. Ready for next session
|
||||||
|
|
||||||
|
## Project Registry
|
||||||
|
|
||||||
|
When you run `claude-mem cursor install`, the project is registered in `~/.claude-mem/cursor-projects.json`. This allows the worker to automatically update your context file whenever a new summary is generated - even if it happens from Claude Code or another IDE working on the same project.
|
||||||
|
|
||||||
|
To see registered projects:
|
||||||
|
```bash
|
||||||
|
cat ~/.claude-mem/cursor-projects.json
|
||||||
|
```
|
||||||
|
|
||||||
## Comparison with Claude Code
|
## Comparison with Claude Code
|
||||||
|
|
||||||
| Feature | Claude Code | Cursor |
|
| Feature | Claude Code | Cursor |
|
||||||
|---------|-------------|--------|
|
|---------|-------------|--------|
|
||||||
| Context injection | ✅ `additionalContext` in hook output | ✅ Auto-updated rules file |
|
| Context injection | ✅ `additionalContext` in hook output | ✅ Auto-updated rules file |
|
||||||
| Injection timing | Immediate (same prompt) | Next session (after stop hook) |
|
| Injection timing | Immediate (same prompt) | Before prompt + after summary + after session |
|
||||||
| Persistence | Session only | File-based (persists across restarts) |
|
| Persistence | Session only | File-based (persists across restarts) |
|
||||||
| Initial setup | Automatic | `claude-mem cursor install` creates initial context |
|
| Initial setup | Automatic | `claude-mem cursor install` creates initial context |
|
||||||
| MCP tool access | ✅ Full support | ✅ Full support |
|
| MCP tool access | ✅ Full support | ✅ Full support |
|
||||||
@@ -88,7 +139,9 @@ When you run `claude-mem cursor install`:
|
|||||||
- If worker is running with existing memory → initial context is generated
|
- If worker is running with existing memory → initial context is generated
|
||||||
- If no existing memory → placeholder file created
|
- If no existing memory → placeholder file created
|
||||||
|
|
||||||
After each session ends, context is updated for the next session.
|
Context is then automatically refreshed:
|
||||||
|
- Before each prompt (ensures latest observations are included)
|
||||||
|
- After each session ends (captures new observations from the session)
|
||||||
|
|
||||||
## Additional Access Methods
|
## Additional Access Methods
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
# Context Hook for Cursor (beforeSubmitPrompt)
|
# Context Hook for Cursor (beforeSubmitPrompt)
|
||||||
# Ensures worker is running before prompt submission
|
# Ensures worker is running and refreshes context before prompt submission
|
||||||
#
|
#
|
||||||
# NOTE: Context is NOT updated here. Context updates happen in the stop hook
|
# Context is updated in BOTH places:
|
||||||
# (session-summary.sh) after the session completes, so new observations are included.
|
# - Here (beforeSubmitPrompt): Fresh context at session start
|
||||||
|
# - stop hook (session-summary.sh): Updated context after observations are made
|
||||||
|
|
||||||
# Source common utilities
|
# Source common utilities
|
||||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
@@ -18,12 +19,50 @@ check_dependencies >/dev/null 2>&1 || true
|
|||||||
# Read JSON input from stdin
|
# Read JSON input from stdin
|
||||||
input=$(read_json_input)
|
input=$(read_json_input)
|
||||||
|
|
||||||
|
# Extract workspace root
|
||||||
|
workspace_root=$(json_get "$input" "workspace_roots[0]" "")
|
||||||
|
if is_empty "$workspace_root"; then
|
||||||
|
workspace_root=$(pwd)
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Get project name
|
||||||
|
project_name=$(get_project_name "$workspace_root")
|
||||||
|
|
||||||
# Get worker port from settings
|
# Get worker port from settings
|
||||||
worker_port=$(get_worker_port)
|
worker_port=$(get_worker_port)
|
||||||
|
|
||||||
# Ensure worker is running (with retries)
|
# Ensure worker is running (with retries)
|
||||||
# This primes the worker before the session starts
|
# This primes the worker before the session starts
|
||||||
ensure_worker_running "$worker_port" >/dev/null 2>&1 || true
|
if ensure_worker_running "$worker_port" >/dev/null 2>&1; then
|
||||||
|
# Refresh context file with latest observations
|
||||||
|
project_encoded=$(url_encode "$project_name")
|
||||||
|
context=$(curl -s -f "http://127.0.0.1:${worker_port}/api/context/inject?project=${project_encoded}" 2>/dev/null || echo "")
|
||||||
|
|
||||||
|
if [ -n "$context" ]; then
|
||||||
|
rules_dir="${workspace_root}/.cursor/rules"
|
||||||
|
rules_file="${rules_dir}/claude-mem-context.mdc"
|
||||||
|
|
||||||
|
# Create rules directory if it doesn't exist
|
||||||
|
mkdir -p "$rules_dir" 2>/dev/null || true
|
||||||
|
|
||||||
|
# Write context as a Cursor rule with alwaysApply: true
|
||||||
|
cat > "$rules_file" 2>/dev/null << EOF
|
||||||
|
---
|
||||||
|
alwaysApply: true
|
||||||
|
description: "Claude-mem context from past sessions (auto-updated)"
|
||||||
|
---
|
||||||
|
|
||||||
|
# Memory Context from Past Sessions
|
||||||
|
|
||||||
|
The following context is from claude-mem, a persistent memory system that tracks your coding sessions.
|
||||||
|
|
||||||
|
${context}
|
||||||
|
|
||||||
|
---
|
||||||
|
*Updated after last session. Use claude-mem's MCP search tools for more detailed queries.*
|
||||||
|
EOF
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
# Allow prompt to continue
|
# Allow prompt to continue
|
||||||
echo '{"continue": true}'
|
echo '{"continue": true}'
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -30,6 +30,7 @@ const BUILT_IN_VERSION = typeof __DEFAULT_PACKAGE_VERSION__ !== 'undefined'
|
|||||||
// PID file management for self-spawn pattern
|
// PID file management for self-spawn pattern
|
||||||
const DATA_DIR = path.join(homedir(), '.claude-mem');
|
const DATA_DIR = path.join(homedir(), '.claude-mem');
|
||||||
const PID_FILE = path.join(DATA_DIR, 'worker.pid');
|
const PID_FILE = path.join(DATA_DIR, 'worker.pid');
|
||||||
|
const CURSOR_REGISTRY_FILE = path.join(DATA_DIR, 'cursor-projects.json');
|
||||||
const HOOK_RESPONSE = '{"continue": true, "suppressOutput": true}';
|
const HOOK_RESPONSE = '{"continue": true, "suppressOutput": true}';
|
||||||
|
|
||||||
interface PidInfo {
|
interface PidInfo {
|
||||||
@@ -62,6 +63,100 @@ function removePidFile(): void {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Cursor Project Registry
|
||||||
|
// Tracks which projects have Cursor hooks installed for auto-context updates
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
interface CursorProjectRegistry {
|
||||||
|
[projectName: string]: {
|
||||||
|
workspacePath: string;
|
||||||
|
installedAt: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function readCursorRegistry(): CursorProjectRegistry {
|
||||||
|
try {
|
||||||
|
if (!existsSync(CURSOR_REGISTRY_FILE)) return {};
|
||||||
|
return JSON.parse(readFileSync(CURSOR_REGISTRY_FILE, 'utf-8'));
|
||||||
|
} catch {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function writeCursorRegistry(registry: CursorProjectRegistry): void {
|
||||||
|
mkdirSync(DATA_DIR, { recursive: true });
|
||||||
|
writeFileSync(CURSOR_REGISTRY_FILE, JSON.stringify(registry, null, 2));
|
||||||
|
}
|
||||||
|
|
||||||
|
function registerCursorProject(projectName: string, workspacePath: string): void {
|
||||||
|
const registry = readCursorRegistry();
|
||||||
|
registry[projectName] = {
|
||||||
|
workspacePath,
|
||||||
|
installedAt: new Date().toISOString()
|
||||||
|
};
|
||||||
|
writeCursorRegistry(registry);
|
||||||
|
logger.info('CURSOR', 'Registered project for auto-context updates', { projectName, workspacePath });
|
||||||
|
}
|
||||||
|
|
||||||
|
function unregisterCursorProject(projectName: string): void {
|
||||||
|
const registry = readCursorRegistry();
|
||||||
|
if (registry[projectName]) {
|
||||||
|
delete registry[projectName];
|
||||||
|
writeCursorRegistry(registry);
|
||||||
|
logger.info('CURSOR', 'Unregistered project', { projectName });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update Cursor context files for all registered projects matching this project name.
|
||||||
|
* Called by SDK agents after saving a summary.
|
||||||
|
*/
|
||||||
|
export async function updateCursorContextForProject(projectName: string, port: number): Promise<void> {
|
||||||
|
const registry = readCursorRegistry();
|
||||||
|
const entry = registry[projectName];
|
||||||
|
|
||||||
|
if (!entry) return; // Project doesn't have Cursor hooks installed
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Fetch fresh context from worker
|
||||||
|
const response = await fetch(
|
||||||
|
`http://127.0.0.1:${port}/api/context/inject?project=${encodeURIComponent(projectName)}`
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!response.ok) return;
|
||||||
|
|
||||||
|
const context = await response.text();
|
||||||
|
if (!context || !context.trim()) return;
|
||||||
|
|
||||||
|
// Write to the project's Cursor rules file
|
||||||
|
const rulesDir = path.join(entry.workspacePath, '.cursor', 'rules');
|
||||||
|
const rulesFile = path.join(rulesDir, 'claude-mem-context.mdc');
|
||||||
|
|
||||||
|
mkdirSync(rulesDir, { recursive: true });
|
||||||
|
|
||||||
|
const content = `---
|
||||||
|
alwaysApply: true
|
||||||
|
description: "Claude-mem context from past sessions (auto-updated)"
|
||||||
|
---
|
||||||
|
|
||||||
|
# Memory Context from Past Sessions
|
||||||
|
|
||||||
|
The following context is from claude-mem, a persistent memory system that tracks your coding sessions.
|
||||||
|
|
||||||
|
${context}
|
||||||
|
|
||||||
|
---
|
||||||
|
*Updated after last session. Use claude-mem's MCP search tools for more detailed queries.*
|
||||||
|
`;
|
||||||
|
|
||||||
|
writeFileSync(rulesFile, content);
|
||||||
|
logger.debug('CURSOR', 'Updated context file', { projectName, rulesFile });
|
||||||
|
} catch (error) {
|
||||||
|
logger.warn('CURSOR', 'Failed to update context file', { projectName, error: (error as Error).message });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// No lock file needed - health checks and port binding provide coordination
|
// No lock file needed - health checks and port binding provide coordination
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -1205,6 +1300,10 @@ Use claude-mem's MCP search tools for manual memory queries.
|
|||||||
writeFileSync(rulesFile, placeholderContent);
|
writeFileSync(rulesFile, placeholderContent);
|
||||||
console.log(` ✓ Created placeholder context file (will populate after first session)`);
|
console.log(` ✓ Created placeholder context file (will populate after first session)`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Register project for automatic context updates after summaries
|
||||||
|
registerCursorProject(projectName, workspaceRoot);
|
||||||
|
console.log(` ✓ Registered for auto-context updates`);
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(`
|
console.log(`
|
||||||
@@ -1285,13 +1384,18 @@ function uninstallCursorHooks(target: string): number {
|
|||||||
console.log(` ✓ Removed hooks.json`);
|
console.log(` ✓ Removed hooks.json`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove context file if project-level
|
// Remove context file and unregister if project-level
|
||||||
if (target === 'project') {
|
if (target === 'project') {
|
||||||
const contextFile = path.join(targetDir, 'rules', 'claude-mem-context.mdc');
|
const contextFile = path.join(targetDir, 'rules', 'claude-mem-context.mdc');
|
||||||
if (existsSync(contextFile)) {
|
if (existsSync(contextFile)) {
|
||||||
unlinkSync(contextFile);
|
unlinkSync(contextFile);
|
||||||
console.log(` ✓ Removed context file`);
|
console.log(` ✓ Removed context file`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Unregister from auto-context updates
|
||||||
|
const projectName = path.basename(process.cwd());
|
||||||
|
unregisterCursorProject(projectName);
|
||||||
|
console.log(` ✓ Unregistered from auto-context updates`);
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(`\n✅ Uninstallation complete!\n`);
|
console.log(`\n✅ Uninstallation complete!\n`);
|
||||||
|
|||||||
@@ -20,6 +20,8 @@ import { buildInitPrompt, buildObservationPrompt, buildSummaryPrompt, buildConti
|
|||||||
import { SettingsDefaultsManager } from '../../shared/SettingsDefaultsManager.js';
|
import { SettingsDefaultsManager } from '../../shared/SettingsDefaultsManager.js';
|
||||||
import type { ActiveSession, ConversationMessage } from '../worker-types.js';
|
import type { ActiveSession, ConversationMessage } from '../worker-types.js';
|
||||||
import { ModeManager } from '../domain/ModeManager.js';
|
import { ModeManager } from '../domain/ModeManager.js';
|
||||||
|
import { updateCursorContextForProject } from '../worker-service.js';
|
||||||
|
import { getWorkerPort } from '../../shared/worker-utils.js';
|
||||||
|
|
||||||
// Gemini API endpoint
|
// Gemini API endpoint
|
||||||
const GEMINI_API_URL = 'https://generativelanguage.googleapis.com/v1beta/models';
|
const GEMINI_API_URL = 'https://generativelanguage.googleapis.com/v1beta/models';
|
||||||
@@ -493,6 +495,9 @@ export class GeminiAgent {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Update Cursor context file for registered projects (fire-and-forget)
|
||||||
|
updateCursorContextForProject(session.project, getWorkerPort()).catch(() => {});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Mark messages as processed
|
// Mark messages as processed
|
||||||
|
|||||||
@@ -20,6 +20,8 @@ import { SettingsDefaultsManager } from '../../shared/SettingsDefaultsManager.js
|
|||||||
import { USER_SETTINGS_PATH } from '../../shared/paths.js';
|
import { USER_SETTINGS_PATH } from '../../shared/paths.js';
|
||||||
import type { ActiveSession, ConversationMessage } from '../worker-types.js';
|
import type { ActiveSession, ConversationMessage } from '../worker-types.js';
|
||||||
import { ModeManager } from '../domain/ModeManager.js';
|
import { ModeManager } from '../domain/ModeManager.js';
|
||||||
|
import { updateCursorContextForProject } from '../worker-service.js';
|
||||||
|
import { getWorkerPort } from '../../shared/worker-utils.js';
|
||||||
|
|
||||||
// OpenRouter API endpoint
|
// OpenRouter API endpoint
|
||||||
const OPENROUTER_API_URL = 'https://openrouter.ai/api/v1/chat/completions';
|
const OPENROUTER_API_URL = 'https://openrouter.ai/api/v1/chat/completions';
|
||||||
@@ -536,6 +538,9 @@ export class OpenRouterAgent {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Update Cursor context file for registered projects (fire-and-forget)
|
||||||
|
updateCursorContextForProject(session.project, getWorkerPort()).catch(() => {});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Mark messages as processed
|
// Mark messages as processed
|
||||||
|
|||||||
@@ -20,6 +20,8 @@ import { SettingsDefaultsManager } from '../../shared/SettingsDefaultsManager.js
|
|||||||
import { USER_SETTINGS_PATH } from '../../shared/paths.js';
|
import { USER_SETTINGS_PATH } from '../../shared/paths.js';
|
||||||
import type { ActiveSession, SDKUserMessage, PendingMessage } from '../worker-types.js';
|
import type { ActiveSession, SDKUserMessage, PendingMessage } from '../worker-types.js';
|
||||||
import { ModeManager } from '../domain/ModeManager.js';
|
import { ModeManager } from '../domain/ModeManager.js';
|
||||||
|
import { updateCursorContextForProject } from '../worker-service.js';
|
||||||
|
import { getWorkerPort } from '../../shared/worker-utils.js';
|
||||||
|
|
||||||
// Import Agent SDK (assumes it's installed)
|
// Import Agent SDK (assumes it's installed)
|
||||||
// @ts-ignore - Agent SDK types may not be available
|
// @ts-ignore - Agent SDK types may not be available
|
||||||
@@ -474,6 +476,9 @@ export class SDKAgent {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Update Cursor context file for registered projects (fire-and-forget)
|
||||||
|
updateCursorContextForProject(session.project, getWorkerPort()).catch(() => {});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Mark messages as processed after successful observation/summary storage
|
// Mark messages as processed after successful observation/summary storage
|
||||||
|
|||||||
Reference in New Issue
Block a user