Fix: Windows Terminal tab accumulation and Windows 11 compatibility (#625) (#628)

* 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:
Alex Newman
2026-01-09 23:13:31 -05:00
committed by GitHub
parent 817c4348b1
commit 601596f5cb
67 changed files with 1621 additions and 2665 deletions
+223
View File
@@ -0,0 +1,223 @@
# Plan: PR #628 Polish Items
**PR**: #628 - Windows Terminal Tab Accumulation & Windows 11 Compatibility
**Status**: APPROVED by 3 reviewers with minor suggestions
**Branch**: `feature/no-more-hook-files`
---
## Phase 0: Documentation Discovery (Completed by Orchestrator)
### Allowed APIs and Patterns
**Exit Code Constants** - `src/shared/hook-constants.ts:18-23`:
```typescript
export const HOOK_EXIT_CODES = {
SUCCESS: 0,
FAILURE: 1,
BLOCKING_ERROR: 2,
} as const;
```
**Timeout Constants** - `src/shared/hook-constants.ts:1-8`:
```typescript
export const HOOK_TIMEOUTS = {
DEFAULT: 300000,
HEALTH_CHECK: 30000,
WORKER_STARTUP_WAIT: 1000,
WORKER_STARTUP_RETRIES: 300,
PRE_RESTART_SETTLE_DELAY: 2000,
WINDOWS_MULTIPLIER: 1.5
} as const;
```
**Platform Timeout Function** - `src/services/infrastructure/ProcessManager.ts:70-73`:
```typescript
export function getPlatformTimeout(baseMs: number): number {
const WINDOWS_MULTIPLIER = 2.0;
return process.platform === 'win32' ? Math.round(baseMs * WINDOWS_MULTIPLIER) : baseMs;
}
```
**Migration Guide Pattern** - `docs/public/architecture/pm2-to-bun-migration.mdx`:
- Uses MDX format with frontmatter
- Starts with `<Note>` for historical context
- Uses `<AccordionGroup>` for before/after comparisons
- Includes executive summary, key benefits, migration impact sections
**Exit Code Documentation** - `private/context/claude-code/exit-codes.md`:
- Defines exit code 0, 2, and other behaviors
- Per-hook event behavior table
### Files to Modify
| File | Change | Lines |
|------|--------|-------|
| `src/services/infrastructure/ProcessManager.ts` | Add POWERSHELL_TIMEOUT constant, reduce from 60000 to 10000 | 93, 123, 175, 241 |
| `src/shared/hook-constants.ts` | Add POWERSHELL_TIMEOUT constant | After line 8 |
| `CLAUDE.md` | Document exit code strategy | Architecture section |
### Anti-Patterns to Avoid
- DO NOT invent new exit code values (only 0, 1, 2 exist)
- DO NOT change Windows multiplier (1.5x in hooks, 2.0x in ProcessManager - they serve different purposes)
- DO NOT add upper bound PID validation (not in existing pattern, reviewers marked as "nice to have")
- DO NOT create migration guide for Cursor (shell scripts still exist in cursor-hooks/, not removed)
---
## Phase 1: Extract PowerShell Timeout Constant
### What to Implement
Add a `POWERSHELL_TIMEOUT` constant to centralize the magic number `60000` and reduce to `10000` (10 seconds) as recommended by reviewers.
### Documentation References
1. Copy constant pattern from `src/shared/hook-constants.ts:1-8`
2. Copy usage pattern from `src/services/infrastructure/ProcessManager.ts:93`
### Implementation Steps
1. **Add constant to hook-constants.ts** after line 8:
```typescript
POWERSHELL_COMMAND: 10000, // PowerShell process enumeration (10s - typically completes in <1s)
```
2. **Import and use in ProcessManager.ts**:
- Import `HOOK_TIMEOUTS` from `../../shared/hook-constants.js`
- Replace `{ timeout: 60000 }` with `{ timeout: HOOK_TIMEOUTS.POWERSHELL_COMMAND }` at lines 93, 123, 175, 241
### Verification Checklist
- [ ] `grep -n "60000" src/services/infrastructure/ProcessManager.ts` returns 0 matches
- [ ] `grep -n "POWERSHELL_COMMAND" src/services/infrastructure/ProcessManager.ts` returns 4 matches
- [ ] `npm run build` succeeds
- [ ] `npm test` passes (22/22 PowerShell tests still pass)
### Anti-Pattern Guards
- DO NOT use `getPlatformTimeout()` for PowerShell commands (they already run only on Windows)
- DO NOT change timeout values in other files (only ProcessManager.ts uses PowerShell)
---
## Phase 2: Document Exit Code Strategy in CLAUDE.md
### What to Implement
Add an "Exit Code Strategy" section to the main CLAUDE.md to explain the graceful exit philosophy adopted in this PR.
### Documentation References
1. Copy exit code definitions from `private/context/claude-code/exit-codes.md`
2. Follow format of existing CLAUDE.md sections
### Implementation Steps
1. **Add section after "File Locations"** in `/Users/alexnewman/Scripts/claude-mem/CLAUDE.md`:
```markdown
## Exit Code Strategy
Claude-mem hooks use specific exit codes per Claude Code's hook contract:
- **Exit 0**: Success or graceful shutdown (Windows Terminal closes tabs)
- **Exit 1**: Non-blocking error (stderr shown to user, continues)
- **Exit 2**: Blocking error (stderr fed to Claude for processing)
**Philosophy**: Worker/hook errors exit with code 0 to prevent Windows Terminal tab accumulation. The wrapper/plugin layer handles restart logic. ERROR-level logging is maintained for diagnostics.
See `private/context/claude-code/exit-codes.md` for full hook behavior matrix.
```
### Verification Checklist
- [ ] `grep -n "Exit Code Strategy" CLAUDE.md` returns 1 match
- [ ] Section appears after "File Locations" section
- [ ] No duplicate sections added
### Anti-Pattern Guards
- DO NOT copy the full exit-codes.md table (keep it brief, reference the source)
- DO NOT change actual exit code behavior in code files
---
## Phase 3: Update Tests for New Timeout Constant
### What to Implement
Add test coverage for the new `POWERSHELL_COMMAND` timeout constant.
### Documentation References
1. Copy test pattern from `tests/hook-constants.test.ts:26-48`
### Implementation Steps
1. **Add test to hook-constants.test.ts** after line 42:
```typescript
test('POWERSHELL_COMMAND timeout is 10000ms', () => {
expect(HOOK_TIMEOUTS.POWERSHELL_COMMAND).toBe(10000);
});
```
### Verification Checklist
- [ ] `npm test -- tests/hook-constants.test.ts` passes
- [ ] New test appears in test output
- [ ] All 22 PowerShell parsing tests still pass
### Anti-Pattern Guards
- DO NOT modify PowerShell parsing tests (they test parsing, not timeouts)
- DO NOT add integration tests for actual PowerShell execution (out of scope)
---
## Phase 4: Final Verification
### Verification Checklist
1. **Build passes**: `npm run build`
2. **All tests pass**: `npm test`
3. **No magic numbers remain**: `grep -rn "60000" src/services/infrastructure/ProcessManager.ts` returns 0
4. **Exit code documentation exists**: `grep -n "Exit Code Strategy" CLAUDE.md` returns 1
5. **Constant is used**: `grep -rn "POWERSHELL_COMMAND" src/` returns multiple matches
### Anti-Pattern Grep Checks
- [ ] `grep -rn "timeout: 60000" src/` returns 0 matches (no hardcoded 60s timeouts in ProcessManager)
- [ ] `grep -rn "process.exit(3)" src/` returns 0 matches (exit code 3 not used)
### Commit Message Template
```
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>
```
---
## Summary
| Phase | Description | Files Changed | Verification |
|-------|-------------|---------------|--------------|
| 0 | Documentation Discovery | N/A | Patterns identified |
| 1 | Extract PowerShell timeout | hook-constants.ts, ProcessManager.ts | grep + build + test |
| 2 | Document exit strategy | CLAUDE.md | grep |
| 3 | Add test coverage | hook-constants.test.ts | npm test |
| 4 | Final verification | N/A | All checks pass |
**Estimated Changes**: ~20 lines added/modified across 4 files
**Risk Level**: Low (constants extraction, documentation only)
**Breaking Changes**: None
+7
View File
@@ -0,0 +1,7 @@
<claude-mem-context>
# Recent Activity
<!-- This section is auto-generated by claude-mem. Edit content outside the tags. -->
*No recent activity*
</claude-mem-context>
+12
View File
@@ -41,6 +41,18 @@ Settings are managed in `~/.claude-mem/settings.json`. The file is auto-created
- **Database**: `~/.claude-mem/claude-mem.db`
- **Chroma**: `~/.claude-mem/chroma/`
## Exit Code Strategy
Claude-mem hooks use specific exit codes per Claude Code's hook contract:
- **Exit 0**: Success or graceful shutdown (Windows Terminal closes tabs)
- **Exit 1**: Non-blocking error (stderr shown to user, continues)
- **Exit 2**: Blocking error (stderr fed to Claude for processing)
**Philosophy**: Worker/hook errors exit with code 0 to prevent Windows Terminal tab accumulation. The wrapper/plugin layer handles restart logic. ERROR-level logging is maintained for diagnostics.
See `private/context/claude-code/exit-codes.md` for full hook behavior matrix.
## Requirements
- **Bun** (all platforms - auto-installed if missing)
-192
View File
@@ -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
}
}
-140
View File
@@ -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" ]
}
-74
View File
@@ -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
-70
View File
@@ -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
-71
View File
@@ -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 ""
-126
View File
@@ -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
-112
View File
@@ -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
-126
View File
@@ -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
-129
View File
@@ -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
-79
View File
@@ -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
-93
View File
@@ -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
-108
View File
@@ -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
-111
View File
@@ -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
-103
View File
@@ -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
-70
View File
@@ -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
@@ -0,0 +1,64 @@
# Context Hook Investigation Report
**Date:** 2026-01-05
**Branch:** `feature/no-more-hook-files`
**Status:** Partial fix committed, additional issues identified
## Problem
User reported no startup context appearing when testing the new unified CLI hook architecture.
## Root Cause Identified
**SessionStart hooks don't receive stdin data from Claude Code.**
The unified CLI architecture assumed all hooks receive stdin JSON data. When `readJsonFromStdin()` returns `undefined` for SessionStart, the platform adapters crashed:
```
TypeError: undefined is not an object (evaluating 'e.session_id')
```
**Location:** `src/cli/adapters/claude-code.ts:6` and `src/cli/adapters/cursor.ts:7`
The adapters did `const r = raw as any;` then accessed `r.session_id`, which fails when `raw` is `undefined`.
## Fix Applied
Changed both adapters to handle undefined input:
```typescript
// Before
const r = raw as any;
// After
const r = (raw ?? {}) as any;
```
**Commit:** `78c2a0ef` - Pushed to `feature/no-more-hook-files`
## Additional Issue Discovered (Not Yet Fixed)
There's a **path mismatch** in the hooks.json that may cause issues:
- hooks.json references: `${CLAUDE_PLUGIN_ROOT}/scripts/worker-service.cjs`
- Actual file location: `${CLAUDE_PLUGIN_ROOT}/plugin/scripts/worker-service.cjs`
The marketplace sync copies the whole repo structure, so files end up in a `plugin/` subdirectory. Need to verify what `CLAUDE_PLUGIN_ROOT` resolves to and whether the paths are correct.
## Verification Needed
1. Start a new Claude Code session and verify context appears
2. Check that `CLAUDE_PLUGIN_ROOT` points to correct directory
3. Verify hooks.json paths match actual file locations
## Files Changed
- `src/cli/adapters/claude-code.ts` - Added null coalescing for stdin
- `src/cli/adapters/cursor.ts` - Added null coalescing for stdin
- `plugin/scripts/worker-service.cjs` - Rebuilt with fix
## Next Steps
1. Test the fix in a live Claude Code session
2. Investigate the `CLAUDE_PLUGIN_ROOT` path resolution
3. Fix paths in hooks.json if needed
+4 -24
View File
@@ -3,35 +3,15 @@
<!-- This section is auto-generated by claude-mem. Edit content outside the tags. -->
### Jan 5, 2026
### Jan 3, 2026
| ID | Time | T | Title | Read |
|----|------|---|-------|------|
| #37962 | 8:18 PM | 🔴 | Fixed SessionStart hook crash when stdin is undefined | ~440 |
| #36651 | 11:03 PM | 🔵 | Critical Design Decision Documented: Memory Session ID Must Never Equal Content Session ID | ~481 |
### Jan 7, 2026
### Jan 8, 2026
| ID | Time | T | Title | Read |
|----|------|---|-------|------|
| #38475 | 10:31 PM | ⚖️ | Log Level Philosophy: Error-Adjacent Messages Promoted to ERROR | ~412 |
| #38467 | 10:29 PM | ⚖️ | Log Level Audit Strategy: Tighten ERROR Messages for Runtime Issue Discovery | ~464 |
| #38445 | 10:26 PM | 🔵 | DEBUG Level Logging: SessionRoutes Line 211 Error-During-Recovery Pattern | ~500 |
| #38442 | 10:25 PM | 🔵 | Log Audit Contains 409 Source File Log Entries | ~293 |
| #38441 | " | 🔵 | DEBUG-level logging patterns for diagnostics and non-critical operations | ~595 |
| #38439 | " | 🔵 | Log Audit Shows SessionRoutes.ts Has Two WARN Messages for Generator Failures | ~493 |
| #38438 | " | 🔵 | WARN Level Log Patterns: Graceful Degradation and Fallback Behavior | ~539 |
| #38437 | 10:24 PM | 🔵 | Claude-mem core functionality and logging patterns identified | ~710 |
| #38428 | " | 🔵 | Log level audit report structure and content examined | ~559 |
| #38425 | " | ⚖️ | Log Level Architecture: Fail-Critical Over Fail-Fast for Chroma | ~467 |
| #38416 | 10:22 PM | 🔵 | ChromaDB Is Critical Not Optional - Log Audit Findings Challenged | ~405 |
| #38405 | 10:07 PM | ⚖️ | DEBUG Log Level Analysis - One Message Requires WARN Promotion | ~819 |
| #38404 | 10:06 PM | ⚖️ | Log Level Audit Analysis - WARN to ERROR Promotion Criteria Established | ~769 |
| #38403 | 10:04 PM | 🔵 | Log Level Audit - INFO and DEBUG Level Messages Catalogued | ~688 |
| #38402 | 10:03 PM | 🔵 | Log Level Audit Report Analysis - Critical Error Messages Identified | ~642 |
| #38401 | 10:00 PM | 🔵 | Enhanced Audit Report Reveals Error Logging Patterns and Message Extraction Issues | ~498 |
| #38394 | 9:58 PM | ✅ | Created Log Level Audit Report Documentation | ~319 |
| #38393 | " | ✅ | Enhanced Log Audit Report Format with Component Tags and Full Logger Calls | ~393 |
| #38386 | 9:56 PM | ✅ | Log Audit Report Generated and Saved to Documentation | ~442 |
| #38385 | " | ✅ | Log Level Audit Report Saved to Documentation | ~379 |
| #38251 | 7:46 PM | 🔵 | Comprehensive Windows Platform Issues Report | ~982 |
| #38731 | 6:49 PM | 🟣 | Comprehensive Sonnet vs Opus Behavioral Analysis Report Generated and Saved | ~700 |
</claude-mem-context>
+9 -4
View File
@@ -17,7 +17,12 @@
},
{
"type": "command",
"command": "bun \"${CLAUDE_PLUGIN_ROOT}/scripts/context-hook.js\"",
"command": "bun \"${CLAUDE_PLUGIN_ROOT}/scripts/worker-service.cjs\" hook claude-code context",
"timeout": 60
},
{
"type": "command",
"command": "bun \"${CLAUDE_PLUGIN_ROOT}/scripts/worker-service.cjs\" hook claude-code user-message",
"timeout": 60
}
]
@@ -33,7 +38,7 @@
},
{
"type": "command",
"command": "bun \"${CLAUDE_PLUGIN_ROOT}/scripts/new-hook.js\"",
"command": "bun \"${CLAUDE_PLUGIN_ROOT}/scripts/worker-service.cjs\" hook claude-code session-init",
"timeout": 60
}
]
@@ -50,7 +55,7 @@
},
{
"type": "command",
"command": "bun \"${CLAUDE_PLUGIN_ROOT}/scripts/save-hook.js\"",
"command": "bun \"${CLAUDE_PLUGIN_ROOT}/scripts/worker-service.cjs\" hook claude-code observation",
"timeout": 120
}
]
@@ -66,7 +71,7 @@
},
{
"type": "command",
"command": "bun \"${CLAUDE_PLUGIN_ROOT}/scripts/summary-hook.js\"",
"command": "bun \"${CLAUDE_PLUGIN_ROOT}/scripts/worker-service.cjs\" hook claude-code summarize",
"timeout": 120
}
]
-19
View File
@@ -1,19 +0,0 @@
#!/usr/bin/env bun
import{stdin as A}from"process";import C from"path";import{homedir as z}from"os";import{readFileSync as q}from"fs";import{appendFileSync as j,existsSync as h,mkdirSync as H,readFileSync as G}from"fs";import{join as T}from"path";import{homedir as K}from"os";var S=(i=>(i[i.DEBUG=0]="DEBUG",i[i.INFO=1]="INFO",i[i.WARN=2]="WARN",i[i.ERROR=3]="ERROR",i[i.SILENT=4]="SILENT",i))(S||{}),U=T(K(),".claude-mem"),M=class{level=null;useColor;logFilePath=null;logFileInitialized=!1;constructor(){this.useColor=process.stdout.isTTY??!1}ensureLogFileInitialized(){if(!this.logFileInitialized){this.logFileInitialized=!0;try{let t=T(U,"logs");h(t)||H(t,{recursive:!0});let r=new Date().toISOString().split("T")[0];this.logFilePath=T(t,`claude-mem-${r}.log`)}catch(t){console.error("[LOGGER] Failed to initialize log file:",t),this.logFilePath=null}}}getLevel(){if(this.level===null)try{let t=T(U,"settings.json");if(h(t)){let r=G(t,"utf-8"),n=(JSON.parse(r).CLAUDE_MEM_LOG_LEVEL||"INFO").toUpperCase();this.level=S[n]??1}else this.level=1}catch{this.level=1}return this.level}correlationId(t,r){return`obs-${t}-${r}`}sessionId(t){return`session-${t}`}formatData(t){if(t==null)return"";if(typeof t=="string")return t;if(typeof t=="number"||typeof t=="boolean")return t.toString();if(typeof t=="object"){if(t instanceof Error)return this.getLevel()===0?`${t.message}
${t.stack}`:t.message;if(Array.isArray(t))return`[${t.length} items]`;let r=Object.keys(t);return r.length===0?"{}":r.length<=3?JSON.stringify(t):`{${r.length} keys: ${r.slice(0,3).join(", ")}...}`}return String(t)}formatTool(t,r){if(!r)return t;let e=r;if(typeof r=="string")try{e=JSON.parse(r)}catch{e=r}if(t==="Bash"&&e.command)return`${t}(${e.command})`;if(e.file_path)return`${t}(${e.file_path})`;if(e.notebook_path)return`${t}(${e.notebook_path})`;if(t==="Glob"&&e.pattern)return`${t}(${e.pattern})`;if(t==="Grep"&&e.pattern)return`${t}(${e.pattern})`;if(e.url)return`${t}(${e.url})`;if(e.query)return`${t}(${e.query})`;if(t==="Task"){if(e.subagent_type)return`${t}(${e.subagent_type})`;if(e.description)return`${t}(${e.description})`}return t==="Skill"&&e.skill?`${t}(${e.skill})`:t==="LSP"&&e.operation?`${t}(${e.operation})`:t}formatTimestamp(t){let r=t.getFullYear(),e=String(t.getMonth()+1).padStart(2,"0"),n=String(t.getDate()).padStart(2,"0"),i=String(t.getHours()).padStart(2,"0"),s=String(t.getMinutes()).padStart(2,"0"),E=String(t.getSeconds()).padStart(2,"0"),c=String(t.getMilliseconds()).padStart(3,"0");return`${r}-${e}-${n} ${i}:${s}:${E}.${c}`}log(t,r,e,n,i){if(t<this.getLevel())return;this.ensureLogFileInitialized();let s=this.formatTimestamp(new Date),E=S[t].padEnd(5),c=r.padEnd(6),a="";n?.correlationId?a=`[${n.correlationId}] `:n?.sessionId&&(a=`[session-${n.sessionId}] `);let _="";i!=null&&(i instanceof Error?_=this.getLevel()===0?`
${i.message}
${i.stack}`:` ${i.message}`:this.getLevel()===0&&typeof i=="object"?_=`
`+JSON.stringify(i,null,2):_=" "+this.formatData(i));let p="";if(n){let{sessionId:d,memorySessionId:at,correlationId:lt,...R}=n;Object.keys(R).length>0&&(p=` {${Object.entries(R).map(([x,b])=>`${x}=${b}`).join(", ")}}`)}let D=`[${s}] [${E}] [${c}] ${a}${e}${p}${_}`;if(this.logFilePath)try{j(this.logFilePath,D+`
`,"utf8")}catch(d){process.stderr.write(`[LOGGER] Failed to write to log file: ${d}
`)}else process.stderr.write(D+`
`)}debug(t,r,e,n){this.log(0,t,r,e,n)}info(t,r,e,n){this.log(1,t,r,e,n)}warn(t,r,e,n){this.log(2,t,r,e,n)}error(t,r,e,n){this.log(3,t,r,e,n)}dataIn(t,r,e,n){this.info(t,`\u2192 ${r}`,e,n)}dataOut(t,r,e,n){this.info(t,`\u2190 ${r}`,e,n)}success(t,r,e,n){this.info(t,`\u2713 ${r}`,e,n)}failure(t,r,e,n){this.error(t,`\u2717 ${r}`,e,n)}timing(t,r,e,n){this.info(t,`\u23F1 ${r}`,n,{duration:`${e}ms`})}happyPathError(t,r,e,n,i=""){let a=((new Error().stack||"").split(`
`)[2]||"").match(/at\s+(?:.*\s+)?\(?([^:]+):(\d+):(\d+)\)?/),_=a?`${a[1].split("/").pop()}:${a[2]}`:"unknown",p={...e,location:_};return this.warn(t,`[HAPPY-PATH] ${r}`,p,n),i}},l=new M;var m={DEFAULT:3e5,HEALTH_CHECK:3e4,WORKER_STARTUP_WAIT:1e3,WORKER_STARTUP_RETRIES:300,PRE_RESTART_SETTLE_DELAY:2e3,WINDOWS_MULTIPLIER:1.5};function N(o){return process.platform==="win32"?Math.round(o*m.WINDOWS_MULTIPLIER):o}import{readFileSync as X,writeFileSync as P,existsSync as y,mkdirSync as V}from"fs";import{join as B,dirname as Y}from"path";import{homedir as J}from"os";var I="bugfix,feature,refactor,discovery,decision,change",k="how-it-works,why-it-exists,what-changed,problem-solution,gotcha,pattern,trade-off";var g=class{static DEFAULTS={CLAUDE_MEM_MODEL:"claude-sonnet-4-5",CLAUDE_MEM_CONTEXT_OBSERVATIONS:"50",CLAUDE_MEM_WORKER_PORT:"37777",CLAUDE_MEM_WORKER_HOST:"127.0.0.1",CLAUDE_MEM_SKIP_TOOLS:"ListMcpResourcesTool,SlashCommand,Skill,TodoWrite,AskUserQuestion",CLAUDE_MEM_PROVIDER:"claude",CLAUDE_MEM_GEMINI_API_KEY:"",CLAUDE_MEM_GEMINI_MODEL:"gemini-2.5-flash-lite",CLAUDE_MEM_GEMINI_RATE_LIMITING_ENABLED:"true",CLAUDE_MEM_OPENROUTER_API_KEY:"",CLAUDE_MEM_OPENROUTER_MODEL:"xiaomi/mimo-v2-flash:free",CLAUDE_MEM_OPENROUTER_SITE_URL:"",CLAUDE_MEM_OPENROUTER_APP_NAME:"claude-mem",CLAUDE_MEM_OPENROUTER_MAX_CONTEXT_MESSAGES:"20",CLAUDE_MEM_OPENROUTER_MAX_TOKENS:"100000",CLAUDE_MEM_DATA_DIR:B(J(),".claude-mem"),CLAUDE_MEM_LOG_LEVEL:"INFO",CLAUDE_MEM_PYTHON_VERSION:"3.13",CLAUDE_CODE_PATH:"",CLAUDE_MEM_MODE:"code",CLAUDE_MEM_CONTEXT_SHOW_READ_TOKENS:"true",CLAUDE_MEM_CONTEXT_SHOW_WORK_TOKENS:"true",CLAUDE_MEM_CONTEXT_SHOW_SAVINGS_AMOUNT:"true",CLAUDE_MEM_CONTEXT_SHOW_SAVINGS_PERCENT:"true",CLAUDE_MEM_CONTEXT_OBSERVATION_TYPES:I,CLAUDE_MEM_CONTEXT_OBSERVATION_CONCEPTS:k,CLAUDE_MEM_CONTEXT_FULL_COUNT:"5",CLAUDE_MEM_CONTEXT_FULL_FIELD:"narrative",CLAUDE_MEM_CONTEXT_SESSION_COUNT:"10",CLAUDE_MEM_CONTEXT_SHOW_LAST_SUMMARY:"true",CLAUDE_MEM_CONTEXT_SHOW_LAST_MESSAGE:"false"};static getAllDefaults(){return{...this.DEFAULTS}}static get(t){return this.DEFAULTS[t]}static getInt(t){let r=this.get(t);return parseInt(r,10)}static getBool(t){return this.get(t)==="true"}static loadFromFile(t){try{if(!y(t)){let s=this.getAllDefaults();try{let E=Y(t);y(E)||V(E,{recursive:!0}),P(t,JSON.stringify(s,null,2),"utf-8"),console.log("[SETTINGS] Created settings file with defaults:",t)}catch(E){console.warn("[SETTINGS] Failed to create settings file, using in-memory defaults:",t,E)}return s}let r=X(t,"utf-8"),e=JSON.parse(r),n=e;if(e.env&&typeof e.env=="object"){n=e.env;try{P(t,JSON.stringify(n,null,2),"utf-8"),console.log("[SETTINGS] Migrated settings file from nested to flat schema:",t)}catch(s){console.warn("[SETTINGS] Failed to auto-migrate settings file:",t,s)}}let i={...this.DEFAULTS};for(let s of Object.keys(this.DEFAULTS))n[s]!==void 0&&(i[s]=n[s]);return i}catch(r){return console.warn("[SETTINGS] Failed to load settings, using defaults:",t,r),this.getAllDefaults()}}};function $(o={}){let{port:t,includeSkillFallback:r=!1,customPrefix:e,actualError:n}=o,i=e||"Worker service connection failed.",s=t?` (port ${t})`:"",E=`${i}${s}
`;return E+=`To restart the worker:
`,E+=`1. Exit Claude Code completely
`,E+=`2. Run: npm run worker:restart
`,E+="3. Restart Claude Code",r&&(E+=`
If that doesn't work, try: /troubleshoot`),n&&(E=`Worker Error: ${n}
${E}`),E}var Q=C.join(z(),".claude","plugins","marketplaces","thedotmack"),It=N(m.HEALTH_CHECK),O=null;function u(){if(O!==null)return O;let o=C.join(g.get("CLAUDE_MEM_DATA_DIR"),"settings.json"),t=g.loadFromFile(o);return O=parseInt(t.CLAUDE_MEM_WORKER_PORT,10),O}async function Z(){let o=u();return(await fetch(`http://127.0.0.1:${o}/api/readiness`)).ok}function tt(){let o=C.join(Q,"package.json");return JSON.parse(q(o,"utf-8")).version}async function et(){let o=u(),t=await fetch(`http://127.0.0.1:${o}/api/version`);if(!t.ok)throw new Error(`Failed to get worker version: ${t.status}`);return(await t.json()).version}async function rt(){let o=tt(),t=await et();o!==t&&l.debug("SYSTEM","Version check",{pluginVersion:o,workerVersion:t,note:"Mismatch will be auto-restarted by worker-service start command"})}async function v(){for(let r=0;r<75;r++){try{if(await Z()){await rt();return}}catch(e){l.debug("SYSTEM","Worker health check failed, will retry",{attempt:r+1,maxRetries:75,error:e instanceof Error?e.message:String(e)})}await new Promise(e=>setTimeout(e,200))}throw new Error($({port:u(),customPrefix:"Worker did not become ready within 15 seconds."}))}import it from"path";import{statSync as nt,readFileSync as ot}from"fs";import L from"path";var f={isWorktree:!1,worktreeName:null,parentRepoPath:null,parentProjectName:null};function W(o){let t=L.join(o,".git"),r;try{r=nt(t)}catch{return f}if(!r.isFile())return f;let e;try{e=ot(t,"utf-8").trim()}catch{return f}let n=e.match(/^gitdir:\s*(.+)$/);if(!n)return f;let s=n[1].match(/^(.+)[/\\]\.git[/\\]worktrees[/\\]([^/\\]+)$/);if(!s)return f;let E=s[1],c=L.basename(o),a=L.basename(E);return{isWorktree:!0,worktreeName:c,parentRepoPath:E,parentProjectName:a}}function st(o){if(!o||o.trim()==="")return l.warn("PROJECT_NAME","Empty cwd provided, using fallback",{cwd:o}),"unknown-project";let t=it.basename(o);if(t===""){if(process.platform==="win32"){let e=o.match(/^([A-Z]):\\/i);if(e){let i=`drive-${e[1].toUpperCase()}`;return l.info("PROJECT_NAME","Drive root detected",{cwd:o,projectName:i}),i}}return l.warn("PROJECT_NAME","Root directory detected, using fallback",{cwd:o}),"unknown-project"}return t}function F(o){let t=st(o);if(!o)return{primary:t,parent:null,isWorktree:!1,allProjects:[t]};let r=W(o);return r.isWorktree&&r.parentProjectName?{primary:t,parent:r.parentProjectName,isWorktree:!0,allProjects:[r.parentProjectName,t]}:{primary:t,parent:null,isWorktree:!1,allProjects:[t]}}async function w(o){await v();let t=o?.cwd??process.cwd(),r=F(t),e=u(),n=r.allProjects.join(","),i=`http://127.0.0.1:${e}/api/context/inject?projects=${encodeURIComponent(n)}`,s=await fetch(i);if(!s.ok)throw new Error(`Context generation failed: ${s.status}`);return(await s.text()).trim()}var Et=process.argv.includes("--colors");if(A.isTTY||Et)w(void 0).then(o=>{console.log(o),process.exit(0)});else{let o="";A.on("data",t=>o+=t),A.on("end",async()=>{let t;try{t=o.trim()?JSON.parse(o):void 0}catch(e){throw new Error(`Failed to parse hook input: ${e instanceof Error?e.message:String(e)}`)}let r=await w(t);console.log(JSON.stringify({hookSpecificOutput:{hookEventName:"SessionStart",additionalContext:r}})),process.exit(0)})}
File diff suppressed because one or more lines are too long
-19
View File
@@ -1,19 +0,0 @@
#!/usr/bin/env bun
import{stdin as b}from"process";var T=JSON.stringify({continue:!0,suppressOutput:!0});import L from"path";import{homedir as Y}from"os";import{readFileSync as J}from"fs";import{appendFileSync as F,existsSync as R,mkdirSync as H,readFileSync as x}from"fs";import{join as f}from"path";import{homedir as j}from"os";var S=(i=>(i[i.DEBUG=0]="DEBUG",i[i.INFO=1]="INFO",i[i.WARN=2]="WARN",i[i.ERROR=3]="ERROR",i[i.SILENT=4]="SILENT",i))(S||{}),h=f(j(),".claude-mem"),m=class{level=null;useColor;logFilePath=null;logFileInitialized=!1;constructor(){this.useColor=process.stdout.isTTY??!1}ensureLogFileInitialized(){if(!this.logFileInitialized){this.logFileInitialized=!0;try{let t=f(h,"logs");R(t)||H(t,{recursive:!0});let r=new Date().toISOString().split("T")[0];this.logFilePath=f(t,`claude-mem-${r}.log`)}catch(t){console.error("[LOGGER] Failed to initialize log file:",t),this.logFilePath=null}}}getLevel(){if(this.level===null)try{let t=f(h,"settings.json");if(R(t)){let r=x(t,"utf-8"),n=(JSON.parse(r).CLAUDE_MEM_LOG_LEVEL||"INFO").toUpperCase();this.level=S[n]??1}else this.level=1}catch{this.level=1}return this.level}correlationId(t,r){return`obs-${t}-${r}`}sessionId(t){return`session-${t}`}formatData(t){if(t==null)return"";if(typeof t=="string")return t;if(typeof t=="number"||typeof t=="boolean")return t.toString();if(typeof t=="object"){if(t instanceof Error)return this.getLevel()===0?`${t.message}
${t.stack}`:t.message;if(Array.isArray(t))return`[${t.length} items]`;let r=Object.keys(t);return r.length===0?"{}":r.length<=3?JSON.stringify(t):`{${r.length} keys: ${r.slice(0,3).join(", ")}...}`}return String(t)}formatTool(t,r){if(!r)return t;let e=r;if(typeof r=="string")try{e=JSON.parse(r)}catch{e=r}if(t==="Bash"&&e.command)return`${t}(${e.command})`;if(e.file_path)return`${t}(${e.file_path})`;if(e.notebook_path)return`${t}(${e.notebook_path})`;if(t==="Glob"&&e.pattern)return`${t}(${e.pattern})`;if(t==="Grep"&&e.pattern)return`${t}(${e.pattern})`;if(e.url)return`${t}(${e.url})`;if(e.query)return`${t}(${e.query})`;if(t==="Task"){if(e.subagent_type)return`${t}(${e.subagent_type})`;if(e.description)return`${t}(${e.description})`}return t==="Skill"&&e.skill?`${t}(${e.skill})`:t==="LSP"&&e.operation?`${t}(${e.operation})`:t}formatTimestamp(t){let r=t.getFullYear(),e=String(t.getMonth()+1).padStart(2,"0"),n=String(t.getDate()).padStart(2,"0"),i=String(t.getHours()).padStart(2,"0"),E=String(t.getMinutes()).padStart(2,"0"),s=String(t.getSeconds()).padStart(2,"0"),c=String(t.getMilliseconds()).padStart(3,"0");return`${r}-${e}-${n} ${i}:${E}:${s}.${c}`}log(t,r,e,n,i){if(t<this.getLevel())return;this.ensureLogFileInitialized();let E=this.formatTimestamp(new Date),s=S[t].padEnd(5),c=r.padEnd(6),l="";n?.correlationId?l=`[${n.correlationId}] `:n?.sessionId&&(l=`[session-${n.sessionId}] `);let _="";i!=null&&(i instanceof Error?_=this.getLevel()===0?`
${i.message}
${i.stack}`:` ${i.message}`:this.getLevel()===0&&typeof i=="object"?_=`
`+JSON.stringify(i,null,2):_=" "+this.formatData(i));let u="";if(n){let{sessionId:d,memorySessionId:nt,correlationId:ot,...D}=n;Object.keys(D).length>0&&(u=` {${Object.entries(D).map(([w,W])=>`${w}=${W}`).join(", ")}}`)}let A=`[${E}] [${s}] [${c}] ${l}${e}${u}${_}`;if(this.logFilePath)try{F(this.logFilePath,A+`
`,"utf8")}catch(d){process.stderr.write(`[LOGGER] Failed to write to log file: ${d}
`)}else process.stderr.write(A+`
`)}debug(t,r,e,n){this.log(0,t,r,e,n)}info(t,r,e,n){this.log(1,t,r,e,n)}warn(t,r,e,n){this.log(2,t,r,e,n)}error(t,r,e,n){this.log(3,t,r,e,n)}dataIn(t,r,e,n){this.info(t,`\u2192 ${r}`,e,n)}dataOut(t,r,e,n){this.info(t,`\u2190 ${r}`,e,n)}success(t,r,e,n){this.info(t,`\u2713 ${r}`,e,n)}failure(t,r,e,n){this.error(t,`\u2717 ${r}`,e,n)}timing(t,r,e,n){this.info(t,`\u23F1 ${r}`,n,{duration:`${e}ms`})}happyPathError(t,r,e,n,i=""){let l=((new Error().stack||"").split(`
`)[2]||"").match(/at\s+(?:.*\s+)?\(?([^:]+):(\d+):(\d+)\)?/),_=l?`${l[1].split("/").pop()}:${l[2]}`:"unknown",u={...e,location:_};return this.warn(t,`[HAPPY-PATH] ${r}`,u,n),i}},a=new m;var M={DEFAULT:3e5,HEALTH_CHECK:3e4,WORKER_STARTUP_WAIT:1e3,WORKER_STARTUP_RETRIES:300,PRE_RESTART_SETTLE_DELAY:2e3,WINDOWS_MULTIPLIER:1.5};function N(o){return process.platform==="win32"?Math.round(o*M.WINDOWS_MULTIPLIER):o}import{readFileSync as K,writeFileSync as k,existsSync as P,mkdirSync as G}from"fs";import{join as X,dirname as V}from"path";import{homedir as B}from"os";var I="bugfix,feature,refactor,discovery,decision,change",U="how-it-works,why-it-exists,what-changed,problem-solution,gotcha,pattern,trade-off";var g=class{static DEFAULTS={CLAUDE_MEM_MODEL:"claude-sonnet-4-5",CLAUDE_MEM_CONTEXT_OBSERVATIONS:"50",CLAUDE_MEM_WORKER_PORT:"37777",CLAUDE_MEM_WORKER_HOST:"127.0.0.1",CLAUDE_MEM_SKIP_TOOLS:"ListMcpResourcesTool,SlashCommand,Skill,TodoWrite,AskUserQuestion",CLAUDE_MEM_PROVIDER:"claude",CLAUDE_MEM_GEMINI_API_KEY:"",CLAUDE_MEM_GEMINI_MODEL:"gemini-2.5-flash-lite",CLAUDE_MEM_GEMINI_RATE_LIMITING_ENABLED:"true",CLAUDE_MEM_OPENROUTER_API_KEY:"",CLAUDE_MEM_OPENROUTER_MODEL:"xiaomi/mimo-v2-flash:free",CLAUDE_MEM_OPENROUTER_SITE_URL:"",CLAUDE_MEM_OPENROUTER_APP_NAME:"claude-mem",CLAUDE_MEM_OPENROUTER_MAX_CONTEXT_MESSAGES:"20",CLAUDE_MEM_OPENROUTER_MAX_TOKENS:"100000",CLAUDE_MEM_DATA_DIR:X(B(),".claude-mem"),CLAUDE_MEM_LOG_LEVEL:"INFO",CLAUDE_MEM_PYTHON_VERSION:"3.13",CLAUDE_CODE_PATH:"",CLAUDE_MEM_MODE:"code",CLAUDE_MEM_CONTEXT_SHOW_READ_TOKENS:"true",CLAUDE_MEM_CONTEXT_SHOW_WORK_TOKENS:"true",CLAUDE_MEM_CONTEXT_SHOW_SAVINGS_AMOUNT:"true",CLAUDE_MEM_CONTEXT_SHOW_SAVINGS_PERCENT:"true",CLAUDE_MEM_CONTEXT_OBSERVATION_TYPES:I,CLAUDE_MEM_CONTEXT_OBSERVATION_CONCEPTS:U,CLAUDE_MEM_CONTEXT_FULL_COUNT:"5",CLAUDE_MEM_CONTEXT_FULL_FIELD:"narrative",CLAUDE_MEM_CONTEXT_SESSION_COUNT:"10",CLAUDE_MEM_CONTEXT_SHOW_LAST_SUMMARY:"true",CLAUDE_MEM_CONTEXT_SHOW_LAST_MESSAGE:"false"};static getAllDefaults(){return{...this.DEFAULTS}}static get(t){return this.DEFAULTS[t]}static getInt(t){let r=this.get(t);return parseInt(r,10)}static getBool(t){return this.get(t)==="true"}static loadFromFile(t){try{if(!P(t)){let E=this.getAllDefaults();try{let s=V(t);P(s)||G(s,{recursive:!0}),k(t,JSON.stringify(E,null,2),"utf-8"),console.log("[SETTINGS] Created settings file with defaults:",t)}catch(s){console.warn("[SETTINGS] Failed to create settings file, using in-memory defaults:",t,s)}return E}let r=K(t,"utf-8"),e=JSON.parse(r),n=e;if(e.env&&typeof e.env=="object"){n=e.env;try{k(t,JSON.stringify(n,null,2),"utf-8"),console.log("[SETTINGS] Migrated settings file from nested to flat schema:",t)}catch(E){console.warn("[SETTINGS] Failed to auto-migrate settings file:",t,E)}}let i={...this.DEFAULTS};for(let E of Object.keys(this.DEFAULTS))n[E]!==void 0&&(i[E]=n[E]);return i}catch(r){return console.warn("[SETTINGS] Failed to load settings, using defaults:",t,r),this.getAllDefaults()}}};function y(o={}){let{port:t,includeSkillFallback:r=!1,customPrefix:e,actualError:n}=o,i=e||"Worker service connection failed.",E=t?` (port ${t})`:"",s=`${i}${E}
`;return s+=`To restart the worker:
`,s+=`1. Exit Claude Code completely
`,s+=`2. Run: npm run worker:restart
`,s+="3. Restart Claude Code",r&&(s+=`
If that doesn't work, try: /troubleshoot`),n&&(s=`Worker Error: ${n}
${s}`),s}var z=L.join(Y(),".claude","plugins","marketplaces","thedotmack"),Rt=N(M.HEALTH_CHECK),O=null;function p(){if(O!==null)return O;let o=L.join(g.get("CLAUDE_MEM_DATA_DIR"),"settings.json"),t=g.loadFromFile(o);return O=parseInt(t.CLAUDE_MEM_WORKER_PORT,10),O}async function q(){let o=p();return(await fetch(`http://127.0.0.1:${o}/api/readiness`)).ok}function Q(){let o=L.join(z,"package.json");return JSON.parse(J(o,"utf-8")).version}async function Z(){let o=p(),t=await fetch(`http://127.0.0.1:${o}/api/version`);if(!t.ok)throw new Error(`Failed to get worker version: ${t.status}`);return(await t.json()).version}async function tt(){let o=Q(),t=await Z();o!==t&&a.debug("SYSTEM","Version check",{pluginVersion:o,workerVersion:t,note:"Mismatch will be auto-restarted by worker-service start command"})}async function $(){for(let r=0;r<75;r++){try{if(await q()){await tt();return}}catch(e){a.debug("SYSTEM","Worker health check failed, will retry",{attempt:r+1,maxRetries:75,error:e instanceof Error?e.message:String(e)})}await new Promise(e=>setTimeout(e,200))}throw new Error(y({port:p(),customPrefix:"Worker did not become ready within 15 seconds."}))}import et from"path";function v(o){if(!o||o.trim()==="")return a.warn("PROJECT_NAME","Empty cwd provided, using fallback",{cwd:o}),"unknown-project";let t=et.basename(o);if(t===""){if(process.platform==="win32"){let e=o.match(/^([A-Z]):\\/i);if(e){let i=`drive-${e[1].toUpperCase()}`;return a.info("PROJECT_NAME","Drive root detected",{cwd:o,projectName:i}),i}}return a.warn("PROJECT_NAME","Root directory detected, using fallback",{cwd:o}),"unknown-project"}return t}async function rt(o){if(await $(),!o)throw new Error("newHook requires input");let{session_id:t,cwd:r,prompt:e}=o,n=v(r),i=p();a.debug("HOOK","new-hook: Calling /api/sessions/init",{contentSessionId:t,project:n});let E=await fetch(`http://127.0.0.1:${i}/api/sessions/init`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({contentSessionId:t,project:n,prompt:e})});if(!E.ok)throw new Error(`Session initialization failed: ${E.status}`);let s=await E.json(),c=s.sessionDbId,l=s.promptNumber;if(a.debug("HOOK","new-hook: Received from /api/sessions/init",{sessionDbId:c,promptNumber:l,skipped:s.skipped}),a.debug("HOOK",`[ALIGNMENT] Hook Entry | contentSessionId=${t} | prompt#=${l} | sessionDbId=${c}`),s.skipped&&s.reason==="private"){a.info("HOOK",`INIT_COMPLETE | sessionDbId=${c} | promptNumber=${l} | skipped=true | reason=private`,{sessionId:c}),console.log(T);return}let _=e.startsWith("/")?e.substring(1):e;a.debug("HOOK","new-hook: Calling /sessions/{sessionDbId}/init",{sessionDbId:c,promptNumber:l});let u=await fetch(`http://127.0.0.1:${i}/sessions/${c}/init`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({userPrompt:_,promptNumber:l})});if(!u.ok)throw new Error(`SDK agent start failed: ${u.status}`);a.info("HOOK",`INIT_COMPLETE | sessionDbId=${c} | promptNumber=${l} | project=${n}`,{sessionId:c}),console.log(T)}var C="";b.on("data",o=>C+=o);b.on("end",async()=>{try{let o;try{o=C?JSON.parse(C):void 0}catch(t){throw new Error(`Failed to parse hook input: ${t instanceof Error?t.message:String(t)}`)}await rt(o)}catch(o){a.error("HOOK","new-hook failed",{},o)}finally{process.exit(0)}});
-19
View File
@@ -1,19 +0,0 @@
#!/usr/bin/env bun
import{stdin as v}from"process";var d=JSON.stringify({continue:!0,suppressOutput:!0});import{appendFileSync as H,existsSync as U,mkdirSync as W,readFileSync as b}from"fs";import{join as T}from"path";import{homedir as x}from"os";var f=(o=>(o[o.DEBUG=0]="DEBUG",o[o.INFO=1]="INFO",o[o.WARN=2]="WARN",o[o.ERROR=3]="ERROR",o[o.SILENT=4]="SILENT",o))(f||{}),R=T(x(),".claude-mem"),M=class{level=null;useColor;logFilePath=null;logFileInitialized=!1;constructor(){this.useColor=process.stdout.isTTY??!1}ensureLogFileInitialized(){if(!this.logFileInitialized){this.logFileInitialized=!0;try{let t=T(R,"logs");U(t)||W(t,{recursive:!0});let r=new Date().toISOString().split("T")[0];this.logFilePath=T(t,`claude-mem-${r}.log`)}catch(t){console.error("[LOGGER] Failed to initialize log file:",t),this.logFilePath=null}}}getLevel(){if(this.level===null)try{let t=T(R,"settings.json");if(U(t)){let r=b(t,"utf-8"),n=(JSON.parse(r).CLAUDE_MEM_LOG_LEVEL||"INFO").toUpperCase();this.level=f[n]??1}else this.level=1}catch{this.level=1}return this.level}correlationId(t,r){return`obs-${t}-${r}`}sessionId(t){return`session-${t}`}formatData(t){if(t==null)return"";if(typeof t=="string")return t;if(typeof t=="number"||typeof t=="boolean")return t.toString();if(typeof t=="object"){if(t instanceof Error)return this.getLevel()===0?`${t.message}
${t.stack}`:t.message;if(Array.isArray(t))return`[${t.length} items]`;let r=Object.keys(t);return r.length===0?"{}":r.length<=3?JSON.stringify(t):`{${r.length} keys: ${r.slice(0,3).join(", ")}...}`}return String(t)}formatTool(t,r){if(!r)return t;let e=r;if(typeof r=="string")try{e=JSON.parse(r)}catch{e=r}if(t==="Bash"&&e.command)return`${t}(${e.command})`;if(e.file_path)return`${t}(${e.file_path})`;if(e.notebook_path)return`${t}(${e.notebook_path})`;if(t==="Glob"&&e.pattern)return`${t}(${e.pattern})`;if(t==="Grep"&&e.pattern)return`${t}(${e.pattern})`;if(e.url)return`${t}(${e.url})`;if(e.query)return`${t}(${e.query})`;if(t==="Task"){if(e.subagent_type)return`${t}(${e.subagent_type})`;if(e.description)return`${t}(${e.description})`}return t==="Skill"&&e.skill?`${t}(${e.skill})`:t==="LSP"&&e.operation?`${t}(${e.operation})`:t}formatTimestamp(t){let r=t.getFullYear(),e=String(t.getMonth()+1).padStart(2,"0"),n=String(t.getDate()).padStart(2,"0"),o=String(t.getHours()).padStart(2,"0"),E=String(t.getMinutes()).padStart(2,"0"),i=String(t.getSeconds()).padStart(2,"0"),a=String(t.getMilliseconds()).padStart(3,"0");return`${r}-${e}-${n} ${o}:${E}:${i}.${a}`}log(t,r,e,n,o){if(t<this.getLevel())return;this.ensureLogFileInitialized();let E=this.formatTimestamp(new Date),i=f[t].padEnd(5),a=r.padEnd(6),l="";n?.correlationId?l=`[${n.correlationId}] `:n?.sessionId&&(l=`[session-${n.sessionId}] `);let c="";o!=null&&(o instanceof Error?c=this.getLevel()===0?`
${o.message}
${o.stack}`:` ${o.message}`:this.getLevel()===0&&typeof o=="object"?c=`
`+JSON.stringify(o,null,2):c=" "+this.formatData(o));let O="";if(n){let{sessionId:D,memorySessionId:et,correlationId:rt,...m}=n;Object.keys(m).length>0&&(O=` {${Object.entries(m).map(([F,w])=>`${F}=${w}`).join(", ")}}`)}let C=`[${E}] [${i}] [${a}] ${l}${e}${O}${c}`;if(this.logFilePath)try{H(this.logFilePath,C+`
`,"utf8")}catch(D){process.stderr.write(`[LOGGER] Failed to write to log file: ${D}
`)}else process.stderr.write(C+`
`)}debug(t,r,e,n){this.log(0,t,r,e,n)}info(t,r,e,n){this.log(1,t,r,e,n)}warn(t,r,e,n){this.log(2,t,r,e,n)}error(t,r,e,n){this.log(3,t,r,e,n)}dataIn(t,r,e,n){this.info(t,`\u2192 ${r}`,e,n)}dataOut(t,r,e,n){this.info(t,`\u2190 ${r}`,e,n)}success(t,r,e,n){this.info(t,`\u2713 ${r}`,e,n)}failure(t,r,e,n){this.error(t,`\u2717 ${r}`,e,n)}timing(t,r,e,n){this.info(t,`\u23F1 ${r}`,n,{duration:`${e}ms`})}happyPathError(t,r,e,n,o=""){let l=((new Error().stack||"").split(`
`)[2]||"").match(/at\s+(?:.*\s+)?\(?([^:]+):(\d+):(\d+)\)?/),c=l?`${l[1].split("/").pop()}:${l[2]}`:"unknown",O={...e,location:c};return this.warn(t,`[HAPPY-PATH] ${r}`,O,n),o}},_=new M;import L from"path";import{homedir as B}from"os";import{readFileSync as Y}from"fs";var p={DEFAULT:3e5,HEALTH_CHECK:3e4,WORKER_STARTUP_WAIT:1e3,WORKER_STARTUP_RETRIES:300,PRE_RESTART_SETTLE_DELAY:2e3,WINDOWS_MULTIPLIER:1.5};function I(s){return process.platform==="win32"?Math.round(s*p.WINDOWS_MULTIPLIER):s}import{readFileSync as G,writeFileSync as y,existsSync as P,mkdirSync as K}from"fs";import{join as X,dirname as V}from"path";import{homedir as j}from"os";var h="bugfix,feature,refactor,discovery,decision,change",N="how-it-works,why-it-exists,what-changed,problem-solution,gotcha,pattern,trade-off";var g=class{static DEFAULTS={CLAUDE_MEM_MODEL:"claude-sonnet-4-5",CLAUDE_MEM_CONTEXT_OBSERVATIONS:"50",CLAUDE_MEM_WORKER_PORT:"37777",CLAUDE_MEM_WORKER_HOST:"127.0.0.1",CLAUDE_MEM_SKIP_TOOLS:"ListMcpResourcesTool,SlashCommand,Skill,TodoWrite,AskUserQuestion",CLAUDE_MEM_PROVIDER:"claude",CLAUDE_MEM_GEMINI_API_KEY:"",CLAUDE_MEM_GEMINI_MODEL:"gemini-2.5-flash-lite",CLAUDE_MEM_GEMINI_RATE_LIMITING_ENABLED:"true",CLAUDE_MEM_OPENROUTER_API_KEY:"",CLAUDE_MEM_OPENROUTER_MODEL:"xiaomi/mimo-v2-flash:free",CLAUDE_MEM_OPENROUTER_SITE_URL:"",CLAUDE_MEM_OPENROUTER_APP_NAME:"claude-mem",CLAUDE_MEM_OPENROUTER_MAX_CONTEXT_MESSAGES:"20",CLAUDE_MEM_OPENROUTER_MAX_TOKENS:"100000",CLAUDE_MEM_DATA_DIR:X(j(),".claude-mem"),CLAUDE_MEM_LOG_LEVEL:"INFO",CLAUDE_MEM_PYTHON_VERSION:"3.13",CLAUDE_CODE_PATH:"",CLAUDE_MEM_MODE:"code",CLAUDE_MEM_CONTEXT_SHOW_READ_TOKENS:"true",CLAUDE_MEM_CONTEXT_SHOW_WORK_TOKENS:"true",CLAUDE_MEM_CONTEXT_SHOW_SAVINGS_AMOUNT:"true",CLAUDE_MEM_CONTEXT_SHOW_SAVINGS_PERCENT:"true",CLAUDE_MEM_CONTEXT_OBSERVATION_TYPES:h,CLAUDE_MEM_CONTEXT_OBSERVATION_CONCEPTS:N,CLAUDE_MEM_CONTEXT_FULL_COUNT:"5",CLAUDE_MEM_CONTEXT_FULL_FIELD:"narrative",CLAUDE_MEM_CONTEXT_SESSION_COUNT:"10",CLAUDE_MEM_CONTEXT_SHOW_LAST_SUMMARY:"true",CLAUDE_MEM_CONTEXT_SHOW_LAST_MESSAGE:"false"};static getAllDefaults(){return{...this.DEFAULTS}}static get(t){return this.DEFAULTS[t]}static getInt(t){let r=this.get(t);return parseInt(r,10)}static getBool(t){return this.get(t)==="true"}static loadFromFile(t){try{if(!P(t)){let E=this.getAllDefaults();try{let i=V(t);P(i)||K(i,{recursive:!0}),y(t,JSON.stringify(E,null,2),"utf-8"),console.log("[SETTINGS] Created settings file with defaults:",t)}catch(i){console.warn("[SETTINGS] Failed to create settings file, using in-memory defaults:",t,i)}return E}let r=G(t,"utf-8"),e=JSON.parse(r),n=e;if(e.env&&typeof e.env=="object"){n=e.env;try{y(t,JSON.stringify(n,null,2),"utf-8"),console.log("[SETTINGS] Migrated settings file from nested to flat schema:",t)}catch(E){console.warn("[SETTINGS] Failed to auto-migrate settings file:",t,E)}}let o={...this.DEFAULTS};for(let E of Object.keys(this.DEFAULTS))n[E]!==void 0&&(o[E]=n[E]);return o}catch(r){return console.warn("[SETTINGS] Failed to load settings, using defaults:",t,r),this.getAllDefaults()}}};function $(s={}){let{port:t,includeSkillFallback:r=!1,customPrefix:e,actualError:n}=s,o=e||"Worker service connection failed.",E=t?` (port ${t})`:"",i=`${o}${E}
`;return i+=`To restart the worker:
`,i+=`1. Exit Claude Code completely
`,i+=`2. Run: npm run worker:restart
`,i+="3. Restart Claude Code",r&&(i+=`
If that doesn't work, try: /troubleshoot`),n&&(i=`Worker Error: ${n}
${i}`),i}var J=L.join(B(),".claude","plugins","marketplaces","thedotmack"),mt=I(p.HEALTH_CHECK),S=null;function u(){if(S!==null)return S;let s=L.join(g.get("CLAUDE_MEM_DATA_DIR"),"settings.json"),t=g.loadFromFile(s);return S=parseInt(t.CLAUDE_MEM_WORKER_PORT,10),S}async function z(){let s=u();return(await fetch(`http://127.0.0.1:${s}/api/readiness`)).ok}function q(){let s=L.join(J,"package.json");return JSON.parse(Y(s,"utf-8")).version}async function Q(){let s=u(),t=await fetch(`http://127.0.0.1:${s}/api/version`);if(!t.ok)throw new Error(`Failed to get worker version: ${t.status}`);return(await t.json()).version}async function Z(){let s=q(),t=await Q();s!==t&&_.debug("SYSTEM","Version check",{pluginVersion:s,workerVersion:t,note:"Mismatch will be auto-restarted by worker-service start command"})}async function k(){for(let r=0;r<75;r++){try{if(await z()){await Z();return}}catch(e){_.debug("SYSTEM","Worker health check failed, will retry",{attempt:r+1,maxRetries:75,error:e instanceof Error?e.message:String(e)})}await new Promise(e=>setTimeout(e,200))}throw new Error($({port:u(),customPrefix:"Worker did not become ready within 15 seconds."}))}async function tt(s){if(await k(),!s)throw new Error("saveHook requires input");let{session_id:t,cwd:r,tool_name:e,tool_input:n,tool_response:o}=s,E=u(),i=_.formatTool(e,n);if(_.dataIn("HOOK",`PostToolUse: ${i}`,{workerPort:E}),!r)throw new Error(`Missing cwd in PostToolUse hook input for session ${t}, tool ${e}`);let a=await fetch(`http://127.0.0.1:${E}/api/sessions/observations`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({contentSessionId:t,tool_name:e,tool_input:n,tool_response:o,cwd:r})});if(!a.ok)throw new Error(`Observation storage failed: ${a.status}`);_.debug("HOOK","Observation sent successfully",{toolName:e}),console.log(d)}var A="";v.on("data",s=>A+=s);v.on("end",async()=>{try{let s;try{s=A?JSON.parse(A):void 0}catch(t){throw new Error(`Failed to parse hook input: ${t instanceof Error?t.message:String(t)}`)}await tt(s)}catch(s){_.error("HOOK","save-hook failed",{},s)}finally{process.exit(0)}});
-23
View File
@@ -1,23 +0,0 @@
#!/usr/bin/env bun
import{stdin as v}from"process";var T=JSON.stringify({continue:!0,suppressOutput:!0});import{appendFileSync as H,existsSync as R,mkdirSync as W,readFileSync as b}from"fs";import{join as O}from"path";import{homedir as G}from"os";var p=(o=>(o[o.DEBUG=0]="DEBUG",o[o.INFO=1]="INFO",o[o.WARN=2]="WARN",o[o.ERROR=3]="ERROR",o[o.SILENT=4]="SILENT",o))(p||{}),U=O(G(),".claude-mem"),M=class{level=null;useColor;logFilePath=null;logFileInitialized=!1;constructor(){this.useColor=process.stdout.isTTY??!1}ensureLogFileInitialized(){if(!this.logFileInitialized){this.logFileInitialized=!0;try{let t=O(U,"logs");R(t)||W(t,{recursive:!0});let r=new Date().toISOString().split("T")[0];this.logFilePath=O(t,`claude-mem-${r}.log`)}catch(t){console.error("[LOGGER] Failed to initialize log file:",t),this.logFilePath=null}}}getLevel(){if(this.level===null)try{let t=O(U,"settings.json");if(R(t)){let r=b(t,"utf-8"),n=(JSON.parse(r).CLAUDE_MEM_LOG_LEVEL||"INFO").toUpperCase();this.level=p[n]??1}else this.level=1}catch{this.level=1}return this.level}correlationId(t,r){return`obs-${t}-${r}`}sessionId(t){return`session-${t}`}formatData(t){if(t==null)return"";if(typeof t=="string")return t;if(typeof t=="number"||typeof t=="boolean")return t.toString();if(typeof t=="object"){if(t instanceof Error)return this.getLevel()===0?`${t.message}
${t.stack}`:t.message;if(Array.isArray(t))return`[${t.length} items]`;let r=Object.keys(t);return r.length===0?"{}":r.length<=3?JSON.stringify(t):`{${r.length} keys: ${r.slice(0,3).join(", ")}...}`}return String(t)}formatTool(t,r){if(!r)return t;let e=r;if(typeof r=="string")try{e=JSON.parse(r)}catch{e=r}if(t==="Bash"&&e.command)return`${t}(${e.command})`;if(e.file_path)return`${t}(${e.file_path})`;if(e.notebook_path)return`${t}(${e.notebook_path})`;if(t==="Glob"&&e.pattern)return`${t}(${e.pattern})`;if(t==="Grep"&&e.pattern)return`${t}(${e.pattern})`;if(e.url)return`${t}(${e.url})`;if(e.query)return`${t}(${e.query})`;if(t==="Task"){if(e.subagent_type)return`${t}(${e.subagent_type})`;if(e.description)return`${t}(${e.description})`}return t==="Skill"&&e.skill?`${t}(${e.skill})`:t==="LSP"&&e.operation?`${t}(${e.operation})`:t}formatTimestamp(t){let r=t.getFullYear(),e=String(t.getMonth()+1).padStart(2,"0"),n=String(t.getDate()).padStart(2,"0"),o=String(t.getHours()).padStart(2,"0"),E=String(t.getMinutes()).padStart(2,"0"),i=String(t.getSeconds()).padStart(2,"0"),_=String(t.getMilliseconds()).padStart(3,"0");return`${r}-${e}-${n} ${o}:${E}:${i}.${_}`}log(t,r,e,n,o){if(t<this.getLevel())return;this.ensureLogFileInitialized();let E=this.formatTimestamp(new Date),i=p[t].padEnd(5),_=r.padEnd(6),a="";n?.correlationId?a=`[${n.correlationId}] `:n?.sessionId&&(a=`[session-${n.sessionId}] `);let l="";o!=null&&(o instanceof Error?l=this.getLevel()===0?`
${o.message}
${o.stack}`:` ${o.message}`:this.getLevel()===0&&typeof o=="object"?l=`
`+JSON.stringify(o,null,2):l=" "+this.formatData(o));let f="";if(n){let{sessionId:D,memorySessionId:st,correlationId:ot,...d}=n;Object.keys(d).length>0&&(f=` {${Object.entries(d).map(([F,x])=>`${F}=${x}`).join(", ")}}`)}let C=`[${E}] [${i}] [${_}] ${a}${e}${f}${l}`;if(this.logFilePath)try{H(this.logFilePath,C+`
`,"utf8")}catch(D){process.stderr.write(`[LOGGER] Failed to write to log file: ${D}
`)}else process.stderr.write(C+`
`)}debug(t,r,e,n){this.log(0,t,r,e,n)}info(t,r,e,n){this.log(1,t,r,e,n)}warn(t,r,e,n){this.log(2,t,r,e,n)}error(t,r,e,n){this.log(3,t,r,e,n)}dataIn(t,r,e,n){this.info(t,`\u2192 ${r}`,e,n)}dataOut(t,r,e,n){this.info(t,`\u2190 ${r}`,e,n)}success(t,r,e,n){this.info(t,`\u2713 ${r}`,e,n)}failure(t,r,e,n){this.error(t,`\u2717 ${r}`,e,n)}timing(t,r,e,n){this.info(t,`\u23F1 ${r}`,n,{duration:`${e}ms`})}happyPathError(t,r,e,n,o=""){let a=((new Error().stack||"").split(`
`)[2]||"").match(/at\s+(?:.*\s+)?\(?([^:]+):(\d+):(\d+)\)?/),l=a?`${a[1].split("/").pop()}:${a[2]}`:"unknown",f={...e,location:l};return this.warn(t,`[HAPPY-PATH] ${r}`,f,n),o}},c=new M;import L from"path";import{homedir as Y}from"os";import{readFileSync as J}from"fs";var m={DEFAULT:3e5,HEALTH_CHECK:3e4,WORKER_STARTUP_WAIT:1e3,WORKER_STARTUP_RETRIES:300,PRE_RESTART_SETTLE_DELAY:2e3,WINDOWS_MULTIPLIER:1.5};function h(s){return process.platform==="win32"?Math.round(s*m.WINDOWS_MULTIPLIER):s}import{readFileSync as K,writeFileSync as N,existsSync as $,mkdirSync as X}from"fs";import{join as V,dirname as j}from"path";import{homedir as B}from"os";var I="bugfix,feature,refactor,discovery,decision,change",y="how-it-works,why-it-exists,what-changed,problem-solution,gotcha,pattern,trade-off";var g=class{static DEFAULTS={CLAUDE_MEM_MODEL:"claude-sonnet-4-5",CLAUDE_MEM_CONTEXT_OBSERVATIONS:"50",CLAUDE_MEM_WORKER_PORT:"37777",CLAUDE_MEM_WORKER_HOST:"127.0.0.1",CLAUDE_MEM_SKIP_TOOLS:"ListMcpResourcesTool,SlashCommand,Skill,TodoWrite,AskUserQuestion",CLAUDE_MEM_PROVIDER:"claude",CLAUDE_MEM_GEMINI_API_KEY:"",CLAUDE_MEM_GEMINI_MODEL:"gemini-2.5-flash-lite",CLAUDE_MEM_GEMINI_RATE_LIMITING_ENABLED:"true",CLAUDE_MEM_OPENROUTER_API_KEY:"",CLAUDE_MEM_OPENROUTER_MODEL:"xiaomi/mimo-v2-flash:free",CLAUDE_MEM_OPENROUTER_SITE_URL:"",CLAUDE_MEM_OPENROUTER_APP_NAME:"claude-mem",CLAUDE_MEM_OPENROUTER_MAX_CONTEXT_MESSAGES:"20",CLAUDE_MEM_OPENROUTER_MAX_TOKENS:"100000",CLAUDE_MEM_DATA_DIR:V(B(),".claude-mem"),CLAUDE_MEM_LOG_LEVEL:"INFO",CLAUDE_MEM_PYTHON_VERSION:"3.13",CLAUDE_CODE_PATH:"",CLAUDE_MEM_MODE:"code",CLAUDE_MEM_CONTEXT_SHOW_READ_TOKENS:"true",CLAUDE_MEM_CONTEXT_SHOW_WORK_TOKENS:"true",CLAUDE_MEM_CONTEXT_SHOW_SAVINGS_AMOUNT:"true",CLAUDE_MEM_CONTEXT_SHOW_SAVINGS_PERCENT:"true",CLAUDE_MEM_CONTEXT_OBSERVATION_TYPES:I,CLAUDE_MEM_CONTEXT_OBSERVATION_CONCEPTS:y,CLAUDE_MEM_CONTEXT_FULL_COUNT:"5",CLAUDE_MEM_CONTEXT_FULL_FIELD:"narrative",CLAUDE_MEM_CONTEXT_SESSION_COUNT:"10",CLAUDE_MEM_CONTEXT_SHOW_LAST_SUMMARY:"true",CLAUDE_MEM_CONTEXT_SHOW_LAST_MESSAGE:"false"};static getAllDefaults(){return{...this.DEFAULTS}}static get(t){return this.DEFAULTS[t]}static getInt(t){let r=this.get(t);return parseInt(r,10)}static getBool(t){return this.get(t)==="true"}static loadFromFile(t){try{if(!$(t)){let E=this.getAllDefaults();try{let i=j(t);$(i)||X(i,{recursive:!0}),N(t,JSON.stringify(E,null,2),"utf-8"),console.log("[SETTINGS] Created settings file with defaults:",t)}catch(i){console.warn("[SETTINGS] Failed to create settings file, using in-memory defaults:",t,i)}return E}let r=K(t,"utf-8"),e=JSON.parse(r),n=e;if(e.env&&typeof e.env=="object"){n=e.env;try{N(t,JSON.stringify(n,null,2),"utf-8"),console.log("[SETTINGS] Migrated settings file from nested to flat schema:",t)}catch(E){console.warn("[SETTINGS] Failed to auto-migrate settings file:",t,E)}}let o={...this.DEFAULTS};for(let E of Object.keys(this.DEFAULTS))n[E]!==void 0&&(o[E]=n[E]);return o}catch(r){return console.warn("[SETTINGS] Failed to load settings, using defaults:",t,r),this.getAllDefaults()}}};function k(s={}){let{port:t,includeSkillFallback:r=!1,customPrefix:e,actualError:n}=s,o=e||"Worker service connection failed.",E=t?` (port ${t})`:"",i=`${o}${E}
`;return i+=`To restart the worker:
`,i+=`1. Exit Claude Code completely
`,i+=`2. Run: npm run worker:restart
`,i+="3. Restart Claude Code",r&&(i+=`
If that doesn't work, try: /troubleshoot`),n&&(i=`Worker Error: ${n}
${i}`),i}var z=L.join(Y(),".claude","plugins","marketplaces","thedotmack"),Ut=h(m.HEALTH_CHECK),S=null;function u(){if(S!==null)return S;let s=L.join(g.get("CLAUDE_MEM_DATA_DIR"),"settings.json"),t=g.loadFromFile(s);return S=parseInt(t.CLAUDE_MEM_WORKER_PORT,10),S}async function q(){let s=u();return(await fetch(`http://127.0.0.1:${s}/api/readiness`)).ok}function Q(){let s=L.join(z,"package.json");return JSON.parse(J(s,"utf-8")).version}async function Z(){let s=u(),t=await fetch(`http://127.0.0.1:${s}/api/version`);if(!t.ok)throw new Error(`Failed to get worker version: ${t.status}`);return(await t.json()).version}async function tt(){let s=Q(),t=await Z();s!==t&&c.debug("SYSTEM","Version check",{pluginVersion:s,workerVersion:t,note:"Mismatch will be auto-restarted by worker-service start command"})}async function P(){for(let r=0;r<75;r++){try{if(await q()){await tt();return}}catch(e){c.debug("SYSTEM","Worker health check failed, will retry",{attempt:r+1,maxRetries:75,error:e instanceof Error?e.message:String(e)})}await new Promise(e=>setTimeout(e,200))}throw new Error(k({port:u(),customPrefix:"Worker did not become ready within 15 seconds."}))}import{readFileSync as et,existsSync as rt}from"fs";function w(s,t,r=!1){if(!s||!rt(s))throw new Error(`Transcript path missing or file does not exist: ${s}`);let e=et(s,"utf-8").trim();if(!e)throw new Error(`Transcript file exists but is empty: ${s}`);let n=e.split(`
`),o=!1;for(let E=n.length-1;E>=0;E--){let i=JSON.parse(n[E]);if(i.type===t&&(o=!0,i.message?.content)){let _="",a=i.message.content;if(typeof a=="string")_=a;else if(Array.isArray(a))_=a.filter(l=>l.type==="text").map(l=>l.text).join(`
`);else throw new Error(`Unknown message content format in transcript. Type: ${typeof a}`);return r&&(_=_.replace(/<system-reminder>[\s\S]*?<\/system-reminder>/g,""),_=_.replace(/\n{3,}/g,`
`).trim()),_}}if(!o)throw new Error(`No message found for role '${t}' in transcript: ${s}`);return""}async function nt(s){if(await P(),!s)throw new Error("summaryHook requires input");let{session_id:t}=s,r=u();if(!s.transcript_path)throw new Error(`Missing transcript_path in Stop hook input for session ${t}`);let e=w(s.transcript_path,"assistant",!0);c.dataIn("HOOK","Stop: Requesting summary",{workerPort:r,hasLastAssistantMessage:!!e});let n=await fetch(`http://127.0.0.1:${r}/api/sessions/summarize`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({contentSessionId:t,last_assistant_message:e})});if(!n.ok)throw console.log(T),new Error(`Summary generation failed: ${n.status}`);c.debug("HOOK","Summary request sent successfully"),console.log(T)}var A="";v.on("data",s=>A+=s);v.on("end",async()=>{try{let s;try{s=A?JSON.parse(A):void 0}catch(t){throw new Error(`Failed to parse hook input: ${t instanceof Error?t.message:String(t)}`)}await nt(s)}catch(s){c.error("HOOK","summary-hook failed",{},s)}finally{process.exit(0)}});
-30
View File
@@ -1,30 +0,0 @@
#!/usr/bin/env bun
import{basename as Z}from"path";import p from"path";import{homedir as j}from"os";import{readFileSync as B}from"fs";import{appendFileSync as w,existsSync as R,mkdirSync as W,readFileSync as b}from"fs";import{join as T}from"path";import{homedir as x}from"os";var M=(o=>(o[o.DEBUG=0]="DEBUG",o[o.INFO=1]="INFO",o[o.WARN=2]="WARN",o[o.ERROR=3]="ERROR",o[o.SILENT=4]="SILENT",o))(M||{}),U=T(x(),".claude-mem"),S=class{level=null;useColor;logFilePath=null;logFileInitialized=!1;constructor(){this.useColor=process.stdout.isTTY??!1}ensureLogFileInitialized(){if(!this.logFileInitialized){this.logFileInitialized=!0;try{let t=T(U,"logs");R(t)||W(t,{recursive:!0});let r=new Date().toISOString().split("T")[0];this.logFilePath=T(t,`claude-mem-${r}.log`)}catch(t){console.error("[LOGGER] Failed to initialize log file:",t),this.logFilePath=null}}}getLevel(){if(this.level===null)try{let t=T(U,"settings.json");if(R(t)){let r=b(t,"utf-8"),n=(JSON.parse(r).CLAUDE_MEM_LOG_LEVEL||"INFO").toUpperCase();this.level=M[n]??1}else this.level=1}catch{this.level=1}return this.level}correlationId(t,r){return`obs-${t}-${r}`}sessionId(t){return`session-${t}`}formatData(t){if(t==null)return"";if(typeof t=="string")return t;if(typeof t=="number"||typeof t=="boolean")return t.toString();if(typeof t=="object"){if(t instanceof Error)return this.getLevel()===0?`${t.message}
${t.stack}`:t.message;if(Array.isArray(t))return`[${t.length} items]`;let r=Object.keys(t);return r.length===0?"{}":r.length<=3?JSON.stringify(t):`{${r.length} keys: ${r.slice(0,3).join(", ")}...}`}return String(t)}formatTool(t,r){if(!r)return t;let e=r;if(typeof r=="string")try{e=JSON.parse(r)}catch{e=r}if(t==="Bash"&&e.command)return`${t}(${e.command})`;if(e.file_path)return`${t}(${e.file_path})`;if(e.notebook_path)return`${t}(${e.notebook_path})`;if(t==="Glob"&&e.pattern)return`${t}(${e.pattern})`;if(t==="Grep"&&e.pattern)return`${t}(${e.pattern})`;if(e.url)return`${t}(${e.url})`;if(e.query)return`${t}(${e.query})`;if(t==="Task"){if(e.subagent_type)return`${t}(${e.subagent_type})`;if(e.description)return`${t}(${e.description})`}return t==="Skill"&&e.skill?`${t}(${e.skill})`:t==="LSP"&&e.operation?`${t}(${e.operation})`:t}formatTimestamp(t){let r=t.getFullYear(),e=String(t.getMonth()+1).padStart(2,"0"),n=String(t.getDate()).padStart(2,"0"),o=String(t.getHours()).padStart(2,"0"),E=String(t.getMinutes()).padStart(2,"0"),s=String(t.getSeconds()).padStart(2,"0"),g=String(t.getMilliseconds()).padStart(3,"0");return`${r}-${e}-${n} ${o}:${E}:${s}.${g}`}log(t,r,e,n,o){if(t<this.getLevel())return;this.ensureLogFileInitialized();let E=this.formatTimestamp(new Date),s=M[t].padEnd(5),g=r.padEnd(6),_="";n?.correlationId?_=`[${n.correlationId}] `:n?.sessionId&&(_=`[session-${n.sessionId}] `);let a="";o!=null&&(o instanceof Error?a=this.getLevel()===0?`
${o.message}
${o.stack}`:` ${o.message}`:this.getLevel()===0&&typeof o=="object"?a=`
`+JSON.stringify(o,null,2):a=" "+this.formatData(o));let u="";if(n){let{sessionId:D,memorySessionId:rt,correlationId:nt,...m}=n;Object.keys(m).length>0&&(u=` {${Object.entries(m).map(([P,F])=>`${P}=${F}`).join(", ")}}`)}let A=`[${E}] [${s}] [${g}] ${_}${e}${u}${a}`;if(this.logFilePath)try{w(this.logFilePath,A+`
`,"utf8")}catch(D){process.stderr.write(`[LOGGER] Failed to write to log file: ${D}
`)}else process.stderr.write(A+`
`)}debug(t,r,e,n){this.log(0,t,r,e,n)}info(t,r,e,n){this.log(1,t,r,e,n)}warn(t,r,e,n){this.log(2,t,r,e,n)}error(t,r,e,n){this.log(3,t,r,e,n)}dataIn(t,r,e,n){this.info(t,`\u2192 ${r}`,e,n)}dataOut(t,r,e,n){this.info(t,`\u2190 ${r}`,e,n)}success(t,r,e,n){this.info(t,`\u2713 ${r}`,e,n)}failure(t,r,e,n){this.error(t,`\u2717 ${r}`,e,n)}timing(t,r,e,n){this.info(t,`\u23F1 ${r}`,n,{duration:`${e}ms`})}happyPathError(t,r,e,n,o=""){let _=((new Error().stack||"").split(`
`)[2]||"").match(/at\s+(?:.*\s+)?\(?([^:]+):(\d+):(\d+)\)?/),a=_?`${_[1].split("/").pop()}:${_[2]}`:"unknown",u={...e,location:a};return this.warn(t,`[HAPPY-PATH] ${r}`,u,n),o}},f=new S;var L={DEFAULT:3e5,HEALTH_CHECK:3e4,WORKER_STARTUP_WAIT:1e3,WORKER_STARTUP_RETRIES:300,PRE_RESTART_SETTLE_DELAY:2e3,WINDOWS_MULTIPLIER:1.5};function d(i){return process.platform==="win32"?Math.round(i*L.WINDOWS_MULTIPLIER):i}import{readFileSync as G,writeFileSync as N,existsSync as y,mkdirSync as H}from"fs";import{join as K,dirname as X}from"path";import{homedir as V}from"os";var h="bugfix,feature,refactor,discovery,decision,change",I="how-it-works,why-it-exists,what-changed,problem-solution,gotcha,pattern,trade-off";var l=class{static DEFAULTS={CLAUDE_MEM_MODEL:"claude-sonnet-4-5",CLAUDE_MEM_CONTEXT_OBSERVATIONS:"50",CLAUDE_MEM_WORKER_PORT:"37777",CLAUDE_MEM_WORKER_HOST:"127.0.0.1",CLAUDE_MEM_SKIP_TOOLS:"ListMcpResourcesTool,SlashCommand,Skill,TodoWrite,AskUserQuestion",CLAUDE_MEM_PROVIDER:"claude",CLAUDE_MEM_GEMINI_API_KEY:"",CLAUDE_MEM_GEMINI_MODEL:"gemini-2.5-flash-lite",CLAUDE_MEM_GEMINI_RATE_LIMITING_ENABLED:"true",CLAUDE_MEM_OPENROUTER_API_KEY:"",CLAUDE_MEM_OPENROUTER_MODEL:"xiaomi/mimo-v2-flash:free",CLAUDE_MEM_OPENROUTER_SITE_URL:"",CLAUDE_MEM_OPENROUTER_APP_NAME:"claude-mem",CLAUDE_MEM_OPENROUTER_MAX_CONTEXT_MESSAGES:"20",CLAUDE_MEM_OPENROUTER_MAX_TOKENS:"100000",CLAUDE_MEM_DATA_DIR:K(V(),".claude-mem"),CLAUDE_MEM_LOG_LEVEL:"INFO",CLAUDE_MEM_PYTHON_VERSION:"3.13",CLAUDE_CODE_PATH:"",CLAUDE_MEM_MODE:"code",CLAUDE_MEM_CONTEXT_SHOW_READ_TOKENS:"true",CLAUDE_MEM_CONTEXT_SHOW_WORK_TOKENS:"true",CLAUDE_MEM_CONTEXT_SHOW_SAVINGS_AMOUNT:"true",CLAUDE_MEM_CONTEXT_SHOW_SAVINGS_PERCENT:"true",CLAUDE_MEM_CONTEXT_OBSERVATION_TYPES:h,CLAUDE_MEM_CONTEXT_OBSERVATION_CONCEPTS:I,CLAUDE_MEM_CONTEXT_FULL_COUNT:"5",CLAUDE_MEM_CONTEXT_FULL_FIELD:"narrative",CLAUDE_MEM_CONTEXT_SESSION_COUNT:"10",CLAUDE_MEM_CONTEXT_SHOW_LAST_SUMMARY:"true",CLAUDE_MEM_CONTEXT_SHOW_LAST_MESSAGE:"false"};static getAllDefaults(){return{...this.DEFAULTS}}static get(t){return this.DEFAULTS[t]}static getInt(t){let r=this.get(t);return parseInt(r,10)}static getBool(t){return this.get(t)==="true"}static loadFromFile(t){try{if(!y(t)){let E=this.getAllDefaults();try{let s=X(t);y(s)||H(s,{recursive:!0}),N(t,JSON.stringify(E,null,2),"utf-8"),console.log("[SETTINGS] Created settings file with defaults:",t)}catch(s){console.warn("[SETTINGS] Failed to create settings file, using in-memory defaults:",t,s)}return E}let r=G(t,"utf-8"),e=JSON.parse(r),n=e;if(e.env&&typeof e.env=="object"){n=e.env;try{N(t,JSON.stringify(n,null,2),"utf-8"),console.log("[SETTINGS] Migrated settings file from nested to flat schema:",t)}catch(E){console.warn("[SETTINGS] Failed to auto-migrate settings file:",t,E)}}let o={...this.DEFAULTS};for(let E of Object.keys(this.DEFAULTS))n[E]!==void 0&&(o[E]=n[E]);return o}catch(r){return console.warn("[SETTINGS] Failed to load settings, using defaults:",t,r),this.getAllDefaults()}}};function $(i={}){let{port:t,includeSkillFallback:r=!1,customPrefix:e,actualError:n}=i,o=e||"Worker service connection failed.",E=t?` (port ${t})`:"",s=`${o}${E}
`;return s+=`To restart the worker:
`,s+=`1. Exit Claude Code completely
`,s+=`2. Run: npm run worker:restart
`,s+="3. Restart Claude Code",r&&(s+=`
If that doesn't work, try: /troubleshoot`),n&&(s=`Worker Error: ${n}
${s}`),s}var Y=p.join(j(),".claude","plugins","marketplaces","thedotmack"),mt=d(L.HEALTH_CHECK),O=null;function c(){if(O!==null)return O;let i=p.join(l.get("CLAUDE_MEM_DATA_DIR"),"settings.json"),t=l.loadFromFile(i);return O=parseInt(t.CLAUDE_MEM_WORKER_PORT,10),O}async function J(){let i=c();return(await fetch(`http://127.0.0.1:${i}/api/readiness`)).ok}function z(){let i=p.join(Y,"package.json");return JSON.parse(B(i,"utf-8")).version}async function q(){let i=c(),t=await fetch(`http://127.0.0.1:${i}/api/version`);if(!t.ok)throw new Error(`Failed to get worker version: ${t.status}`);return(await t.json()).version}async function Q(){let i=z(),t=await q();i!==t&&f.debug("SYSTEM","Version check",{pluginVersion:i,workerVersion:t,note:"Mismatch will be auto-restarted by worker-service start command"})}async function k(){for(let r=0;r<75;r++){try{if(await J()){await Q();return}}catch(e){f.debug("SYSTEM","Worker health check failed, will retry",{attempt:r+1,maxRetries:75,error:e instanceof Error?e.message:String(e)})}await new Promise(e=>setTimeout(e,200))}throw new Error($({port:c(),customPrefix:"Worker did not become ready within 15 seconds."}))}await k();var v=c(),tt=Z(process.cwd()),C=await fetch(`http://127.0.0.1:${v}/api/context/inject?project=${encodeURIComponent(tt)}&colors=true`,{method:"GET"});if(!C.ok)throw new Error(`Failed to fetch context: ${C.status}`);var et=await C.text();console.error(`
\u{1F4DD} Claude-Mem Context Loaded
\u2139\uFE0F Note: This appears as stderr but is informational only
`+et+`
\u{1F4A1} New! Wrap all or part of any message with <private> ... </private> to prevent storing sensitive information in your observation history.
\u{1F4AC} Community https://discord.gg/J4wttp9vDu
\u{1F4FA} Watch live in browser http://localhost:${v}/
`);process.exit(1);
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -1,2 +1,2 @@
#!/usr/bin/env bun
"use strict";var m=Object.create;var w=Object.defineProperty;var u=Object.getOwnPropertyDescriptor;var I=Object.getOwnPropertyNames;var f=Object.getPrototypeOf,x=Object.prototype.hasOwnProperty;var g=(e,i,n,o)=>{if(i&&typeof i=="object"||typeof i=="function")for(let s of I(i))!x.call(e,s)&&s!==n&&w(e,s,{get:()=>i[s],enumerable:!(o=u(i,s))||o.enumerable});return e};var k=(e,i,n)=>(n=e!=null?m(f(e)):{},g(i||!e||!e.__esModule?w(n,"default",{value:e,enumerable:!0}):n,e));var c=require("child_process"),p=k(require("path"),1),y=process.platform==="win32",P=__dirname,l=p.default.join(P,"worker-service.cjs"),t=null,a=!1;function r(e){let i=new Date().toISOString();console.log(`[${i}] [wrapper] ${e}`)}function h(){r(`Spawning inner worker: ${l}`),t=(0,c.spawn)(process.execPath,[l],{stdio:["inherit","inherit","inherit","ipc"],env:{...process.env,CLAUDE_MEM_MANAGED:"true"},cwd:p.default.dirname(l)}),t.on("message",async e=>{(e.type==="restart"||e.type==="shutdown")&&(r(`${e.type} requested by inner`),a=!0,await d(),r("Exiting wrapper"),process.exit(0))}),t.on("exit",(e,i)=>{r(`Inner exited with code=${e}, signal=${i}`),t=null,a||(r("Inner exited unexpectedly, wrapper exiting (hooks will restart if needed)"),process.exit(e??1))}),t.on("error",e=>{r(`Inner error: ${e.message}`)})}async function d(){if(!t||!t.pid){r("No inner process to kill");return}let e=t.pid;if(r(`Killing inner process tree (pid=${e})`),y)try{(0,c.execSync)(`taskkill /PID ${e} /T /F`,{timeout:1e4,stdio:"ignore"}),r(`taskkill completed for pid=${e}`)}catch(i){r(`taskkill failed (process may be dead): ${i}`)}else{t.kill("SIGTERM");let i=new Promise(o=>{if(!t){o();return}t.on("exit",()=>o())}),n=new Promise(o=>setTimeout(()=>o(),5e3));await Promise.race([i,n]),t&&!t.killed&&(r("Inner did not exit gracefully, force killing"),t.kill("SIGKILL"))}await S(e,5e3),t=null,r("Inner process terminated")}async function S(e,i){let n=Date.now();for(;Date.now()-n<i;)try{process.kill(e,0),await new Promise(o=>setTimeout(o,100))}catch{return}r(`Timeout waiting for process ${e} to exit`)}process.on("SIGTERM",async()=>{r("Wrapper received SIGTERM"),a=!0,await d(),process.exit(0)});process.on("SIGINT",async()=>{r("Wrapper received SIGINT"),a=!0,await d(),process.exit(0)});r("Wrapper starting");h();
"use strict";var m=Object.create;var w=Object.defineProperty;var u=Object.getOwnPropertyDescriptor;var I=Object.getOwnPropertyNames;var f=Object.getPrototypeOf,x=Object.prototype.hasOwnProperty;var g=(e,i,n,o)=>{if(i&&typeof i=="object"||typeof i=="function")for(let s of I(i))!x.call(e,s)&&s!==n&&w(e,s,{get:()=>i[s],enumerable:!(o=u(i,s))||o.enumerable});return e};var k=(e,i,n)=>(n=e!=null?m(f(e)):{},g(i||!e||!e.__esModule?w(n,"default",{value:e,enumerable:!0}):n,e));var c=require("child_process"),p=k(require("path"),1),y=process.platform==="win32",P=__dirname,l=p.default.join(P,"worker-service.cjs"),t=null,a=!1;function r(e){let i=new Date().toISOString();console.log(`[${i}] [wrapper] ${e}`)}function h(){r(`Spawning inner worker: ${l}`),t=(0,c.spawn)(process.execPath,[l],{stdio:["inherit","inherit","inherit","ipc"],env:{...process.env,CLAUDE_MEM_MANAGED:"true"},cwd:p.default.dirname(l)}),t.on("message",async e=>{(e.type==="restart"||e.type==="shutdown")&&(r(`${e.type} requested by inner`),a=!0,await d(),r("Exiting wrapper"),process.exit(0))}),t.on("exit",(e,i)=>{r(`Inner exited with code=${e}, signal=${i}`),t=null,a||(r("Inner exited unexpectedly, wrapper exiting (hooks will restart if needed)"),process.exit(e??0))}),t.on("error",e=>{r(`Inner error: ${e.message}`)})}async function d(){if(!t||!t.pid){r("No inner process to kill");return}let e=t.pid;if(r(`Killing inner process tree (pid=${e})`),y)try{(0,c.execSync)(`taskkill /PID ${e} /T /F`,{timeout:1e4,stdio:"ignore"}),r(`taskkill completed for pid=${e}`)}catch(i){r(`taskkill failed (process may be dead): ${i}`)}else{t.kill("SIGTERM");let i=new Promise(o=>{if(!t){o();return}t.on("exit",()=>o())}),n=new Promise(o=>setTimeout(()=>o(),5e3));await Promise.race([i,n]),t&&!t.killed&&(r("Inner did not exit gracefully, force killing"),t.kill("SIGKILL"))}await S(e,5e3),t=null,r("Inner process terminated")}async function S(e,i){let n=Date.now();for(;Date.now()-n<i;)try{process.kill(e,0),await new Promise(o=>setTimeout(o,100))}catch{return}r(`Timeout waiting for process ${e} to exit`)}process.on("SIGTERM",async()=>{r("Wrapper received SIGTERM"),a=!0,await d(),process.exit(0)});process.on("SIGINT",async()=>{r("Wrapper received SIGINT"),a=!0,await d(),process.exit(0)});r("Wrapper starting");h();
+2 -44
View File
@@ -12,14 +12,6 @@ import { fileURLToPath } from 'url';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const HOOKS = [
{ name: 'context-hook', source: 'src/hooks/context-hook.ts' },
{ name: 'new-hook', source: 'src/hooks/new-hook.ts' },
{ name: 'save-hook', source: 'src/hooks/save-hook.ts' },
{ name: 'summary-hook', source: 'src/hooks/summary-hook.ts' },
{ name: 'user-message-hook', source: 'src/hooks/user-message-hook.ts' }
];
const WORKER_SERVICE = {
name: 'worker-service',
source: 'src/services/worker-service.ts'
@@ -159,45 +151,11 @@ async function buildHooks() {
const contextGenStats = fs.statSync(`${hooksDir}/${CONTEXT_GENERATOR.name}.cjs`);
console.log(`✓ context-generator built (${(contextGenStats.size / 1024).toFixed(2)} KB)`);
// Build each hook
for (const hook of HOOKS) {
console.log(`\n🔧 Building ${hook.name}...`);
const outfile = `${hooksDir}/${hook.name}.js`;
await build({
entryPoints: [hook.source],
bundle: true,
platform: 'node',
target: 'node18',
format: 'esm',
outfile,
minify: true,
external: ['bun:sqlite'],
define: {
'__DEFAULT_PACKAGE_VERSION__': `"${version}"`
},
banner: {
js: '#!/usr/bin/env bun'
}
});
// Make executable
fs.chmodSync(outfile, 0o755);
// Check file size
const stats = fs.statSync(outfile);
const sizeInKB = (stats.size / 1024).toFixed(2);
console.log(`${hook.name} built (${sizeInKB} KB)`);
}
console.log('\n✅ All hooks, worker service, and MCP server built successfully!');
console.log('\n✅ Worker service, MCP server, and context generator built successfully!');
console.log(` Output: ${hooksDir}/`);
console.log(` - Hooks: *-hook.js`);
console.log(` - Worker: worker-service.cjs`);
console.log(` - MCP Server: mcp-server.cjs`);
console.log('\n💡 Note: Dependencies will be auto-installed on first hook execution');
console.log('📝 Cursor hooks are in cursor-hooks/ (no build needed - plain shell scripts)');
console.log(` - Context Generator: context-generator.cjs`);
} catch (error) {
console.error('\n❌ Build failed:', error.message);
+13 -105
View File
@@ -3,126 +3,34 @@
<!-- This section is auto-generated by claude-mem. Edit content outside the tags. -->
### Oct 25, 2025
### Dec 10, 2025
**settings-cli.ts**
| ID | Time | T | Title | Read |
|----|------|---|-------|------|
| #2415 | 4:06 PM | 🟣 | Settings System with Multi-Interface Configuration Management | ~516 |
| #2414 | " | 🟣 | Multi-Interface Settings System Implementation | ~498 |
| #2413 | " | 🟣 | Settings System with Schema, Service, and CLI Implementation | ~428 |
| #23825 | 11:12 PM | | Worker Port Set to 38888 for Migration Phase | ~283 |
| #23824 | " | 🔵 | Worker Port Sourced from getWorkerPort() Utility | ~247 |
| #23816 | 10:52 PM | 🟣 | Worker CLI Command Interface Created | ~325 |
### Dec 11, 2025
**worker-cli.ts**
| ID | Time | T | Title | Read |
|----|------|---|-------|------|
| #24076 | 3:16 PM | 🔵 | 45 files modified with nearly equal additions and deletions during Bun migration | ~443 |
| #24061 | 2:58 PM | 🔴 | Added explicit process.exit() to worker CLI commands | ~274 |
| #23961 | 1:59 PM | 🔵 | CLI Module Contains Worker Command Interface | ~205 |
| #23949 | 1:45 PM | 🔴 | CLI Worker Management Unified to Settings-Based Port | ~373 |
| #23947 | 1:40 PM | 🔵 | Comprehensive Port Configuration Audit Complete | ~532 |
| #23939 | 1:38 PM | 🔵 | worker-cli.ts Hardcoded MIGRATION_PORT Usage | ~372 |
| #23934 | 1:36 PM | 🔵 | Port 38888 Hardcoded in Two Migration Files | ~341 |
| #23933 | " | 🔵 | Comprehensive Port 37777 References Across Documentation and Code | ~427 |
| #23931 | " | 🔵 | Comprehensive Port Usage Mapping Across Codebase | ~449 |
**cli**
| ID | Time | T | Title | Read |
|----|------|---|-------|------|
| #23986 | 2:04 PM | 🔵 | Code Quality Audit Completed - 25 Issues Identified Across Six Principles | ~602 |
| #24060 | 2:58 PM | 🔴 | Worker CLI Start Command Exit Behavior Fixed | ~232 |
### Dec 12, 2025
**worker-cli.ts**
| ID | Time | T | Title | Read |
|----|------|---|-------|------|
| #24394 | 7:24 PM | 🟣 | Phase 6 Agent Completed Git Commit and PR Update | ~444 |
| #24391 | " | ✅ | Committed Complete Migration from better-sqlite3 to bun:sqlite | ~436 |
| #24390 | 7:23 PM | | Staged All 19 Modified Files for Git Commit | ~301 |
| #24389 | " | 🟣 | Phase 5 Agent Completed Full System Verification | ~726 |
| #24388 | 7:22 PM | 🟣 | Phase 5 Complete - All Verification Passed for Production Deployment | ~600 |
| #24387 | " | 🔵 | Uncommitted Changes Identified Across Documentation and Core Services | ~368 |
| #24369 | 7:10 PM | 🔵 | Worker CLI Process Management Architecture | ~242 |
| #24328 | 6:51 PM | 🔵 | Worker CLI Switch Statement Structure Confirmed | ~634 |
| #24327 | " | ⚖️ | Exploration Phase Plan for PR #248 Fixes | ~695 |
| #24324 | 6:50 PM | ⚖️ | Pre-Merge Scope Definition and Implementation Path | ~651 |
| #24322 | 6:49 PM | 🔵 | Missing Break Statement in Worker CLI Switch Case | ~542 |
| #24320 | " | ⚖️ | PR #248 Issue Prioritization Strategy | ~616 |
| #24319 | 6:48 PM | 🔵 | PR #248 Review Status: PM2 to Bun Migration Assessment | ~736 |
| #24276 | 5:20 PM | 🔵 | worker-cli.ts provides command-line interface for worker management | ~529 |
| #24359 | 7:00 PM | 🟣 | Phase 1 Critical Code Fixes Completed via Agent Task | ~441 |
| #24358 | 6:59 PM | ✅ | Completed Phase 1 Code Fixes for better-sqlite3 Migration | ~385 |
| #24348 | 6:57 PM | 🔴 | Added Defensive Break Statement to worker-cli.ts Restart Case | ~269 |
| #24345 | " | 🔵 | worker-cli.ts Missing Break Statement in Switch Case | ~318 |
### Dec 13, 2025
### Dec 14, 2025
**worker-cli.ts**
| ID | Time | T | Title | Read |
|----|------|---|-------|------|
| #25370 | 9:26 PM | 🔵 | Worker CLI Process Management Interface | ~429 |
| #25343 | 9:17 PM | 🔵 | Console.error Used for Migration Progress and Error Logging | ~451 |
| #25321 | 9:12 PM | 🔵 | Console.error Usage Found in 29 Files | ~366 |
| #24757 | 4:46 PM | 🔵 | Worker CLI Provides Direct Interface to ProcessManager | ~342 |
### Dec 16, 2025
**worker-cli.ts**
| ID | Time | T | Title | Read |
|----|------|---|-------|------|
| #27825 | 6:56 PM | 🔵 | CLI Commands Rely on ProcessManager PID File Methods | ~301 |
| #27373 | 3:15 PM | 🔵 | Worker CLI Command Interface | ~331 |
### Dec 17, 2025
**worker-cli.ts**
| ID | Time | T | Title | Read |
|----|------|---|-------|------|
| #28930 | 7:30 PM | 🔵 | Worker CLI Distribution and Build System | ~275 |
| #28929 | " | 🔵 | ProcessManager Usage Across Codebase | ~319 |
### Dec 21, 2025
**worker-cli.ts**
| ID | Time | T | Title | Read |
|----|------|---|-------|------|
| #31603 | 8:21 PM | 🔵 | Complete Console.* Statement Audit Across Codebase | ~813 |
| #31599 | 8:19 PM | 🔵 | 136 console logging statements found in TypeScript source files | ~538 |
### Dec 24, 2025
**worker-cli.ts**
| ID | Time | T | Title | Read |
|----|------|---|-------|------|
| #32344 | 8:42 PM | 🔵 | Timeline Reveals Cleanup Hook Caused Data Loss via Premature SDK Agent Termination | ~627 |
| #32317 | 8:41 PM | 🔄 | Removed unreachable break statement from worker CLI start command | ~204 |
| #32191 | 7:38 PM | 🔴 | Worker CLI now outputs HOOK_STANDARD_RESPONSE format | ~277 |
| #32189 | 7:37 PM | 🔴 | Worker CLI Now Outputs Standard Hook Response Format | ~306 |
| #32068 | 3:23 PM | 🔵 | Worker CLI Supports Start/Stop/Restart/Status Commands | ~275 |
### Dec 25, 2025
**worker-cli.ts**
| ID | Time | T | Title | Read |
|----|------|---|-------|------|
| #32456 | 5:41 PM | ✅ | Completed merge of main branch into feature/titans-phase1-3 | ~354 |
### Dec 26, 2025
**worker-cli.ts**
| ID | Time | T | Title | Read |
|----|------|---|-------|------|
| #32855 | 7:04 PM | 🔄 | Consolidated worker process management into single service | ~322 |
### Dec 28, 2025
**worker-cli.ts**
| ID | Time | T | Title | Read |
|----|------|---|-------|------|
| #33370 | 3:47 PM | 🔵 | ToxMox Wrapper Architecture Deleted December 26, Six Days After Implementation | ~506 |
| #33284 | 3:07 PM | 🔄 | Consolidated Worker Lifecycle Management (-580 Lines) | ~327 |
### Jan 5, 2026
**types.ts**
| ID | Time | T | Title | Read |
|----|------|---|-------|------|
| #38071 | 9:52 PM | 🟣 | Unified CLI Phase 1 - Infrastructure Foundation with Type System and Stdin Reader | ~451 |
| #26766 | 11:30 PM | ⚖️ | Root Cause Identified: Missing Post-Install Worker Restart Trigger in Plugin Update Flow | ~604 |
| #26722 | 11:23 PM | 🔵 | Worker CLI TypeScript Source Shows Simple ProcessManager Delegation | ~394 |
| #26721 | " | 🔵 | Worker CLI Source Code Shows Simple Restart Logic Without Delays | ~425 |
</claude-mem-context>
+7
View File
@@ -0,0 +1,7 @@
<claude-mem-context>
# Recent Activity
<!-- This section is auto-generated by claude-mem. Edit content outside the tags. -->
*No recent activity*
</claude-mem-context>
+24
View File
@@ -0,0 +1,24 @@
import type { PlatformAdapter, NormalizedHookInput, HookResult } from '../types.js';
// Maps Claude Code stdin format (session_id, cwd, tool_name, etc.)
// SessionStart hooks receive no stdin, so we must handle undefined input gracefully
export const claudeCodeAdapter: PlatformAdapter = {
normalizeInput(raw) {
const r = (raw ?? {}) as any;
return {
sessionId: r.session_id,
cwd: r.cwd ?? process.cwd(),
prompt: r.prompt,
toolName: r.tool_name,
toolInput: r.tool_input,
toolResponse: r.tool_response,
transcriptPath: r.transcript_path,
};
},
formatOutput(result) {
if (result.hookSpecificOutput) {
return { hookSpecificOutput: result.hookSpecificOutput };
}
return { continue: result.continue ?? true, suppressOutput: result.suppressOutput ?? true };
}
};
+28
View File
@@ -0,0 +1,28 @@
import type { PlatformAdapter, NormalizedHookInput, HookResult } from '../types.js';
// Maps Cursor stdin format - field names differ from Claude Code
// Cursor uses: conversation_id, workspace_roots[], result_json, command/output
// Handle undefined input gracefully for hooks that don't receive stdin
export const cursorAdapter: PlatformAdapter = {
normalizeInput(raw) {
const r = (raw ?? {}) as any;
// Cursor-specific: shell commands come as command/output instead of tool_name/input/response
const isShellCommand = !!r.command && !r.tool_name;
return {
sessionId: r.conversation_id || r.generation_id, // conversation_id preferred
cwd: r.workspace_roots?.[0] ?? process.cwd(), // First workspace root
prompt: r.prompt,
toolName: isShellCommand ? 'Bash' : r.tool_name,
toolInput: isShellCommand ? { command: r.command } : r.tool_input,
toolResponse: isShellCommand ? { output: r.output } : r.result_json, // result_json not tool_response
transcriptPath: undefined, // Cursor doesn't provide transcript
// Cursor-specific fields for file edits
filePath: r.file_path,
edits: r.edits,
};
},
formatOutput(result) {
// Cursor expects simpler response - just continue flag
return { continue: result.continue ?? true };
}
};
+15
View File
@@ -0,0 +1,15 @@
import type { PlatformAdapter } from '../types.js';
import { claudeCodeAdapter } from './claude-code.js';
import { cursorAdapter } from './cursor.js';
import { rawAdapter } from './raw.js';
export function getPlatformAdapter(platform: string): PlatformAdapter {
switch (platform) {
case 'claude-code': return claudeCodeAdapter;
case 'cursor': return cursorAdapter;
case 'raw': return rawAdapter;
default: throw new Error(`Unknown platform: ${platform}`);
}
}
export { claudeCodeAdapter, cursorAdapter, rawAdapter };
+22
View File
@@ -0,0 +1,22 @@
import type { PlatformAdapter, NormalizedHookInput, HookResult } from '../types.js';
// Raw adapter passes through with minimal transformation - useful for testing
export const rawAdapter: PlatformAdapter = {
normalizeInput(raw) {
const r = raw as any;
return {
sessionId: r.sessionId ?? r.session_id ?? 'unknown',
cwd: r.cwd ?? process.cwd(),
prompt: r.prompt,
toolName: r.toolName ?? r.tool_name,
toolInput: r.toolInput ?? r.tool_input,
toolResponse: r.toolResponse ?? r.tool_response,
transcriptPath: r.transcriptPath ?? r.transcript_path,
filePath: r.filePath ?? r.file_path,
edits: r.edits,
};
},
formatOutput(result) {
return result;
}
};
+7
View File
@@ -0,0 +1,7 @@
<claude-mem-context>
# Recent Activity
<!-- This section is auto-generated by claude-mem. Edit content outside the tags. -->
*No recent activity*
</claude-mem-context>
+43
View File
@@ -0,0 +1,43 @@
/**
* Context Handler - SessionStart
*
* Extracted from context-hook.ts - calls worker to generate context.
* Returns context as hookSpecificOutput for Claude Code to inject.
*/
import type { EventHandler, NormalizedHookInput, HookResult } from '../types.js';
import { ensureWorkerRunning, getWorkerPort } from '../../shared/worker-utils.js';
import { getProjectContext } from '../../utils/project-name.js';
export const contextHandler: EventHandler = {
async execute(input: NormalizedHookInput): Promise<HookResult> {
// Ensure worker is running before any other logic
await ensureWorkerRunning();
const cwd = input.cwd ?? process.cwd();
const context = getProjectContext(cwd);
const port = getWorkerPort();
// Pass all projects (parent + worktree if applicable) for unified timeline
const projectsParam = context.allProjects.join(',');
const url = `http://127.0.0.1:${port}/api/context/inject?projects=${encodeURIComponent(projectsParam)}`;
// Note: Removed AbortSignal.timeout due to Windows Bun cleanup issue (libuv assertion)
// Worker service has its own timeouts, so client-side timeout is redundant
const response = await fetch(url);
if (!response.ok) {
throw new Error(`Context generation failed: ${response.status}`);
}
const result = await response.text();
const additionalContext = result.trim();
return {
hookSpecificOutput: {
hookEventName: 'SessionStart',
additionalContext
}
};
}
};
+58
View File
@@ -0,0 +1,58 @@
/**
* File Edit Handler - Cursor-specific afterFileEdit
*
* Handles file edit observations from Cursor IDE.
* Similar to observation handler but with file-specific metadata.
*/
import type { EventHandler, NormalizedHookInput, HookResult } from '../types.js';
import { ensureWorkerRunning, getWorkerPort } from '../../shared/worker-utils.js';
import { logger } from '../../utils/logger.js';
export const fileEditHandler: EventHandler = {
async execute(input: NormalizedHookInput): Promise<HookResult> {
// Ensure worker is running before any other logic
await ensureWorkerRunning();
const { sessionId, cwd, filePath, edits } = input;
if (!filePath) {
throw new Error('fileEditHandler requires filePath');
}
const port = getWorkerPort();
logger.dataIn('HOOK', `FileEdit: ${filePath}`, {
workerPort: port,
editCount: edits?.length ?? 0
});
// Validate required fields before sending to worker
if (!cwd) {
throw new Error(`Missing cwd in FileEdit hook input for session ${sessionId}, file ${filePath}`);
}
// Send to worker as an observation with file edit metadata
// The observation handler on the worker will process this appropriately
const response = await fetch(`http://127.0.0.1:${port}/api/sessions/observations`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
contentSessionId: sessionId,
tool_name: 'write_file',
tool_input: { filePath, edits },
tool_response: { success: true },
cwd
})
// Note: Removed signal to avoid Windows Bun cleanup issue (libuv assertion)
});
if (!response.ok) {
throw new Error(`File edit observation storage failed: ${response.status}`);
}
logger.debug('HOOK', 'File edit observation sent successfully', { filePath });
return { continue: true, suppressOutput: true };
}
};
+53
View File
@@ -0,0 +1,53 @@
/**
* Event Handler Factory
*
* Returns the appropriate handler for a given event type.
*/
import type { EventHandler } from '../types.js';
import { contextHandler } from './context.js';
import { sessionInitHandler } from './session-init.js';
import { observationHandler } from './observation.js';
import { summarizeHandler } from './summarize.js';
import { userMessageHandler } from './user-message.js';
import { fileEditHandler } from './file-edit.js';
export type EventType =
| 'context' // SessionStart - inject context
| 'session-init' // UserPromptSubmit - initialize session
| 'observation' // PostToolUse - save observation
| 'summarize' // Stop - generate summary
| 'user-message' // SessionStart (parallel) - display to user
| 'file-edit'; // Cursor afterFileEdit
const handlers: Record<EventType, EventHandler> = {
'context': contextHandler,
'session-init': sessionInitHandler,
'observation': observationHandler,
'summarize': summarizeHandler,
'user-message': userMessageHandler,
'file-edit': fileEditHandler
};
/**
* Get the event handler for a given event type.
*
* @param eventType The type of event to handle
* @returns The appropriate EventHandler
* @throws Error if event type is not recognized
*/
export function getEventHandler(eventType: EventType): EventHandler {
const handler = handlers[eventType];
if (!handler) {
throw new Error(`Unknown event type: ${eventType}`);
}
return handler;
}
// Re-export individual handlers for direct access if needed
export { contextHandler } from './context.js';
export { sessionInitHandler } from './session-init.js';
export { observationHandler } from './observation.js';
export { summarizeHandler } from './summarize.js';
export { userMessageHandler } from './user-message.js';
export { fileEditHandler } from './file-edit.js';
+57
View File
@@ -0,0 +1,57 @@
/**
* Observation Handler - PostToolUse
*
* Extracted from save-hook.ts - sends tool usage to worker for storage.
*/
import type { EventHandler, NormalizedHookInput, HookResult } from '../types.js';
import { ensureWorkerRunning, getWorkerPort } from '../../shared/worker-utils.js';
import { logger } from '../../utils/logger.js';
export const observationHandler: EventHandler = {
async execute(input: NormalizedHookInput): Promise<HookResult> {
// Ensure worker is running before any other logic
await ensureWorkerRunning();
const { sessionId, cwd, toolName, toolInput, toolResponse } = input;
if (!toolName) {
throw new Error('observationHandler requires toolName');
}
const port = getWorkerPort();
const toolStr = logger.formatTool(toolName, toolInput);
logger.dataIn('HOOK', `PostToolUse: ${toolStr}`, {
workerPort: port
});
// Validate required fields before sending to worker
if (!cwd) {
throw new Error(`Missing cwd in PostToolUse hook input for session ${sessionId}, tool ${toolName}`);
}
// Send to worker - worker handles privacy check and database operations
const response = await fetch(`http://127.0.0.1:${port}/api/sessions/observations`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
contentSessionId: sessionId,
tool_name: toolName,
tool_input: toolInput,
tool_response: toolResponse,
cwd
})
// Note: Removed signal to avoid Windows Bun cleanup issue (libuv assertion)
});
if (!response.ok) {
throw new Error(`Observation storage failed: ${response.status}`);
}
logger.debug('HOOK', 'Observation sent successfully', { toolName });
return { continue: true, suppressOutput: true };
}
};
+96
View File
@@ -0,0 +1,96 @@
/**
* Session Init Handler - UserPromptSubmit
*
* Extracted from new-hook.ts - initializes session and starts SDK agent.
*/
import type { EventHandler, NormalizedHookInput, HookResult } from '../types.js';
import { ensureWorkerRunning, getWorkerPort } from '../../shared/worker-utils.js';
import { getProjectName } from '../../utils/project-name.js';
import { logger } from '../../utils/logger.js';
export const sessionInitHandler: EventHandler = {
async execute(input: NormalizedHookInput): Promise<HookResult> {
// Ensure worker is running before any other logic
await ensureWorkerRunning();
const { sessionId, cwd, prompt } = input;
if (!prompt) {
throw new Error('sessionInitHandler requires prompt');
}
const project = getProjectName(cwd);
const port = getWorkerPort();
logger.debug('HOOK', 'session-init: Calling /api/sessions/init', { contentSessionId: sessionId, project });
// Initialize session via HTTP - handles DB operations and privacy checks
const initResponse = await fetch(`http://127.0.0.1:${port}/api/sessions/init`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
contentSessionId: sessionId,
project,
prompt
})
// Note: Removed signal to avoid Windows Bun cleanup issue (libuv assertion)
});
if (!initResponse.ok) {
throw new Error(`Session initialization failed: ${initResponse.status}`);
}
const initResult = await initResponse.json() as {
sessionDbId: number;
promptNumber: number;
skipped?: boolean;
reason?: string;
};
const sessionDbId = initResult.sessionDbId;
const promptNumber = initResult.promptNumber;
logger.debug('HOOK', 'session-init: Received from /api/sessions/init', { sessionDbId, promptNumber, skipped: initResult.skipped });
// Debug-level alignment log for detailed tracing
logger.debug('HOOK', `[ALIGNMENT] Hook Entry | contentSessionId=${sessionId} | prompt#=${promptNumber} | sessionDbId=${sessionDbId}`);
// Check if prompt was entirely private (worker performs privacy check)
if (initResult.skipped && initResult.reason === 'private') {
logger.info('HOOK', `INIT_COMPLETE | sessionDbId=${sessionDbId} | promptNumber=${promptNumber} | skipped=true | reason=private`, {
sessionId: sessionDbId
});
return { continue: true, suppressOutput: true };
}
// Only initialize SDK agent for Claude Code (not Cursor)
// Cursor doesn't use the SDK agent - it only needs session/observation storage
if (input.platform !== 'cursor' && sessionDbId) {
// Strip leading slash from commands for memory agent
// /review 101 -> review 101 (more semantic for observations)
const cleanedPrompt = prompt.startsWith('/') ? prompt.substring(1) : prompt;
logger.debug('HOOK', 'session-init: Calling /sessions/{sessionDbId}/init', { sessionDbId, promptNumber });
// Initialize SDK agent session via HTTP (starts the agent!)
const response = await fetch(`http://127.0.0.1:${port}/sessions/${sessionDbId}/init`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ userPrompt: cleanedPrompt, promptNumber })
// Note: Removed signal to avoid Windows Bun cleanup issue (libuv assertion)
});
if (!response.ok) {
throw new Error(`SDK agent start failed: ${response.status}`);
}
} else if (input.platform === 'cursor') {
logger.debug('HOOK', 'session-init: Skipping SDK agent init for Cursor platform', { sessionDbId, promptNumber });
}
logger.info('HOOK', `INIT_COMPLETE | sessionDbId=${sessionDbId} | promptNumber=${promptNumber} | project=${project}`, {
sessionId: sessionDbId
});
return { continue: true, suppressOutput: true };
}
};
+58
View File
@@ -0,0 +1,58 @@
/**
* Summarize Handler - Stop
*
* Extracted from summary-hook.ts - sends summary request to worker.
* Transcript parsing stays in the hook because only the hook has access to
* the transcript file path.
*/
import type { EventHandler, NormalizedHookInput, HookResult } from '../types.js';
import { ensureWorkerRunning, getWorkerPort } from '../../shared/worker-utils.js';
import { logger } from '../../utils/logger.js';
import { extractLastMessage } from '../../shared/transcript-parser.js';
export const summarizeHandler: EventHandler = {
async execute(input: NormalizedHookInput): Promise<HookResult> {
// Ensure worker is running before any other logic
await ensureWorkerRunning();
const { sessionId, transcriptPath } = input;
const port = getWorkerPort();
// Validate required fields before processing
if (!transcriptPath) {
throw new Error(`Missing transcriptPath in Stop hook input for session ${sessionId}`);
}
// Extract last assistant message from transcript (the work Claude did)
// Note: "user" messages in transcripts are mostly tool_results, not actual user input.
// The user's original request is already stored in user_prompts table.
const lastAssistantMessage = extractLastMessage(transcriptPath, 'assistant', true);
logger.dataIn('HOOK', 'Stop: Requesting summary', {
workerPort: port,
hasLastAssistantMessage: !!lastAssistantMessage
});
// Send to worker - worker handles privacy check and database operations
const response = await fetch(`http://127.0.0.1:${port}/api/sessions/summarize`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
contentSessionId: sessionId,
last_assistant_message: lastAssistantMessage
})
// Note: Removed signal to avoid Windows Bun cleanup issue (libuv assertion)
});
if (!response.ok) {
// Return standard response even on failure (matches original behavior)
return { continue: true, suppressOutput: true };
}
logger.debug('HOOK', 'Summary request sent successfully');
return { continue: true, suppressOutput: true };
}
};
+46
View File
@@ -0,0 +1,46 @@
/**
* User Message Handler - SessionStart (parallel)
*
* Extracted from user-message-hook.ts - displays context info to user via stderr.
* Uses exit code 3 to show user message without injecting into Claude's context.
*/
import { basename } from 'path';
import type { EventHandler, NormalizedHookInput, HookResult } from '../types.js';
import { ensureWorkerRunning, getWorkerPort } from '../../shared/worker-utils.js';
import { HOOK_EXIT_CODES } from '../../shared/hook-constants.js';
export const userMessageHandler: EventHandler = {
async execute(input: NormalizedHookInput): Promise<HookResult> {
// Ensure worker is running
await ensureWorkerRunning();
const port = getWorkerPort();
const project = basename(input.cwd ?? process.cwd());
// Fetch formatted context directly from worker API
// Note: Removed AbortSignal.timeout to avoid Windows Bun cleanup issue (libuv assertion)
const response = await fetch(
`http://127.0.0.1:${port}/api/context/inject?project=${encodeURIComponent(project)}&colors=true`,
{ method: 'GET' }
);
if (!response.ok) {
throw new Error(`Failed to fetch context: ${response.status}`);
}
const output = await response.text();
// Write to stderr for user visibility (Claude Code UI shows stderr)
console.error(
"\n\n" + String.fromCodePoint(0x1F4DD) + " Claude-Mem Context Loaded\n" +
" " + String.fromCodePoint(0x2139, 0xFE0F) + " Note: This appears as stderr but is informational only\n\n" +
output +
"\n\n" + String.fromCodePoint(0x1F4A1) + " New! Wrap all or part of any message with <private> ... </private> to prevent storing sensitive information in your observation history.\n" +
"\n" + String.fromCodePoint(0x1F4AC) + " Community https://discord.gg/J4wttp9vDu" +
`\n` + String.fromCodePoint(0x1F4FA) + ` Watch live in browser http://localhost:${port}/\n`
);
return { exitCode: HOOK_EXIT_CODES.USER_MESSAGE_ONLY };
}
};
+25
View File
@@ -0,0 +1,25 @@
import { readJsonFromStdin } from './stdin-reader.js';
import { getPlatformAdapter } from './adapters/index.js';
import { getEventHandler } from './handlers/index.js';
import { HOOK_EXIT_CODES } from '../shared/hook-constants.js';
export async function hookCommand(platform: string, event: string): Promise<void> {
try {
const adapter = getPlatformAdapter(platform);
const handler = getEventHandler(event);
const rawInput = await readJsonFromStdin();
const input = adapter.normalizeInput(rawInput);
input.platform = platform; // Inject platform for handler-level decisions
const result = await handler.execute(input);
const output = adapter.formatOutput(result);
console.log(JSON.stringify(output));
process.exit(result.exitCode ?? HOOK_EXIT_CODES.SUCCESS);
} catch (error) {
console.error(`Hook error: ${error}`);
// Use exit code 2 (blocking error) so users see the error message
// Exit code 1 only shows in verbose mode per Claude Code docs
process.exit(HOOK_EXIT_CODES.BLOCKING_ERROR); // = 2
}
}
+16
View File
@@ -0,0 +1,16 @@
// Stdin reading utility extracted from hook patterns
// See src/hooks/save-hook.ts for the original pattern
export async function readJsonFromStdin(): Promise<unknown> {
return new Promise((resolve, reject) => {
let input = '';
process.stdin.on('data', (chunk) => input += chunk);
process.stdin.on('end', () => {
try {
resolve(input.trim() ? JSON.parse(input) : undefined);
} catch (e) {
reject(new Error(`Failed to parse hook input: ${e}`));
}
});
});
}
+29
View File
@@ -0,0 +1,29 @@
export interface NormalizedHookInput {
sessionId: string;
cwd: string;
platform?: string; // 'claude-code' or 'cursor'
prompt?: string;
toolName?: string;
toolInput?: unknown;
toolResponse?: unknown;
transcriptPath?: string;
// Cursor-specific fields
filePath?: string; // afterFileEdit
edits?: unknown[]; // afterFileEdit
}
export interface HookResult {
continue?: boolean;
suppressOutput?: boolean;
hookSpecificOutput?: { hookEventName: string; additionalContext: string };
exitCode?: number;
}
export interface PlatformAdapter {
normalizeInput(raw: unknown): NormalizedHookInput;
formatOutput(result: HookResult): unknown;
}
export interface EventHandler {
execute(input: NormalizedHookInput): Promise<HookResult>;
}
-76
View File
@@ -1,76 +0,0 @@
/**
* Context Hook - SessionStart
*
* Pure HTTP client - calls worker to generate context.
* This allows the hook to run under any runtime (Node.js or Bun) since it has no
* native module dependencies.
*/
import { stdin } from "process";
import { ensureWorkerRunning, getWorkerPort } from "../shared/worker-utils.js";
import { HOOK_TIMEOUTS } from "../shared/hook-constants.js";
import { getProjectContext } from "../utils/project-name.js";
import { logger } from "../utils/logger.js";
export interface SessionStartInput {
session_id: string;
transcript_path: string;
cwd: string;
hook_event_name?: string;
}
async function contextHook(input?: SessionStartInput): Promise<string> {
// Ensure worker is running before any other logic
await ensureWorkerRunning();
const cwd = input?.cwd ?? process.cwd();
const context = getProjectContext(cwd);
const port = getWorkerPort();
// Pass all projects (parent + worktree if applicable) for unified timeline
const projectsParam = context.allProjects.join(',');
const url = `http://127.0.0.1:${port}/api/context/inject?projects=${encodeURIComponent(projectsParam)}`;
// Note: Removed AbortSignal.timeout due to Windows Bun cleanup issue (libuv assertion)
// Worker service has its own timeouts, so client-side timeout is redundant
const response = await fetch(url);
if (!response.ok) {
throw new Error(`Context generation failed: ${response.status}`);
}
const result = await response.text();
return result.trim();
}
// Entry Point - handle stdin/stdout
const forceColors = process.argv.includes("--colors");
if (stdin.isTTY || forceColors) {
contextHook(undefined).then((text) => {
console.log(text);
process.exit(0);
});
} else {
let input = "";
stdin.on("data", (chunk) => (input += chunk));
stdin.on("end", async () => {
let parsed: SessionStartInput | undefined;
try {
parsed = input.trim() ? JSON.parse(input) : undefined;
} catch (error) {
throw new Error(`Failed to parse hook input: ${error instanceof Error ? error.message : String(error)}`);
}
const text = await contextHook(parsed);
console.log(
JSON.stringify({
hookSpecificOutput: {
hookEventName: "SessionStart",
additionalContext: text,
},
})
);
process.exit(0);
});
}
-107
View File
@@ -1,107 +0,0 @@
import { stdin } from 'process';
import { STANDARD_HOOK_RESPONSE } from './hook-response.js';
import { ensureWorkerRunning, getWorkerPort } from '../shared/worker-utils.js';
import { getProjectName } from '../utils/project-name.js';
import { logger } from '../utils/logger.js';
export interface UserPromptSubmitInput {
session_id: string;
cwd: string;
prompt: string;
}
/**
* New Hook Main Logic
*/
async function newHook(input?: UserPromptSubmitInput): Promise<void> {
// Ensure worker is running before any other logic
await ensureWorkerRunning();
if (!input) {
throw new Error('newHook requires input');
}
const { session_id, cwd, prompt } = input;
const project = getProjectName(cwd);
const port = getWorkerPort();
logger.debug('HOOK', 'new-hook: Calling /api/sessions/init', { contentSessionId: session_id, project });
// Initialize session via HTTP - handles DB operations and privacy checks
const initResponse = await fetch(`http://127.0.0.1:${port}/api/sessions/init`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
contentSessionId: session_id,
project,
prompt
})
// Note: Removed signal to avoid Windows Bun cleanup issue (libuv assertion)
});
if (!initResponse.ok) {
throw new Error(`Session initialization failed: ${initResponse.status}`);
}
const initResult = await initResponse.json();
const sessionDbId = initResult.sessionDbId;
const promptNumber = initResult.promptNumber;
logger.debug('HOOK', 'new-hook: Received from /api/sessions/init', { sessionDbId, promptNumber, skipped: initResult.skipped });
// Debug-level alignment log for detailed tracing
logger.debug('HOOK', `[ALIGNMENT] Hook Entry | contentSessionId=${session_id} | prompt#=${promptNumber} | sessionDbId=${sessionDbId}`);
// Check if prompt was entirely private (worker performs privacy check)
if (initResult.skipped && initResult.reason === 'private') {
logger.info('HOOK', `INIT_COMPLETE | sessionDbId=${sessionDbId} | promptNumber=${promptNumber} | skipped=true | reason=private`, {
sessionId: sessionDbId
});
console.log(STANDARD_HOOK_RESPONSE);
return;
}
// Strip leading slash from commands for memory agent
// /review 101 → review 101 (more semantic for observations)
const cleanedPrompt = prompt.startsWith('/') ? prompt.substring(1) : prompt;
logger.debug('HOOK', 'new-hook: Calling /sessions/{sessionDbId}/init', { sessionDbId, promptNumber });
// Initialize SDK agent session via HTTP (starts the agent!)
const response = await fetch(`http://127.0.0.1:${port}/sessions/${sessionDbId}/init`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ userPrompt: cleanedPrompt, promptNumber })
// Note: Removed signal to avoid Windows Bun cleanup issue (libuv assertion)
});
if (!response.ok) {
throw new Error(`SDK agent start failed: ${response.status}`);
}
logger.info('HOOK', `INIT_COMPLETE | sessionDbId=${sessionDbId} | promptNumber=${promptNumber} | project=${project}`, {
sessionId: sessionDbId
});
console.log(STANDARD_HOOK_RESPONSE);
}
// Entry Point
let input = '';
stdin.on('data', (chunk) => input += chunk);
stdin.on('end', async () => {
try {
let parsed: UserPromptSubmitInput | undefined;
try {
parsed = input ? JSON.parse(input) : undefined;
} catch (error) {
throw new Error(`Failed to parse hook input: ${error instanceof Error ? error.message : String(error)}`);
}
await newHook(parsed);
} catch (error) {
logger.error('HOOK', 'new-hook failed', {}, error as Error);
} finally {
process.exit(0);
}
});
-89
View File
@@ -1,89 +0,0 @@
/**
* Save Hook - PostToolUse
*
* Pure HTTP client - sends data to worker, worker handles all database operations
* including privacy checks. This allows the hook to run under any runtime
* (Node.js or Bun) since it has no native module dependencies.
*/
import { stdin } from 'process';
import { STANDARD_HOOK_RESPONSE } from './hook-response.js';
import { logger } from '../utils/logger.js';
import { ensureWorkerRunning, getWorkerPort } from '../shared/worker-utils.js';
import { HOOK_TIMEOUTS } from '../shared/hook-constants.js';
export interface PostToolUseInput {
session_id: string;
cwd: string;
tool_name: string;
tool_input: any;
tool_response: any;
}
/**
* Save Hook Main Logic - Fire-and-forget HTTP client
*/
async function saveHook(input?: PostToolUseInput): Promise<void> {
// Ensure worker is running before any other logic
await ensureWorkerRunning();
if (!input) {
throw new Error('saveHook requires input');
}
const { session_id, cwd, tool_name, tool_input, tool_response } = input;
const port = getWorkerPort();
const toolStr = logger.formatTool(tool_name, tool_input);
logger.dataIn('HOOK', `PostToolUse: ${toolStr}`, {
workerPort: port
});
// Validate required fields before sending to worker
if (!cwd) {
throw new Error(`Missing cwd in PostToolUse hook input for session ${session_id}, tool ${tool_name}`);
}
// Send to worker - worker handles privacy check and database operations
const response = await fetch(`http://127.0.0.1:${port}/api/sessions/observations`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
contentSessionId: session_id,
tool_name,
tool_input,
tool_response,
cwd
})
// Note: Removed signal to avoid Windows Bun cleanup issue (libuv assertion)
});
if (!response.ok) {
throw new Error(`Observation storage failed: ${response.status}`);
}
logger.debug('HOOK', 'Observation sent successfully', { toolName: tool_name });
console.log(STANDARD_HOOK_RESPONSE);
}
// Entry Point
let input = '';
stdin.on('data', (chunk) => input += chunk);
stdin.on('end', async () => {
try {
let parsed: PostToolUseInput | undefined;
try {
parsed = input ? JSON.parse(input) : undefined;
} catch (error) {
throw new Error(`Failed to parse hook input: ${error instanceof Error ? error.message : String(error)}`);
}
await saveHook(parsed);
} catch (error) {
logger.error('HOOK', 'save-hook failed', {}, error as Error);
} finally {
process.exit(0);
}
});
-93
View File
@@ -1,93 +0,0 @@
/**
* Summary Hook - Stop
*
* Pure HTTP client - sends data to worker, worker handles all database operations
* including privacy checks. This allows the hook to run under any runtime
* (Node.js or Bun) since it has no native module dependencies.
*
* Transcript parsing stays in the hook because only the hook has access to
* the transcript file path.
*/
import { stdin } from 'process';
import { STANDARD_HOOK_RESPONSE } from './hook-response.js';
import { logger } from '../utils/logger.js';
import { ensureWorkerRunning, getWorkerPort } from '../shared/worker-utils.js';
import { HOOK_TIMEOUTS } from '../shared/hook-constants.js';
import { extractLastMessage } from '../shared/transcript-parser.js';
export interface StopInput {
session_id: string;
cwd: string;
transcript_path: string;
}
/**
* Summary Hook Main Logic - Fire-and-forget HTTP client
*/
async function summaryHook(input?: StopInput): Promise<void> {
// Ensure worker is running before any other logic
await ensureWorkerRunning();
if (!input) {
throw new Error('summaryHook requires input');
}
const { session_id } = input;
const port = getWorkerPort();
// Validate required fields before processing
if (!input.transcript_path) {
throw new Error(`Missing transcript_path in Stop hook input for session ${session_id}`);
}
// Extract last assistant message from transcript (the work Claude did)
// Note: "user" messages in transcripts are mostly tool_results, not actual user input.
// The user's original request is already stored in user_prompts table.
const lastAssistantMessage = extractLastMessage(input.transcript_path, 'assistant', true);
logger.dataIn('HOOK', 'Stop: Requesting summary', {
workerPort: port,
hasLastAssistantMessage: !!lastAssistantMessage
});
// Send to worker - worker handles privacy check and database operations
const response = await fetch(`http://127.0.0.1:${port}/api/sessions/summarize`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
contentSessionId: session_id,
last_assistant_message: lastAssistantMessage
})
// Note: Removed signal to avoid Windows Bun cleanup issue (libuv assertion)
});
if (!response.ok) {
console.log(STANDARD_HOOK_RESPONSE);
throw new Error(`Summary generation failed: ${response.status}`);
}
logger.debug('HOOK', 'Summary request sent successfully');
console.log(STANDARD_HOOK_RESPONSE);
}
// Entry Point
let input = '';
stdin.on('data', (chunk) => input += chunk);
stdin.on('end', async () => {
try {
let parsed: StopInput | undefined;
try {
parsed = input ? JSON.parse(input) : undefined;
} catch (error) {
throw new Error(`Failed to parse hook input: ${error instanceof Error ? error.message : String(error)}`);
}
await summaryHook(parsed);
} catch (error) {
logger.error('HOOK', 'summary-hook failed', {}, error as Error);
} finally {
process.exit(0);
}
});
-47
View File
@@ -1,47 +0,0 @@
/**
* User Message Hook - SessionStart
*
* @deprecated This hook is no longer used as of Claude Code 2.1.0 (ultrathink update).
* SessionStart hooks no longer display any user-visible messages in the Claude Code UI.
* Context is still injected via hookSpecificOutput.additionalContext in context-hook.ts,
* but users don't see any startup output.
*
* This file is kept for reference but is not registered in hooks.json.
*
* Historical behavior:
* - Displayed context information to the user via stderr
* - Ran in parallel with context-hook to show users what context was loaded
* - Used stderr + exit code 1 to display to user only without adding to Claude's context
*/
import { basename } from "path";
import { ensureWorkerRunning, getWorkerPort } from "../shared/worker-utils.js";
// Ensure worker is running
await ensureWorkerRunning();
const port = getWorkerPort();
const project = basename(process.cwd());
// Fetch formatted context directly from worker API
// Note: Removed AbortSignal.timeout to avoid Windows Bun cleanup issue (libuv assertion)
const response = await fetch(
`http://127.0.0.1:${port}/api/context/inject?project=${encodeURIComponent(project)}&colors=true`,
{ method: 'GET' }
);
if (!response.ok) {
throw new Error(`Failed to fetch context: ${response.status}`);
}
const output = await response.text();
console.error(
"\n\n📝 Claude-Mem Context Loaded\n" +
" ️ Note: This appears as stderr but is informational only\n\n" +
output +
"\n\n💡 New! Wrap all or part of any message with <private> ... </private> to prevent storing sensitive information in your observation history.\n" +
"\n💬 Community https://discord.gg/J4wttp9vDu" +
`\n📺 Watch live in browser http://localhost:${port}/\n`
);
process.exit(1); // Exit code 1 for SessionStart = show stderr to user only
+94 -1
View File
@@ -3,5 +3,98 @@
<!-- This section is auto-generated by claude-mem. Edit content outside the tags. -->
*No recent activity*
### Nov 6, 2025
| ID | Time | T | Title | Read |
|----|------|---|-------|------|
| #4185 | 10:25 PM | 🔴 | Prefixed unused id parameters with underscore in filter callbacks | ~299 |
### Nov 8, 2025
| ID | Time | T | Title | Read |
|----|------|---|-------|------|
| #5539 | 10:20 PM | 🔵 | Harsh critical audit of context-hook reveals systematic anti-patterns | ~3154 |
| #5497 | 9:29 PM | 🔵 | Harsh critical audit of context-hook reveals systematic anti-patterns | ~2815 |
### Nov 9, 2025
| ID | Time | T | Title | Read |
|----|------|---|-------|------|
| #5757 | 5:16 PM | 🔵 | MCP search server exposes 9 tools consuming ~2,000-3,000 tokens per session | ~421 |
| #5754 | 5:14 PM | 🔵 | MCP search server provides 9 search tools with hybrid semantic/FTS5 | ~402 |
### Nov 10, 2025
| ID | Time | T | Title | Read |
|----|------|---|-------|------|
| #6250 | 12:54 PM | 🔵 | MCP Search Server Connection Failure Reported | ~329 |
### Nov 17, 2025
| ID | Time | T | Title | Read |
|----|------|---|-------|------|
| #10744 | 11:47 PM | ✅ | Search Query Parameter Made Optional for Filter-Only Queries | ~373 |
| #10572 | 7:47 PM | 🟣 | Unified cross-type search with search_everything tool | ~501 |
| #10571 | 7:46 PM | 🔵 | Search server architecture and hybrid search implementation | ~553 |
### Nov 18, 2025
| ID | Time | T | Title | Read |
|----|------|---|-------|------|
| #11462 | 7:55 PM | 🔵 | Ready to Apply Fix to Contextualize Handler | ~261 |
| #11460 | " | 🔴 | Identified Root Cause of Contextualize Endpoint Bug | ~413 |
| #11454 | 7:54 PM | 🔵 | Unified Search Handler Shows Correct Pattern for Filter-Only Queries | ~334 |
| #11447 | " | 🔵 | Contextualize Handler Calls Search Methods with Query='*' | ~279 |
| #11432 | 7:52 PM | 🔵 | Contextualize Handler Formats Results with Sections | ~286 |
| #11431 | 7:51 PM | 🔵 | Confirmed Empty Results Trigger in Contextualize Handler | ~289 |
| #11430 | " | 🔵 | Contextualize Handler Implementation Uses Search Methods | ~424 |
| #11429 | " | 🔵 | Search Server Defines Six Main Search Tools | ~358 |
| #11428 | " | 🔵 | Contextualize Tool Definition Found in Search Server | ~357 |
| #11332 | 3:55 PM | 🔵 | Comprehensive FTS5 Removal Audit Completed for Architecture Migration | ~792 |
| #11206 | 3:01 PM | 🔵 | mem-search skill architecture and migration details retrieved in full format | ~538 |
| #11181 | 4:09 AM | 🔵 | Store methods for ID-based lookups exist but not exposed as MCP tools | ~495 |
| #11013 | 2:12 AM | 🔵 | Search Server Implements Three-Path Query Strategy with ChromaDB Primary and FTS5 Fallback | ~462 |
### Nov 28, 2025
| ID | Time | T | Title | Read |
|----|------|---|-------|------|
| #16711 | 4:34 PM | 🟣 | include_inactive Parameter Extracted in Search Handler | ~369 |
| #16710 | " | 🔵 | Search Tool Schema Definition with Type and Filter Parameters | ~527 |
| #16708 | " | 🔵 | Search Server MCP Tool Architecture and ChromaDB Integration | ~491 |
| #16682 | 4:10 PM | 🔵 | Comprehensive Exploration Task Completed on Observation System | ~601 |
### Dec 14, 2025
| ID | Time | T | Title | Read |
|----|------|---|-------|------|
| #26238 | 8:28 PM | 🔵 | MCP Server Architecture Maps Tools to Worker API Endpoints | ~355 |
| #26138 | 7:55 PM | ✅ | Updated Comment to Reference progressive_description Tool | ~238 |
| #26137 | " | ✅ | Completed Tool Description Minimization - All 9 Tools Updated | ~335 |
| #26136 | " | ✅ | Minimized Get Session Tool Description | ~218 |
| #26135 | " | ✅ | Minimized Get Batch Observations Tool Description | ~258 |
| #26134 | " | ✅ | Minimized Get Observation Tool Description | ~228 |
| #26133 | " | ✅ | Minimized Get Context Timeline Tool Description | ~245 |
| #26132 | 7:54 PM | ✅ | Minimized Get Recent Context Tool Description | ~214 |
| #26131 | " | ✅ | Minimized Timeline Tool Description | ~232 |
| #26130 | " | ✅ | Minimized Search Tool Description | ~235 |
| #26129 | " | ✅ | Renamed progressive_ix Tool to progressive_description with Minimized Description | ~296 |
| #26128 | " | ✅ | Renamed Tool Endpoint Mapping from progressive_ix to progressive_description | ~229 |
| #26127 | " | ✅ | Completed Format Parameter Removal from All Four MCP Tools | ~318 |
| #26126 | 7:53 PM | ✅ | Removed Format Parameter from Get Recent Context Tool Schema | ~244 |
| #26125 | " | ✅ | Removed Format Parameter from Timeline Tool Schema | ~248 |
| #26124 | " | ✅ | Removed Format Parameter from Search Tool Schema | ~283 |
| #26123 | " | 🔵 | Current MCP Server Tool Schema Analysis | ~337 |
| #25815 | 5:31 PM | 🔵 | Comprehensive MCP Server and SKILL.md Structure Analysis | ~575 |
| #25807 | 5:30 PM | 🔵 | MCP Server Architecture with 14 HTTP-Delegating Tools | ~545 |
| #25788 | 5:15 PM | 🔵 | MCP Server Capabilities and Request Handlers | ~256 |
### Dec 17, 2025
| ID | Time | T | Title | Read |
|----|------|---|-------|------|
| #29078 | 10:16 PM | ✅ | Updated get_recent_context tool schema to accept dynamic parameters | ~318 |
| #29077 | 10:15 PM | ✅ | Updated timeline tool schema to accept dynamic parameters | ~292 |
| #29076 | " | ✅ | Updated search tool schema to accept dynamic parameters | ~315 |
| #28923 | 7:28 PM | 🔵 | MCP Server Architecture: Thin HTTP Wrapper Pattern | ~402 |
</claude-mem-context>
+3 -1
View File
@@ -310,5 +310,7 @@ async function main() {
main().catch((error) => {
logger.error('SYSTEM', 'Fatal error', undefined, error);
process.exit(1);
// Exit gracefully: Windows Terminal won't keep tab open on exit 0
// The wrapper/plugin will handle restart logic if needed
process.exit(0);
});
+50 -25
View File
@@ -3,36 +3,61 @@
<!-- This section is auto-generated by claude-mem. Edit content outside the tags. -->
### Jan 5, 2026
### Dec 10, 2025
| ID | Time | T | Title | Read |
|----|------|---|-------|------|
| #37989 | 8:39 PM | 🔵 | Worker Service CLI Commands | ~370 |
| #37988 | " | 🔵 | Worker Service Hook Command Integration | ~356 |
| #37943 | 8:09 PM | 🔵 | Worker service provides hook command interface | ~372 |
| #37936 | 8:08 PM | 🔵 | Worker service module located in budapest architecture | ~173 |
| #37837 | 6:55 PM | 🔵 | Worker Service CLI Structure and Extension Pattern | ~536 |
| #37836 | 6:54 PM | 🔵 | Worker Service HTTP API Architecture and Hook Integration Pattern | ~639 |
| #37830 | 6:50 PM | ⚖️ | Architectural Refactoring Plan: Unified CLI Hook System | ~631 |
| #37812 | 6:43 PM | 🔵 | WorkerService Architecture and CLI Entry Points | ~447 |
| #37738 | 6:17 PM | 🔵 | ActiveSession and PendingMessage Structures Analyzed for Phase 3 | ~327 |
| #37701 | 6:01 PM | 🔵 | Complete cwd data flow traced from hooks through observation processing | ~447 |
| #37691 | 5:55 PM | 🔵 | ActiveSession interface contains project name but no project root path | ~346 |
| #37418 | 1:04 AM | 🔴 | Fixed CLAUDE.md duplicate content by filtering to direct children only | ~469 |
| #37391 | 12:48 AM | ✅ | Staged 23 CLAUDE.md files with mix of new and modified content | ~400 |
| #37390 | 12:47 AM | ✅ | Regenerated 23 CLAUDE.md files in budapest workspace | ~365 |
| #23832 | 11:15 PM | 🔵 | Current worker-service.ts Lacks Admin Endpoints | ~393 |
### Jan 7, 2026
### Dec 14, 2025
| ID | Time | T | Title | Read |
|----|------|---|-------|------|
| #38467 | 10:29 PM | ⚖️ | Log Level Audit Strategy: Tighten ERROR Messages for Runtime Issue Discovery | ~464 |
| #38446 | 10:26 PM | 🔵 | Worker Service Logs Startup Recovery Failures at WARN Level | ~419 |
| #38444 | 10:25 PM | 🔵 | Worker service auto-recovery error handling pattern | ~393 |
| #38431 | 10:24 PM | 🔵 | ERROR-Level Logging Used Across 21 Source Files | ~480 |
| #38404 | 10:06 PM | ⚖️ | Log Level Audit Analysis - WARN to ERROR Promotion Criteria Established | ~769 |
| #38354 | 9:10 PM | 🔵 | Provider Selection Architecture and Default Settings Location | ~478 |
| #38352 | 9:09 PM | 🔵 | Worker Service Environment Inheritance and Process Spawning Mechanisms | ~469 |
| #38269 | 7:53 PM | 🔵 | Worker Types Shared Type Definitions | ~847 |
| #38256 | 7:49 PM | 🔵 | Worker Service Orchestration Architecture | ~1166 |
| #26740 | 11:26 PM | 🔵 | Worker Service Refactored to Orchestrator with Background Initialization | ~421 |
| #26739 | 11:25 PM | 🔵 | Worker Service Architecture Uses Domain Services and Background Initialization | ~438 |
| #26255 | 8:31 PM | 🔵 | Context Generator Timeline Rendering Logic Details File Grouping Implementation | ~397 |
| #26251 | 8:30 PM | 🔵 | Worker Service Orchestrates Domain Services and Route Handlers | ~292 |
| #26246 | 8:29 PM | 🔵 | Context Generator Implements Rich Date-Grouped Timeline Format | ~468 |
### Dec 17, 2025
| ID | Time | T | Title | Read |
|----|------|---|-------|------|
| #28548 | 4:49 PM | 🔵 | Worker service cleanup method uses Unix-specific process management | ~323 |
| #28446 | 4:23 PM | 🔵 | Worker Service Refactored to Orchestrator Pattern | ~529 |
### Dec 18, 2025
| ID | Time | T | Title | Read |
|----|------|---|-------|------|
| #29340 | 3:11 PM | ✅ | Constructor Initialization Comment Updated | ~267 |
| #29339 | " | ✅ | Class Member Comment Updated in WorkerService | ~267 |
| #29338 | " | ✅ | Service Import Comment Updated | ~222 |
| #29337 | 3:10 PM | ✅ | Terminology Update in Worker Service Documentation | ~268 |
| #29239 | 12:11 AM | 🔵 | Worker Service Refactored as Domain-Driven Orchestrator | ~477 |
### Dec 20, 2025
| ID | Time | T | Title | Read |
|----|------|---|-------|------|
| #30808 | 6:05 PM | 🔴 | Fixed worker readiness check to fail on initialization errors | ~315 |
| #30800 | 6:03 PM | 🔵 | Dual Error Logging in Background Initialization | ~367 |
| #30799 | " | 🔵 | Background Initialization Invocation Pattern | ~365 |
| #30797 | " | 🔵 | Background Initialization Sequence and Error Handler Confirmed | ~450 |
| #30795 | 6:02 PM | 🔵 | Readiness Endpoint Returns 503 During Initialization | ~397 |
| #30793 | " | 🔵 | Dual Initialization State Tracking Pattern | ~388 |
| #30791 | " | 🔵 | Worker Service Constructor Defers SearchRoutes Initialization | ~387 |
| #30790 | " | 🔵 | Initialization Promise Resolver Pattern Located | ~321 |
| #30788 | " | 🔵 | Worker Service Initialization Resolves Promise Despite Errors | ~388 |
### Jan 1, 2026
| ID | Time | T | Title | Read |
|----|------|---|-------|------|
| #35654 | 11:29 PM | ✅ | Added APPROVED OVERRIDE annotation for instruction loading HTTP route error handler | ~339 |
| #35651 | 11:28 PM | ✅ | Added APPROVED OVERRIDE annotation for shutdown error handler with process.exit | ~354 |
| #35649 | " | ✅ | Added APPROVED OVERRIDE annotation for readiness check retry loop error handling | ~374 |
| #35647 | " | ✅ | Added APPROVED OVERRIDE annotation for port availability probe error handling | ~327 |
| #35646 | " | ✅ | Added APPROVED OVERRIDE annotation for Cursor context file update error handling | ~342 |
| #35643 | 11:27 PM | ✅ | Added APPROVED OVERRIDE annotation for PID file cleanup error handling | ~320 |
</claude-mem-context>
+6 -1
View File
@@ -3,5 +3,10 @@
<!-- This section is auto-generated by claude-mem. Edit content outside the tags. -->
*No recent activity*
### Jan 4, 2026
| ID | Time | T | Title | Read |
|----|------|---|-------|------|
| #36864 | 1:52 AM | 🔵 | ProcessManager Module Imports Reviewed | ~245 |
| #36860 | 1:50 AM | 🔵 | ProcessManager Source Code Reviewed for WMIC Implementation | ~608 |
</claude-mem-context>
+27 -22
View File
@@ -14,6 +14,7 @@ import { existsSync, writeFileSync, readFileSync, unlinkSync, mkdirSync } from '
import { exec, execSync, spawn } from 'child_process';
import { promisify } from 'util';
import { logger } from '../../utils/logger.js';
import { HOOK_TIMEOUTS } from '../../shared/hook-constants.js';
const execAsync = promisify(exec);
@@ -88,16 +89,16 @@ export async function getChildProcesses(parentPid: number): Promise<number[]> {
}
try {
const cmd = `wmic process where "parentprocessid=${parentPid}" get processid /format:list`;
const { stdout } = await execAsync(cmd, { timeout: 60000 });
// PowerShell Get-Process instead of WMIC (deprecated in Windows 11)
const cmd = `powershell -NoProfile -NonInteractive -Command "Get-Process | Where-Object { \\$_.ParentProcessId -eq ${parentPid} } | Select-Object -ExpandProperty Id"`;
const { stdout } = await execAsync(cmd, { timeout: HOOK_TIMEOUTS.POWERSHELL_COMMAND });
// PowerShell outputs just numbers (one per line), simpler than WMIC's "ProcessId=1234" format
return stdout
.trim()
.split('\n')
.map(line => {
const match = line.match(/ProcessId=(\d+)/i);
return match ? parseInt(match[1], 10) : NaN;
})
.filter(n => !isNaN(n) && Number.isInteger(n) && n > 0);
.map(line => line.trim())
.filter(line => line.length > 0 && /^\d+$/.test(line))
.map(line => parseInt(line, 10))
.filter(pid => pid > 0);
} catch (error) {
// Shutdown cleanup - failure is non-critical, continue without child process cleanup
logger.error('SYSTEM', 'Failed to enumerate child processes', { parentPid }, error as Error);
@@ -120,7 +121,7 @@ export async function forceKillProcess(pid: number): Promise<void> {
try {
if (process.platform === 'win32') {
// /T kills entire process tree, /F forces termination
await execAsync(`taskkill /PID ${pid} /T /F`, { timeout: 60000 });
await execAsync(`taskkill /PID ${pid} /T /F`, { timeout: HOOK_TIMEOUTS.POWERSHELL_COMMAND });
} else {
process.kill(pid, 'SIGKILL');
}
@@ -170,24 +171,26 @@ export async function cleanupOrphanedProcesses(): Promise<void> {
try {
if (isWindows) {
// Windows: Use WMIC to find chroma-mcp processes (avoids PowerShell $_ issues in Git Bash/WSL)
const cmd = `wmic process where "name like '%python%' and commandline like '%chroma-mcp%'" get processid /format:list`;
const { stdout } = await execAsync(cmd, { timeout: 60000 });
// Windows: Use PowerShell Get-CimInstance instead of WMIC (deprecated in Windows 11)
const cmd = `powershell -NoProfile -NonInteractive -Command "Get-CimInstance Win32_Process | Where-Object { \\$_.Name -like '*python*' -and \\$_.CommandLine -like '*chroma-mcp*' } | Select-Object -ExpandProperty ProcessId"`;
const { stdout } = await execAsync(cmd, { timeout: HOOK_TIMEOUTS.POWERSHELL_COMMAND });
if (!stdout.trim()) {
logger.debug('SYSTEM', 'No orphaned chroma-mcp processes found (Windows)');
return;
}
const lines = stdout.trim().split('\n');
// PowerShell outputs just numbers (one per line), simpler than WMIC's "ProcessId=1234" format
const lines = stdout
.split('\n')
.map(line => line.trim())
.filter(line => line.length > 0 && /^\d+$/.test(line));
for (const line of lines) {
const match = line.match(/ProcessId=(\d+)/i);
if (match) {
const pid = parseInt(match[1], 10);
// SECURITY: Validate PID is positive integer before adding to list
if (!isNaN(pid) && Number.isInteger(pid) && pid > 0) {
pids.push(pid);
}
const pid = parseInt(line, 10);
// SECURITY: Validate PID is positive integer before adding to list
if (!isNaN(pid) && Number.isInteger(pid) && pid > 0) {
pids.push(pid);
}
}
} else {
@@ -236,7 +239,7 @@ export async function cleanupOrphanedProcesses(): Promise<void> {
continue;
}
try {
execSync(`taskkill /PID ${pid} /T /F`, { timeout: 60000, stdio: 'ignore' });
execSync(`taskkill /PID ${pid} /T /F`, { timeout: HOOK_TIMEOUTS.POWERSHELL_COMMAND, stdio: 'ignore' });
} catch (error) {
// [ANTI-PATTERN IGNORED]: Cleanup loop - process may have exited, continue to next PID
logger.debug('SYSTEM', 'Failed to kill process, may have already exited', { pid }, error as Error);
@@ -306,7 +309,9 @@ export function createSignalHandler(
} catch (error) {
// Top-level signal handler - log any shutdown error and exit
logger.error('SYSTEM', 'Error during shutdown', {}, error as Error);
process.exit(1);
// Exit gracefully: Windows Terminal won't keep tab open on exit 0
// Even on shutdown errors, exit cleanly to prevent tab accumulation
process.exit(0);
}
};
}
@@ -171,6 +171,28 @@ export function findMcpServerPath(): string | null {
return null;
}
/**
* Find worker-service.cjs path for unified CLI
* Searches in order: marketplace install, source repo
*/
export function findWorkerServicePath(): string | null {
const possiblePaths = [
// Marketplace install location
path.join(homedir(), '.claude', 'plugins', 'marketplaces', 'thedotmack', 'plugin', 'scripts', 'worker-service.cjs'),
// Development/source location (relative to built worker-service.cjs in plugin/scripts/)
path.join(path.dirname(__filename), 'worker-service.cjs'),
// Alternative dev location
path.join(process.cwd(), 'plugin', 'scripts', 'worker-service.cjs'),
];
for (const p of possiblePaths) {
if (existsSync(p)) {
return p;
}
}
return null;
}
/**
* Get the target directory for Cursor hooks based on install target
*/
@@ -261,13 +283,11 @@ export function configureCursorMcp(target: CursorInstallTarget): number {
// ============================================================================
/**
* Install Cursor hooks
* Install Cursor hooks using unified CLI
* No longer copies shell scripts - uses node CLI directly
*/
export async function installCursorHooks(sourceDir: string, target: CursorInstallTarget): Promise<number> {
const platform = detectPlatform();
const scriptExt = getScriptExtension();
console.log(`\nInstalling Claude-Mem Cursor hooks (${target} level, ${platform})...\n`);
export async function installCursorHooks(_sourceDir: string, target: CursorInstallTarget): Promise<number> {
console.log(`\nInstalling Claude-Mem Cursor hooks (${target} level)...\n`);
const targetDir = getTargetDir(target);
if (!targetDir) {
@@ -275,52 +295,30 @@ export async function installCursorHooks(sourceDir: string, target: CursorInstal
return 1;
}
const hooksDir = path.join(targetDir, 'hooks');
// Find the worker-service.cjs path
const workerServicePath = findWorkerServicePath();
if (!workerServicePath) {
console.error('Could not find worker-service.cjs');
console.error(' Expected at: ~/.claude/plugins/marketplaces/thedotmack/plugin/scripts/worker-service.cjs');
return 1;
}
const workspaceRoot = process.cwd();
try {
// Create directories
mkdirSync(hooksDir, { recursive: true });
// Create target directory
mkdirSync(targetDir, { recursive: true });
// Determine which scripts to copy based on platform
const commonScript = platform === 'windows' ? 'common.ps1' : 'common.sh';
const hookScripts = [
`session-init${scriptExt}`,
`context-inject${scriptExt}`,
`save-observation${scriptExt}`,
`save-file-edit${scriptExt}`,
`session-summary${scriptExt}`
];
const scripts = [commonScript, ...hookScripts];
for (const script of scripts) {
const srcPath = path.join(sourceDir, script);
const dstPath = path.join(hooksDir, script);
if (existsSync(srcPath)) {
const content = readFileSync(srcPath, 'utf-8');
// Unix scripts need execute permission; Windows PowerShell doesn't need it
const mode = platform === 'windows' ? undefined : 0o755;
writeFileSync(dstPath, content, mode ? { mode } : undefined);
console.log(` Copied ${script}`);
} else {
console.warn(` ${script} not found in source`);
}
}
// Generate hooks.json with correct paths and platform-appropriate commands
// Generate hooks.json with unified CLI commands
const hooksJsonPath = path.join(targetDir, 'hooks.json');
const hookPrefix = target === 'project' ? './.cursor/hooks/' : `${hooksDir}/`;
// For PowerShell, we need to invoke via powershell.exe
const makeHookCommand = (scriptName: string) => {
const scriptPath = `${hookPrefix}${scriptName}${scriptExt}`;
if (platform === 'windows') {
// PowerShell execution: use -ExecutionPolicy Bypass to ensure scripts run
return `powershell.exe -ExecutionPolicy Bypass -File "${scriptPath}"`;
}
return scriptPath;
// Use the absolute path to worker-service.cjs
// Escape backslashes for JSON on Windows
const escapedWorkerPath = workerServicePath.replace(/\\/g, '\\\\');
// Helper to create hook command using unified CLI
const makeHookCommand = (command: string) => {
return `node "${escapedWorkerPath}" hook cursor ${command}`;
};
const hooksJson: CursorHooksJson = {
@@ -328,25 +326,26 @@ export async function installCursorHooks(sourceDir: string, target: CursorInstal
hooks: {
beforeSubmitPrompt: [
{ command: makeHookCommand('session-init') },
{ command: makeHookCommand('context-inject') }
{ command: makeHookCommand('context') }
],
afterMCPExecution: [
{ command: makeHookCommand('save-observation') }
{ command: makeHookCommand('observation') }
],
afterShellExecution: [
{ command: makeHookCommand('save-observation') }
{ command: makeHookCommand('observation') }
],
afterFileEdit: [
{ command: makeHookCommand('save-file-edit') }
{ command: makeHookCommand('file-edit') }
],
stop: [
{ command: makeHookCommand('session-summary') }
{ command: makeHookCommand('summarize') }
]
}
};
writeFileSync(hooksJsonPath, JSON.stringify(hooksJson, null, 2));
console.log(` Created hooks.json (${platform} mode)`);
console.log(` Created hooks.json (unified CLI mode)`);
console.log(` Worker service: ${workerServicePath}`);
// For project-level: create initial context file
if (target === 'project') {
@@ -357,7 +356,7 @@ export async function installCursorHooks(sourceDir: string, target: CursorInstal
Installation complete!
Hooks installed to: ${targetDir}/hooks.json
Scripts installed to: ${hooksDir}
Using unified CLI: node worker-service.cjs hook cursor <command>
Next steps:
1. Start claude-mem worker: claude-mem start
@@ -453,7 +452,7 @@ export function uninstallCursorHooks(target: CursorInstallTarget): number {
const hooksDir = path.join(targetDir, 'hooks');
const hooksJsonPath = path.join(targetDir, 'hooks.json');
// Remove hook scripts for both platforms (in case user switches platforms)
// Remove legacy shell scripts if they exist (from old installations)
const bashScripts = ['common.sh', 'session-init.sh', 'context-inject.sh',
'save-observation.sh', 'save-file-edit.sh', 'session-summary.sh'];
const psScripts = ['common.ps1', 'session-init.ps1', 'context-inject.ps1',
@@ -465,7 +464,7 @@ export function uninstallCursorHooks(target: CursorInstallTarget): number {
const scriptPath = path.join(hooksDir, script);
if (existsSync(scriptPath)) {
unlinkSync(scriptPath);
console.log(` Removed ${script}`);
console.log(` Removed legacy script: ${script}`);
}
}
@@ -527,32 +526,36 @@ export function checkCursorHooksStatus(): number {
console.log(`${loc.name}: Installed`);
console.log(` Config: ${hooksJson}`);
// Detect which platform's scripts are installed
const bashScripts = ['session-init.sh', 'context-inject.sh', 'save-observation.sh'];
const psScripts = ['session-init.ps1', 'context-inject.ps1', 'save-observation.ps1'];
// Check if using unified CLI mode or legacy shell scripts
try {
const hooksContent = JSON.parse(readFileSync(hooksJson, 'utf-8'));
const firstCommand = hooksContent?.hooks?.beforeSubmitPrompt?.[0]?.command || '';
const hasBash = bashScripts.some(s => existsSync(path.join(hooksDir, s)));
const hasPs = psScripts.some(s => existsSync(path.join(hooksDir, s)));
if (firstCommand.includes('worker-service.cjs') && firstCommand.includes('hook cursor')) {
console.log(` Mode: Unified CLI (node worker-service.cjs)`);
} else {
// Detect legacy shell scripts
const bashScripts = ['session-init.sh', 'context-inject.sh', 'save-observation.sh'];
const psScripts = ['session-init.ps1', 'context-inject.ps1', 'save-observation.ps1'];
if (hasBash && hasPs) {
console.log(` Platform: Both (bash + PowerShell)`);
} else if (hasBash) {
console.log(` Platform: Unix (bash)`);
} else if (hasPs) {
console.log(` Platform: Windows (PowerShell)`);
} else {
console.log(` No hook scripts found`);
}
const hasBash = bashScripts.some(s => existsSync(path.join(hooksDir, s)));
const hasPs = psScripts.some(s => existsSync(path.join(hooksDir, s)));
// Check for appropriate scripts based on current platform
const platform = detectPlatform();
const scripts = platform === 'windows' ? psScripts : bashScripts;
const missing = scripts.filter(s => !existsSync(path.join(hooksDir, s)));
if (missing.length > 0) {
console.log(` Missing ${platform} scripts: ${missing.join(', ')}`);
} else {
console.log(` Scripts: All present for ${platform}`);
if (hasBash || hasPs) {
console.log(` Mode: Legacy shell scripts (consider reinstalling for unified CLI)`);
if (hasBash && hasPs) {
console.log(` Platform: Both (bash + PowerShell)`);
} else if (hasBash) {
console.log(` Platform: Unix (bash)`);
} else if (hasPs) {
console.log(` Platform: Windows (PowerShell)`);
}
} else {
console.log(` Mode: Unknown configuration`);
}
}
} catch {
console.log(` Mode: Unable to parse hooks.json`);
}
// Check for context file (project only)
+38 -8
View File
@@ -636,7 +636,9 @@ async function main() {
const freed = await waitForPortFree(port, getPlatformTimeout(15000));
if (!freed) {
logger.error('SYSTEM', 'Port did not free up after shutdown for version mismatch restart', { port });
process.exit(1);
// Exit gracefully: Windows Terminal won't keep tab open on exit 0
// The wrapper/plugin will handle restart logic if needed
process.exit(0);
}
removePidFile();
} else {
@@ -654,14 +656,18 @@ async function main() {
process.exit(0);
}
logger.error('SYSTEM', 'Port in use but worker not responding to health checks');
process.exit(1);
// Exit gracefully: Windows Terminal won't keep tab open on exit 0
// The wrapper/plugin will handle restart logic if needed
process.exit(0);
}
logger.info('SYSTEM', 'Starting worker daemon');
const pid = spawnDaemon(__filename, port);
if (pid === undefined) {
logger.error('SYSTEM', 'Failed to spawn worker daemon');
process.exit(1);
// Exit gracefully: Windows Terminal won't keep tab open on exit 0
// The wrapper/plugin will handle restart logic if needed
process.exit(0);
}
writePidFile({ pid, port, startedAt: new Date().toISOString() });
@@ -670,7 +676,9 @@ async function main() {
if (!healthy) {
removePidFile();
logger.error('SYSTEM', 'Worker failed to start (health check timeout)');
process.exit(1);
// Exit gracefully: Windows Terminal won't keep tab open on exit 0
// The wrapper/plugin will handle restart logic if needed
process.exit(0);
}
logger.info('SYSTEM', 'Worker started successfully');
@@ -694,14 +702,18 @@ async function main() {
const freed = await waitForPortFree(port, getPlatformTimeout(15000));
if (!freed) {
logger.error('SYSTEM', 'Port did not free up after shutdown, aborting restart', { port });
process.exit(1);
// Exit gracefully: Windows Terminal won't keep tab open on exit 0
// The wrapper/plugin will handle restart logic if needed
process.exit(0);
}
removePidFile();
const pid = spawnDaemon(__filename, port);
if (pid === undefined) {
logger.error('SYSTEM', 'Failed to spawn worker daemon during restart');
process.exit(1);
// Exit gracefully: Windows Terminal won't keep tab open on exit 0
// The wrapper/plugin will handle restart logic if needed
process.exit(0);
}
writePidFile({ pid, port, startedAt: new Date().toISOString() });
@@ -710,7 +722,9 @@ async function main() {
if (!healthy) {
removePidFile();
logger.error('SYSTEM', 'Worker failed to restart');
process.exit(1);
// Exit gracefully: Windows Terminal won't keep tab open on exit 0
// The wrapper/plugin will handle restart logic if needed
process.exit(0);
}
logger.info('SYSTEM', 'Worker restarted successfully');
@@ -737,13 +751,29 @@ async function main() {
process.exit(cursorResult);
}
case 'hook': {
const platform = process.argv[3];
const event = process.argv[4];
if (!platform || !event) {
console.error('Usage: claude-mem hook <platform> <event>');
console.error('Platforms: claude-code, cursor, raw');
console.error('Events: context, session-init, observation, summarize, user-message');
process.exit(1);
}
const { hookCommand } = await import('../cli/hook-command.js');
await hookCommand(platform, event);
break;
}
case '--daemon':
default: {
const worker = new WorkerService();
worker.start().catch((error) => {
logger.failure('SYSTEM', 'Worker failed to start', {}, error as Error);
removePidFile();
process.exit(1);
// Exit gracefully: Windows Terminal won't keep tab open on exit 0
// The wrapper/plugin will handle restart logic if needed
process.exit(0);
});
}
}
+112 -36
View File
@@ -3,47 +3,123 @@
<!-- This section is auto-generated by claude-mem. Edit content outside the tags. -->
### Jan 5, 2026
### Dec 10, 2025
| ID | Time | T | Title | Read |
|----|------|---|-------|------|
| #37751 | 6:21 PM | 🔴 | OpenRouterAgent Fully Updated with projectRoot Threading | ~307 |
| #37748 | " | | OpenRouterAgent Observation Handler Updated with lastCwd | ~276 |
| #37746 | 6:20 PM | | GeminiAgent Summary Handler Updated with lastCwd | ~278 |
| #37745 | " | | OpenRouterAgent Init Prompt Call Updated with undefined projectRoot | ~299 |
| #37744 | " | | GeminiAgent First processAgentResponse Updated with lastCwd | ~274 |
| #37743 | " | | Added lastCwd Tracking to OpenRouterAgent | ~265 |
| #37742 | 6:19 PM | | Phase 3 Started: Added lastCwd Tracking to GeminiAgent | ~291 |
| #37741 | " | 🔵 | OpenRouterAgent Structure Analyzed for Phase 3 Implementation | ~367 |
| #37740 | " | 🔵 | SDKAgent Structure Analyzed for Phase 3 Implementation | ~430 |
| #37739 | 6:18 PM | 🔵 | GeminiAgent Structure Analyzed for Phase 3 Implementation | ~381 |
| #37737 | 6:16 PM | 🔵 | Identified processAgentResponse Callers for Phase 3 | ~275 |
| #37701 | 6:01 PM | 🔵 | Complete cwd data flow traced from hooks through observation processing | ~447 |
| #37421 | 1:06 AM | 🔵 | Identified by-file API endpoint usage across codebase | ~406 |
| #37416 | 1:02 AM | 🔴 | Fixed find_by_file Parameter Handling for Comma-Separated Values | ~375 |
| #37415 | " | 🟣 | SearchManager Parameter Normalization Enhanced for Folder Queries | ~406 |
| #37414 | 1:01 AM | 🔵 | SearchManager findByFile Uses filePath Parameter for File Search | ~429 |
| #37413 | " | 🔵 | Hybrid Search Strategy: Metadata-First with Semantic Ranking | ~475 |
| #37391 | 12:48 AM | ✅ | Staged 23 CLAUDE.md files with mix of new and modified content | ~400 |
| #37390 | 12:47 AM | ✅ | Regenerated 23 CLAUDE.md files in budapest workspace | ~365 |
| #23673 | 8:36 PM | | Add Project Filter Parameter to Session and Prompt Hydration in Search | ~306 |
| #23596 | 5:54 PM | ⚖️ | Import/Export Bug Fix Priority and Scope | ~415 |
| #23595 | 5:53 PM | 🔴 | SearchManager Returns Wrong Format for Empty Results | ~320 |
| #23594 | " | 🔵 | SearchManager Search Method Control Flow | ~313 |
| #23591 | 5:51 PM | 🔵 | SearchManager JSON Response Structure | ~231 |
| #23590 | " | 🔵 | Import/Export Feature Status Review | ~490 |
| #23583 | 5:50 PM | 🔵 | SearchManager Hybrid Search Architecture | ~495 |
### Jan 7, 2026
### Dec 13, 2025
| ID | Time | T | Title | Read |
|----|------|---|-------|------|
| #38467 | 10:29 PM | ⚖️ | Log Level Audit Strategy: Tighten ERROR Messages for Runtime Issue Discovery | ~464 |
| #38437 | 10:24 PM | 🔵 | Claude-mem core functionality and logging patterns identified | ~710 |
| #38433 | " | 🔵 | SessionManager Uses ERROR Logging for Database Persistence Failures | ~503 |
| #38431 | " | 🔵 | ERROR-Level Logging Used Across 21 Source Files | ~480 |
| #38425 | " | ⚖️ | Log Level Architecture: Fail-Critical Over Fail-Fast for Chroma | ~467 |
| #38418 | 10:22 PM | 🔵 | ChromaDB Operates as Optional Subsystem with Graceful Degradation | ~586 |
| #38416 | " | 🔵 | ChromaDB Is Critical Not Optional - Log Audit Findings Challenged | ~405 |
| #38405 | 10:07 PM | ⚖️ | DEBUG Log Level Analysis - One Message Requires WARN Promotion | ~819 |
| #38404 | 10:06 PM | ⚖️ | Log Level Audit Analysis - WARN to ERROR Promotion Criteria Established | ~769 |
| #38372 | 9:43 PM | 🔵 | Logging Anti-Pattern Found: Critical Errors Logged as DEBUG Instead of ERROR | ~464 |
| #38354 | 9:10 PM | 🔵 | Provider Selection Architecture and Default Settings Location | ~478 |
| #38353 | " | 🔵 | Session ID Architecture and Message Yielding for SDK Interaction | ~536 |
| #38253 | 7:47 PM | 🔵 | SDK Agent Implementation with Resume Logic | ~1036 |
| #38250 | 7:46 PM | 🔵 | Session Manager Event-Driven Architecture | ~865 |
| #38248 | 7:45 PM | 🔵 | OpenRouter Agent Implementation | ~851 |
| #25191 | 8:04 PM | 🔵 | ChromaSync Instantiated in DatabaseManager Constructor | ~315 |
### Dec 14, 2025
| ID | Time | T | Title | Read |
|----|------|---|-------|------|
| #26263 | 8:32 PM | 🔵 | SearchManager Timeline Methods Use Rich Formatting, Search Method Uses Flat Tables | ~464 |
| #26243 | 8:29 PM | 🔵 | FormattingService Provides Basic Table Format Without Dates or File Grouping | ~390 |
| #26240 | " | 🔵 | SearchManager Formats Results as Tables, Timeline Uses Rich Date-Grouped Format | ~416 |
| #26108 | 7:43 PM | | changes() Method Format Logic Removed | ~401 |
| #26107 | " | | changes() Method Format Parameter Removed | ~317 |
| #26106 | 7:42 PM | | decisions() Method Format Logic Removed | ~405 |
| #26105 | " | | decisions() Method Format Parameter Removed | ~310 |
| #26104 | " | | Main search() Method Format Handling Removed | ~430 |
| #26103 | 7:41 PM | | FormattingService.ts Rewritten to Table Format | ~457 |
| #26102 | " | 🔵 | SearchManager.ts Format Parameter Removal Status | ~478 |
### Dec 15, 2025
| ID | Time | T | Title | Read |
|----|------|---|-------|------|
| #27043 | 6:04 PM | 🔵 | Subagent confirms no version switcher UI exists, only orphaned backend infrastructure | ~539 |
| #27041 | 6:03 PM | 🔵 | Branch switching code isolated to two backend files, no frontend UI components | ~473 |
| #27037 | 6:02 PM | 🔵 | Branch switching functionality exists in SettingsRoutes with UI switcher removal intent | ~463 |
### Dec 16, 2025
| ID | Time | T | Title | Read |
|----|------|---|-------|------|
| #27727 | 5:45 PM | 🔵 | SearchManager returns raw data arrays when format=json is specified | ~349 |
### Dec 17, 2025
| ID | Time | T | Title | Read |
|----|------|---|-------|------|
| #28473 | 4:25 PM | 🔵 | PaginationHelper LIMIT+1 Trick and Project Path Sanitization | ~499 |
| #28458 | 4:24 PM | 🔵 | SDK Agent Observer-Only Event-Driven Query Loop | ~513 |
| #28455 | " | 🔵 | Event-Driven Session Manager with Zero-Latency Queuing | ~566 |
### Dec 18, 2025
| ID | Time | T | Title | Read |
|----|------|---|-------|------|
| #29240 | 12:12 AM | 🔵 | SDK Agent Event-Driven Query Loop with Tool Restrictions | ~507 |
### Dec 20, 2025
| ID | Time | T | Title | Read |
|----|------|---|-------|------|
| #31100 | 8:01 PM | 🔵 | Summary and Memory Message Generation in SDK Agent | ~324 |
### Dec 25, 2025
| ID | Time | T | Title | Read |
|----|------|---|-------|------|
| #32616 | 8:43 PM | 🔵 | Comprehensive analysis of "enable billing" setting and its impact on rate limiting | ~533 |
| #32599 | 8:40 PM | 🔄 | Added validation and explicit default for Gemini model configuration | ~393 |
| #32598 | " | 🔵 | Gemini configuration loaded from settings or environment variables | ~363 |
| #32591 | 8:38 PM | 🔴 | Removed Unsupported Gemini Model from Agent | ~282 |
| #32583 | " | 🔵 | Gemini Agent Implementation Details | ~434 |
| #32543 | 7:29 PM | 🔄 | Rate limiting applied conditionally based on billing status | ~164 |
| #32542 | " | 🔄 | Query Gemini now accepts billing status | ~163 |
| #32541 | " | 🔄 | Gemini config now includes billing status | ~182 |
| #32540 | " | 🔄 | Rate limiting logic refactored for Gemini billing | ~164 |
### Dec 26, 2025
| ID | Time | T | Title | Read |
|----|------|---|-------|------|
| #32949 | 10:55 PM | 🔵 | Complete settings persistence flow for Xiaomi MIMO v2 Flash model | ~320 |
| #32948 | 10:53 PM | 🔵 | OpenRouterAgent uses CLAUDE_MEM_OPENROUTER_MODEL setting with Xiaomi as default | ~183 |
### Dec 27, 2025
| ID | Time | T | Title | Read |
|----|------|---|-------|------|
| #33215 | 9:06 PM | 🔵 | SessionManager Implements Event-Driven Lifecycle with Database-First Persistence and Auto-Initialization | ~853 |
| #33214 | " | 🔵 | SDKAgent Implements Event-Driven Query Loop with Init/Continuation Prompt Selection | ~769 |
### Dec 28, 2025
| ID | Time | T | Title | Read |
|----|------|---|-------|------|
| #33551 | 11:00 PM | 🔵 | GeminiAgent Does Not Implement Resume Functionality | ~307 |
| #33550 | " | 🔵 | OpenRouterAgent Does Not Implement Resume Functionality | ~294 |
| #33549 | 10:59 PM | 🔴 | SDKAgent Now Checks memorySessionId Differs From contentSessionId Before Resume | ~419 |
| #33547 | " | 🔵 | All Agents Call storeObservation with contentSessionId Instead of memorySessionId | ~407 |
| #33543 | 10:56 PM | 🔵 | SDKAgent Already Implements Memory Session ID Capture and Resume Logic | ~467 |
| #33542 | " | 🔵 | SessionManager Already Uses Renamed Session ID Fields | ~390 |
### Dec 30, 2025
| ID | Time | T | Title | Read |
|----|------|---|-------|------|
| #34504 | 2:31 PM | 🔵 | SDKAgent V2 Message Handling and Processing Flow Detailed | ~583 |
| #34459 | 2:23 PM | 🔵 | Complete SDKAgent V2 Architecture with Comprehensive Message Processing | ~619 |
| #34453 | 2:21 PM | 🔵 | Memory Agent Configured as Observer-Only | ~379 |
### Jan 4, 2026
| ID | Time | T | Title | Read |
|----|------|---|-------|------|
| #36853 | 1:49 AM | 🔵 | GeminiAgent Implementation Reviewed for Model Support | ~555 |
</claude-mem-context>
+1
View File
@@ -4,6 +4,7 @@ export const HOOK_TIMEOUTS = {
WORKER_STARTUP_WAIT: 1000,
WORKER_STARTUP_RETRIES: 300,
PRE_RESTART_SETTLE_DELAY: 2000, // Give files time to sync before restart
POWERSHELL_COMMAND: 10000, // PowerShell process enumeration (10s - typically completes in <1s)
WINDOWS_MULTIPLIER: 1.5 // Platform-specific adjustment
} as const;
+54 -1
View File
@@ -3,5 +3,58 @@
<!-- This section is auto-generated by claude-mem. Edit content outside the tags. -->
*No recent activity*
### Nov 5, 2025
| ID | Time | T | Title | Read |
|----|------|---|-------|------|
| #4035 | 10:24 PM | 🔵 | logger.ts file exists but is empty | ~220 |
### Nov 10, 2025
| ID | Time | T | Title | Read |
|----|------|---|-------|------|
| #6521 | 5:43 PM | 🔵 | Code Review: Enhanced HTTP Logging and Double Entries Bug Fix | ~482 |
### Nov 17, 2025
| ID | Time | T | Title | Read |
|----|------|---|-------|------|
| #10019 | 12:14 AM | 🔵 | TranscriptParser Utility: JSONL Parsing with Type-Safe Entry Filtering | ~569 |
### Nov 23, 2025
| ID | Time | T | Title | Read |
|----|------|---|-------|------|
| #14626 | 6:25 PM | 🔵 | Stop Hook Summary Not in Transcript Validator Schema | ~359 |
### Nov 28, 2025
| ID | Time | T | Title | Read |
|----|------|---|-------|------|
| #17238 | 11:34 PM | 🔵 | Existing TranscriptParser TypeScript implementation handles nested message structure | ~493 |
### Dec 5, 2025
| ID | Time | T | Title | Read |
|----|------|---|-------|------|
| #20407 | 7:20 PM | 🔵 | Tag stripping utilities implement dual-tag privacy system with ReDoS protection | ~415 |
### Dec 8, 2025
| ID | Time | T | Title | Read |
|----|------|---|-------|------|
| #22310 | 9:46 PM | 🟣 | Complete Hook Lifecycle Documentation Generated | ~603 |
| #22306 | 9:45 PM | 🔵 | Dual-Tag Privacy System with ReDoS Protection | ~461 |
### Dec 14, 2025
| ID | Time | T | Title | Read |
|----|------|---|-------|------|
| #25691 | 4:24 PM | 🔵 | happy_path_error__with_fallback utility logs errors to silent.log and returns fallback values | ~460 |
### Dec 20, 2025
| ID | Time | T | Title | Read |
|----|------|---|-------|------|
| #30883 | 6:38 PM | 🔵 | Tag-Stripping DRY Violation Analysis | ~152 |
</claude-mem-context>
+7
View File
@@ -0,0 +1,7 @@
<claude-mem-context>
# Recent Activity
<!-- This section is auto-generated by claude-mem. Edit content outside the tags. -->
*No recent activity*
</claude-mem-context>
+4
View File
@@ -47,6 +47,10 @@ describe('hook-constants', () => {
it('should define WINDOWS_MULTIPLIER', () => {
expect(HOOK_TIMEOUTS.WINDOWS_MULTIPLIER).toBe(1.5);
});
it('should define POWERSHELL_COMMAND timeout as 10000ms', () => {
expect(HOOK_TIMEOUTS.POWERSHELL_COMMAND).toBe(10000);
});
});
describe('HOOK_EXIT_CODES', () => {
+49 -63
View File
@@ -1,7 +1,7 @@
import { describe, it, expect, beforeEach, afterEach } from 'bun:test';
/**
* Tests for WMIC output parsing logic used in Windows process enumeration.
* Tests for PowerShell output parsing logic used in Windows process enumeration.
*
* This tests the parsing behavior directly since mocking promisified exec
* is unreliable across module boundaries. The parsing logic matches exactly
@@ -9,16 +9,14 @@ import { describe, it, expect, beforeEach, afterEach } from 'bun:test';
*/
// Extract the parsing logic from ProcessManager for direct testing
// This matches the implementation in src/services/infrastructure/ProcessManager.ts lines 93-100
function parseWmicOutput(stdout: string): number[] {
// This matches the implementation in src/services/infrastructure/ProcessManager.ts lines 95-100
function parsePowerShellOutput(stdout: string): number[] {
return stdout
.trim()
.split('\n')
.map(line => {
const match = line.match(/ProcessId=(\d+)/i);
return match ? parseInt(match[1], 10) : NaN;
})
.filter(n => !isNaN(n) && Number.isInteger(n) && n > 0);
.map(line => line.trim())
.filter(line => line.length > 0 && /^\d+$/.test(line))
.map(line => parseInt(line, 10))
.filter(pid => pid > 0);
}
// Validate parent PID - matches ProcessManager.getChildProcesses() lines 85-88
@@ -26,131 +24,119 @@ function isValidParentPid(parentPid: number): boolean {
return Number.isInteger(parentPid) && parentPid > 0;
}
describe('WMIC output parsing (Windows)', () => {
describe('parseWmicOutput - ProcessId format parsing', () => {
it('should parse ProcessId=12345 format correctly', () => {
const stdout = 'ProcessId=12345\r\nProcessId=67890\r\n';
describe('PowerShell output parsing (Windows)', () => {
describe('parsePowerShellOutput - simple number format parsing', () => {
it('should parse simple number format correctly', () => {
const stdout = '12345\r\n67890\r\n';
const result = parseWmicOutput(stdout);
const result = parsePowerShellOutput(stdout);
expect(result).toEqual([12345, 67890]);
});
it('should parse single PID from WMIC output', () => {
const stdout = 'ProcessId=54321\r\n';
it('should parse single PID from PowerShell output', () => {
const stdout = '54321\r\n';
const result = parseWmicOutput(stdout);
const result = parsePowerShellOutput(stdout);
expect(result).toEqual([54321]);
});
it('should handle WMIC output with mixed case', () => {
// WMIC output can vary in case on different Windows versions
const stdout = 'PROCESSID=11111\r\nprocessid=22222\r\nProcessId=33333\r\n';
const result = parseWmicOutput(stdout);
expect(result).toEqual([11111, 22222, 33333]);
});
it('should handle empty WMIC output', () => {
it('should handle empty PowerShell output', () => {
const stdout = '';
const result = parseWmicOutput(stdout);
const result = parsePowerShellOutput(stdout);
expect(result).toEqual([]);
});
it('should handle WMIC output with only whitespace', () => {
it('should handle PowerShell output with only whitespace', () => {
const stdout = ' \r\n \r\n';
const result = parseWmicOutput(stdout);
const result = parsePowerShellOutput(stdout);
expect(result).toEqual([]);
});
it('should filter invalid PIDs from WMIC output', () => {
const stdout = 'ProcessId=12345\r\nProcessId=invalid\r\nProcessId=67890\r\n';
it('should filter invalid PIDs from PowerShell output', () => {
const stdout = '12345\r\ninvalid\r\n67890\r\n';
const result = parseWmicOutput(stdout);
const result = parsePowerShellOutput(stdout);
expect(result).toEqual([12345, 67890]);
});
it('should filter negative PIDs from WMIC output', () => {
// Negative PIDs won't match the regex /ProcessId=(\d+)/i (only digits)
const stdout = 'ProcessId=12345\r\nProcessId=-1\r\nProcessId=67890\r\n';
it('should filter negative PIDs from PowerShell output', () => {
const stdout = '12345\r\n-1\r\n67890\r\n';
const result = parseWmicOutput(stdout);
const result = parsePowerShellOutput(stdout);
expect(result).toEqual([12345, 67890]);
});
it('should filter zero PIDs from WMIC output', () => {
// Zero is filtered out by the n > 0 check
const stdout = 'ProcessId=0\r\nProcessId=12345\r\n';
it('should filter zero PIDs from PowerShell output', () => {
const stdout = '0\r\n12345\r\n';
const result = parseWmicOutput(stdout);
const result = parsePowerShellOutput(stdout);
expect(result).toEqual([12345]);
});
it('should handle WMIC output with extra lines and noise', () => {
const stdout = '\r\n\r\nProcessId=12345\r\n\r\nSome other output\r\nProcessId=67890\r\n\r\n';
it('should handle PowerShell output with extra lines and noise', () => {
const stdout = '\r\n\r\n12345\r\n\r\nSome other output\r\n67890\r\n\r\n';
const result = parseWmicOutput(stdout);
const result = parsePowerShellOutput(stdout);
expect(result).toEqual([12345, 67890]);
});
it('should handle Windows line endings (CRLF)', () => {
const stdout = 'ProcessId=111\r\nProcessId=222\r\nProcessId=333\r\n';
const stdout = '111\r\n222\r\n333\r\n';
const result = parseWmicOutput(stdout);
const result = parsePowerShellOutput(stdout);
expect(result).toEqual([111, 222, 333]);
});
it('should handle Unix line endings (LF)', () => {
const stdout = 'ProcessId=111\nProcessId=222\nProcessId=333\n';
const stdout = '111\n222\n333\n';
const result = parseWmicOutput(stdout);
const result = parsePowerShellOutput(stdout);
expect(result).toEqual([111, 222, 333]);
});
it('should handle lines with extra equals signs', () => {
const stdout = 'ProcessId=12345\r\nSomeOther=value=with=equals\r\nProcessId=67890\r\n';
const result = parseWmicOutput(stdout);
expect(result).toEqual([12345, 67890]);
});
it('should handle very large PIDs', () => {
// Windows PIDs can be large but are still 32-bit integers
const stdout = 'ProcessId=2147483647\r\n';
const stdout = '2147483647\r\n';
const result = parseWmicOutput(stdout);
const result = parsePowerShellOutput(stdout);
expect(result).toEqual([2147483647]);
});
it('should handle typical WMIC list format output', () => {
// Real WMIC output often has blank lines and extra spacing
it('should handle typical PowerShell output with blank lines and extra spacing', () => {
const stdout = `
ProcessId=1234
1234
ProcessId=5678
5678
`;
const result = parseWmicOutput(stdout);
const result = parsePowerShellOutput(stdout);
expect(result).toEqual([1234, 5678]);
});
it('should filter lines with text and numbers mixed', () => {
const stdout = '12345\r\nPID: 67890\r\n11111\r\n';
const result = parsePowerShellOutput(stdout);
expect(result).toEqual([12345, 11111]);
});
});
describe('parent PID validation', () => {
+20
View File
@@ -0,0 +1,20 @@
<claude-mem-context>
# Recent Activity
<!-- This section is auto-generated by claude-mem. Edit content outside the tags. -->
### Jan 5, 2026
| ID | Time | T | Title | Read |
|----|------|---|-------|------|
| #37417 | 1:04 AM | ✅ | Created Implementation Summary Document for CLAUDE.md Duplicate Fix | ~488 |
| #37416 | 1:02 AM | 🔴 | Fixed find_by_file Parameter Handling for Comma-Separated Values | ~375 |
| #37415 | " | 🟣 | SearchManager Parameter Normalization Enhanced for Folder Queries | ~406 |
| #37414 | 1:01 AM | 🔵 | SearchManager findByFile Uses filePath Parameter for File Search | ~429 |
| #37413 | " | 🔵 | Hybrid Search Strategy: Metadata-First with Semantic Ranking | ~475 |
| #37412 | 1:00 AM | 🔴 | Fixed Folder Query to Return Only Direct Children Files | ~509 |
| #37411 | " | 🟣 | Added isFolder Flag to SearchOptions Interface | ~325 |
| #37410 | " | 🔵 | SessionSearch Class Design and FTS5 Deprecation Strategy | ~450 |
| #37409 | 12:58 AM | 🔵 | SessionSearch Uses JSON Array Search for File Filtering | ~404 |
| #37408 | " | 🔵 | SearchRoutes API Architecture with Worktree Support | ~543 |
</claude-mem-context>
+7
View File
@@ -0,0 +1,7 @@
<claude-mem-context>
# Recent Activity
<!-- This section is auto-generated by claude-mem. Edit content outside the tags. -->
*No recent activity*
</claude-mem-context>