* docs: add folder index generator plan RFC for auto-generating folder-level CLAUDE.md files with observation timelines. Includes IDE symlink support and root CLAUDE.md integration. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * feat: implement folder index generator (Phase 1) Add automatic CLAUDE.md generation for folders containing observed files. This enables IDE context providers to access relevant memory observations. Core modules: - FolderDiscovery: Extract folders from observation file paths - FolderTimelineCompiler: Compile chronological timeline per folder - ClaudeMdGenerator: Write CLAUDE.md with tag-based content replacement - FolderIndexOrchestrator: Coordinate regeneration on observation save Integration: - Event-driven regeneration after observation save in ResponseProcessor - HTTP endpoints for folder discovery, timeline, and manual generation - Settings for enabling/configuring folder index behavior The <claude-mem-context> tag wrapping ensures: - Manual CLAUDE.md content is preserved - Auto-generated content won't be recursively observed - Clean separation between user and system content 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * feat: add updateFolderClaudeMd function to CursorHooksInstaller Adds function to update CLAUDE.md files for folders touched by observations. Uses existing /api/search/by-file endpoint, preserves content outside <claude-mem-context> tags, and writes atomically via temp file + rename. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * feat: hook updateFolderClaudeMd into ResponseProcessor Calls updateFolderClaudeMd after observation save to update folder-level CLAUDE.md files. Uses fire-and-forget pattern with error logging. Extracts file paths from saved observations and workspace path from registry. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * feat: add timeline formatting for folder CLAUDE.md files Implements formatTimelineForClaudeMd function that transforms API response into compact markdown table format. Converts emojis to text labels, handles ditto marks for timestamps, and groups under "Recent" header. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * refactor: remove old folder-index implementation Deletes redundant folder-index services that were replaced by the simpler updateFolderClaudeMd approach in CursorHooksInstaller.ts. Removed: - src/services/folder-index/ directory (5 files) - FolderIndexRoutes.ts - folder-index settings from SettingsDefaultsManager - folder-index route registration from worker-service 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * feat: add worktree-aware project filtering for unified timelines Detect git worktrees and show both parent repo and worktree observations in the session start timeline. When running in a worktree, the context now includes observations from both projects, interleaved chronologically. - Add detectWorktree() utility to identify worktree directories - Add getProjectContext() to return parent + worktree projects - Update context hook to pass multi-project queries - Add queryObservationsMulti() and querySummariesMulti() for IN clauses - Maintain backward compatibility with single-project queries 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com> * fix: restructure logging to prove session correctness and reduce noise Add critical logging at each stage of the session lifecycle to prove the session ID chain (contentSessionId → sessionDbId → memorySessionId) stays aligned. New logs include CREATED, ENQUEUED, CLAIMED, MEMORY_ID_CAPTURED, STORING, and STORED. Move intermediate migration and backfill progress logs to DEBUG level to reduce noise, keeping only essential initialization and completion logs at INFO level. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com> * refactor: extract folder CLAUDE.md utils to shared location Moves folder CLAUDE.md utilities from CursorHooksInstaller to a new shared utils file. Removes Cursor registry dependency - file paths from observations are already absolute, no workspace lookup needed. New file: src/utils/claude-md-utils.ts - replaceTaggedContent() - preserves user content outside tags - writeClaudeMdToFolder() - atomic writes with tag preservation - formatTimelineForClaudeMd() - API response to compact markdown - updateFolderClaudeMdFiles() - orchestrates folder updates 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix: trigger folder CLAUDE.md updates when observations are saved The folder CLAUDE.md update was previously only triggered in syncAndBroadcastSummary, but summaries run with observationCount=0 (observations are saved separately). Moved the update logic to syncAndBroadcastObservations where file paths are available. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * all the claudes * test: add unit tests for claude-md-utils pure functions Add 11 tests covering replaceTaggedContent and formatTimelineForClaudeMd: - replaceTaggedContent: empty content, tag replacement, appending, partial tags - formatTimelineForClaudeMd: empty input, parsing, ditto marks, session IDs 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * test: add integration tests for file operation functions Add 9 tests for writeClaudeMdToFolder and updateFolderClaudeMdFiles: - writeClaudeMdToFolder: folder creation, content preservation, nested dirs, atomic writes - updateFolderClaudeMdFiles: empty skip, fetch/write, deduplication, error handling 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * test: add unit tests for timeline-formatting utilities Add 14 tests for extractFirstFile and groupByDate functions: - extractFirstFile: relative paths, fallback to files_read, null handling, invalid JSON - groupByDate: empty arrays, date grouping, chronological sorting, item preservation 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * chore: rebuild plugin scripts with merged features * docs: add project-specific CLAUDE.md with architecture and development notes * fix: exclude project root from auto-generated CLAUDE.md updates Skip folders containing .git directory when auto-updating subfolder CLAUDE.md files. This ensures: 1. Root CLAUDE.md remains user-managed and untouched by the system 2. SessionStart context injection stays pristine throughout the session 3. Subfolder CLAUDE.md files continue to receive live context updates 4. Cleaner separation between user-authored root docs and auto-generated folder indexes 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix: prevent crash from resuming stale SDK sessions on worker restart When the worker restarts, it was incorrectly passing the `resume` parameter to INIT prompts (lastPromptNumber=1) when a memorySessionId existed from a previous SDK session. This caused "Claude Code process exited with code 1" crashes because the SDK tried to resume into a session that no longer exists. Root cause: The resume condition only checked `hasRealMemorySessionId` but did not verify that this was a CONTINUATION prompt (lastPromptNumber > 1). Fix: Add `session.lastPromptNumber > 1` check to the resume condition: - Before: `...(hasRealMemorySessionId && { resume: session.memorySessionId })` - After: `...(hasRealMemorySessionId && session.lastPromptNumber > 1 && { resume: ... })` Also added: - Enhanced debug logging that warns when skipping resume for INIT prompts - Unit tests in tests/sdk-agent-resume.test.ts (9 test cases) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix: properly handle Chroma MCP connection errors Previously, ensureCollection() caught ALL errors from chroma_get_collection_info and assumed they meant "collection doesn't exist", triggering unnecessary collection creation attempts. Connection errors like "Not connected" or "MCP error -32000: Connection closed" would cascade into failed creation attempts. Similarly, queryChroma() would silently return empty results when the MCP call failed, masking the underlying connection problem. Changes: - ensureCollection(): Detect connection errors and re-throw immediately instead of attempting collection creation - queryChroma(): Wrap MCP call in try-catch and throw connection errors instead of returning empty results - Both methods reset connection state (connected=false, client=null) on connection errors so subsequent operations can attempt to reconnect 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * pushed * fix: scope regenerate-claude-md.ts to current working directory Critical bug fix: The script was querying ALL observations from the database across ALL projects ever recorded (1396+ folders), then attempting to write CLAUDE.md files everywhere including other projects, non-existent paths, and ignored directories. Changes: - Use git ls-files to discover folders (respects .gitignore automatically) - Filter database query to current project only (by folder name) - Use relative paths for database queries (matches storage format) - Add --clean flag to remove auto-generated CLAUDE.md files - Add fallback directory walker for non-git repos Now correctly scopes to 26 folders with observations instead of 1396+. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * docs and adjustments * fix: cleanup mode strips tags instead of deleting files blindly The cleanup mode was incorrectly deleting entire files that contained <claude-mem-context> tags. The correct behavior (per original design): 1. Strip the <claude-mem-context>...</claude-mem-context> section 2. If empty after stripping → delete the file 3. If has remaining content → save the stripped version Now properly preserves user content in CLAUDE.md files while removing only the auto-generated sections. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * deleted some files * chore: regenerate folder CLAUDE.md files with fixed script Regenerated 23 folder CLAUDE.md files using the corrected script that: - Scopes to current working directory only - Uses git ls-files to respect .gitignore - Filters by project name 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * Update CLAUDE.md files for January 5, 2026 - Regenerated and staged 23 CLAUDE.md files with a mix of new and modified content. - Fixed cleanup mode to properly strip tags instead of deleting files blindly. - Cleaned up empty CLAUDE.md files from various directories, including ~/.claude and ~/Scripts. - Conducted dry-run cleanup that identified a significant reduction in auto-generated CLAUDE.md files. - Removed the isAutoGeneratedClaudeMd function due to incorrect file deletion behavior. * feat: use settings for observation limit in batch regeneration script Replace hard-coded limit of 10 with configurable CLAUDE_MEM_CONTEXT_OBSERVATIONS setting (default: 50). This allows users to control how many observations appear in folder CLAUDE.md files. Changes: - Import SettingsDefaultsManager and load settings at script startup - Use OBSERVATION_LIMIT constant derived from settings at both call sites - Remove stale default parameter from findObservationsByFolder function 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * feat: use settings for observation limit in event-driven updates Replace hard-coded limit of 10 in updateFolderClaudeMdFiles with configurable CLAUDE_MEM_CONTEXT_OBSERVATIONS setting (default: 50). Changes: - Import SettingsDefaultsManager and os module - Load settings at function start (once, not in loop) - Use limit from settings in API call 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * feat: Implement configurable observation limits and enhance search functionality - Added configurable observation limits to batch regeneration scripts. - Enhanced SearchManager to handle folder queries and normalize parameters. - Introduced methods to check for direct child files in observations and sessions. - Updated SearchOptions interface to include isFolder flag for filtering. - Improved code quality with comprehensive reviews and anti-pattern checks. - Cleaned up auto-generated CLAUDE.md files across various directories. - Documented recent changes and improvements in CLAUDE.md files. * build asset * Project Context from Claude-Mem auto-added (can be auto removed at any time) * CLAUDE.md updates * fix: resolve CLAUDE.md files to correct directory in worktree setups When using git worktrees, CLAUDE.md files were being written relative to the worker's process.cwd() instead of the actual project directory. This fix threads the project's cwd from message processing through to the file writing utilities, ensuring CLAUDE.md files are created in the correct project directory regardless of where the worker was started. Changes: - Add projectRoot parameter to updateFolderClaudeMdFiles for path resolution - Thread projectRoot through ResponseProcessor call chain - Track lastCwd from messages in SDKAgent, GeminiAgent, OpenRouterAgent - Add tests for relative/absolute path handling with projectRoot 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * more project context updates * context updates * planning context * feat: add CLI infrastructure for unified hook architecture (Phase 1) - Add src/cli/types.ts with NormalizedHookInput, HookResult, PlatformAdapter, EventHandler interfaces - Add src/cli/stdin-reader.ts with readJsonFromStdin() extracted from save-hook.ts pattern 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * feat: add platform adapters for unified hook CLI (Phase 2) - Add claude-code adapter mapping session_id, tool_name, etc. - Add cursor adapter mapping conversation_id, workspace_roots, result_json - Add raw adapter for testing/passthrough - Add getPlatformAdapter() factory function 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * feat: add event handlers for unified hook CLI (Phase 3) - Add context handler (GET /api/context/inject) - Add session-init handler (POST /api/sessions/init) - Add observation handler (POST /api/sessions/observations) - Add summarize handler (POST /api/sessions/summarize) - Add user-message handler (stderr output, exit code 3) - Add file-edit handler for Cursor afterFileEdit events - Add getEventHandler() factory function All handlers copy exact HTTP calls from original hooks. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * feat: add hook command dispatcher (Phase 4) - Add src/cli/hook-command.ts dispatching stdin to adapters and handlers - Add 'hook' case to worker-service.ts CLI switch - Usage: bun worker-service.cjs hook <platform> <event> 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * feat: update hooks.json to use unified CLI (Phase 5) - Change context-hook.js to: hook claude-code context - Change new-hook.js to: hook claude-code session-init - Change save-hook.js to: hook claude-code observation - Change summary-hook.js to: hook claude-code summarize - Change user-message-hook.js to: hook claude-code user-message All hooks now route through unified CLI: bun worker-service.cjs hook <platform> <event> 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * feat: update Cursor integration to use unified CLI (Phase 6) - Update CursorHooksInstaller to generate unified CLI commands - Use node instead of shell scripts for Cursor hooks - Add platform field to NormalizedHookInput for handler decisions - Skip SDK agent init for Cursor platform (not supported) - Fix file-edit handler to use 'write_file' tool name Cursor event mapping: - beforeSubmitPrompt → session-init, context - afterMCPExecution/afterShellExecution → observation - afterFileEdit → file-edit - stop → summarize 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * feat: remove old hook files, complete unified CLI migration (Phase 7) Deleted files: - src/hooks/context-hook.ts, new-hook.ts, save-hook.ts, summary-hook.ts, user-message-hook.ts - cursor-hooks/*.sh and *.ps1 shell scripts - plugin/scripts/*-hook.js built files Modified: - scripts/build-hooks.js: removed hook build loop Build now produces only: worker-service.cjs, mcp-server.cjs, context-generator.cjs All hooks route through: bun worker-service.cjs hook <platform> <event> 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix: handle undefined stdin in platform adapters for SessionStart hooks SessionStart hooks don't receive stdin data from Claude Code, causing the adapters to crash when trying to access properties on undefined. Added null coalescing to handle empty input gracefully. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * context update * fix: replace deprecated WMIC with PowerShell for Windows 11 compatibility Fixes #625 Changes: - Replace WMIC process queries with PowerShell Get-Process and Get-CimInstance - WMIC is deprecated in Windows 11 and causes terminal tab accumulation - PowerShell provides simpler output format (just PIDs, not "ProcessId=1234") - Update tests to match new PowerShell output parsing logic Benefits: - Windows 11 compatibility (WMIC removal planned) - Fixes terminal window accumulation issue on Windows - Cleaner, more maintainable parsing logic - Same security validation (PID > 0, integer checks) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com> * fix: graceful exit strategy to prevent Windows Terminal tab accumulation (#625) Problem: Windows Terminal keeps tabs open when processes exit with code 1, leading to tab accumulation during worker lifecycle operations (start, stop, restart, version mismatches, port conflicts). This created a poor user experience with dozens of orphaned terminal tabs. Solution: Implemented graceful exit strategy using exit code 0 for all expected failure scenarios. The wrapper and plugin handle restart logic, so child processes don't need to signal failure with non-zero exit codes. Key Changes: - worker-service.ts: Changed all process.exit(1) to process.exit(0) in: - Port conflict scenarios - Version mismatch recovery - Daemon spawn failures - Health check timeouts - Restart failures - Worker startup errors - mcp-server.ts: Changed fatal error exit from 1 to 0 - ProcessManager.ts: Changed signal handler error exit from 1 to 0 - hook-command.ts: Changed hook error exit code from 1 to 2 (BLOCKING_ERROR) to ensure users see error messages (per Claude Code docs, exit 1 only shows in verbose mode) All exits include explanatory comments documenting the graceful exit strategy. Fixes #625 Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com> * docs: update CLAUDE.md activity logs Auto-generated context updates from work on issue #625: - Windows 11 WMIC migration - PowerShell process enumeration - Graceful exit strategy implementation - PR creation for Windows Terminal tab fix Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com> * docs: update activity logs in CLAUDE.md files across multiple directories * chore: update CLAUDE.md files with recent activity and documentation improvements - Adjusted dates and entries in CLAUDE.md for various components including reports and services. - Added detailed activity logs for worker services, CLI commands, and server interactions. - Documented exit code strategy in CLAUDE.md to clarify graceful exit philosophy. - Extracted PowerShell timeout constant and updated related documentation and tests. - Enhanced log level audit strategy and clarified logging patterns across services. * polish: extract PowerShell timeout constant and document exit code strategy - Extract magic number 60000ms to HOOK_TIMEOUTS.POWERSHELL_COMMAND (10000ms) - Reduce PowerShell timeout from 60s to 10s per review feedback - Document exit code strategy in CLAUDE.md - Add test coverage for new constant Addresses review feedback from PR #628 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * build assets --------- Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,192 +0,0 @@
|
||||
# Common utility functions for Cursor hooks (PowerShell)
|
||||
# Dot-source this file in hook scripts: . "$PSScriptRoot\common.ps1"
|
||||
# Note: ErrorActionPreference should be set in each script, not globally here
|
||||
|
||||
# Get worker port from settings with validation
|
||||
function Get-WorkerPort {
|
||||
$settingsPath = Join-Path $env:USERPROFILE ".claude-mem\settings.json"
|
||||
$port = 37777
|
||||
|
||||
if (Test-Path $settingsPath) {
|
||||
try {
|
||||
$settings = Get-Content $settingsPath -Raw | ConvertFrom-Json
|
||||
if ($settings.CLAUDE_MEM_WORKER_PORT) {
|
||||
$parsedPort = [int]$settings.CLAUDE_MEM_WORKER_PORT
|
||||
if ($parsedPort -ge 1 -and $parsedPort -le 65535) {
|
||||
$port = $parsedPort
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
# Ignore parse errors, use default
|
||||
}
|
||||
}
|
||||
|
||||
return $port
|
||||
}
|
||||
|
||||
# Ensure worker is running with retries
|
||||
function Test-WorkerReady {
|
||||
param(
|
||||
[int]$Port = 37777,
|
||||
[int]$MaxRetries = 75
|
||||
)
|
||||
|
||||
for ($i = 0; $i -lt $MaxRetries; $i++) {
|
||||
try {
|
||||
$response = Invoke-RestMethod -Uri "http://127.0.0.1:$Port/api/readiness" -Method Get -TimeoutSec 1 -ErrorAction Stop
|
||||
return $true
|
||||
} catch {
|
||||
Start-Sleep -Milliseconds 200
|
||||
}
|
||||
}
|
||||
|
||||
return $false
|
||||
}
|
||||
|
||||
# Get project name from workspace root
|
||||
function Get-ProjectName {
|
||||
param([string]$WorkspaceRoot)
|
||||
|
||||
if ([string]::IsNullOrEmpty($WorkspaceRoot)) {
|
||||
return "unknown-project"
|
||||
}
|
||||
|
||||
# Handle Windows drive root (e.g., "C:\")
|
||||
if ($WorkspaceRoot -match '^([A-Za-z]):\\?$') {
|
||||
return "drive-$($Matches[1].ToUpper())"
|
||||
}
|
||||
|
||||
$projectName = Split-Path $WorkspaceRoot -Leaf
|
||||
if ([string]::IsNullOrEmpty($projectName)) {
|
||||
return "unknown-project"
|
||||
}
|
||||
|
||||
return $projectName
|
||||
}
|
||||
|
||||
# URL encode a string
|
||||
function Get-UrlEncodedString {
|
||||
param([string]$String)
|
||||
|
||||
if ([string]::IsNullOrEmpty($String)) {
|
||||
return ""
|
||||
}
|
||||
|
||||
return [System.Uri]::EscapeDataString($String)
|
||||
}
|
||||
|
||||
# Check if string is empty or null
|
||||
function Test-IsEmpty {
|
||||
param([string]$String)
|
||||
|
||||
return [string]::IsNullOrEmpty($String) -or $String -eq "null" -or $String -eq "empty"
|
||||
}
|
||||
|
||||
# Safely read JSON from stdin with error handling
|
||||
function Read-JsonInput {
|
||||
try {
|
||||
$input = [Console]::In.ReadToEnd()
|
||||
if ([string]::IsNullOrEmpty($input)) {
|
||||
return @{}
|
||||
}
|
||||
return $input | ConvertFrom-Json -ErrorAction Stop
|
||||
} catch {
|
||||
return @{}
|
||||
}
|
||||
}
|
||||
|
||||
# Safely get JSON field with fallback
|
||||
function Get-JsonField {
|
||||
param(
|
||||
[PSObject]$Json,
|
||||
[string]$Field,
|
||||
[string]$Fallback = ""
|
||||
)
|
||||
|
||||
if ($null -eq $Json) {
|
||||
return $Fallback
|
||||
}
|
||||
|
||||
# Handle array access syntax (e.g., "workspace_roots[0]")
|
||||
if ($Field -match '^(.+)\[(\d+)\]$') {
|
||||
$arrayField = $Matches[1]
|
||||
$index = [int]$Matches[2]
|
||||
|
||||
if ($Json.PSObject.Properties.Name -contains $arrayField) {
|
||||
$array = $Json.$arrayField
|
||||
if ($null -ne $array -and $array.Count -gt $index) {
|
||||
$value = $array[$index]
|
||||
if (-not (Test-IsEmpty $value)) {
|
||||
return $value
|
||||
}
|
||||
}
|
||||
}
|
||||
return $Fallback
|
||||
}
|
||||
|
||||
# Simple field access
|
||||
if ($Json.PSObject.Properties.Name -contains $Field) {
|
||||
$value = $Json.$Field
|
||||
if (-not (Test-IsEmpty $value)) {
|
||||
return $value
|
||||
}
|
||||
}
|
||||
|
||||
return $Fallback
|
||||
}
|
||||
|
||||
# Convert object to JSON string (compact)
|
||||
function ConvertTo-JsonCompact {
|
||||
param([object]$Object)
|
||||
|
||||
return $Object | ConvertTo-Json -Compress -Depth 10
|
||||
}
|
||||
|
||||
# Send HTTP POST request (fire-and-forget style)
|
||||
function Send-HttpPostAsync {
|
||||
param(
|
||||
[string]$Uri,
|
||||
[object]$Body
|
||||
)
|
||||
|
||||
try {
|
||||
$bodyJson = ConvertTo-JsonCompact $Body
|
||||
Start-Job -ScriptBlock {
|
||||
param($u, $b)
|
||||
try {
|
||||
Invoke-RestMethod -Uri $u -Method Post -Body $b -ContentType "application/json" -TimeoutSec 5 -ErrorAction SilentlyContinue | Out-Null
|
||||
} catch {}
|
||||
} -ArgumentList $Uri, $bodyJson | Out-Null
|
||||
} catch {
|
||||
# Ignore errors - fire and forget
|
||||
}
|
||||
}
|
||||
|
||||
# Send HTTP POST request (synchronous)
|
||||
function Send-HttpPost {
|
||||
param(
|
||||
[string]$Uri,
|
||||
[object]$Body
|
||||
)
|
||||
|
||||
try {
|
||||
$bodyJson = ConvertTo-JsonCompact $Body
|
||||
Invoke-RestMethod -Uri $u -Method Post -Body $bodyJson -ContentType "application/json" -TimeoutSec 5 -ErrorAction SilentlyContinue | Out-Null
|
||||
} catch {
|
||||
# Ignore errors
|
||||
}
|
||||
}
|
||||
|
||||
# Get HTTP response
|
||||
function Get-HttpResponse {
|
||||
param(
|
||||
[string]$Uri,
|
||||
[int]$TimeoutSec = 5
|
||||
)
|
||||
|
||||
try {
|
||||
return Invoke-RestMethod -Uri $Uri -Method Get -TimeoutSec $TimeoutSec -ErrorAction Stop
|
||||
} catch {
|
||||
return $null
|
||||
}
|
||||
}
|
||||
@@ -1,140 +0,0 @@
|
||||
#!/bin/bash
|
||||
# Common utility functions for Cursor hooks
|
||||
# Source this file in hook scripts: source "$(dirname "$0")/common.sh"
|
||||
|
||||
# Check if required commands exist
|
||||
check_dependencies() {
|
||||
local missing=()
|
||||
|
||||
if ! command -v jq >/dev/null 2>&1; then
|
||||
missing+=("jq")
|
||||
fi
|
||||
|
||||
if ! command -v curl >/dev/null 2>&1; then
|
||||
missing+=("curl")
|
||||
fi
|
||||
|
||||
if [ ${#missing[@]} -gt 0 ]; then
|
||||
echo "Error: Missing required dependencies: ${missing[*]}" >&2
|
||||
echo "Please install: ${missing[*]}" >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
# Safely read JSON from stdin with error handling
|
||||
read_json_input() {
|
||||
local input
|
||||
input=$(cat 2>/dev/null || echo "{}")
|
||||
|
||||
# Validate JSON
|
||||
if ! echo "$input" | jq empty 2>/dev/null; then
|
||||
# Invalid JSON - return empty object
|
||||
echo "{}"
|
||||
return 1
|
||||
fi
|
||||
|
||||
echo "$input"
|
||||
return 0
|
||||
}
|
||||
|
||||
# Get worker port from settings with validation
|
||||
get_worker_port() {
|
||||
local data_dir="${HOME}/.claude-mem"
|
||||
local settings_file="${data_dir}/settings.json"
|
||||
local port="37777"
|
||||
|
||||
if [ -f "$settings_file" ]; then
|
||||
local parsed_port
|
||||
parsed_port=$(jq -r '.CLAUDE_MEM_WORKER_PORT // "37777"' "$settings_file" 2>/dev/null || echo "37777")
|
||||
|
||||
# Validate port is a number between 1-65535
|
||||
if [[ "$parsed_port" =~ ^[0-9]+$ ]] && [ "$parsed_port" -ge 1 ] && [ "$parsed_port" -le 65535 ]; then
|
||||
port="$parsed_port"
|
||||
fi
|
||||
fi
|
||||
|
||||
echo "$port"
|
||||
}
|
||||
|
||||
# Ensure worker is running with retries
|
||||
ensure_worker_running() {
|
||||
local port="${1:-37777}"
|
||||
local max_retries="${2:-75}" # 15 seconds total (75 * 0.2s)
|
||||
local retry_count=0
|
||||
|
||||
while [ $retry_count -lt $max_retries ]; do
|
||||
if curl -s -f "http://127.0.0.1:${port}/api/readiness" >/dev/null 2>&1; then
|
||||
return 0
|
||||
fi
|
||||
sleep 0.2
|
||||
retry_count=$((retry_count + 1))
|
||||
done
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
# URL encode a string (basic implementation)
|
||||
url_encode() {
|
||||
local string="$1"
|
||||
# Use printf to URL encode
|
||||
printf '%s' "$string" | jq -sRr @uri
|
||||
}
|
||||
|
||||
# Get project name from workspace root
|
||||
get_project_name() {
|
||||
local workspace_root="$1"
|
||||
|
||||
if [ -z "$workspace_root" ]; then
|
||||
echo "unknown-project"
|
||||
return
|
||||
fi
|
||||
|
||||
# Use basename, fallback to unknown-project
|
||||
local project_name
|
||||
project_name=$(basename "$workspace_root" 2>/dev/null || echo "unknown-project")
|
||||
|
||||
# Handle edge case: empty basename (root directory)
|
||||
if [ -z "$project_name" ]; then
|
||||
# Check if it's a Windows drive root
|
||||
if [[ "$workspace_root" =~ ^[A-Za-z]:\\?$ ]]; then
|
||||
local drive_letter
|
||||
drive_letter=$(echo "$workspace_root" | grep -oE '^[A-Za-z]' | tr '[:lower:]' '[:upper:]')
|
||||
echo "drive-${drive_letter}"
|
||||
else
|
||||
echo "unknown-project"
|
||||
fi
|
||||
else
|
||||
echo "$project_name"
|
||||
fi
|
||||
}
|
||||
|
||||
# Safely extract JSON field with fallback
|
||||
# Supports both simple fields (e.g., "conversation_id") and array access (e.g., "workspace_roots[0]")
|
||||
json_get() {
|
||||
local json="$1"
|
||||
local field="$2"
|
||||
local fallback="${3:-}"
|
||||
|
||||
local value
|
||||
|
||||
# Handle array access syntax (e.g., "workspace_roots[0]")
|
||||
if [[ "$field" =~ ^(.+)\[([0-9]+)\]$ ]]; then
|
||||
local array_field="${BASH_REMATCH[1]}"
|
||||
local index="${BASH_REMATCH[2]}"
|
||||
value=$(echo "$json" | jq -r --arg f "$array_field" --arg i "$index" --arg fb "$fallback" '.[$f] // [] | .[$i | tonumber] // $fb' 2>/dev/null || echo "$fallback")
|
||||
else
|
||||
# Simple field access
|
||||
value=$(echo "$json" | jq -r --arg f "$field" --arg fb "$fallback" '.[$f] // $fb' 2>/dev/null || echo "$fallback")
|
||||
fi
|
||||
|
||||
echo "$value"
|
||||
}
|
||||
|
||||
# Check if string is empty or null
|
||||
is_empty() {
|
||||
local str="$1"
|
||||
[ -z "$str" ] || [ "$str" = "null" ] || [ "$str" = "empty" ]
|
||||
}
|
||||
|
||||
@@ -1,74 +0,0 @@
|
||||
# Context Hook for Cursor (beforeSubmitPrompt) - PowerShell
|
||||
# Ensures worker is running and refreshes context before prompt submission
|
||||
#
|
||||
# Context is updated in BOTH places:
|
||||
# - Here (beforeSubmitPrompt): Fresh context at session start
|
||||
# - stop hook (session-summary.ps1): Updated context after observations are made
|
||||
|
||||
$ErrorActionPreference = "SilentlyContinue"
|
||||
|
||||
# Source common utilities
|
||||
$commonPath = Join-Path $PSScriptRoot "common.ps1"
|
||||
if (Test-Path $commonPath) {
|
||||
. $commonPath
|
||||
} else {
|
||||
Write-Output '{"continue": true}'
|
||||
exit 0
|
||||
}
|
||||
|
||||
# Read JSON input from stdin
|
||||
$input = Read-JsonInput
|
||||
|
||||
# Extract workspace root
|
||||
$workspaceRoot = Get-JsonField $input "workspace_roots[0]" ""
|
||||
if (Test-IsEmpty $workspaceRoot) {
|
||||
$workspaceRoot = Get-Location
|
||||
}
|
||||
|
||||
# Get project name
|
||||
$projectName = Get-ProjectName $workspaceRoot
|
||||
|
||||
# Get worker port from settings
|
||||
$workerPort = Get-WorkerPort
|
||||
|
||||
# Ensure worker is running (with retries)
|
||||
# This primes the worker before the session starts
|
||||
if (Test-WorkerReady -Port $workerPort) {
|
||||
# Refresh context file with latest observations
|
||||
$projectEncoded = Get-UrlEncodedString $projectName
|
||||
$contextUri = "http://127.0.0.1:$workerPort/api/context/inject?project=$projectEncoded"
|
||||
$context = Get-HttpResponse -Uri $contextUri
|
||||
|
||||
if (-not [string]::IsNullOrEmpty($context)) {
|
||||
$rulesDir = Join-Path $workspaceRoot ".cursor\rules"
|
||||
$rulesFile = Join-Path $rulesDir "claude-mem-context.mdc"
|
||||
|
||||
# Create rules directory if it doesn't exist
|
||||
if (-not (Test-Path $rulesDir)) {
|
||||
New-Item -ItemType Directory -Path $rulesDir -Force | Out-Null
|
||||
}
|
||||
|
||||
# Write context as a Cursor rule with alwaysApply: true
|
||||
$ruleContent = @"
|
||||
---
|
||||
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.*
|
||||
"@
|
||||
|
||||
Set-Content -Path $rulesFile -Value $ruleContent -Encoding UTF8 -Force
|
||||
}
|
||||
}
|
||||
|
||||
# Allow prompt to continue
|
||||
Write-Output '{"continue": true}'
|
||||
exit 0
|
||||
@@ -1,70 +0,0 @@
|
||||
#!/bin/bash
|
||||
# Context Hook for Cursor (beforeSubmitPrompt)
|
||||
# Ensures worker is running and refreshes context before prompt submission
|
||||
#
|
||||
# Context is updated in BOTH places:
|
||||
# - Here (beforeSubmitPrompt): Fresh context at session start
|
||||
# - stop hook (session-summary.sh): Updated context after observations are made
|
||||
|
||||
# Source common utilities
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
source "${SCRIPT_DIR}/common.sh" 2>/dev/null || {
|
||||
echo '{"continue": true}'
|
||||
exit 0
|
||||
}
|
||||
|
||||
# Check dependencies (non-blocking)
|
||||
check_dependencies >/dev/null 2>&1 || true
|
||||
|
||||
# Read JSON input from stdin
|
||||
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
|
||||
worker_port=$(get_worker_port)
|
||||
|
||||
# Ensure worker is running (with retries)
|
||||
# This primes the worker before the session starts
|
||||
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
|
||||
echo '{"continue": true}'
|
||||
exit 0
|
||||
|
||||
@@ -1,71 +0,0 @@
|
||||
#!/bin/bash
|
||||
# Installation script for claude-mem Cursor hooks
|
||||
|
||||
set -e
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
INSTALL_TYPE="${1:-user}" # 'user' (recommended) or 'project'
|
||||
|
||||
echo "Installing claude-mem Cursor hooks (${INSTALL_TYPE} level)..."
|
||||
|
||||
case "$INSTALL_TYPE" in
|
||||
"project")
|
||||
if [ ! -d ".cursor" ]; then
|
||||
mkdir -p .cursor
|
||||
fi
|
||||
TARGET_DIR=".cursor"
|
||||
HOOKS_DIR=".cursor/hooks"
|
||||
;;
|
||||
"user")
|
||||
TARGET_DIR="${HOME}/.cursor"
|
||||
HOOKS_DIR="${HOME}/.cursor/hooks"
|
||||
;;
|
||||
*)
|
||||
echo "Invalid install type: $INSTALL_TYPE"
|
||||
echo "Usage: $0 [user (recommended)|project]"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
# Create hooks directory
|
||||
mkdir -p "$HOOKS_DIR"
|
||||
|
||||
# Copy hook scripts
|
||||
echo "Copying hook scripts..."
|
||||
cp "$SCRIPT_DIR"/*.sh "$HOOKS_DIR/"
|
||||
chmod +x "$HOOKS_DIR"/*.sh
|
||||
|
||||
# Copy hooks.json
|
||||
echo "Copying hooks.json..."
|
||||
cp "$SCRIPT_DIR/hooks.json" "$TARGET_DIR/hooks.json"
|
||||
|
||||
# Update paths in hooks.json if needed
|
||||
# Use portable sed approach that works on both BSD (macOS) and GNU (Linux) sed
|
||||
if [ "$INSTALL_TYPE" = "project" ]; then
|
||||
# For project-level, paths should be relative
|
||||
# Create temp file, modify, then move (portable across sed variants)
|
||||
tmp_file=$(mktemp)
|
||||
sed 's|\./cursor-hooks/|\./\.cursor/hooks/|g' "$TARGET_DIR/hooks.json" > "$tmp_file"
|
||||
mv "$tmp_file" "$TARGET_DIR/hooks.json"
|
||||
else
|
||||
# For user-level, use absolute paths
|
||||
tmp_file=$(mktemp)
|
||||
sed "s|\./cursor-hooks/|${HOOKS_DIR}/|g" "$TARGET_DIR/hooks.json" > "$tmp_file"
|
||||
mv "$tmp_file" "$TARGET_DIR/hooks.json"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "✓ Installation complete!"
|
||||
echo ""
|
||||
echo "Hooks installed to: $TARGET_DIR/hooks.json"
|
||||
echo "Scripts installed to: $HOOKS_DIR"
|
||||
echo ""
|
||||
echo "Next steps:"
|
||||
echo "1. Ensure claude-mem worker is running:"
|
||||
echo " cd ~/.claude/plugins/marketplaces/thedotmack && npm run worker:start"
|
||||
echo ""
|
||||
echo "2. Restart Cursor to load the hooks"
|
||||
echo ""
|
||||
echo "3. Check Cursor Settings → Hooks tab to verify hooks are active"
|
||||
echo ""
|
||||
|
||||
@@ -1,126 +0,0 @@
|
||||
# Save File Edit Hook for Cursor (PowerShell)
|
||||
# Captures file edits made by the agent
|
||||
# Maps file edits to claude-mem observations
|
||||
|
||||
$ErrorActionPreference = "SilentlyContinue"
|
||||
|
||||
# Source common utilities
|
||||
$commonPath = Join-Path $PSScriptRoot "common.ps1"
|
||||
if (Test-Path $commonPath) {
|
||||
. $commonPath
|
||||
} else {
|
||||
Write-Warning "common.ps1 not found, using fallback functions"
|
||||
exit 0
|
||||
}
|
||||
|
||||
# Read JSON input from stdin with error handling
|
||||
$input = Read-JsonInput
|
||||
|
||||
# Extract common fields with safe fallbacks
|
||||
$conversationId = Get-JsonField $input "conversation_id" ""
|
||||
$generationId = Get-JsonField $input "generation_id" ""
|
||||
$filePath = Get-JsonField $input "file_path" ""
|
||||
$workspaceRoot = Get-JsonField $input "workspace_roots[0]" ""
|
||||
|
||||
# Fallback to current directory if no workspace root
|
||||
if (Test-IsEmpty $workspaceRoot) {
|
||||
$workspaceRoot = Get-Location
|
||||
}
|
||||
|
||||
# Exit if no file_path
|
||||
if (Test-IsEmpty $filePath) {
|
||||
exit 0
|
||||
}
|
||||
|
||||
# Use conversation_id as session_id, fallback to generation_id
|
||||
$sessionId = $conversationId
|
||||
if (Test-IsEmpty $sessionId) {
|
||||
$sessionId = $generationId
|
||||
}
|
||||
|
||||
# Exit if no session_id available
|
||||
if (Test-IsEmpty $sessionId) {
|
||||
exit 0
|
||||
}
|
||||
|
||||
# Get worker port from settings with validation
|
||||
$workerPort = Get-WorkerPort
|
||||
|
||||
# Extract edits array, defaulting to [] if invalid
|
||||
$edits = @()
|
||||
if ($input.PSObject.Properties.Name -contains "edits") {
|
||||
$edits = $input.edits
|
||||
if ($null -eq $edits -or -not ($edits -is [array])) {
|
||||
$edits = @()
|
||||
}
|
||||
}
|
||||
|
||||
# Exit if no edits
|
||||
if ($edits.Count -eq 0) {
|
||||
exit 0
|
||||
}
|
||||
|
||||
# Create a summary of the edits for the observation
|
||||
$editSummaries = @()
|
||||
foreach ($edit in $edits) {
|
||||
$oldStr = ""
|
||||
$newStr = ""
|
||||
|
||||
if ($edit.PSObject.Properties.Name -contains "old_string") {
|
||||
$oldStr = $edit.old_string
|
||||
if ($oldStr.Length -gt 50) {
|
||||
$oldStr = $oldStr.Substring(0, 50) + "..."
|
||||
}
|
||||
}
|
||||
|
||||
if ($edit.PSObject.Properties.Name -contains "new_string") {
|
||||
$newStr = $edit.new_string
|
||||
if ($newStr.Length -gt 50) {
|
||||
$newStr = $newStr.Substring(0, 50) + "..."
|
||||
}
|
||||
}
|
||||
|
||||
$editSummaries += "$oldStr → $newStr"
|
||||
}
|
||||
|
||||
$editSummary = $editSummaries -join "; "
|
||||
if ([string]::IsNullOrEmpty($editSummary)) {
|
||||
$editSummary = "File edited"
|
||||
}
|
||||
|
||||
# Treat file edits as a "write_file" tool usage
|
||||
$toolInput = @{
|
||||
file_path = $filePath
|
||||
edits = $edits
|
||||
}
|
||||
|
||||
$toolResponse = @{
|
||||
success = $true
|
||||
summary = $editSummary
|
||||
}
|
||||
|
||||
$payload = @{
|
||||
contentSessionId = $sessionId
|
||||
tool_name = "write_file"
|
||||
tool_input = $toolInput
|
||||
tool_response = $toolResponse
|
||||
cwd = $workspaceRoot
|
||||
}
|
||||
|
||||
# Ensure worker is running (with retries like claude-mem hooks)
|
||||
if (-not (Test-WorkerReady -Port $workerPort)) {
|
||||
# Worker not ready - exit gracefully (don't block Cursor)
|
||||
exit 0
|
||||
}
|
||||
|
||||
# Send observation to claude-mem worker (fire-and-forget)
|
||||
$uri = "http://127.0.0.1:$workerPort/api/sessions/observations"
|
||||
|
||||
try {
|
||||
$bodyJson = ConvertTo-JsonCompact $payload
|
||||
Invoke-RestMethod -Uri $uri -Method Post -Body $bodyJson -ContentType "application/json" -TimeoutSec 5 -ErrorAction SilentlyContinue | Out-Null
|
||||
} catch {
|
||||
# Ignore errors - don't block Cursor
|
||||
}
|
||||
|
||||
exit 0
|
||||
@@ -1,112 +0,0 @@
|
||||
#!/bin/bash
|
||||
# Save File Edit Hook for Cursor
|
||||
# Captures file edits made by the agent
|
||||
# Maps file edits to claude-mem observations
|
||||
|
||||
# Source common utilities
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
source "${SCRIPT_DIR}/common.sh" 2>/dev/null || {
|
||||
echo "Warning: common.sh not found, using fallback functions" >&2
|
||||
}
|
||||
|
||||
# Check dependencies (non-blocking)
|
||||
check_dependencies >/dev/null 2>&1 || true
|
||||
|
||||
# Read JSON input from stdin with error handling
|
||||
input=$(read_json_input)
|
||||
|
||||
# Extract common fields with safe fallbacks
|
||||
conversation_id=$(json_get "$input" "conversation_id" "")
|
||||
generation_id=$(json_get "$input" "generation_id" "")
|
||||
file_path=$(json_get "$input" "file_path" "")
|
||||
workspace_root=$(json_get "$input" "workspace_roots[0]" "")
|
||||
|
||||
# Fallback to current directory if no workspace root
|
||||
if is_empty "$workspace_root"; then
|
||||
workspace_root=$(pwd)
|
||||
fi
|
||||
|
||||
# Exit if no file_path
|
||||
if is_empty "$file_path"; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Use conversation_id as session_id, fallback to generation_id
|
||||
session_id="$conversation_id"
|
||||
if is_empty "$session_id"; then
|
||||
session_id="$generation_id"
|
||||
fi
|
||||
|
||||
# Exit if no session_id available
|
||||
if is_empty "$session_id"; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Get worker port from settings with validation
|
||||
worker_port=$(get_worker_port)
|
||||
|
||||
# Extract edits array, defaulting to [] if invalid
|
||||
edits=$(echo "$input" | jq -c '.edits // []' 2>/dev/null || echo "[]")
|
||||
|
||||
# Validate edits is a valid JSON array
|
||||
if ! echo "$edits" | jq 'type == "array"' 2>/dev/null | grep -q true; then
|
||||
edits="[]"
|
||||
fi
|
||||
|
||||
# Exit if no edits
|
||||
if [ "$edits" = "[]" ] || is_empty "$edits"; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Create a summary of the edits for the observation (with error handling)
|
||||
edit_summary=$(echo "$edits" | jq -r '[.[] | "\(.old_string[0:50] // "")... → \(.new_string[0:50] // "")..."] | join("; ")' 2>/dev/null || echo "File edited")
|
||||
|
||||
# Treat file edits as a "write_file" tool usage
|
||||
tool_input=$(jq -n \
|
||||
--arg path "$file_path" \
|
||||
--argjson edits "$edits" \
|
||||
'{
|
||||
file_path: $path,
|
||||
edits: $edits
|
||||
}' 2>/dev/null || echo '{}')
|
||||
|
||||
tool_response=$(jq -n \
|
||||
--arg summary "$edit_summary" \
|
||||
'{
|
||||
success: true,
|
||||
summary: $summary
|
||||
}' 2>/dev/null || echo '{}')
|
||||
|
||||
payload=$(jq -n \
|
||||
--arg sessionId "$session_id" \
|
||||
--arg cwd "$workspace_root" \
|
||||
--argjson toolInput "$tool_input" \
|
||||
--argjson toolResponse "$tool_response" \
|
||||
'{
|
||||
contentSessionId: $sessionId,
|
||||
tool_name: "write_file",
|
||||
tool_input: $toolInput,
|
||||
tool_response: $toolResponse,
|
||||
cwd: $cwd
|
||||
}' 2>/dev/null)
|
||||
|
||||
# Exit if payload creation failed
|
||||
if [ -z "$payload" ]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Ensure worker is running (with retries like claude-mem hooks)
|
||||
if ! ensure_worker_running "$worker_port"; then
|
||||
# Worker not ready - exit gracefully (don't block Cursor)
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Send observation to claude-mem worker (fire-and-forget)
|
||||
curl -s -X POST \
|
||||
"http://127.0.0.1:${worker_port}/api/sessions/observations" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "$payload" \
|
||||
>/dev/null 2>&1 || true
|
||||
|
||||
exit 0
|
||||
|
||||
@@ -1,126 +0,0 @@
|
||||
# Save Observation Hook for Cursor (PowerShell)
|
||||
# Captures MCP tool usage and shell command execution
|
||||
# Maps to claude-mem's save-hook functionality
|
||||
|
||||
$ErrorActionPreference = "SilentlyContinue"
|
||||
|
||||
# Source common utilities
|
||||
$commonPath = Join-Path $PSScriptRoot "common.ps1"
|
||||
if (Test-Path $commonPath) {
|
||||
. $commonPath
|
||||
} else {
|
||||
Write-Warning "common.ps1 not found, using fallback functions"
|
||||
exit 0
|
||||
}
|
||||
|
||||
# Read JSON input from stdin with error handling
|
||||
$input = Read-JsonInput
|
||||
|
||||
# Extract common fields with safe fallbacks
|
||||
$conversationId = Get-JsonField $input "conversation_id" ""
|
||||
$generationId = Get-JsonField $input "generation_id" ""
|
||||
$workspaceRoot = Get-JsonField $input "workspace_roots[0]" ""
|
||||
|
||||
# Fallback to current directory if no workspace root
|
||||
if (Test-IsEmpty $workspaceRoot) {
|
||||
$workspaceRoot = Get-Location
|
||||
}
|
||||
|
||||
# Use conversation_id as session_id (stable across turns), fallback to generation_id
|
||||
$sessionId = $conversationId
|
||||
if (Test-IsEmpty $sessionId) {
|
||||
$sessionId = $generationId
|
||||
}
|
||||
|
||||
# Exit if no session_id available
|
||||
if (Test-IsEmpty $sessionId) {
|
||||
exit 0
|
||||
}
|
||||
|
||||
# Get worker port from settings with validation
|
||||
$workerPort = Get-WorkerPort
|
||||
|
||||
# Determine hook type and extract relevant data
|
||||
$hookEvent = Get-JsonField $input "hook_event_name" ""
|
||||
|
||||
$payload = $null
|
||||
|
||||
if ($hookEvent -eq "afterMCPExecution") {
|
||||
# MCP tool execution
|
||||
$toolName = Get-JsonField $input "tool_name" ""
|
||||
|
||||
if (Test-IsEmpty $toolName) {
|
||||
exit 0
|
||||
}
|
||||
|
||||
# Extract tool_input and tool_response, defaulting to {} if invalid
|
||||
$toolInput = @{}
|
||||
$toolResponse = @{}
|
||||
|
||||
if ($input.PSObject.Properties.Name -contains "tool_input") {
|
||||
$toolInput = $input.tool_input
|
||||
if ($null -eq $toolInput) { $toolInput = @{} }
|
||||
}
|
||||
|
||||
if ($input.PSObject.Properties.Name -contains "result_json") {
|
||||
$toolResponse = $input.result_json
|
||||
if ($null -eq $toolResponse) { $toolResponse = @{} }
|
||||
}
|
||||
|
||||
# Prepare observation payload
|
||||
$payload = @{
|
||||
contentSessionId = $sessionId
|
||||
tool_name = $toolName
|
||||
tool_input = $toolInput
|
||||
tool_response = $toolResponse
|
||||
cwd = $workspaceRoot
|
||||
}
|
||||
|
||||
} elseif ($hookEvent -eq "afterShellExecution") {
|
||||
# Shell command execution
|
||||
$command = Get-JsonField $input "command" ""
|
||||
|
||||
if (Test-IsEmpty $command) {
|
||||
exit 0
|
||||
}
|
||||
|
||||
$output = Get-JsonField $input "output" ""
|
||||
|
||||
# Treat shell commands as "Bash" tool usage
|
||||
$toolInput = @{ command = $command }
|
||||
$toolResponse = @{ output = $output }
|
||||
|
||||
$payload = @{
|
||||
contentSessionId = $sessionId
|
||||
tool_name = "Bash"
|
||||
tool_input = $toolInput
|
||||
tool_response = $toolResponse
|
||||
cwd = $workspaceRoot
|
||||
}
|
||||
|
||||
} else {
|
||||
exit 0
|
||||
}
|
||||
|
||||
# Exit if payload creation failed
|
||||
if ($null -eq $payload) {
|
||||
exit 0
|
||||
}
|
||||
|
||||
# Ensure worker is running (with retries like claude-mem hooks)
|
||||
if (-not (Test-WorkerReady -Port $workerPort)) {
|
||||
# Worker not ready - exit gracefully (don't block Cursor)
|
||||
exit 0
|
||||
}
|
||||
|
||||
# Send observation to claude-mem worker (fire-and-forget)
|
||||
$uri = "http://127.0.0.1:$workerPort/api/sessions/observations"
|
||||
|
||||
try {
|
||||
$bodyJson = ConvertTo-JsonCompact $payload
|
||||
Invoke-RestMethod -Uri $uri -Method Post -Body $bodyJson -ContentType "application/json" -TimeoutSec 5 -ErrorAction SilentlyContinue | Out-Null
|
||||
} catch {
|
||||
# Ignore errors - don't block Cursor
|
||||
}
|
||||
|
||||
exit 0
|
||||
@@ -1,129 +0,0 @@
|
||||
#!/bin/bash
|
||||
# Save Observation Hook for Cursor
|
||||
# Captures MCP tool usage and shell command execution
|
||||
# Maps to claude-mem's save-hook functionality
|
||||
|
||||
# Source common utilities
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
source "${SCRIPT_DIR}/common.sh" 2>/dev/null || {
|
||||
echo "Warning: common.sh not found, using fallback functions" >&2
|
||||
}
|
||||
|
||||
# Check dependencies (non-blocking)
|
||||
check_dependencies >/dev/null 2>&1 || true
|
||||
|
||||
# Read JSON input from stdin with error handling
|
||||
input=$(read_json_input)
|
||||
|
||||
# Extract common fields with safe fallbacks
|
||||
conversation_id=$(json_get "$input" "conversation_id" "")
|
||||
generation_id=$(json_get "$input" "generation_id" "")
|
||||
workspace_root=$(json_get "$input" "workspace_roots[0]" "")
|
||||
|
||||
# Fallback to current directory if no workspace root
|
||||
if is_empty "$workspace_root"; then
|
||||
workspace_root=$(pwd)
|
||||
fi
|
||||
|
||||
# Use conversation_id as session_id (stable across turns), fallback to generation_id
|
||||
session_id="$conversation_id"
|
||||
if is_empty "$session_id"; then
|
||||
session_id="$generation_id"
|
||||
fi
|
||||
|
||||
# Exit if no session_id available
|
||||
if is_empty "$session_id"; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Get worker port from settings with validation
|
||||
worker_port=$(get_worker_port)
|
||||
|
||||
# Determine hook type and extract relevant data
|
||||
hook_event=$(json_get "$input" "hook_event_name" "")
|
||||
|
||||
if [ "$hook_event" = "afterMCPExecution" ]; then
|
||||
# MCP tool execution
|
||||
tool_name=$(json_get "$input" "tool_name" "")
|
||||
|
||||
if is_empty "$tool_name"; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Extract tool_input and tool_response, defaulting to {} if invalid
|
||||
tool_input=$(echo "$input" | jq -c '.tool_input // {}' 2>/dev/null || echo "{}")
|
||||
tool_response=$(echo "$input" | jq -c '.result_json // {}' 2>/dev/null || echo "{}")
|
||||
|
||||
# Validate JSON
|
||||
if ! echo "$tool_input" | jq empty 2>/dev/null; then
|
||||
tool_input="{}"
|
||||
fi
|
||||
if ! echo "$tool_response" | jq empty 2>/dev/null; then
|
||||
tool_response="{}"
|
||||
fi
|
||||
|
||||
# Prepare observation payload
|
||||
payload=$(jq -n \
|
||||
--arg sessionId "$session_id" \
|
||||
--arg toolName "$tool_name" \
|
||||
--argjson toolInput "$tool_input" \
|
||||
--argjson toolResponse "$tool_response" \
|
||||
--arg cwd "$workspace_root" \
|
||||
'{
|
||||
contentSessionId: $sessionId,
|
||||
tool_name: $toolName,
|
||||
tool_input: $toolInput,
|
||||
tool_response: $toolResponse,
|
||||
cwd: $cwd
|
||||
}' 2>/dev/null)
|
||||
|
||||
elif [ "$hook_event" = "afterShellExecution" ]; then
|
||||
# Shell command execution
|
||||
command=$(json_get "$input" "command" "")
|
||||
|
||||
if is_empty "$command"; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
output=$(json_get "$input" "output" "")
|
||||
|
||||
# Treat shell commands as "Bash" tool usage
|
||||
tool_input=$(jq -n --arg cmd "$command" '{command: $cmd}' 2>/dev/null || echo '{}')
|
||||
tool_response=$(jq -n --arg out "$output" '{output: $out}' 2>/dev/null || echo '{}')
|
||||
|
||||
payload=$(jq -n \
|
||||
--arg sessionId "$session_id" \
|
||||
--arg cwd "$workspace_root" \
|
||||
--argjson toolInput "$tool_input" \
|
||||
--argjson toolResponse "$tool_response" \
|
||||
'{
|
||||
contentSessionId: $sessionId,
|
||||
tool_name: "Bash",
|
||||
tool_input: $toolInput,
|
||||
tool_response: $toolResponse,
|
||||
cwd: $cwd
|
||||
}' 2>/dev/null)
|
||||
else
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Exit if payload creation failed
|
||||
if [ -z "$payload" ]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Ensure worker is running (with retries like claude-mem hooks)
|
||||
if ! ensure_worker_running "$worker_port"; then
|
||||
# Worker not ready - exit gracefully (don't block Cursor)
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Send observation to claude-mem worker (fire-and-forget)
|
||||
curl -s -X POST \
|
||||
"http://127.0.0.1:${worker_port}/api/sessions/observations" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "$payload" \
|
||||
>/dev/null 2>&1 || true
|
||||
|
||||
exit 0
|
||||
|
||||
@@ -1,79 +0,0 @@
|
||||
# Session Initialization Hook for Cursor (PowerShell)
|
||||
# Maps to claude-mem's new-hook functionality
|
||||
# Initializes a new session when a prompt is submitted
|
||||
#
|
||||
# NOTE: This hook runs as part of beforeSubmitPrompt and MUST output valid JSON
|
||||
# with at least {"continue": true} to allow prompt submission.
|
||||
|
||||
$ErrorActionPreference = "SilentlyContinue"
|
||||
|
||||
# Source common utilities
|
||||
$commonPath = Join-Path $PSScriptRoot "common.ps1"
|
||||
if (Test-Path $commonPath) {
|
||||
. $commonPath
|
||||
} else {
|
||||
# Fallback - output continue and exit
|
||||
Write-Output '{"continue": true}'
|
||||
exit 0
|
||||
}
|
||||
|
||||
# Read JSON input from stdin with error handling
|
||||
$input = Read-JsonInput
|
||||
|
||||
# Extract common fields with safe fallbacks
|
||||
$conversationId = Get-JsonField $input "conversation_id" ""
|
||||
$generationId = Get-JsonField $input "generation_id" ""
|
||||
$prompt = Get-JsonField $input "prompt" ""
|
||||
$workspaceRoot = Get-JsonField $input "workspace_roots[0]" ""
|
||||
|
||||
# Fallback to current directory if no workspace root
|
||||
if (Test-IsEmpty $workspaceRoot) {
|
||||
$workspaceRoot = Get-Location
|
||||
}
|
||||
|
||||
# Get project name from workspace root
|
||||
$projectName = Get-ProjectName $workspaceRoot
|
||||
|
||||
# Use conversation_id as session_id (stable across turns), fallback to generation_id
|
||||
$sessionId = $conversationId
|
||||
if (Test-IsEmpty $sessionId) {
|
||||
$sessionId = $generationId
|
||||
}
|
||||
|
||||
# Exit gracefully if no session_id available (still allow prompt)
|
||||
if (Test-IsEmpty $sessionId) {
|
||||
Write-Output '{"continue": true}'
|
||||
exit 0
|
||||
}
|
||||
|
||||
# Get worker port from settings with validation
|
||||
$workerPort = Get-WorkerPort
|
||||
|
||||
# Ensure worker is running (with retries like claude-mem hooks)
|
||||
if (-not (Test-WorkerReady -Port $workerPort)) {
|
||||
# Worker not ready - still allow prompt to continue
|
||||
Write-Output '{"continue": true}'
|
||||
exit 0
|
||||
}
|
||||
|
||||
# Strip leading slash from commands for memory agent (parity with new-hook.ts)
|
||||
# /review 101 → review 101 (more semantic for observations)
|
||||
$cleanedPrompt = $prompt
|
||||
if (-not [string]::IsNullOrEmpty($prompt) -and $prompt.StartsWith("/")) {
|
||||
$cleanedPrompt = $prompt.Substring(1)
|
||||
}
|
||||
|
||||
# Initialize session via HTTP - handles DB operations and privacy checks
|
||||
$payload = @{
|
||||
contentSessionId = $sessionId
|
||||
project = $projectName
|
||||
prompt = $cleanedPrompt
|
||||
}
|
||||
|
||||
# Send request to worker (fire-and-forget, don't wait for response)
|
||||
$uri = "http://127.0.0.1:$workerPort/api/sessions/init"
|
||||
Send-HttpPostAsync -Uri $uri -Body $payload
|
||||
|
||||
# Always allow prompt to continue
|
||||
Write-Output '{"continue": true}'
|
||||
exit 0
|
||||
@@ -1,93 +0,0 @@
|
||||
#!/bin/bash
|
||||
# Session Initialization Hook for Cursor
|
||||
# Maps to claude-mem's new-hook functionality
|
||||
# Initializes a new session when a prompt is submitted
|
||||
#
|
||||
# NOTE: This hook runs as part of beforeSubmitPrompt and MUST output valid JSON
|
||||
# with at least {"continue": true} to allow prompt submission.
|
||||
|
||||
# Source common utilities
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
source "${SCRIPT_DIR}/common.sh" 2>/dev/null || {
|
||||
# Fallback - output continue and exit
|
||||
echo '{"continue": true}'
|
||||
exit 0
|
||||
}
|
||||
|
||||
# Check dependencies (non-blocking - just warn)
|
||||
check_dependencies >/dev/null 2>&1 || true
|
||||
|
||||
# Read JSON input from stdin with error handling
|
||||
input=$(read_json_input)
|
||||
|
||||
# Extract common fields with safe fallbacks
|
||||
conversation_id=$(json_get "$input" "conversation_id" "")
|
||||
generation_id=$(json_get "$input" "generation_id" "")
|
||||
prompt=$(json_get "$input" "prompt" "")
|
||||
workspace_root=$(json_get "$input" "workspace_roots[0]" "")
|
||||
|
||||
# Fallback to current directory if no workspace root
|
||||
if is_empty "$workspace_root"; then
|
||||
workspace_root=$(pwd)
|
||||
fi
|
||||
|
||||
# Get project name from workspace root
|
||||
project_name=$(get_project_name "$workspace_root")
|
||||
|
||||
# Use conversation_id as session_id (stable across turns), fallback to generation_id
|
||||
session_id="$conversation_id"
|
||||
if is_empty "$session_id"; then
|
||||
session_id="$generation_id"
|
||||
fi
|
||||
|
||||
# Exit gracefully if no session_id available (still allow prompt)
|
||||
if is_empty "$session_id"; then
|
||||
echo '{"continue": true}'
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Get worker port from settings with validation
|
||||
worker_port=$(get_worker_port)
|
||||
|
||||
# Ensure worker is running (with retries like claude-mem hooks)
|
||||
if ! ensure_worker_running "$worker_port"; then
|
||||
# Worker not ready - still allow prompt to continue
|
||||
echo '{"continue": true}'
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Strip leading slash from commands for memory agent (parity with new-hook.ts)
|
||||
# /review 101 → review 101 (more semantic for observations)
|
||||
cleaned_prompt="$prompt"
|
||||
if [ -n "$prompt" ] && [ "${prompt:0:1}" = "/" ]; then
|
||||
cleaned_prompt="${prompt:1}"
|
||||
fi
|
||||
|
||||
# Initialize session via HTTP - handles DB operations and privacy checks
|
||||
payload=$(jq -n \
|
||||
--arg sessionId "$session_id" \
|
||||
--arg project "$project_name" \
|
||||
--arg promptText "$cleaned_prompt" \
|
||||
'{
|
||||
contentSessionId: $sessionId,
|
||||
project: $project,
|
||||
prompt: $promptText
|
||||
}' 2>/dev/null)
|
||||
|
||||
# Exit if payload creation failed (still allow prompt)
|
||||
if [ -z "$payload" ]; then
|
||||
echo '{"continue": true}'
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Send request to worker (fire-and-forget, don't wait for response)
|
||||
curl -s -X POST \
|
||||
"http://127.0.0.1:${worker_port}/api/sessions/init" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "$payload" \
|
||||
>/dev/null 2>&1 &
|
||||
|
||||
# Always allow prompt to continue
|
||||
echo '{"continue": true}'
|
||||
exit 0
|
||||
|
||||
@@ -1,108 +0,0 @@
|
||||
# Session Summary Hook for Cursor (stop) - PowerShell
|
||||
# Called when agent loop ends
|
||||
#
|
||||
# This hook:
|
||||
# 1. Generates session summary
|
||||
# 2. Updates context file for next session
|
||||
#
|
||||
# Output: Empty JSON {} or {"followup_message": "..."} for auto-iteration
|
||||
|
||||
$ErrorActionPreference = "SilentlyContinue"
|
||||
|
||||
# Source common utilities
|
||||
$commonPath = Join-Path $PSScriptRoot "common.ps1"
|
||||
if (Test-Path $commonPath) {
|
||||
. $commonPath
|
||||
} else {
|
||||
Write-Output '{}'
|
||||
exit 0
|
||||
}
|
||||
|
||||
# Read JSON input from stdin with error handling
|
||||
$input = Read-JsonInput
|
||||
|
||||
# Extract common fields with safe fallbacks
|
||||
$conversationId = Get-JsonField $input "conversation_id" ""
|
||||
$generationId = Get-JsonField $input "generation_id" ""
|
||||
$workspaceRoot = Get-JsonField $input "workspace_roots[0]" ""
|
||||
$status = Get-JsonField $input "status" "completed"
|
||||
|
||||
# Fallback workspace to current directory
|
||||
if (Test-IsEmpty $workspaceRoot) {
|
||||
$workspaceRoot = Get-Location
|
||||
}
|
||||
|
||||
# Get project name
|
||||
$projectName = Get-ProjectName $workspaceRoot
|
||||
|
||||
# Use conversation_id as session_id, fallback to generation_id
|
||||
$sessionId = $conversationId
|
||||
if (Test-IsEmpty $sessionId) {
|
||||
$sessionId = $generationId
|
||||
}
|
||||
|
||||
# Exit if no session_id available
|
||||
if (Test-IsEmpty $sessionId) {
|
||||
Write-Output '{}'
|
||||
exit 0
|
||||
}
|
||||
|
||||
# Get worker port from settings with validation
|
||||
$workerPort = Get-WorkerPort
|
||||
|
||||
# Ensure worker is running (with retries)
|
||||
if (-not (Test-WorkerReady -Port $workerPort)) {
|
||||
Write-Output '{}'
|
||||
exit 0
|
||||
}
|
||||
|
||||
# 1. Request summary generation (fire-and-forget)
|
||||
# Note: Cursor doesn't provide transcript_path like Claude Code does,
|
||||
# so we can't extract last_user_message and last_assistant_message.
|
||||
$summaryPayload = @{
|
||||
contentSessionId = $sessionId
|
||||
last_user_message = ""
|
||||
last_assistant_message = ""
|
||||
}
|
||||
|
||||
$summaryUri = "http://127.0.0.1:$workerPort/api/sessions/summarize"
|
||||
Send-HttpPostAsync -Uri $summaryUri -Body $summaryPayload
|
||||
|
||||
# 2. Update context file for next session
|
||||
# Fetch fresh context (includes observations from this session)
|
||||
$projectEncoded = Get-UrlEncodedString $projectName
|
||||
$contextUri = "http://127.0.0.1:$workerPort/api/context/inject?project=$projectEncoded"
|
||||
$context = Get-HttpResponse -Uri $contextUri
|
||||
|
||||
if (-not [string]::IsNullOrEmpty($context)) {
|
||||
$rulesDir = Join-Path $workspaceRoot ".cursor\rules"
|
||||
$rulesFile = Join-Path $rulesDir "claude-mem-context.mdc"
|
||||
|
||||
# Create rules directory if it doesn't exist
|
||||
if (-not (Test-Path $rulesDir)) {
|
||||
New-Item -ItemType Directory -Path $rulesDir -Force | Out-Null
|
||||
}
|
||||
|
||||
# Write context as a Cursor rule with alwaysApply: true
|
||||
$ruleContent = @"
|
||||
---
|
||||
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.*
|
||||
"@
|
||||
|
||||
Set-Content -Path $rulesFile -Value $ruleContent -Encoding UTF8 -Force
|
||||
}
|
||||
|
||||
# Output empty JSON - no followup message
|
||||
Write-Output '{}'
|
||||
exit 0
|
||||
@@ -1,111 +0,0 @@
|
||||
#!/bin/bash
|
||||
# Session Summary Hook for Cursor (stop)
|
||||
# Called when agent loop ends
|
||||
#
|
||||
# This hook:
|
||||
# 1. Generates session summary
|
||||
# 2. Updates context file for next session
|
||||
#
|
||||
# Output: Empty JSON {} or {"followup_message": "..."} for auto-iteration
|
||||
|
||||
# Source common utilities
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
source "${SCRIPT_DIR}/common.sh" 2>/dev/null || {
|
||||
echo '{}'
|
||||
exit 0
|
||||
}
|
||||
|
||||
# Check dependencies (non-blocking)
|
||||
check_dependencies >/dev/null 2>&1 || true
|
||||
|
||||
# Read JSON input from stdin with error handling
|
||||
input=$(read_json_input)
|
||||
|
||||
# Extract common fields with safe fallbacks
|
||||
conversation_id=$(json_get "$input" "conversation_id" "")
|
||||
generation_id=$(json_get "$input" "generation_id" "")
|
||||
workspace_root=$(json_get "$input" "workspace_roots[0]" "")
|
||||
status=$(json_get "$input" "status" "completed")
|
||||
|
||||
# Fallback workspace to current directory
|
||||
if is_empty "$workspace_root"; then
|
||||
workspace_root=$(pwd)
|
||||
fi
|
||||
|
||||
# Get project name
|
||||
project_name=$(get_project_name "$workspace_root")
|
||||
|
||||
# Use conversation_id as session_id, fallback to generation_id
|
||||
session_id="$conversation_id"
|
||||
if is_empty "$session_id"; then
|
||||
session_id="$generation_id"
|
||||
fi
|
||||
|
||||
# Exit if no session_id available
|
||||
if is_empty "$session_id"; then
|
||||
echo '{}'
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Get worker port from settings with validation
|
||||
worker_port=$(get_worker_port)
|
||||
|
||||
# Ensure worker is running (with retries)
|
||||
if ! ensure_worker_running "$worker_port"; then
|
||||
echo '{}'
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# 1. Request summary generation (fire-and-forget)
|
||||
# Note: Cursor doesn't provide transcript_path like Claude Code does,
|
||||
# so we can't extract last_user_message and last_assistant_message.
|
||||
payload=$(jq -n \
|
||||
--arg sessionId "$session_id" \
|
||||
'{
|
||||
contentSessionId: $sessionId,
|
||||
last_user_message: "",
|
||||
last_assistant_message: ""
|
||||
}' 2>/dev/null)
|
||||
|
||||
if [ -n "$payload" ]; then
|
||||
curl -s -X POST \
|
||||
"http://127.0.0.1:${worker_port}/api/sessions/summarize" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "$payload" \
|
||||
>/dev/null 2>&1 &
|
||||
fi
|
||||
|
||||
# 2. Update context file for next session
|
||||
# Fetch fresh context (includes observations from this session)
|
||||
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
|
||||
|
||||
# Output empty JSON - no followup message
|
||||
echo '{}'
|
||||
exit 0
|
||||
|
||||
@@ -1,103 +0,0 @@
|
||||
# User Message Hook for Cursor (PowerShell)
|
||||
# Displays context information to the user
|
||||
# Maps to claude-mem's user-message-hook functionality
|
||||
# Note: Cursor doesn't have a direct equivalent, but we can output to stderr
|
||||
# for visibility in Cursor's output channels
|
||||
#
|
||||
# This is an OPTIONAL hook. It can be added to beforeSubmitPrompt if desired,
|
||||
# but may be verbose since it runs on every prompt submission.
|
||||
|
||||
$ErrorActionPreference = "SilentlyContinue"
|
||||
|
||||
# Read JSON input from stdin (if any)
|
||||
$inputJson = $null
|
||||
try {
|
||||
$inputText = [Console]::In.ReadToEnd()
|
||||
if (-not [string]::IsNullOrEmpty($inputText)) {
|
||||
$inputJson = $inputText | ConvertFrom-Json -ErrorAction SilentlyContinue
|
||||
}
|
||||
} catch {
|
||||
$inputJson = $null
|
||||
}
|
||||
|
||||
# Extract workspace root
|
||||
$workspaceRoot = ""
|
||||
if ($null -ne $inputJson -and $inputJson.PSObject.Properties.Name -contains "workspace_roots") {
|
||||
$wsRoots = $inputJson.workspace_roots
|
||||
if ($null -ne $wsRoots -and $wsRoots.Count -gt 0) {
|
||||
$workspaceRoot = $wsRoots[0]
|
||||
}
|
||||
}
|
||||
|
||||
if ([string]::IsNullOrEmpty($workspaceRoot)) {
|
||||
$workspaceRoot = Get-Location
|
||||
}
|
||||
|
||||
# Get project name
|
||||
$projectName = Split-Path $workspaceRoot -Leaf
|
||||
if ([string]::IsNullOrEmpty($projectName)) {
|
||||
$projectName = "unknown-project"
|
||||
}
|
||||
|
||||
# Get worker port from settings
|
||||
$settingsPath = Join-Path $env:USERPROFILE ".claude-mem\settings.json"
|
||||
$workerPort = 37777
|
||||
|
||||
if (Test-Path $settingsPath) {
|
||||
try {
|
||||
$settings = Get-Content $settingsPath -Raw | ConvertFrom-Json
|
||||
if ($settings.CLAUDE_MEM_WORKER_PORT) {
|
||||
$workerPort = [int]$settings.CLAUDE_MEM_WORKER_PORT
|
||||
}
|
||||
} catch {
|
||||
# Use default
|
||||
}
|
||||
}
|
||||
|
||||
# Ensure worker is running
|
||||
$maxRetries = 75
|
||||
$workerReady = $false
|
||||
|
||||
for ($i = 0; $i -lt $maxRetries; $i++) {
|
||||
try {
|
||||
$response = Invoke-RestMethod -Uri "http://127.0.0.1:$workerPort/api/readiness" -Method Get -TimeoutSec 1 -ErrorAction Stop
|
||||
$workerReady = $true
|
||||
break
|
||||
} catch {
|
||||
Start-Sleep -Milliseconds 200
|
||||
}
|
||||
}
|
||||
|
||||
# If worker not ready, exit silently
|
||||
if (-not $workerReady) {
|
||||
exit 0
|
||||
}
|
||||
|
||||
# Fetch formatted context from worker API (with colors)
|
||||
$projectEncoded = [System.Uri]::EscapeDataString($projectName)
|
||||
$contextUrl = "http://127.0.0.1:$workerPort/api/context/inject?project=$projectEncoded&colors=true"
|
||||
|
||||
$output = $null
|
||||
try {
|
||||
$output = Invoke-RestMethod -Uri $contextUrl -Method Get -TimeoutSec 5 -ErrorAction Stop
|
||||
} catch {
|
||||
$output = $null
|
||||
}
|
||||
|
||||
# Output to stderr for visibility (parity with user-message-hook.ts)
|
||||
# Note: Cursor may not display stderr the same way Claude Code does,
|
||||
# but this is the best we can do without direct UI integration
|
||||
if (-not [string]::IsNullOrEmpty($output)) {
|
||||
[Console]::Error.WriteLine("")
|
||||
[Console]::Error.WriteLine("📝 Claude-Mem Context Loaded")
|
||||
[Console]::Error.WriteLine(" ℹ️ Viewing context from past sessions")
|
||||
[Console]::Error.WriteLine("")
|
||||
[Console]::Error.WriteLine($output)
|
||||
[Console]::Error.WriteLine("")
|
||||
[Console]::Error.WriteLine("💡 Tip: Wrap content with <private> ... </private> to prevent storing sensitive information.")
|
||||
[Console]::Error.WriteLine("💬 Community: https://discord.gg/J4wttp9vDu")
|
||||
[Console]::Error.WriteLine("📺 Web Viewer: http://localhost:$workerPort/")
|
||||
[Console]::Error.WriteLine("")
|
||||
}
|
||||
|
||||
exit 0
|
||||
@@ -1,70 +0,0 @@
|
||||
#!/bin/bash
|
||||
# User Message Hook for Cursor
|
||||
# Displays context information to the user
|
||||
# Maps to claude-mem's user-message-hook functionality
|
||||
# Note: Cursor doesn't have a direct equivalent, but we can output to stderr
|
||||
# for visibility in Cursor's output channels
|
||||
#
|
||||
# This is an OPTIONAL hook. It can be added to beforeSubmitPrompt if desired,
|
||||
# but may be verbose since it runs on every prompt submission.
|
||||
|
||||
# Read JSON input from stdin (if any)
|
||||
input=$(cat 2>/dev/null || echo "{}")
|
||||
|
||||
# Extract workspace root
|
||||
workspace_root=$(echo "$input" | jq -r '.workspace_roots[0] // empty' 2>/dev/null || echo "")
|
||||
|
||||
if [ -z "$workspace_root" ]; then
|
||||
workspace_root=$(pwd)
|
||||
fi
|
||||
|
||||
# Get project name
|
||||
project_name=$(basename "$workspace_root" 2>/dev/null || echo "unknown-project")
|
||||
|
||||
# Get worker port from settings
|
||||
data_dir="${HOME}/.claude-mem"
|
||||
settings_file="${data_dir}/settings.json"
|
||||
worker_port="37777"
|
||||
|
||||
if [ -f "$settings_file" ]; then
|
||||
worker_port=$(jq -r '.CLAUDE_MEM_WORKER_PORT // "37777"' "$settings_file" 2>/dev/null || echo "37777")
|
||||
fi
|
||||
|
||||
# Ensure worker is running
|
||||
max_retries=75
|
||||
retry_count=0
|
||||
while [ $retry_count -lt $max_retries ]; do
|
||||
if curl -s -f "http://127.0.0.1:${worker_port}/api/readiness" > /dev/null 2>&1; then
|
||||
break
|
||||
fi
|
||||
sleep 0.2
|
||||
retry_count=$((retry_count + 1))
|
||||
done
|
||||
|
||||
# If worker not ready, exit silently
|
||||
if [ $retry_count -eq $max_retries ]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Fetch formatted context from worker API (with colors)
|
||||
context_url="http://127.0.0.1:${worker_port}/api/context/inject?project=${project_name}&colors=true"
|
||||
output=$(curl -s -f "$context_url" 2>/dev/null || echo "")
|
||||
|
||||
# Output to stderr for visibility (parity with user-message-hook.ts)
|
||||
# Note: Cursor may not display stderr the same way Claude Code does,
|
||||
# but this is the best we can do without direct UI integration
|
||||
if [ -n "$output" ]; then
|
||||
echo "" >&2
|
||||
echo "📝 Claude-Mem Context Loaded" >&2
|
||||
echo " ℹ️ Viewing context from past sessions" >&2
|
||||
echo "" >&2
|
||||
echo "$output" >&2
|
||||
echo "" >&2
|
||||
echo "💡 Tip: Wrap content with <private> ... </private> to prevent storing sensitive information." >&2
|
||||
echo "💬 Community: https://discord.gg/J4wttp9vDu" >&2
|
||||
echo "📺 Web Viewer: http://localhost:${worker_port}/" >&2
|
||||
echo "" >&2
|
||||
fi
|
||||
|
||||
exit 0
|
||||
|
||||
Reference in New Issue
Block a user