feat(cursor): Add Windows PowerShell support for Cursor hooks

Complete Windows parity with bash scripts:
- Create 7 PowerShell scripts mirroring bash functionality
- Update installer to detect platform and install appropriate scripts
- Generate platform-specific hooks.json with PowerShell invocation
- Add enterprise support for Windows (ProgramData/Cursor)
- Update findCursorHooksDir to check for both .sh and .ps1
- Add comprehensive Windows documentation to STANDALONE-SETUP.md

Scripts added: common.ps1, session-init.ps1, context-inject.ps1,
save-observation.ps1, save-file-edit.ps1, session-summary.ps1, user-message.ps1

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Alex Newman
2025-12-29 21:16:41 -05:00
parent ed8269250d
commit a82d1a24b9
10 changed files with 1086 additions and 117 deletions
+96
View File
@@ -16,6 +16,7 @@ Use claude-mem's persistent memory in Cursor without a Claude Code subscription.
## Prerequisites
### macOS / Linux
- Cursor IDE
- Node.js 18+
- Git
@@ -23,6 +24,12 @@ Use claude-mem's persistent memory in Cursor without a Claude Code subscription.
- **macOS**: `brew install jq curl`
- **Linux**: `apt install jq curl`
### Windows
- Cursor IDE
- Node.js 18+
- Git
- PowerShell 5.1+ (included with Windows 10/11)
## Step 1: Clone Claude-Mem
```bash
@@ -195,3 +202,92 @@ If you hit the 1500 requests/day limit:
| `npm run worker:start` | Start the background worker |
| `npm run worker:stop` | Stop the background worker |
| `npm run worker:restart` | Restart the worker |
---
## Windows Installation
Windows users get full support via PowerShell scripts. The installer automatically detects Windows and installs the appropriate scripts.
### Enable Script Execution (if needed)
PowerShell may require you to enable script execution:
```powershell
Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser
```
### Step-by-Step for Windows
```powershell
# Clone and build
git clone https://github.com/thedotmack/claude-mem.git
cd claude-mem
npm install
npm run build
# Configure provider (Gemini example)
$settingsDir = "$env:USERPROFILE\.claude-mem"
New-Item -ItemType Directory -Force -Path $settingsDir
@"
{
"CLAUDE_MEM_PROVIDER": "gemini",
"CLAUDE_MEM_GEMINI_API_KEY": "YOUR_GEMINI_API_KEY"
}
"@ | Out-File -FilePath "$settingsDir\settings.json" -Encoding UTF8
# Interactive setup (recommended - walks you through everything)
npm run cursor:setup
# Or manual installation
npm run cursor:install
npm run worker:start
```
### What Gets Installed on Windows
The installer copies these PowerShell scripts to `.cursor\hooks\`:
| Script | Purpose |
|--------|---------|
| `common.ps1` | Shared utilities |
| `session-init.ps1` | Initialize session on prompt |
| `context-inject.ps1` | Inject memory context |
| `save-observation.ps1` | Capture MCP/shell usage |
| `save-file-edit.ps1` | Capture file edits |
| `session-summary.ps1` | Generate summary on stop |
The `hooks.json` file is configured to invoke PowerShell with `-ExecutionPolicy Bypass` to ensure scripts run without additional configuration.
### Windows Troubleshooting
**"Execution of scripts is disabled on this system"**
Run as Administrator:
```powershell
Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope LocalMachine
```
**PowerShell scripts not running**
Verify the hooks.json contains PowerShell invocations:
```powershell
Get-Content .cursor\hooks.json
```
Should show commands like:
```
powershell.exe -ExecutionPolicy Bypass -File "./.cursor/hooks/session-init.ps1"
```
**Worker not responding**
Check if port 37777 is in use:
```powershell
Get-NetTCPConnection -LocalPort 37777
```
**Antivirus blocking scripts**
Some antivirus software may block PowerShell scripts. Add an exception for the `.cursor\hooks\` directory if needed.
+193
View File
@@ -0,0 +1,193 @@
# Common utility functions for Cursor hooks (PowerShell)
# Dot-source this file in hook scripts: . "$PSScriptRoot\common.ps1"
$ErrorActionPreference = "SilentlyContinue"
# 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
}
}
+74
View File
@@ -0,0 +1,74 @@
# 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
+126
View File
@@ -0,0 +1,126 @@
# 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
+126
View File
@@ -0,0 +1,126 @@
# 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
+79
View File
@@ -0,0 +1,79 @@
# 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
+108
View File
@@ -0,0 +1,108 @@
# 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
+103
View File
@@ -0,0 +1,103 @@
# 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