Compare commits
18 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 79bc3c85b3 | |||
| 6581d2ef45 | |||
| 39db5c4882 | |||
| 3af68b7dfe | |||
| e9b4f75fb2 | |||
| 2af37422da | |||
| a32151a166 | |||
| 97ea9e45fc | |||
| ecb09df420 | |||
| 6c7acfbc1c | |||
| 44a7b2fcb9 | |||
| 7015301d8f | |||
| a5e86ad4ab | |||
| d93bde059e | |||
| d60ae14a9b | |||
| 272391ec9d | |||
| 0e502dbd21 | |||
| 9ab119932a |
@@ -10,7 +10,7 @@
|
||||
"plugins": [
|
||||
{
|
||||
"name": "claude-mem",
|
||||
"version": "10.4.4",
|
||||
"version": "10.5.5",
|
||||
"source": "./plugin",
|
||||
"description": "Persistent memory system for Claude Code - context compression across sessions"
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ datasets/
|
||||
node_modules/
|
||||
dist/
|
||||
!installer/dist/
|
||||
**/_tree-sitter/
|
||||
*.log
|
||||
.DS_Store
|
||||
.env
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"MD013": false
|
||||
}
|
||||
+94
-156
@@ -2,6 +2,100 @@
|
||||
|
||||
All notable changes to claude-mem.
|
||||
|
||||
## [v10.5.4] - 2026-03-09
|
||||
|
||||
## Bug Fixes
|
||||
|
||||
- **fix: restore modes to correct location** — All modes (`code`, code language variants, `email-investigation`) were erroneously moved from `plugin/modes/` to `plugin/hooks/modes/` during the v10.5.3 release, breaking mode loading. This patch restores them to `plugin/modes/` where they belong.
|
||||
|
||||
## [v10.5.3] - 2026-03-09
|
||||
|
||||
## What's New
|
||||
|
||||
### Law Study Mode
|
||||
|
||||
Adds `law-study` — a purpose-built claude-mem mode for law students.
|
||||
|
||||
**Observation Types:**
|
||||
- **Case Holding** — 2-3 sentence brief with extracted legal rule
|
||||
- **Issue Pattern** — exam trigger or fact pattern that signals a legal issue
|
||||
- **Prof Framework** — professor's analytical lens and emphasis for a topic
|
||||
- **Doctrine / Rule** — legal test or standard synthesized from cases/statutes
|
||||
- **Argument Structure** — legal argument or counter-argument worked through analytically
|
||||
- **Cross-Case Connection** — insight linking cases or doctrines to reveal a deeper principle
|
||||
|
||||
**Concepts (cross-cutting tags):**
|
||||
`exam-relevant` · `minority-position` · `gotcha` · `unsettled-law` · `policy-rationale` · `course-theme`
|
||||
|
||||
**Chill Variant** — `law-study--chill` records only high-signal items: issue patterns, gotchas, and professor frameworks. Skips routine case holdings unless the result is counterintuitive.
|
||||
|
||||
**CLAUDE.md Template** — `law-study-CLAUDE.md` is a drop-in template for any law study project directory. It configures Claude as a Socratic legal study partner: precise case briefs, critical document analysis, issue spotting, and doctrine synthesis — without writing exam answers for the student.
|
||||
|
||||
Activate with: `/mode law-study` or `/mode law-study--chill`
|
||||
|
||||
## [v10.5.2] - 2026-02-26
|
||||
|
||||
## Smart Explore Benchmark Docs & Skill Update
|
||||
|
||||
### Documentation
|
||||
- Published smart-explore benchmark report to public docs — full A/B comparison with methodology, raw data tables, quality assessment, and decision framework
|
||||
- Added benchmark report to docs.json navigation under Best Practices
|
||||
|
||||
### Smart Explore Skill
|
||||
- Updated token economics with benchmark-accurate data (11-18x savings on exploration, 4-8x on file understanding)
|
||||
- Added "map first" core principle as decision heuristic for tool selection
|
||||
- Added AST completeness guarantee to smart_unfold documentation (never truncates, unlike Explore agents)
|
||||
- Added Explore agent escalation guidance for multi-file synthesis tasks
|
||||
- Updated smart_unfold token range from ~1-7k to ~400-2,100 based on measurements
|
||||
- Updated Explore agent token range from ~20-40k to ~39-59k based on measurements
|
||||
|
||||
## [v10.5.1] - 2026-02-26
|
||||
|
||||
### Bug Fix
|
||||
|
||||
- Restored hooks.json to pre-smart-explore configuration (re-adds Setup hook, separate worker start command, PostToolUse matcher)
|
||||
|
||||
## [v10.5.0] - 2026-02-26
|
||||
|
||||
## Smart Explore: AST-Powered Code Navigation
|
||||
|
||||
This release introduces **Smart Explore**, a token-optimized structural code search system built on tree-sitter AST parsing. It applies the same progressive disclosure pattern used in human-readable code outlines — but programmatically, for AI agents.
|
||||
|
||||
### Why This Matters
|
||||
|
||||
The standard exploration cycle (Glob → Grep → Read) forces agents to consume entire files to understand code structure. A typical 800-line file costs ~12,000 tokens to read. Smart Explore replaces this with a 3-layer progressive disclosure workflow that delivers the same understanding at **6-12x lower token cost**.
|
||||
|
||||
### 3 New MCP Tools
|
||||
|
||||
- **`smart_search`** — Walks directories, parses all code files via tree-sitter, and returns ranked symbols with signatures and line numbers. Replaces the Glob → Grep discovery cycle in a single call (~2-6k tokens).
|
||||
- **`smart_outline`** — Returns the complete structural skeleton of a file: all functions, classes, methods, properties, imports (~1-2k tokens vs ~12k for a full Read).
|
||||
- **`smart_unfold`** — Expands a single symbol to its full source code including JSDoc, decorators, and implementation (~1-7k tokens).
|
||||
|
||||
### Token Economics
|
||||
|
||||
| Approach | Tokens | Savings |
|
||||
|----------|--------|---------|
|
||||
| smart_outline + smart_unfold | ~3,100 | 8x vs Read |
|
||||
| smart_search (cross-file) | ~2,000-6,000 | 6-12x vs Explore agent |
|
||||
| Read (full file) | ~12,000+ | baseline |
|
||||
| Explore agent | ~20,000-40,000 | baseline |
|
||||
|
||||
### Language Support
|
||||
|
||||
10 languages via tree-sitter grammars: TypeScript, JavaScript, Python, Rust, Go, Java, C, C++, Ruby, PHP.
|
||||
|
||||
### Other Changes
|
||||
|
||||
- Simplified hooks configuration
|
||||
- Removed legacy setup.sh script
|
||||
- Security fix: replaced `execSync` with `execFileSync` to prevent command injection in file path handling
|
||||
|
||||
## [v10.4.4] - 2026-02-26
|
||||
|
||||
## Fix
|
||||
|
||||
- **Remove `save_observation` from MCP tool surface** — This tool was exposed as an MCP tool available to Claude, but it's an internal API-only feature. Removing it from the MCP server prevents unintended tool invocation and keeps the tool surface clean.
|
||||
|
||||
## [v10.4.3] - 2026-02-25
|
||||
|
||||
## Bug Fixes
|
||||
@@ -1093,159 +1187,3 @@ Version 9.0.0 introduces the **Live Context System** - a major new capability th
|
||||
|
||||
See PR #558 for complete details and diagnostic reports.
|
||||
|
||||
## [v8.5.9] - 2026-01-04
|
||||
|
||||
## What's New
|
||||
|
||||
### Context Header Timestamp
|
||||
|
||||
The context injection header now displays the current date and time, making it easier to understand when context was generated.
|
||||
|
||||
**Example:** `[claude-mem] recent context, 2026-01-04 2:46am EST`
|
||||
|
||||
This appears in both terminal (colored) output and markdown format, including empty state messages.
|
||||
|
||||
---
|
||||
|
||||
**Full Changelog**: https://github.com/thedotmack/claude-mem/compare/v8.5.8...v8.5.9
|
||||
|
||||
## [v8.5.8] - 2026-01-04
|
||||
|
||||
## Bug Fixes
|
||||
|
||||
- **#511**: Add `gemini-3-flash` model to GeminiAgent with proper rate limits and validation
|
||||
- **#517**: Fix Windows process management by replacing PowerShell with WMIC (fixes Git Bash/WSL compatibility)
|
||||
- **#527**: Add Apple Silicon Homebrew paths (`/opt/homebrew/bin`) for `bun` and `uv` detection
|
||||
- **#531**: Remove duplicate type definitions from `export-memories.ts` using shared bridge file
|
||||
|
||||
## Tests
|
||||
|
||||
- Added regression tests for PR #542 covering Gemini model support, WMIC parsing, Apple Silicon paths, and export type refactoring
|
||||
|
||||
## Documentation
|
||||
|
||||
- Added detailed analysis reports for GitHub issues #511, #514, #517, #520, #527, #531, #532
|
||||
|
||||
## [v8.5.7] - 2026-01-04
|
||||
|
||||
## Modular Architecture Refactor
|
||||
|
||||
This release refactors the monolithic service architecture into focused, single-responsibility modules with comprehensive test coverage.
|
||||
|
||||
### Architecture Improvements
|
||||
|
||||
- **SQLite Repositories** (`src/services/sqlite/`) - Modular repositories for sessions, observations, prompts, summaries, and timeline
|
||||
- **Worker Agents** (`src/services/worker/agents/`) - Extracted response processing, error handling, and session cleanup
|
||||
- **Search Strategies** (`src/services/worker/search/`) - Modular search with Chroma, SQLite, and Hybrid strategies plus orchestrator
|
||||
- **Context Generation** (`src/services/context/`) - Separated context building, token calculation, formatters, and renderers
|
||||
- **Infrastructure** (`src/services/infrastructure/`) - Graceful shutdown, health monitoring, and process management
|
||||
- **Server** (`src/services/server/`) - Express server setup, middleware, and error handling
|
||||
|
||||
### Test Coverage
|
||||
|
||||
- **595 tests** across 36 test files
|
||||
- **1,120 expect() assertions**
|
||||
- Coverage for SQLite repos, worker agents, search, context, infrastructure, and server modules
|
||||
|
||||
### Session ID Refactor
|
||||
|
||||
- Aligned tests with NULL-based memory session initialization pattern
|
||||
- Updated `SESSION_ID_ARCHITECTURE.md` documentation
|
||||
|
||||
### Other Improvements
|
||||
|
||||
- Added missing logger imports to 34 files for better observability
|
||||
- Updated esbuild and MCP SDK to latest versions
|
||||
- Removed `bun.lock` from version control
|
||||
|
||||
**Full Changelog**: https://github.com/thedotmack/claude-mem/compare/v8.5.6...v8.5.7
|
||||
|
||||
## [v8.5.6] - 2026-01-04
|
||||
|
||||
## Major Architectural Refactoring
|
||||
|
||||
Decomposes monolithic services into modular, maintainable components:
|
||||
|
||||
### Worker Service
|
||||
Extracted infrastructure (GracefulShutdown, HealthMonitor, ProcessManager), server layer (ErrorHandler, Middleware, Server), and integrations (CursorHooksInstaller)
|
||||
|
||||
### Context Generator
|
||||
Split into ContextBuilder, ContextConfigLoader, ObservationCompiler, TokenCalculator, formatters (Color/Markdown), and section renderers (Header/Footer/Summary/Timeline)
|
||||
|
||||
### Search System
|
||||
Extracted SearchOrchestrator, ResultFormatter, TimelineBuilder, and strategy pattern (Chroma/SQLite/Hybrid search strategies) with dedicated filters (Date/Project/Type)
|
||||
|
||||
### Agent System
|
||||
Extracted shared logic into ResponseProcessor, ObservationBroadcaster, FallbackErrorHandler, and SessionCleanupHelper
|
||||
|
||||
### SQLite Layer
|
||||
Decomposed SessionStore into domain modules (observations, prompts, sessions, summaries, timeline) with proper type exports
|
||||
|
||||
## Bug Fixes
|
||||
- Fixed duplicate observation storage bug (observations stored multiple times when messages were batched)
|
||||
- Added duplicate observation cleanup script for production database remediation
|
||||
- Fixed FOREIGN KEY constraint and missing `failed_at_epoch` column issues
|
||||
|
||||
## Coming Next
|
||||
Comprehensive test suite in a new PR, targeting **v8.6.0**
|
||||
|
||||
🤖 Generated with [Claude Code](https://claude.com/claude-code)
|
||||
|
||||
## [v8.5.5] - 2026-01-03
|
||||
|
||||
## Improved Error Handling and Logging
|
||||
|
||||
This patch release enhances error handling and logging across all worker services for better debugging and reliability.
|
||||
|
||||
### Changes
|
||||
- **Enhanced Error Logging**: Improved error context across SessionStore, SearchManager, SDKAgent, GeminiAgent, and OpenRouterAgent
|
||||
- **SearchManager**: Restored error handling for Chroma calls with improved logging
|
||||
- **SessionStore**: Enhanced error logging throughout database operations
|
||||
- **Bug Fix**: Fixed critical bug where `memory_session_id` could incorrectly equal `content_session_id`
|
||||
- **Hooks**: Streamlined error handling and loading states for better maintainability
|
||||
|
||||
### Investigation Reports
|
||||
- Added detailed analysis documents for generator failures and observation duplication regressions
|
||||
|
||||
**Full Changelog**: https://github.com/thedotmack/claude-mem/compare/v8.5.4...v8.5.5
|
||||
|
||||
## [v8.5.4] - 2026-01-02
|
||||
|
||||
## Bug Fixes
|
||||
|
||||
### Chroma Connection Error Handling
|
||||
Fixed a critical bug in ChromaSync where connection-related errors were misinterpreted as missing collections. The `ensureCollection()` method previously caught ALL errors and assumed they meant the collection doesn't exist, which caused connection errors to trigger unnecessary collection creation attempts. Now connection-related errors like "Not connected" are properly distinguished and re-thrown immediately, preventing false error handling paths and inappropriate fallback behavior.
|
||||
|
||||
### Removed Dead last_user_message Code
|
||||
Cleaned up dead code related to `last_user_message` handling in the summary flow. This field was being extracted from transcripts but never used anywhere - in Claude Code transcripts, "user" type messages are mostly tool_results rather than actual user input, and the user's original request is already stored in the user_prompts table. Removing this unused field eliminates confusing warnings like "Missing last_user_message when queueing summary". Changes span summary-hook, SessionRoutes, SessionManager, interface definitions, and all agent implementations.
|
||||
|
||||
## Improvements
|
||||
|
||||
### Enhanced Error Handling Across Services
|
||||
Comprehensive improvement to error handling across 8 core services:
|
||||
- **BranchManager** - Now logs recovery checkout failures
|
||||
- **PaginationHelper** - Logs when file paths are plain strings instead of valid JSON
|
||||
- **SDKAgent** - Enhanced logging for Claude executable detection failures
|
||||
- **SearchManager** - Logs plain string handling for files read and edited
|
||||
- **paths.ts** - Improved logging for git root detection failures
|
||||
- **timeline-formatting** - Enhanced JSON parsing errors with input previews
|
||||
- **transcript-parser** - Logs summary of parse errors after processing
|
||||
- **ChromaSync** - Logs full error context before attempting collection creation
|
||||
|
||||
### Error Handling Documentation & Tooling
|
||||
- Created `error-handling-baseline.txt` establishing baseline error handling practices
|
||||
- Documented error handling anti-pattern rules in CLAUDE.md
|
||||
- Added `detect-error-handling-antipatterns.ts` script to identify empty catch blocks, improper logging practices, and oversized try-catch blocks
|
||||
|
||||
## New Features
|
||||
|
||||
### Console Filter Bar with Log Parsing
|
||||
Implemented interactive log filtering in the viewer UI:
|
||||
- **Structured Log Parsing** - Extracts timestamp, level, component, correlation ID, and message content using regex pattern matching
|
||||
- **Level Filtering** - Toggle visibility for DEBUG, INFO, WARN, ERROR log levels
|
||||
- **Component Filtering** - Filter by 9 component types: HOOK, WORKER, SDK, PARSER, DB, SYSTEM, HTTP, SESSION, CHROMA
|
||||
- **Color-Coded Rendering** - Visual distinction with component-specific icons and log level colors
|
||||
- **Special Message Detection** - Recognizes markers like → (dataIn), ← (dataOut), ✓ (success), ✗ (failure), ⏱ (timing), [HAPPY-PATH]
|
||||
- **Smart Auto-Scroll** - Maintains scroll position when reviewing older logs
|
||||
- **Responsive Design** - Filter bar adapts to smaller screens
|
||||
|
||||
|
||||
@@ -62,7 +62,8 @@
|
||||
"icon": "lightbulb",
|
||||
"pages": [
|
||||
"context-engineering",
|
||||
"progressive-disclosure"
|
||||
"progressive-disclosure",
|
||||
"smart-explore-benchmark"
|
||||
]
|
||||
},
|
||||
{
|
||||
|
||||
@@ -0,0 +1,196 @@
|
||||
---
|
||||
title: "Smart Explore Benchmark"
|
||||
description: "Token efficiency comparison between AST-based and traditional code exploration"
|
||||
---
|
||||
|
||||
# Smart Explore Benchmark
|
||||
|
||||
Smart Explore uses tree-sitter AST parsing to provide structural code navigation through three MCP tools: `smart_search`, `smart_outline`, and `smart_unfold`. This report documents a rigorous A/B comparison against the standard Explore agent (which uses Glob, Grep, and Read tools) to quantify the token savings and quality trade-offs.
|
||||
|
||||
## Executive Summary
|
||||
|
||||
| Metric | Smart Explore | Explore Agent | Advantage |
|
||||
|--------|:---:|:---:|---|
|
||||
| Discovery (cross-file search) | ~14,200 tokens | ~252,500 tokens | **17.8x cheaper** |
|
||||
| Targeted reads (specific symbols) | ~5,650 tokens | ~109,400 tokens | **19.4x cheaper** |
|
||||
| End-to-end (search + read) | ~4,200 tokens | ~45,000 tokens | **10-12x cheaper** |
|
||||
| Completeness | 5/5 full source returned | 4/5 (truncated longest method) | Smart Explore more reliable |
|
||||
| Speed | Under 2s per call | 5-66s per call | **10-30x faster** |
|
||||
|
||||
## Methodology
|
||||
|
||||
### Test Environment
|
||||
|
||||
- **Codebase**: claude-mem (`src/` directory, 194 TypeScript files, 1,206 parsed symbols)
|
||||
- **Model**: Claude Opus 4.6 for both approaches
|
||||
- **Measurement**: Token counts from tool response metadata (`total_tokens` for Explore agents, self-reported `~N tokens for folded view` for Smart Explore)
|
||||
|
||||
### Controls
|
||||
|
||||
The Explore agents were explicitly instructed: *"Do NOT use smart_search, smart_outline, or smart_unfold tools. Only use Glob, Grep, and Read tools."* This was verified necessary after an initial round where agents opportunistically used the Smart Explore tools, invalidating the comparison.
|
||||
|
||||
### Queries
|
||||
|
||||
Five queries were selected to represent common exploration tasks:
|
||||
|
||||
1. **"session processing"** -- Cross-cutting feature spanning multiple services
|
||||
2. **"shutdown"** -- Infrastructure concern touching 6+ files
|
||||
3. **"hook registration"** -- Architecture question about plugin system
|
||||
4. **"sqlite database"** -- Technology-specific search across the data layer
|
||||
5. **"worker-service.ts outline"** -- Single large file (1,225 lines) structural understanding
|
||||
|
||||
## Round 1: Discovery
|
||||
|
||||
*"What exists and where is it?"* -- Finding relevant files and symbols across the codebase.
|
||||
|
||||
### Results
|
||||
|
||||
| Query | Smart Explore | Explore Agent | Ratio | Explore Tool Calls |
|
||||
|-------|:---:|:---:|:---:|:---:|
|
||||
| session processing | ~4,391 t | 51,659 t | **11.8x** | 15 |
|
||||
| shutdown | ~3,852 t | 51,523 t | **13.4x** | 18 |
|
||||
| hook registration | ~1,930 t | 51,688 t | **26.8x** | 37 |
|
||||
| sqlite database | ~2,543 t | 58,633 t | **23.1x** | 16 |
|
||||
| worker-service outline | ~1,500 t | 38,973 t | **26.0x** | 15 |
|
||||
| **Total** | **~14,216 t** | **252,476 t** | **17.8x** | **101** |
|
||||
|
||||
### What Each Returned
|
||||
|
||||
**Smart Explore** (1 tool call each): 10 ranked symbols with signatures, line numbers, and JSDoc summaries, plus folded structural views of all matching files showing every function/class/interface with bodies collapsed.
|
||||
|
||||
**Explore Agent** (15-37 tool calls each): Synthesized narrative reports with architecture diagrams, design pattern analysis, data flow explanations, complete interface dumps, and file structure maps. Significantly more explanatory prose.
|
||||
|
||||
### Analysis
|
||||
|
||||
The token gap is widest for narrowly-scoped queries ("hook registration" at 26.8x) because the Explore agent reads multiple full files to find relatively few relevant symbols. For broad queries ("session processing" at 11.8x), more of the file content is relevant, narrowing the ratio.
|
||||
|
||||
Smart Explore's consistent 1-tool-call pattern means its cost is predictable. The Explore agent's cost varies with how many files it reads and how much it synthesizes -- ranging from 15 to 37 tool calls for comparable scope.
|
||||
|
||||
## Round 2: Targeted Reads
|
||||
|
||||
*"Show me this specific function."* -- Reading the implementation of a known symbol after discovery.
|
||||
|
||||
Based on the Round 1 results, five specific symbols were selected as natural drill-down targets:
|
||||
|
||||
| Target Symbol | File | Lines |
|
||||
|---------------|------|:---:|
|
||||
| `SessionManager.initializeSession` | services/worker/SessionManager.ts | 135 |
|
||||
| `performGracefulShutdown` | services/infrastructure/GracefulShutdown.ts | 48 |
|
||||
| `hookCommand` | cli/hook-command.ts | 45 |
|
||||
| `DatabaseManager.initialize` | services/sqlite/Database.ts | 27 |
|
||||
| `WorkerService.startSessionProcessor` | services/worker-service.ts | 158 |
|
||||
|
||||
### Results
|
||||
|
||||
| Symbol | Smart Unfold | Explore Agent | Ratio | Completeness |
|
||||
|--------|:---:|:---:|:---:|---|
|
||||
| initializeSession (135 lines) | ~1,800 t | 27,816 t | **15.5x** | Both returned full source |
|
||||
| performGracefulShutdown (48 lines) | ~700 t | 19,621 t | **28.0x** | Both returned full source |
|
||||
| hookCommand (45 lines) | ~650 t | 18,680 t | **28.7x** | Both returned full source |
|
||||
| DatabaseManager.initialize (27 lines) | ~400 t | 22,334 t | **55.8x** | Both returned full source |
|
||||
| startSessionProcessor (158 lines) | ~2,100 t | 20,906 t | **10.0x** | Smart Unfold: complete. Explore: **truncated** |
|
||||
| **Total** | **~5,650 t** | **109,357 t** | **19.4x** | |
|
||||
|
||||
### Analysis
|
||||
|
||||
**The ratio scales inversely with symbol size.** The smallest function (`initialize`, 27 lines) shows the biggest gap at 55.8x because the Explore agent still reads the entire 235-line file to extract 27 lines. The largest method (`startSessionProcessor`, 158 lines) narrows to 10x since more of the file is "useful."
|
||||
|
||||
**Smart Unfold returned more complete code.** For the longest method (158 lines), the Explore agent truncated the error handling section with "... error handling continues ...", while `smart_unfold` returned the complete implementation. This is because smart_unfold extracts by AST node boundaries, guaranteeing completeness regardless of symbol size.
|
||||
|
||||
**Explore agents add zero unique information for targeted reads.** When you already know the file path and symbol name, the agent's overhead is pure waste -- it reads the file, locates the function, and echoes it back. The only addition is a brief explanatory paragraph.
|
||||
|
||||
## Combined Workflow
|
||||
|
||||
The realistic workflow is discovery followed by targeted reading. Here is the end-to-end cost comparison for understanding a single function:
|
||||
|
||||
### Smart Explore: search + unfold
|
||||
|
||||
```
|
||||
smart_search("shutdown", path="./src") ~3,852 tokens
|
||||
smart_unfold("GracefulShutdown.ts", "performGracefulShutdown") ~700 tokens
|
||||
────────────────────────────────────────────────────────────────
|
||||
Total: ~4,552 tokens (2 tool calls, under 3 seconds)
|
||||
```
|
||||
|
||||
### Explore Agent: single query
|
||||
|
||||
```
|
||||
"Find and explain the shutdown logic" ~51,523 tokens
|
||||
────────────────────────────────────────────────────────────────
|
||||
Total: ~51,523 tokens (18 tool calls, ~43 seconds)
|
||||
```
|
||||
|
||||
**End-to-end ratio: 11.3x** -- and the Smart Explore workflow gives you the actual source code, while the Explore agent gives you a prose summary that may paraphrase or truncate.
|
||||
|
||||
## Quality Assessment
|
||||
|
||||
Neither approach is universally better. They optimize for different outcomes.
|
||||
|
||||
### Smart Explore Strengths
|
||||
|
||||
- **Predictable cost**: 1 tool call per operation, consistent token ranges
|
||||
- **Complete source code**: AST-based extraction guarantees full symbol bodies
|
||||
- **Structural context**: Folded views show every symbol in matching files
|
||||
- **Speed**: Sub-second responses enable rapid iteration
|
||||
- **Composability**: Search, outline, and unfold chain naturally
|
||||
|
||||
### Explore Agent Strengths
|
||||
|
||||
- **Synthesized understanding**: Produces architecture narratives, data flow diagrams, and design pattern analysis
|
||||
- **Cross-cutting explanation**: Connects concepts across files that individual symbol reads cannot
|
||||
- **Onboarding quality**: Output reads like documentation, not raw code
|
||||
- **Error handling insight**: Identifies edge cases and design decisions that require reading multiple related functions
|
||||
- **No prior knowledge needed**: Can answer open-ended questions without knowing file paths or symbol names
|
||||
|
||||
### Quality by Task Type
|
||||
|
||||
| Task | Better Tool | Why |
|
||||
|------|-------------|-----|
|
||||
| "Where is X defined?" | Smart Explore | One call, exact answer |
|
||||
| "What functions are in this file?" | Smart Explore | Outline returns complete structural map |
|
||||
| "Show me this function" | Smart Explore | Unfold returns exact source, never truncates |
|
||||
| "How does feature X work end-to-end?" | Explore Agent | Reads multiple files and synthesizes narrative |
|
||||
| "What design patterns are used here?" | Explore Agent | Requires reading and interpreting, not just extracting |
|
||||
| "Help me understand this codebase" | Explore Agent | Produces onboarding-quality documentation |
|
||||
|
||||
## When to Use Which
|
||||
|
||||
**Use Smart Explore when:**
|
||||
- You know what you are looking for (function name, concept, file)
|
||||
- You need source code, not explanation
|
||||
- You are iterating quickly (read, modify, read again)
|
||||
- Token budget matters (large codebases, long sessions)
|
||||
- You need file structure at a glance
|
||||
|
||||
**Use the Explore Agent when:**
|
||||
- You need synthesized cross-cutting understanding
|
||||
- The question is open-ended ("how does this system work?")
|
||||
- You are writing documentation or architecture reviews
|
||||
- You need to understand *why*, not just *what*
|
||||
- You are onboarding to an unfamiliar codebase
|
||||
|
||||
**Use both when:**
|
||||
- Start with Smart Explore for discovery and navigation
|
||||
- Escalate to Explore Agent only for deep analysis that requires multi-file synthesis
|
||||
- This hybrid approach captures most of the token savings while preserving access to deep understanding when needed
|
||||
|
||||
## Token Economics Reference
|
||||
|
||||
| Operation | Tokens | Use Case |
|
||||
|-----------|:---:|----------|
|
||||
| `smart_search` | 2,000-6,000 | Cross-file symbol discovery |
|
||||
| `smart_outline` | 1,000-2,000 | Single file structural map |
|
||||
| `smart_unfold` | 400-2,100 | Single symbol full source |
|
||||
| `smart_search` + `smart_unfold` | 3,000-8,000 | End-to-end: find and read |
|
||||
| Explore Agent (targeted) | 18,000-28,000 | Single function with explanation |
|
||||
| Explore Agent (cross-cutting) | 39,000-59,000 | Architecture-level understanding |
|
||||
| Read (full file) | 8,000-15,000+ | Complete file contents |
|
||||
|
||||
### Savings by Workflow
|
||||
|
||||
| Workflow | Smart Explore | Traditional | Savings |
|
||||
|----------|:---:|:---:|:---:|
|
||||
| Understand one file | outline + unfold (~3,100 t) | Read full file (~12,000 t) | **4x** |
|
||||
| Find a function across codebase | search (~3,500 t) | Explore agent (~50,000 t) | **14x** |
|
||||
| Find and read a specific function | search + unfold (~4,500 t) | Explore agent (~50,000 t) | **11x** |
|
||||
| Navigate a 1,200-line file | outline (~1,500 t) | Read full file (~12,000 t) | **8x** |
|
||||
+11
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "claude-mem",
|
||||
"version": "10.4.4",
|
||||
"version": "10.5.5",
|
||||
"description": "Memory compression system for Claude Code - persist context across sessions",
|
||||
"keywords": [
|
||||
"claude",
|
||||
@@ -117,6 +117,16 @@
|
||||
"@types/react-dom": "^18.3.0",
|
||||
"esbuild": "^0.27.2",
|
||||
"np": "^11.0.2",
|
||||
"tree-sitter-c": "^0.24.1",
|
||||
"tree-sitter-cli": "^0.26.5",
|
||||
"tree-sitter-cpp": "^0.23.4",
|
||||
"tree-sitter-go": "^0.25.0",
|
||||
"tree-sitter-java": "^0.23.5",
|
||||
"tree-sitter-javascript": "^0.25.0",
|
||||
"tree-sitter-python": "^0.25.0",
|
||||
"tree-sitter-ruby": "^0.23.1",
|
||||
"tree-sitter-rust": "^0.24.0",
|
||||
"tree-sitter-typescript": "^0.23.2",
|
||||
"tsx": "^4.20.6",
|
||||
"typescript": "^5.3.0"
|
||||
}
|
||||
|
||||
@@ -1,52 +0,0 @@
|
||||
# Fix: SessionStart Hook "startup hook error" — Worker Not Waiting
|
||||
|
||||
## Root Cause
|
||||
|
||||
The **installed plugin** (`~/.claude/plugins/marketplaces/thedotmack/`) is version **10.2.5** and has **none** of the recent fixes:
|
||||
|
||||
| Fix | Repo Status | Installed Status |
|
||||
|-----|-------------|-----------------|
|
||||
| Hook group split (smart-install isolated from worker start) | In `plugin/hooks/hooks.json` | **Missing** — all 3 hooks in one group, smart-install failure blocks worker |
|
||||
| `waitForReadiness()` after spawn | In `src/services/infrastructure/HealthMonitor.ts` | **Missing** — 0 occurrences in installed `worker-service.cjs` |
|
||||
| Early `initializationCompleteFlag` (after DB+search, not MCP) | In `src/services/worker-service.ts` | **Missing** — flag set after MCP connection (5+ minute wait) |
|
||||
|
||||
The changes exist in source code but were **never built and synced** to the installed location.
|
||||
|
||||
---
|
||||
|
||||
## Phase 1: Build and Sync
|
||||
|
||||
```bash
|
||||
npm run build-and-sync
|
||||
```
|
||||
|
||||
### Verification
|
||||
|
||||
```bash
|
||||
# 1. Confirm waitForReadiness exists in installed build
|
||||
grep -c "waitForReadiness" ~/.claude/plugins/marketplaces/thedotmack/plugin/scripts/worker-service.cjs
|
||||
# Expected: > 0
|
||||
|
||||
# 2. Confirm hooks.json has two SessionStart groups (the split)
|
||||
python3 -c "import json; d=json.load(open('$(echo $HOME)/.claude/plugins/marketplaces/thedotmack/plugin/hooks/hooks.json')); print('SessionStart groups:', len(d['hooks']['SessionStart']))"
|
||||
# Expected: 2
|
||||
|
||||
# 3. Confirm initializationCompleteFlag is set before MCP connection
|
||||
grep -n "Core initialization complete" ~/.claude/plugins/marketplaces/thedotmack/plugin/scripts/worker-service.cjs | head -1
|
||||
# Expected: appears BEFORE "MCP server connected"
|
||||
```
|
||||
|
||||
## Phase 2: Restart Worker and Test
|
||||
|
||||
```bash
|
||||
# Stop existing worker
|
||||
bun plugin/scripts/worker-service.cjs stop
|
||||
|
||||
# Verify stopped
|
||||
curl -s http://127.0.0.1:37777/api/health && echo "STILL RUNNING" || echo "STOPPED"
|
||||
```
|
||||
|
||||
Then start a new Claude Code session and verify:
|
||||
- No "SessionStart:startup hook error" messages
|
||||
- Worker is running: `curl http://127.0.0.1:37777/api/health`
|
||||
- Readiness endpoint works: `curl http://127.0.0.1:37777/api/readiness`
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "claude-mem",
|
||||
"version": "10.4.4",
|
||||
"version": "10.5.5",
|
||||
"description": "Persistent memory system for Claude Code - seamlessly preserve context across sessions",
|
||||
"author": {
|
||||
"name": "Alex Newman"
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"name": "Law Study (Chill)",
|
||||
"prompts": {
|
||||
"recording_focus": "WHAT TO RECORD (HIGH SIGNAL ONLY)\n----------------------------------\nOnly record what would be painful to reconstruct later:\n- Issue-spotting triggers: specific fact patterns that signal a testable issue\n- Professor's explicit emphasis, frameworks, or exam tips\n- Counterintuitive holdings or gotchas that contradict intuition\n- Cross-case connections that reframe how a doctrine works\n- A synthesized rule only if it distills something non-obvious from multiple sources\n\nSkip anything that could be looked up in a casebook in under 60 seconds.\n\nUse verbs like: held, established, revealed, distinguished, flagged",
|
||||
"skip_guidance": "WHEN TO SKIP (LIBERAL — WHEN IN DOUBT, SKIP)\n---------------------------------------------\nSkip freely:\n- All case briefs, even condensed ones, unless the holding is counterintuitive\n- Any rule or doctrine stated plainly in the casebook without nuance\n- Definitions of standard legal terms\n- Procedural history\n- Any fact pattern or case that wasn't specifically emphasized by the professor\n- Anything you could find again in under 60 seconds\n- **No output necessary if skipping.**"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
# Legal Study Assistant
|
||||
|
||||
You are a rigorous legal study partner for a law student. Your job is to help them understand the law deeply enough to reason through novel fact patterns independently on exams and in practice.
|
||||
|
||||
---
|
||||
|
||||
## Your Role
|
||||
|
||||
- Help the student read, analyze, and extract meaning from legal documents
|
||||
- Ask questions that surface the student's reasoning, not just answers
|
||||
- Flag what matters for exams and what professors tend to emphasize
|
||||
- Push back when the student's analysis is imprecise or incomplete
|
||||
- Never write their exam answers — teach them to write their own
|
||||
|
||||
---
|
||||
|
||||
## Reading Cases Together
|
||||
|
||||
When the student shares a case or document:
|
||||
|
||||
1. Read it fully before saying anything. No skimming.
|
||||
2. Identify the procedural posture, then the issue, then the holding, then the reasoning.
|
||||
3. Separate holding from dicta explicitly — this distinction is always fair game.
|
||||
4. Surface ambiguity when the court was evasive. That ambiguity is often the exam question.
|
||||
5. Ask: "Which facts were outcome-determinative? What if those facts changed?"
|
||||
|
||||
**Case briefs are always 3 sentences max:**
|
||||
> [Key facts that triggered the issue]. The court held [holding + extracted rule]. [Why this rule exists or how it fits the doctrine — only if non-obvious.]
|
||||
|
||||
---
|
||||
|
||||
## Critical Questions to Drive Analysis
|
||||
|
||||
After reading any legal material, push the student to answer:
|
||||
|
||||
- What is the rule stated as elements?
|
||||
- What did the dissent argue and why does it matter?
|
||||
- How does this fit — or conflict with — earlier cases?
|
||||
- What fact pattern on an exam triggers this rule?
|
||||
- What does the professor emphasize about this? Their framing is the exam framing.
|
||||
- Is the law settled or contested here?
|
||||
|
||||
---
|
||||
|
||||
## Issue Spotting
|
||||
|
||||
When working through a fact pattern:
|
||||
|
||||
1. Read the entire hypo before naming any issues.
|
||||
2. List every potential claim and defense — err toward inclusion.
|
||||
3. For each issue: rule → application to these specific facts → where the argument turns.
|
||||
4. Treat "irrelevant" facts as planted triggers. Nothing in an exam hypo is accidental.
|
||||
5. Calibrate to the professor's emphasis — they wrote the exam.
|
||||
|
||||
---
|
||||
|
||||
## Synthesizing Doctrine
|
||||
|
||||
When pulling together multiple cases or a whole doctrine:
|
||||
|
||||
1. Find the common principle across all the cases.
|
||||
2. Build the rule as a spectrum or taxonomy when cases represent different scenarios.
|
||||
3. State the limiting principle — where does this rule stop and why.
|
||||
4. Majority rule first, then minority positions with their rationale.
|
||||
5. Identify the live tension — what the courts haven't resolved yet.
|
||||
|
||||
---
|
||||
|
||||
## Tone and Pace
|
||||
|
||||
- Be direct. Law school trains precision — model it.
|
||||
- When the student is vague, say so and ask them to be specific.
|
||||
- Celebrate when they spot something sharp. Legal reasoning is hard.
|
||||
- Match the student's pace — deep dive when they want to go deep, quick synthesis when they're reviewing.
|
||||
|
||||
---
|
||||
|
||||
## Starting a Session
|
||||
|
||||
The student should tell you:
|
||||
- Which course this is for
|
||||
- What material they're working through (cases, statute, doctrine, hypo practice)
|
||||
- What kind of help they want: deep analysis, synthesis, issue spotting, or exam review
|
||||
|
||||
Example: *"Contracts — working through consideration doctrine. Here are four cases. Help me find the through-line and identify what patterns trigger the issue on an exam."*
|
||||
@@ -0,0 +1,120 @@
|
||||
{
|
||||
"name": "Law Study",
|
||||
"description": "Legal study and exam preparation for law students",
|
||||
"version": "1.0.0",
|
||||
"observation_types": [
|
||||
{
|
||||
"id": "case-holding",
|
||||
"label": "Case Holding",
|
||||
"description": "Case brief (2-3 sentences: key facts + holding) with extracted legal rule",
|
||||
"emoji": "⚖️",
|
||||
"work_emoji": "📖"
|
||||
},
|
||||
{
|
||||
"id": "issue-pattern",
|
||||
"label": "Issue Pattern",
|
||||
"description": "Exam trigger or fact pattern that signals a legal issue to spot",
|
||||
"emoji": "🎯",
|
||||
"work_emoji": "🔍"
|
||||
},
|
||||
{
|
||||
"id": "prof-framework",
|
||||
"label": "Prof Framework",
|
||||
"description": "Professor's analytical lens, emphasis, or approach to a topic or doctrine",
|
||||
"emoji": "🧑🏫",
|
||||
"work_emoji": "📝"
|
||||
},
|
||||
{
|
||||
"id": "doctrine-rule",
|
||||
"label": "Doctrine / Rule",
|
||||
"description": "Legal test, standard, or doctrine synthesized from cases, statutes, or restatements",
|
||||
"emoji": "📜",
|
||||
"work_emoji": "🔍"
|
||||
},
|
||||
{
|
||||
"id": "argument-structure",
|
||||
"label": "Argument Structure",
|
||||
"description": "Legal argument or counter-argument worked through with analytical steps",
|
||||
"emoji": "🗣️",
|
||||
"work_emoji": "⚖️"
|
||||
},
|
||||
{
|
||||
"id": "cross-case-connection",
|
||||
"label": "Cross-Case Connection",
|
||||
"description": "Insight linking multiple cases, doctrines, or topics that reveals a deeper principle",
|
||||
"emoji": "🔗",
|
||||
"work_emoji": "🔍"
|
||||
}
|
||||
],
|
||||
"observation_concepts": [
|
||||
{
|
||||
"id": "exam-relevant",
|
||||
"label": "Exam Relevant",
|
||||
"description": "Flagged by professor or likely to appear on exams based on emphasis"
|
||||
},
|
||||
{
|
||||
"id": "minority-position",
|
||||
"label": "Minority Position",
|
||||
"description": "Dissent, minority rule, or alternative jurisdictional approach worth knowing"
|
||||
},
|
||||
{
|
||||
"id": "gotcha",
|
||||
"label": "Gotcha",
|
||||
"description": "Subtle nuance, counterintuitive result, or common mistake students get wrong"
|
||||
},
|
||||
{
|
||||
"id": "unsettled-law",
|
||||
"label": "Unsettled Law",
|
||||
"description": "Circuit split, open question, or evolving area of law"
|
||||
},
|
||||
{
|
||||
"id": "policy-rationale",
|
||||
"label": "Policy Rationale",
|
||||
"description": "Normative or policy argument underlying a rule or holding"
|
||||
},
|
||||
{
|
||||
"id": "course-theme",
|
||||
"label": "Course Theme",
|
||||
"description": "How this case or rule connects to the overarching narrative or theory of the course"
|
||||
}
|
||||
],
|
||||
"prompts": {
|
||||
"system_identity": "You are Claude-Mem, a specialized observer tool for creating searchable memory FOR FUTURE SESSIONS.\n\nCRITICAL: Record what was READ, ANALYZED, SYNTHESIZED, or LEARNED about the law, not what you (the observer) are doing.\n\nYou do not have access to tools. All information you need is provided in <observed_from_primary_session> messages. Create observations from what you observe - no investigation needed.",
|
||||
"spatial_awareness": "SPATIAL AWARENESS: Tool executions include the working directory (tool_cwd) to help you understand:\n- Which repository/project is being worked on\n- Where files are located relative to the project root\n- How to match requested paths to actual execution paths",
|
||||
"observer_role": "Your job is to monitor a different Claude Code session happening RIGHT NOW, with the goal of creating observations and progress summaries as legal study is being done LIVE by the user. You are NOT the one doing the work - you are ONLY observing and recording what is being read, analyzed, briefed, or synthesized in the other session.",
|
||||
"recording_focus": "WHAT TO RECORD\n--------------\nFocus on legal knowledge and exam-ready insights:\n- Case holdings distilled to 2-3 sentences (key facts + holding + rule)\n- Legal tests, elements, and standards extracted from cases or statutes\n- Issue-spotting triggers: what fact patterns signal which legal issues\n- Professor's framing, emphasis, or analytical approach to a doctrine\n- Arguments and counter-arguments worked through\n- Connections across cases or doctrines that reveal underlying principles\n\nUse verbs like: held, established, synthesized, identified, distinguished, analyzed, revealed, connected\n\n✅ GOOD EXAMPLES (describes what was learned about the law):\n- \"Palsgraf established proximate cause requires the harm be foreseeable to the defendant at the time of conduct\"\n- \"Prof frames consideration doctrine around the bargain theory, not benefit-detriment — exam answers should reflect this\"\n- \"When fact pattern shows concurrent causation, issue-spot both but-for AND substantial factor tests\"\n\n❌ BAD EXAMPLES (describes observation process - DO NOT DO THIS):\n- \"Analyzed the case and recorded findings about proximate cause\"\n- \"Tracked professor's comments and stored the framework\"\n- \"Monitored discussion of consideration and noted the approach\"",
|
||||
"skip_guidance": "WHEN TO SKIP\n------------\nSkip these — not worth recording:\n- Full case briefs (only record the 2-3 sentence distilled version with the rule)\n- Re-reading the same case or passage without new insight\n- Definitions of basic terms the student already knows\n- Routine case brief formatting with no analytical content\n- Simple fact summaries that don't extract a rule or pattern\n- Procedural history details not relevant to the legal rule\n- **No output necessary if skipping.**",
|
||||
"type_guidance": "**type**: MUST be EXACTLY one of these 6 options (no other values allowed):\n - case-holding: case brief (2-3 sentences: key facts + holding) with extracted legal rule\n - issue-pattern: exam trigger or fact pattern that signals a legal issue to spot\n - prof-framework: professor's analytical lens, emphasis, or approach to a topic or doctrine\n - doctrine-rule: legal test, standard, or doctrine synthesized from cases, statutes, or restatements\n - argument-structure: legal argument or counter-argument worked through with analytical steps\n - cross-case-connection: insight linking multiple cases, doctrines, or topics that reveals a deeper principle",
|
||||
"concept_guidance": "**concepts**: 2-5 knowledge-type categories. MUST use ONLY these exact keywords:\n - exam-relevant: flagged by professor or likely to appear on exams\n - minority-position: dissent, minority rule, or alternative jurisdictional approach\n - gotcha: subtle nuance, counterintuitive result, or common mistake\n - unsettled-law: circuit split, open question, or evolving area\n - policy-rationale: normative or policy argument underlying a rule\n - course-theme: connects to the overarching narrative or theory of the course\n\n IMPORTANT: Do NOT include the observation type (case-holding/issue-pattern/etc.) as a concept.\n Types and concepts are separate dimensions.",
|
||||
"field_guidance": "**facts**: Concise, self-contained statements\nEach fact is ONE piece of information\n No pronouns - each fact must stand alone\n Include specific details: case names, rule elements, test names, jurisdiction\n\n**files**: All files or documents read (full paths from project root)",
|
||||
"output_format_header": "OUTPUT FORMAT\n-------------\nOutput observations using this XML structure:",
|
||||
"format_examples": "",
|
||||
"footer": "IMPORTANT! DO NOT do any work right now other than generating this OBSERVATIONS from tool use messages - and remember that you are a memory agent designed to summarize a DIFFERENT claude code session, not this one.\n\nNever reference yourself or your own actions. Do not output anything other than the observation content formatted in the XML structure above. All other output is ignored by the system, and the system has been designed to be smart about token usage. Please spend your tokens wisely on useful observations.\n\nRemember that we record these observations as a way of helping us stay on track with our progress, and to help us keep important decisions and changes at the forefront of our minds! :) Thank you so much for your help!",
|
||||
|
||||
"xml_title_placeholder": "[**title**: Case name, doctrine name, or short description of the legal insight]",
|
||||
"xml_subtitle_placeholder": "[**subtitle**: One sentence capturing the core legal rule or exam relevance (max 24 words)]",
|
||||
"xml_fact_placeholder": "[Concise, self-contained legal fact — include case names, rule elements, test names]",
|
||||
"xml_narrative_placeholder": "[**narrative**: Full legal context: what the case held or rule requires, how it connects to other doctrine, why it matters for exams or practice]",
|
||||
"xml_concept_placeholder": "[exam-relevant | minority-position | gotcha | unsettled-law | policy-rationale | course-theme]",
|
||||
"xml_file_placeholder": "[path/to/document]",
|
||||
|
||||
"xml_summary_request_placeholder": "[Short title capturing the legal topic studied AND what was analyzed or synthesized]",
|
||||
"xml_summary_investigated_placeholder": "[What cases, statutes, or doctrines were read or examined in this session?]",
|
||||
"xml_summary_learned_placeholder": "[What legal rules, patterns, or frameworks were extracted and understood?]",
|
||||
"xml_summary_completed_placeholder": "[What study work was completed? Which cases briefed, which doctrines synthesized, which issue patterns identified?]",
|
||||
"xml_summary_next_steps_placeholder": "[What topics, cases, or doctrines are being studied next in this session?]",
|
||||
"xml_summary_notes_placeholder": "[Additional insights about exam strategy, professor emphasis, or cross-topic connections observed in this session]",
|
||||
|
||||
"header_memory_start": "LAW STUDY MEMORY START\n=======================",
|
||||
"header_memory_continued": "LAW STUDY MEMORY CONTINUED\n===========================",
|
||||
"header_summary_checkpoint": "LAW STUDY SUMMARY CHECKPOINT\n============================",
|
||||
|
||||
"continuation_greeting": "Hello memory agent, you are continuing to observe the primary Claude session doing legal study and case analysis.",
|
||||
"continuation_instruction": "IMPORTANT: Continue generating observations from tool use messages using the XML structure below.",
|
||||
|
||||
"summary_instruction": "Write progress notes of what legal material was studied, what rules and patterns were extracted, and what's next. This is a checkpoint to capture study progress so far. The session is ongoing - more cases or doctrines may be analyzed after this summary. Write \"next_steps\" as the current study trajectory (what topics or cases are actively being worked through), not as post-session plans. Always write at least a minimal summary explaining current progress, even if study is still early, so that users see a summary output tied to each study block.",
|
||||
"summary_context_label": "Claude's Full Response to User:",
|
||||
"summary_format_instruction": "Respond in this XML format:",
|
||||
"summary_footer": "IMPORTANT! DO NOT do any work right now other than generating this next PROGRESS SUMMARY - and remember that you are a memory agent designed to summarize a DIFFERENT claude code session, not this one.\n\nNever reference yourself or your own actions. Do not output anything other than the summary content formatted in the XML structure above. All other output is ignored by the system, and the system has been designed to be smart about token usage. Please spend your tokens wisely on useful summary content.\n\nThank you, this summary will be very useful for keeping track of legal study progress!"
|
||||
}
|
||||
}
|
||||
+11
-2
@@ -1,11 +1,20 @@
|
||||
{
|
||||
"name": "claude-mem-plugin",
|
||||
"version": "10.4.4",
|
||||
"version": "10.5.5",
|
||||
"private": true,
|
||||
"description": "Runtime dependencies for claude-mem bundled hooks",
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"@chroma-core/default-embed": "^0.1.9"
|
||||
"tree-sitter-cli": "^0.26.5",
|
||||
"tree-sitter-c": "^0.24.1",
|
||||
"tree-sitter-cpp": "^0.23.4",
|
||||
"tree-sitter-go": "^0.25.0",
|
||||
"tree-sitter-java": "^0.23.5",
|
||||
"tree-sitter-javascript": "^0.25.0",
|
||||
"tree-sitter-python": "^0.25.0",
|
||||
"tree-sitter-ruby": "^0.23.1",
|
||||
"tree-sitter-rust": "^0.24.0",
|
||||
"tree-sitter-typescript": "^0.23.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.0.0",
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
Never read built source files in this directory. These are compiled outputs — read the source files in `src/` instead.
|
||||
|
||||
<claude-mem-context>
|
||||
# Recent Activity
|
||||
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1,228 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# claude-mem Setup Hook
|
||||
# Ensures dependencies are installed before plugin runs
|
||||
#
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# Use CLAUDE_PLUGIN_ROOT if available, otherwise detect from script location
|
||||
if [[ -z "${CLAUDE_PLUGIN_ROOT:-}" ]]; then
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
ROOT="$(dirname "$SCRIPT_DIR")"
|
||||
else
|
||||
ROOT="$CLAUDE_PLUGIN_ROOT"
|
||||
fi
|
||||
|
||||
MARKER="$ROOT/.install-version"
|
||||
PKG_JSON="$ROOT/package.json"
|
||||
|
||||
# Colors (when terminal supports it)
|
||||
if [[ -t 2 ]]; then
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[0;33m'
|
||||
BLUE='\033[0;34m'
|
||||
NC='\033[0m' # No Color
|
||||
else
|
||||
RED='' GREEN='' YELLOW='' BLUE='' NC=''
|
||||
fi
|
||||
|
||||
log_info() { echo -e "${BLUE}ℹ${NC} $*" >&2; }
|
||||
log_ok() { echo -e "${GREEN}✓${NC} $*" >&2; }
|
||||
log_warn() { echo -e "${YELLOW}⚠${NC} $*" >&2; }
|
||||
log_error() { echo -e "${RED}✗${NC} $*" >&2; }
|
||||
|
||||
#
|
||||
# Detect Bun - check PATH and common locations
|
||||
#
|
||||
find_bun() {
|
||||
# Try PATH first
|
||||
if command -v bun &>/dev/null; then
|
||||
echo "bun"
|
||||
return 0
|
||||
fi
|
||||
|
||||
# Check common install locations
|
||||
local paths=(
|
||||
"$HOME/.bun/bin/bun"
|
||||
"/usr/local/bin/bun"
|
||||
"/opt/homebrew/bin/bun"
|
||||
)
|
||||
|
||||
for p in "${paths[@]}"; do
|
||||
if [[ -x "$p" ]]; then
|
||||
echo "$p"
|
||||
return 0
|
||||
fi
|
||||
done
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
#
|
||||
# Detect uv - check PATH and common locations
|
||||
#
|
||||
find_uv() {
|
||||
# Try PATH first
|
||||
if command -v uv &>/dev/null; then
|
||||
echo "uv"
|
||||
return 0
|
||||
fi
|
||||
|
||||
# Check common install locations
|
||||
local paths=(
|
||||
"$HOME/.local/bin/uv"
|
||||
"$HOME/.cargo/bin/uv"
|
||||
"/usr/local/bin/uv"
|
||||
"/opt/homebrew/bin/uv"
|
||||
)
|
||||
|
||||
for p in "${paths[@]}"; do
|
||||
if [[ -x "$p" ]]; then
|
||||
echo "$p"
|
||||
return 0
|
||||
fi
|
||||
done
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
#
|
||||
# Get package.json version
|
||||
#
|
||||
get_pkg_version() {
|
||||
if [[ -f "$PKG_JSON" ]]; then
|
||||
# Simple grep-based extraction (no jq dependency)
|
||||
grep -o '"version"[[:space:]]*:[[:space:]]*"[^"]*"' "$PKG_JSON" | head -1 | sed 's/.*"\([^"]*\)"$/\1/'
|
||||
fi
|
||||
}
|
||||
|
||||
#
|
||||
# Get marker version (if exists)
|
||||
#
|
||||
get_marker_version() {
|
||||
if [[ -f "$MARKER" ]]; then
|
||||
grep -o '"version"[[:space:]]*:[[:space:]]*"[^"]*"' "$MARKER" | head -1 | sed 's/.*"\([^"]*\)"$/\1/'
|
||||
fi
|
||||
}
|
||||
|
||||
#
|
||||
# Get marker's recorded bun version
|
||||
#
|
||||
get_marker_bun() {
|
||||
if [[ -f "$MARKER" ]]; then
|
||||
grep -o '"bun"[[:space:]]*:[[:space:]]*"[^"]*"' "$MARKER" | head -1 | sed 's/.*"\([^"]*\)"$/\1/'
|
||||
fi
|
||||
}
|
||||
|
||||
#
|
||||
# Check if install is needed
|
||||
#
|
||||
needs_install() {
|
||||
# No node_modules? Definitely need install
|
||||
if [[ ! -d "$ROOT/node_modules" ]]; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
# No marker? Need install
|
||||
if [[ ! -f "$MARKER" ]]; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
local pkg_ver marker_ver bun_ver marker_bun
|
||||
pkg_ver=$(get_pkg_version)
|
||||
marker_ver=$(get_marker_version)
|
||||
|
||||
# Version mismatch? Need install
|
||||
if [[ "$pkg_ver" != "$marker_ver" ]]; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
# Bun version changed? Need install
|
||||
if BUN_PATH=$(find_bun); then
|
||||
bun_ver=$("$BUN_PATH" --version 2>/dev/null || echo "")
|
||||
marker_bun=$(get_marker_bun)
|
||||
if [[ -n "$bun_ver" && "$bun_ver" != "$marker_bun" ]]; then
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
|
||||
# All good, no install needed
|
||||
return 1
|
||||
}
|
||||
|
||||
#
|
||||
# Write version marker after successful install
|
||||
#
|
||||
write_marker() {
|
||||
local bun_ver uv_ver pkg_ver
|
||||
pkg_ver=$(get_pkg_version)
|
||||
bun_ver=$("$BUN_PATH" --version 2>/dev/null || echo "unknown")
|
||||
|
||||
if UV_PATH=$(find_uv); then
|
||||
uv_ver=$("$UV_PATH" --version 2>/dev/null | head -1 || echo "unknown")
|
||||
else
|
||||
uv_ver="not-installed"
|
||||
fi
|
||||
|
||||
cat > "$MARKER" <<EOF
|
||||
{
|
||||
"version": "$pkg_ver",
|
||||
"bun": "$bun_ver",
|
||||
"uv": "$uv_ver",
|
||||
"installedAt": "$(date -u +%Y-%m-%dT%H:%M:%SZ)"
|
||||
}
|
||||
EOF
|
||||
}
|
||||
|
||||
#
|
||||
# Main
|
||||
#
|
||||
|
||||
# 1. Check for Bun
|
||||
BUN_PATH=$(find_bun) || true
|
||||
if [[ -z "$BUN_PATH" ]]; then
|
||||
log_error "Bun runtime not found!"
|
||||
echo "" >&2
|
||||
echo "claude-mem requires Bun to run. Please install it:" >&2
|
||||
echo "" >&2
|
||||
echo " curl -fsSL https://bun.sh/install | bash" >&2
|
||||
echo "" >&2
|
||||
echo "Or on macOS with Homebrew:" >&2
|
||||
echo "" >&2
|
||||
echo " brew install oven-sh/bun/bun" >&2
|
||||
echo "" >&2
|
||||
echo "Then restart your terminal and try again." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
BUN_VERSION=$("$BUN_PATH" --version 2>/dev/null || echo "unknown")
|
||||
log_ok "Bun $BUN_VERSION found at $BUN_PATH"
|
||||
|
||||
# 2. Check for uv (optional - for Python/Chroma support)
|
||||
UV_PATH=$(find_uv) || true
|
||||
if [[ -z "$UV_PATH" ]]; then
|
||||
log_warn "uv not found (optional - needed for Python/Chroma vector search)"
|
||||
echo " To install: curl -LsSf https://astral.sh/uv/install.sh | sh" >&2
|
||||
else
|
||||
UV_VERSION=$("$UV_PATH" --version 2>/dev/null | head -1 || echo "unknown")
|
||||
log_ok "uv $UV_VERSION found"
|
||||
fi
|
||||
|
||||
# 3. Install dependencies if needed
|
||||
if needs_install; then
|
||||
log_info "Installing dependencies with Bun..."
|
||||
|
||||
if ! "$BUN_PATH" install --cwd "$ROOT"; then
|
||||
log_error "Failed to install dependencies"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
write_marker
|
||||
log_ok "Dependencies installed ($(get_pkg_version))"
|
||||
else
|
||||
log_ok "Dependencies up to date ($(get_marker_version))"
|
||||
fi
|
||||
|
||||
exit 0
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,145 @@
|
||||
---
|
||||
name: smart-explore
|
||||
description: Token-optimized structural code search using tree-sitter AST parsing. Use instead of reading full files when you need to understand code structure, find functions, or explore a codebase efficiently.
|
||||
---
|
||||
|
||||
# Smart Explore
|
||||
|
||||
Structural code exploration using AST parsing. **This skill overrides your default exploration behavior.** While this skill is active, use smart_search/smart_outline/smart_unfold as your primary tools instead of Read, Grep, and Glob.
|
||||
|
||||
**Core principle:** Index first, fetch on demand. Give yourself a map of the code before loading implementation details. The question before every file read should be: "do I need to see all of this, or can I get a structural overview first?" The answer is almost always: get the map.
|
||||
|
||||
## Your Next Tool Call
|
||||
|
||||
This skill only loads instructions. You must call the MCP tools yourself. Your next action should be one of:
|
||||
|
||||
```
|
||||
smart_search(query="<topic>", path="./src") -- discover files + symbols across a directory
|
||||
smart_outline(file_path="<file>") -- structural skeleton of one file
|
||||
smart_unfold(file_path="<file>", symbol_name="<name>") -- full source of one symbol
|
||||
```
|
||||
|
||||
Do NOT run Grep, Glob, Read, or find to discover files first. `smart_search` walks directories, parses all code files, and returns ranked symbols in one call. It replaces the Glob → Grep → Read discovery cycle.
|
||||
|
||||
## 3-Layer Workflow
|
||||
|
||||
### Step 1: Search -- Discover Files and Symbols
|
||||
|
||||
```
|
||||
smart_search(query="shutdown", path="./src", max_results=15)
|
||||
```
|
||||
|
||||
**Returns:** Ranked symbols with signatures, line numbers, match reasons, plus folded file views (~2-6k tokens)
|
||||
|
||||
```
|
||||
-- Matching Symbols --
|
||||
function performGracefulShutdown (services/infrastructure/GracefulShutdown.ts:56)
|
||||
function httpShutdown (services/infrastructure/HealthMonitor.ts:92)
|
||||
method WorkerService.shutdown (services/worker-service.ts:846)
|
||||
|
||||
-- Folded File Views --
|
||||
services/infrastructure/GracefulShutdown.ts (7 symbols)
|
||||
services/worker-service.ts (12 symbols)
|
||||
```
|
||||
|
||||
This is your discovery tool. It finds relevant files AND shows their structure. No Glob/find pre-scan needed.
|
||||
|
||||
**Parameters:**
|
||||
|
||||
- `query` (string, required) -- What to search for (function name, concept, class name)
|
||||
- `path` (string) -- Root directory to search (defaults to cwd)
|
||||
- `max_results` (number) -- Max matching symbols, default 20, max 50
|
||||
- `file_pattern` (string, optional) -- Filter to specific files/paths
|
||||
|
||||
### Step 2: Outline -- Get File Structure
|
||||
|
||||
```
|
||||
smart_outline(file_path="services/worker-service.ts")
|
||||
```
|
||||
|
||||
**Returns:** Complete structural skeleton -- all functions, classes, methods, properties, imports (~1-2k tokens per file)
|
||||
|
||||
**Skip this step** when Step 1's folded file views already provide enough structure. Most useful for files not covered by the search results.
|
||||
|
||||
**Parameters:**
|
||||
|
||||
- `file_path` (string, required) -- Path to the file
|
||||
|
||||
### Step 3: Unfold -- See Implementation
|
||||
|
||||
Review symbols from Steps 1-2. Pick the ones you need. Unfold only those:
|
||||
|
||||
```
|
||||
smart_unfold(file_path="services/worker-service.ts", symbol_name="shutdown")
|
||||
```
|
||||
|
||||
**Returns:** Full source code of the specified symbol including JSDoc, decorators, and complete implementation (~400-2,100 tokens depending on symbol size). AST node boundaries guarantee completeness regardless of symbol size — unlike Read + agent summarization, which may truncate long methods.
|
||||
|
||||
**Parameters:**
|
||||
|
||||
- `file_path` (string, required) -- Path to the file (as returned by search/outline)
|
||||
- `symbol_name` (string, required) -- Name of the function/class/method to expand
|
||||
|
||||
## When to Use Standard Tools Instead
|
||||
|
||||
Use these only when smart_* tools are the wrong fit:
|
||||
|
||||
- **Grep:** Exact string/regex search ("find all TODO comments", "where is `ensureWorkerStarted` defined?")
|
||||
- **Read:** Small files under ~100 lines, non-code files (JSON, markdown, config)
|
||||
- **Glob:** File path patterns ("find all test files")
|
||||
- **Explore agent:** When you need synthesized understanding across 6+ files, architecture narratives, or answers to open-ended questions like "how does this entire system work end-to-end?" Smart-explore is a scalpel — it answers "where is this?" and "show me that." It doesn't synthesize cross-file data flows, design decisions, or edge cases across an entire feature.
|
||||
|
||||
For code files over ~100 lines, prefer smart_outline + smart_unfold over Read.
|
||||
|
||||
## Workflow Examples
|
||||
|
||||
**Discover how a feature works (cross-cutting):**
|
||||
|
||||
```
|
||||
1. smart_search(query="shutdown", path="./src")
|
||||
-> 14 symbols across 7 files, full picture in one call
|
||||
2. smart_unfold(file_path="services/infrastructure/GracefulShutdown.ts", symbol_name="performGracefulShutdown")
|
||||
-> See the core implementation
|
||||
```
|
||||
|
||||
**Navigate a large file:**
|
||||
|
||||
```
|
||||
1. smart_outline(file_path="services/worker-service.ts")
|
||||
-> 1,466 tokens: 12 functions, WorkerService class with 24 members
|
||||
2. smart_unfold(file_path="services/worker-service.ts", symbol_name="startSessionProcessor")
|
||||
-> 1,610 tokens: the specific method you need
|
||||
Total: ~3,076 tokens vs ~12,000 to Read the full file
|
||||
```
|
||||
|
||||
**Write documentation about code (hybrid workflow):**
|
||||
|
||||
```
|
||||
1. smart_search(query="feature name", path="./src") -- discover all relevant files and symbols
|
||||
2. smart_outline on key files -- understand structure
|
||||
3. smart_unfold on important functions -- get implementation details
|
||||
4. Read on small config/markdown/plan files -- get non-code context
|
||||
```
|
||||
|
||||
Use smart_* tools for code exploration, Read for non-code files. Mix freely.
|
||||
|
||||
**Exploration then precision:**
|
||||
|
||||
```
|
||||
1. smart_search(query="session", path="./src", max_results=10)
|
||||
-> 10 ranked symbols: SessionMetadata, SessionQueueProcessor, SessionSummary...
|
||||
2. Pick the relevant one, unfold it
|
||||
```
|
||||
|
||||
## Token Economics
|
||||
|
||||
| Approach | Tokens | Use Case |
|
||||
|----------|--------|----------|
|
||||
| smart_outline | ~1,000-2,000 | "What's in this file?" |
|
||||
| smart_unfold | ~400-2,100 | "Show me this function" |
|
||||
| smart_search | ~2,000-6,000 | "Find all X across the codebase" |
|
||||
| search + unfold | ~3,000-8,000 | End-to-end: find and read (the primary workflow) |
|
||||
| Read (full file) | ~12,000+ | When you truly need everything |
|
||||
| Explore agent | ~39,000-59,000 | Cross-file synthesis with narrative |
|
||||
|
||||
**4-8x savings** on file understanding (outline + unfold vs Read). **11-18x savings** on codebase exploration vs Explore agent. The narrower the query, the wider the gap — a 27-line function costs 55x less to read via unfold than via an Explore agent, because the agent still reads the entire file.
|
||||
+11
-11
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
|
||||
Never read built source files in this directory. These are compiled outputs — read the source files in `src/` instead.
|
||||
+24
-3
@@ -59,8 +59,16 @@ async function buildHooks() {
|
||||
description: 'Runtime dependencies for claude-mem bundled hooks',
|
||||
type: 'module',
|
||||
dependencies: {
|
||||
// Chroma embedding function with native ONNX binaries (can't be bundled)
|
||||
'@chroma-core/default-embed': '^0.1.9'
|
||||
'tree-sitter-cli': '^0.26.5',
|
||||
'tree-sitter-c': '^0.24.1',
|
||||
'tree-sitter-cpp': '^0.23.4',
|
||||
'tree-sitter-go': '^0.25.0',
|
||||
'tree-sitter-java': '^0.23.5',
|
||||
'tree-sitter-javascript': '^0.25.0',
|
||||
'tree-sitter-python': '^0.25.0',
|
||||
'tree-sitter-ruby': '^0.23.1',
|
||||
'tree-sitter-rust': '^0.24.0',
|
||||
'tree-sitter-typescript': '^0.23.2',
|
||||
},
|
||||
engines: {
|
||||
node: '>=18.0.0',
|
||||
@@ -128,7 +136,19 @@ async function buildHooks() {
|
||||
outfile: `${hooksDir}/${MCP_SERVER.name}.cjs`,
|
||||
minify: true,
|
||||
logLevel: 'error',
|
||||
external: ['bun:sqlite'],
|
||||
external: [
|
||||
'bun:sqlite',
|
||||
'tree-sitter-cli',
|
||||
'tree-sitter-javascript',
|
||||
'tree-sitter-typescript',
|
||||
'tree-sitter-python',
|
||||
'tree-sitter-go',
|
||||
'tree-sitter-rust',
|
||||
'tree-sitter-ruby',
|
||||
'tree-sitter-java',
|
||||
'tree-sitter-c',
|
||||
'tree-sitter-cpp',
|
||||
],
|
||||
define: {
|
||||
'__DEFAULT_PACKAGE_VERSION__': `"${version}"`
|
||||
},
|
||||
@@ -166,6 +186,7 @@ async function buildHooks() {
|
||||
console.log('\n📋 Verifying distribution files...');
|
||||
const requiredDistributionFiles = [
|
||||
'plugin/skills/mem-search/SKILL.md',
|
||||
'plugin/skills/smart-explore/SKILL.md',
|
||||
'plugin/hooks/hooks.json',
|
||||
'plugin/.claude-plugin/plugin.json',
|
||||
];
|
||||
|
||||
@@ -76,7 +76,7 @@ try {
|
||||
const gitignoreExcludes = getGitignoreExcludes(rootDir);
|
||||
|
||||
execSync(
|
||||
`rsync -av --delete --exclude=.git --exclude=/.mcp.json --exclude=bun.lock --exclude=package-lock.json ${gitignoreExcludes} ./ ~/.claude/plugins/marketplaces/thedotmack/`,
|
||||
`rsync -av --delete --exclude=.git --exclude=bun.lock --exclude=package-lock.json ${gitignoreExcludes} ./ ~/.claude/plugins/marketplaces/thedotmack/`,
|
||||
{ stdio: 'inherit' }
|
||||
);
|
||||
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
/**
|
||||
* Observation metadata constants
|
||||
* Shared across hooks, worker service, and UI components
|
||||
*
|
||||
* Note: These are fallback defaults for the code mode.
|
||||
* Actual observation types and concepts are defined per-mode in the modes/ directory.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Default observation types (comma-separated string for settings)
|
||||
* Uses code mode defaults as fallback
|
||||
*/
|
||||
export const DEFAULT_OBSERVATION_TYPES_STRING = 'bugfix,feature,refactor,discovery,decision,change';
|
||||
|
||||
/**
|
||||
* Default observation concepts (comma-separated string for settings)
|
||||
* Uses code mode defaults as fallback
|
||||
*/
|
||||
export const DEFAULT_OBSERVATION_CONCEPTS_STRING = 'how-it-works,why-it-exists,what-changed,problem-solution,gotcha,pattern,trade-off';
|
||||
@@ -28,6 +28,10 @@ import {
|
||||
ListToolsRequestSchema,
|
||||
} from '@modelcontextprotocol/sdk/types.js';
|
||||
import { getWorkerPort, getWorkerHost } from '../shared/worker-utils.js';
|
||||
import { searchCodebase, formatSearchResults } from '../services/smart-file-read/search.js';
|
||||
import { parseFile, formatFoldedView, unfoldSymbol } from '../services/smart-file-read/parser.js';
|
||||
import { readFile } from 'node:fs/promises';
|
||||
import { resolve } from 'node:path';
|
||||
|
||||
/**
|
||||
* Worker HTTP API configuration
|
||||
@@ -233,6 +237,118 @@ NEVER fetch full details without filtering first. 10x token savings.`,
|
||||
handler: async (args: any) => {
|
||||
return await callWorkerAPIPost('/api/observations/batch', args);
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'smart_search',
|
||||
description: 'Search codebase for symbols, functions, classes using tree-sitter AST parsing. Returns folded structural views with token counts. Use path parameter to scope the search.',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
query: {
|
||||
type: 'string',
|
||||
description: 'Search term — matches against symbol names, file names, and file content'
|
||||
},
|
||||
path: {
|
||||
type: 'string',
|
||||
description: 'Root directory to search (default: current working directory)'
|
||||
},
|
||||
max_results: {
|
||||
type: 'number',
|
||||
description: 'Maximum results to return (default: 20)'
|
||||
},
|
||||
file_pattern: {
|
||||
type: 'string',
|
||||
description: 'Substring filter for file paths (e.g. ".ts", "src/services")'
|
||||
}
|
||||
},
|
||||
required: ['query']
|
||||
},
|
||||
handler: async (args: any) => {
|
||||
const rootDir = resolve(args.path || process.cwd());
|
||||
const result = await searchCodebase(rootDir, args.query, {
|
||||
maxResults: args.max_results || 20,
|
||||
filePattern: args.file_pattern
|
||||
});
|
||||
const formatted = formatSearchResults(result, args.query);
|
||||
return {
|
||||
content: [{ type: 'text' as const, text: formatted }]
|
||||
};
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'smart_unfold',
|
||||
description: 'Expand a specific symbol (function, class, method) from a file. Returns the full source code of just that symbol. Use after smart_search or smart_outline to read specific code.',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
file_path: {
|
||||
type: 'string',
|
||||
description: 'Path to the source file'
|
||||
},
|
||||
symbol_name: {
|
||||
type: 'string',
|
||||
description: 'Name of the symbol to unfold (function, class, method, etc.)'
|
||||
}
|
||||
},
|
||||
required: ['file_path', 'symbol_name']
|
||||
},
|
||||
handler: async (args: any) => {
|
||||
const filePath = resolve(args.file_path);
|
||||
const content = await readFile(filePath, 'utf-8');
|
||||
const unfolded = unfoldSymbol(content, filePath, args.symbol_name);
|
||||
if (unfolded) {
|
||||
return {
|
||||
content: [{ type: 'text' as const, text: unfolded }]
|
||||
};
|
||||
}
|
||||
// Symbol not found — show available symbols
|
||||
const parsed = parseFile(content, filePath);
|
||||
if (parsed.symbols.length > 0) {
|
||||
const available = parsed.symbols.map(s => ` - ${s.name} (${s.kind})`).join('\n');
|
||||
return {
|
||||
content: [{
|
||||
type: 'text' as const,
|
||||
text: `Symbol "${args.symbol_name}" not found in ${args.file_path}.\n\nAvailable symbols:\n${available}`
|
||||
}]
|
||||
};
|
||||
}
|
||||
return {
|
||||
content: [{
|
||||
type: 'text' as const,
|
||||
text: `Could not parse ${args.file_path}. File may be unsupported or empty.`
|
||||
}]
|
||||
};
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'smart_outline',
|
||||
description: 'Get structural outline of a file — shows all symbols (functions, classes, methods, types) with signatures but bodies folded. Much cheaper than reading the full file.',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
file_path: {
|
||||
type: 'string',
|
||||
description: 'Path to the source file'
|
||||
}
|
||||
},
|
||||
required: ['file_path']
|
||||
},
|
||||
handler: async (args: any) => {
|
||||
const filePath = resolve(args.file_path);
|
||||
const content = await readFile(filePath, 'utf-8');
|
||||
const parsed = parseFile(content, filePath);
|
||||
if (parsed.symbols.length > 0) {
|
||||
return {
|
||||
content: [{ type: 'text' as const, text: formatFoldedView(parsed) }]
|
||||
};
|
||||
}
|
||||
return {
|
||||
content: [{
|
||||
type: 'text' as const,
|
||||
text: `Could not parse ${args.file_path}. File may use an unsupported language or be empty.`
|
||||
}]
|
||||
};
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
|
||||
@@ -18,27 +18,10 @@ export function loadContextConfig(): ContextConfig {
|
||||
const settingsPath = path.join(homedir(), '.claude-mem', 'settings.json');
|
||||
const settings = SettingsDefaultsManager.loadFromFile(settingsPath);
|
||||
|
||||
// For non-code modes, use all types/concepts from active mode instead of settings
|
||||
const modeId = settings.CLAUDE_MEM_MODE;
|
||||
const isCodeMode = modeId === 'code' || modeId.startsWith('code--');
|
||||
|
||||
let observationTypes: Set<string>;
|
||||
let observationConcepts: Set<string>;
|
||||
|
||||
if (isCodeMode) {
|
||||
// Code mode: use settings-based filtering
|
||||
observationTypes = new Set(
|
||||
settings.CLAUDE_MEM_CONTEXT_OBSERVATION_TYPES.split(',').map((t: string) => t.trim()).filter(Boolean)
|
||||
);
|
||||
observationConcepts = new Set(
|
||||
settings.CLAUDE_MEM_CONTEXT_OBSERVATION_CONCEPTS.split(',').map((c: string) => c.trim()).filter(Boolean)
|
||||
);
|
||||
} else {
|
||||
// Non-code modes: use all types/concepts from active mode
|
||||
const mode = ModeManager.getInstance().getActiveMode();
|
||||
observationTypes = new Set(mode.observation_types.map(t => t.id));
|
||||
observationConcepts = new Set(mode.observation_concepts.map(c => c.id));
|
||||
}
|
||||
// Always read types/concepts from the active mode definition
|
||||
const mode = ModeManager.getInstance().getActiveMode();
|
||||
const observationTypes = new Set(mode.observation_types.map(t => t.id));
|
||||
const observationConcepts = new Set(mode.observation_concepts.map(c => c.id));
|
||||
|
||||
return {
|
||||
totalObservationCount: parseInt(settings.CLAUDE_MEM_CONTEXT_OBSERVATIONS, 10),
|
||||
|
||||
@@ -0,0 +1,666 @@
|
||||
/**
|
||||
* Code structure parser — shells out to tree-sitter CLI for AST-based extraction.
|
||||
*
|
||||
* No native bindings. No WASM. Just the CLI binary + query patterns.
|
||||
*
|
||||
* Supported: JS, TS, Python, Go, Rust, Ruby, Java, C, C++
|
||||
*
|
||||
* by Copter Labs
|
||||
*/
|
||||
|
||||
import { execFileSync } from "node:child_process";
|
||||
import { writeFileSync, mkdtempSync, rmSync, existsSync } from "node:fs";
|
||||
import { join, dirname } from "node:path";
|
||||
import { tmpdir } from "node:os";
|
||||
import { createRequire } from "node:module";
|
||||
|
||||
// CJS-safe require for resolving external packages at runtime.
|
||||
// In ESM: import.meta.url works. In CJS bundle (esbuild): __filename works.
|
||||
// typeof check avoids ReferenceError in ESM where __filename doesn't exist.
|
||||
const _require = typeof __filename !== 'undefined'
|
||||
? createRequire(__filename)
|
||||
: createRequire(import.meta.url);
|
||||
|
||||
// --- Types ---
|
||||
|
||||
export interface CodeSymbol {
|
||||
name: string;
|
||||
kind: "function" | "class" | "method" | "interface" | "type" | "const" | "variable" | "export" | "struct" | "enum" | "trait" | "impl" | "property" | "getter" | "setter";
|
||||
signature: string;
|
||||
jsdoc?: string;
|
||||
lineStart: number;
|
||||
lineEnd: number;
|
||||
parent?: string;
|
||||
exported: boolean;
|
||||
children?: CodeSymbol[];
|
||||
}
|
||||
|
||||
export interface FoldedFile {
|
||||
filePath: string;
|
||||
language: string;
|
||||
symbols: CodeSymbol[];
|
||||
imports: string[];
|
||||
totalLines: number;
|
||||
foldedTokenEstimate: number;
|
||||
}
|
||||
|
||||
// --- Language detection ---
|
||||
|
||||
const LANG_MAP: Record<string, string> = {
|
||||
".js": "javascript",
|
||||
".mjs": "javascript",
|
||||
".cjs": "javascript",
|
||||
".jsx": "tsx",
|
||||
".ts": "typescript",
|
||||
".tsx": "tsx",
|
||||
".py": "python",
|
||||
".pyw": "python",
|
||||
".go": "go",
|
||||
".rs": "rust",
|
||||
".rb": "ruby",
|
||||
".java": "java",
|
||||
".c": "c",
|
||||
".h": "c",
|
||||
".cpp": "cpp",
|
||||
".cc": "cpp",
|
||||
".cxx": "cpp",
|
||||
".hpp": "cpp",
|
||||
".hh": "cpp",
|
||||
};
|
||||
|
||||
export function detectLanguage(filePath: string): string {
|
||||
const ext = filePath.slice(filePath.lastIndexOf("."));
|
||||
return LANG_MAP[ext] || "unknown";
|
||||
}
|
||||
|
||||
// --- Grammar path resolution ---
|
||||
|
||||
const GRAMMAR_PACKAGES: Record<string, string> = {
|
||||
javascript: "tree-sitter-javascript",
|
||||
typescript: "tree-sitter-typescript/typescript",
|
||||
tsx: "tree-sitter-typescript/tsx",
|
||||
python: "tree-sitter-python",
|
||||
go: "tree-sitter-go",
|
||||
rust: "tree-sitter-rust",
|
||||
ruby: "tree-sitter-ruby",
|
||||
java: "tree-sitter-java",
|
||||
c: "tree-sitter-c",
|
||||
cpp: "tree-sitter-cpp",
|
||||
};
|
||||
|
||||
function resolveGrammarPath(language: string): string | null {
|
||||
const pkg = GRAMMAR_PACKAGES[language];
|
||||
if (!pkg) return null;
|
||||
try {
|
||||
const packageJsonPath = _require.resolve(pkg + "/package.json");
|
||||
return dirname(packageJsonPath);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// --- Query patterns (declarative symbol extraction) ---
|
||||
|
||||
const QUERIES: Record<string, string> = {
|
||||
jsts: `
|
||||
(function_declaration name: (identifier) @name) @func
|
||||
(lexical_declaration (variable_declarator name: (identifier) @name value: [(arrow_function) (function_expression)])) @const_func
|
||||
(class_declaration name: (type_identifier) @name) @cls
|
||||
(method_definition name: (property_identifier) @name) @method
|
||||
(interface_declaration name: (type_identifier) @name) @iface
|
||||
(type_alias_declaration name: (type_identifier) @name) @tdef
|
||||
(enum_declaration name: (identifier) @name) @enm
|
||||
(import_statement) @imp
|
||||
(export_statement) @exp
|
||||
`,
|
||||
|
||||
python: `
|
||||
(function_definition name: (identifier) @name) @func
|
||||
(class_definition name: (identifier) @name) @cls
|
||||
(import_statement) @imp
|
||||
(import_from_statement) @imp
|
||||
`,
|
||||
|
||||
go: `
|
||||
(function_declaration name: (identifier) @name) @func
|
||||
(method_declaration name: (field_identifier) @name) @method
|
||||
(type_declaration (type_spec name: (type_identifier) @name)) @tdef
|
||||
(import_declaration) @imp
|
||||
`,
|
||||
|
||||
rust: `
|
||||
(function_item name: (identifier) @name) @func
|
||||
(struct_item name: (type_identifier) @name) @struct_def
|
||||
(enum_item name: (type_identifier) @name) @enm
|
||||
(trait_item name: (type_identifier) @name) @trait_def
|
||||
(impl_item type: (type_identifier) @name) @impl_def
|
||||
(use_declaration) @imp
|
||||
`,
|
||||
|
||||
ruby: `
|
||||
(method name: (identifier) @name) @func
|
||||
(class name: (constant) @name) @cls
|
||||
(module name: (constant) @name) @cls
|
||||
(call method: (identifier) @name) @imp
|
||||
`,
|
||||
|
||||
java: `
|
||||
(method_declaration name: (identifier) @name) @method
|
||||
(class_declaration name: (identifier) @name) @cls
|
||||
(interface_declaration name: (identifier) @name) @iface
|
||||
(enum_declaration name: (identifier) @name) @enm
|
||||
(import_declaration) @imp
|
||||
`,
|
||||
|
||||
generic: `
|
||||
(function_declaration name: (identifier) @name) @func
|
||||
(function_definition name: (identifier) @name) @func
|
||||
(class_declaration name: (identifier) @name) @cls
|
||||
(class_definition name: (identifier) @name) @cls
|
||||
(import_statement) @imp
|
||||
(import_declaration) @imp
|
||||
`,
|
||||
};
|
||||
|
||||
function getQueryKey(language: string): string {
|
||||
switch (language) {
|
||||
case "javascript":
|
||||
case "typescript":
|
||||
case "tsx":
|
||||
return "jsts";
|
||||
case "python": return "python";
|
||||
case "go": return "go";
|
||||
case "rust": return "rust";
|
||||
case "ruby": return "ruby";
|
||||
case "java": return "java";
|
||||
default: return "generic";
|
||||
}
|
||||
}
|
||||
|
||||
// --- Temp file management ---
|
||||
|
||||
let queryTmpDir: string | null = null;
|
||||
const queryFileCache = new Map<string, string>();
|
||||
|
||||
function getQueryFile(queryKey: string): string {
|
||||
if (queryFileCache.has(queryKey)) return queryFileCache.get(queryKey)!;
|
||||
|
||||
if (!queryTmpDir) {
|
||||
queryTmpDir = mkdtempSync(join(tmpdir(), "smart-read-queries-"));
|
||||
}
|
||||
|
||||
const filePath = join(queryTmpDir, `${queryKey}.scm`);
|
||||
writeFileSync(filePath, QUERIES[queryKey]);
|
||||
queryFileCache.set(queryKey, filePath);
|
||||
return filePath;
|
||||
}
|
||||
|
||||
// --- CLI execution ---
|
||||
|
||||
let cachedBinPath: string | null = null;
|
||||
|
||||
function getTreeSitterBin(): string {
|
||||
if (cachedBinPath) return cachedBinPath;
|
||||
|
||||
// Try direct binary from tree-sitter-cli package
|
||||
try {
|
||||
const pkgPath = _require.resolve("tree-sitter-cli/package.json");
|
||||
const binPath = join(dirname(pkgPath), "tree-sitter");
|
||||
if (existsSync(binPath)) {
|
||||
cachedBinPath = binPath;
|
||||
return binPath;
|
||||
}
|
||||
} catch { /* fall through */ }
|
||||
|
||||
// Fallback: assume it's on PATH
|
||||
cachedBinPath = "tree-sitter";
|
||||
return cachedBinPath;
|
||||
}
|
||||
|
||||
interface RawCapture {
|
||||
tag: string;
|
||||
startRow: number;
|
||||
startCol: number;
|
||||
endRow: number;
|
||||
endCol: number;
|
||||
text?: string;
|
||||
}
|
||||
|
||||
interface RawMatch {
|
||||
pattern: number;
|
||||
captures: RawCapture[];
|
||||
}
|
||||
|
||||
function runQuery(queryFile: string, sourceFile: string, grammarPath: string): RawMatch[] {
|
||||
const result = runBatchQuery(queryFile, [sourceFile], grammarPath);
|
||||
return result.get(sourceFile) || [];
|
||||
}
|
||||
|
||||
function runBatchQuery(queryFile: string, sourceFiles: string[], grammarPath: string): Map<string, RawMatch[]> {
|
||||
if (sourceFiles.length === 0) return new Map();
|
||||
|
||||
const bin = getTreeSitterBin();
|
||||
const execArgs = ["query", "-p", grammarPath, queryFile, ...sourceFiles];
|
||||
|
||||
let output: string;
|
||||
try {
|
||||
output = execFileSync(bin, execArgs, { encoding: "utf-8", timeout: 30000, stdio: ["pipe", "pipe", "pipe"] });
|
||||
} catch {
|
||||
return new Map();
|
||||
}
|
||||
|
||||
return parseMultiFileQueryOutput(output);
|
||||
}
|
||||
|
||||
function parseMultiFileQueryOutput(output: string): Map<string, RawMatch[]> {
|
||||
const fileMatches = new Map<string, RawMatch[]>();
|
||||
let currentFile: string | null = null;
|
||||
let currentMatch: RawMatch | null = null;
|
||||
|
||||
for (const line of output.split("\n")) {
|
||||
// File header: a line that doesn't start with whitespace and isn't empty
|
||||
if (line.length > 0 && !line.startsWith(" ") && !line.startsWith("\t")) {
|
||||
currentFile = line.trim();
|
||||
if (!fileMatches.has(currentFile)) {
|
||||
fileMatches.set(currentFile, []);
|
||||
}
|
||||
currentMatch = null;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!currentFile) continue;
|
||||
|
||||
const patternMatch = line.match(/^\s+pattern:\s+(\d+)/);
|
||||
if (patternMatch) {
|
||||
currentMatch = { pattern: parseInt(patternMatch[1]), captures: [] };
|
||||
fileMatches.get(currentFile)!.push(currentMatch);
|
||||
continue;
|
||||
}
|
||||
|
||||
const captureMatch = line.match(
|
||||
/^\s+capture:\s+(?:\d+\s*-\s*)?(\w+),\s*start:\s*\((\d+),\s*(\d+)\),\s*end:\s*\((\d+),\s*(\d+)\)(?:,\s*text:\s*`([^`]*)`)?/
|
||||
);
|
||||
if (captureMatch && currentMatch) {
|
||||
currentMatch.captures.push({
|
||||
tag: captureMatch[1],
|
||||
startRow: parseInt(captureMatch[2]),
|
||||
startCol: parseInt(captureMatch[3]),
|
||||
endRow: parseInt(captureMatch[4]),
|
||||
endCol: parseInt(captureMatch[5]),
|
||||
text: captureMatch[6],
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return fileMatches;
|
||||
}
|
||||
|
||||
// --- Symbol building ---
|
||||
|
||||
const KIND_MAP: Record<string, CodeSymbol["kind"]> = {
|
||||
func: "function",
|
||||
const_func: "function",
|
||||
cls: "class",
|
||||
method: "method",
|
||||
iface: "interface",
|
||||
tdef: "type",
|
||||
enm: "enum",
|
||||
struct_def: "struct",
|
||||
trait_def: "trait",
|
||||
impl_def: "impl",
|
||||
};
|
||||
|
||||
const CONTAINER_KINDS = new Set(["class", "struct", "impl", "trait"]);
|
||||
|
||||
function extractSignatureFromLines(lines: string[], startRow: number, endRow: number, maxLen: number = 200): string {
|
||||
const firstLine = lines[startRow] || "";
|
||||
let sig = firstLine;
|
||||
|
||||
if (!sig.trimEnd().endsWith("{") && !sig.trimEnd().endsWith(":")) {
|
||||
const chunk = lines.slice(startRow, Math.min(startRow + 10, endRow + 1)).join("\n");
|
||||
const braceIdx = chunk.indexOf("{");
|
||||
if (braceIdx !== -1 && braceIdx < 500) {
|
||||
sig = chunk.slice(0, braceIdx).replace(/\n/g, " ").replace(/\s+/g, " ").trim();
|
||||
}
|
||||
}
|
||||
|
||||
sig = sig.replace(/\s*[{:]\s*$/, "").trim();
|
||||
if (sig.length > maxLen) sig = sig.slice(0, maxLen - 3) + "...";
|
||||
return sig;
|
||||
}
|
||||
|
||||
function findCommentAbove(lines: string[], startRow: number): string | undefined {
|
||||
const commentLines: string[] = [];
|
||||
let foundComment = false;
|
||||
|
||||
for (let i = startRow - 1; i >= 0; i--) {
|
||||
const trimmed = lines[i].trim();
|
||||
if (trimmed === "") {
|
||||
if (foundComment) break;
|
||||
continue;
|
||||
}
|
||||
if (trimmed.startsWith("/**") || trimmed.startsWith("*") || trimmed.startsWith("*/") ||
|
||||
trimmed.startsWith("//") || trimmed.startsWith("///") || trimmed.startsWith("//!") ||
|
||||
trimmed.startsWith("#") || trimmed.startsWith("@")) {
|
||||
commentLines.unshift(lines[i]);
|
||||
foundComment = true;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return commentLines.length > 0 ? commentLines.join("\n").trim() : undefined;
|
||||
}
|
||||
|
||||
function findPythonDocstringFromLines(lines: string[], startRow: number, endRow: number): string | undefined {
|
||||
for (let i = startRow + 1; i <= Math.min(startRow + 3, endRow); i++) {
|
||||
const trimmed = lines[i]?.trim();
|
||||
if (!trimmed) continue;
|
||||
if (trimmed.startsWith('"""') || trimmed.startsWith("'''")) return trimmed;
|
||||
break;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function isExported(
|
||||
name: string, startRow: number, endRow: number,
|
||||
exportRanges: Array<{ startRow: number; endRow: number }>,
|
||||
lines: string[], language: string
|
||||
): boolean {
|
||||
switch (language) {
|
||||
case "javascript":
|
||||
case "typescript":
|
||||
case "tsx":
|
||||
return exportRanges.some(r => startRow >= r.startRow && endRow <= r.endRow);
|
||||
case "python":
|
||||
return !name.startsWith("_");
|
||||
case "go":
|
||||
return name.length > 0 && name[0] === name[0].toUpperCase() && name[0] !== name[0].toLowerCase();
|
||||
case "rust":
|
||||
return lines[startRow]?.trimStart().startsWith("pub") ?? false;
|
||||
default:
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
function buildSymbols(matches: RawMatch[], lines: string[], language: string): { symbols: CodeSymbol[]; imports: string[] } {
|
||||
const symbols: CodeSymbol[] = [];
|
||||
const imports: string[] = [];
|
||||
const exportRanges: Array<{ startRow: number; endRow: number }> = [];
|
||||
const containers: Array<{ sym: CodeSymbol; startRow: number; endRow: number }> = [];
|
||||
|
||||
// Collect exports and imports
|
||||
for (const match of matches) {
|
||||
for (const cap of match.captures) {
|
||||
if (cap.tag === "exp") {
|
||||
exportRanges.push({ startRow: cap.startRow, endRow: cap.endRow });
|
||||
}
|
||||
if (cap.tag === "imp") {
|
||||
imports.push(cap.text || lines[cap.startRow]?.trim() || "");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Build symbols
|
||||
for (const match of matches) {
|
||||
const kindCapture = match.captures.find(c => KIND_MAP[c.tag]);
|
||||
const nameCapture = match.captures.find(c => c.tag === "name");
|
||||
if (!kindCapture) continue;
|
||||
|
||||
const name = nameCapture?.text || "anonymous";
|
||||
const startRow = kindCapture.startRow;
|
||||
const endRow = kindCapture.endRow;
|
||||
const kind = KIND_MAP[kindCapture.tag];
|
||||
|
||||
const comment = findCommentAbove(lines, startRow);
|
||||
const docstring = language === "python" ? findPythonDocstringFromLines(lines, startRow, endRow) : undefined;
|
||||
|
||||
const sym: CodeSymbol = {
|
||||
name,
|
||||
kind,
|
||||
signature: extractSignatureFromLines(lines, startRow, endRow),
|
||||
jsdoc: comment || docstring,
|
||||
lineStart: startRow,
|
||||
lineEnd: endRow,
|
||||
exported: isExported(name, startRow, endRow, exportRanges, lines, language),
|
||||
};
|
||||
|
||||
if (CONTAINER_KINDS.has(kind)) {
|
||||
sym.children = [];
|
||||
containers.push({ sym, startRow, endRow });
|
||||
}
|
||||
|
||||
symbols.push(sym);
|
||||
}
|
||||
|
||||
// Nest methods inside containers
|
||||
const nested = new Set<CodeSymbol>();
|
||||
for (const container of containers) {
|
||||
for (const sym of symbols) {
|
||||
if (sym === container.sym) continue;
|
||||
if (sym.lineStart > container.startRow && sym.lineEnd <= container.endRow) {
|
||||
if (sym.kind === "function") sym.kind = "method";
|
||||
container.sym.children!.push(sym);
|
||||
nested.add(sym);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { symbols: symbols.filter(s => !nested.has(s)), imports };
|
||||
}
|
||||
|
||||
// --- Main parse functions ---
|
||||
|
||||
export function parseFile(content: string, filePath: string): FoldedFile {
|
||||
const language = detectLanguage(filePath);
|
||||
const lines = content.split("\n");
|
||||
|
||||
const grammarPath = resolveGrammarPath(language);
|
||||
if (!grammarPath) {
|
||||
return {
|
||||
filePath, language, symbols: [], imports: [],
|
||||
totalLines: lines.length, foldedTokenEstimate: 50,
|
||||
};
|
||||
}
|
||||
|
||||
const queryKey = getQueryKey(language);
|
||||
const queryFile = getQueryFile(queryKey);
|
||||
|
||||
// Write content to temp file with correct extension for language detection
|
||||
const ext = filePath.slice(filePath.lastIndexOf(".")) || ".txt";
|
||||
const tmpDir = mkdtempSync(join(tmpdir(), "smart-src-"));
|
||||
const tmpFile = join(tmpDir, `source${ext}`);
|
||||
writeFileSync(tmpFile, content);
|
||||
|
||||
try {
|
||||
const matches = runQuery(queryFile, tmpFile, grammarPath);
|
||||
const result = buildSymbols(matches, lines, language);
|
||||
|
||||
const folded = formatFoldedView({
|
||||
filePath, language,
|
||||
symbols: result.symbols, imports: result.imports,
|
||||
totalLines: lines.length, foldedTokenEstimate: 0,
|
||||
});
|
||||
|
||||
return {
|
||||
filePath, language,
|
||||
symbols: result.symbols, imports: result.imports,
|
||||
totalLines: lines.length,
|
||||
foldedTokenEstimate: Math.ceil(folded.length / 4),
|
||||
};
|
||||
} finally {
|
||||
rmSync(tmpDir, { recursive: true, force: true });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Batch parse multiple on-disk files. Groups by language for one CLI call per language.
|
||||
* Much faster than calling parseFile() per file (one process spawn per language vs per file).
|
||||
*/
|
||||
export function parseFilesBatch(
|
||||
files: Array<{ absolutePath: string; relativePath: string; content: string }>
|
||||
): Map<string, FoldedFile> {
|
||||
const results = new Map<string, FoldedFile>();
|
||||
|
||||
// Group files by language (and thus by query + grammar)
|
||||
const languageGroups = new Map<string, typeof files>();
|
||||
for (const file of files) {
|
||||
const language = detectLanguage(file.relativePath);
|
||||
if (!languageGroups.has(language)) languageGroups.set(language, []);
|
||||
languageGroups.get(language)!.push(file);
|
||||
}
|
||||
|
||||
for (const [language, groupFiles] of languageGroups) {
|
||||
const grammarPath = resolveGrammarPath(language);
|
||||
if (!grammarPath) {
|
||||
// No grammar — return empty results for these files
|
||||
for (const file of groupFiles) {
|
||||
const lines = file.content.split("\n");
|
||||
results.set(file.relativePath, {
|
||||
filePath: file.relativePath, language, symbols: [], imports: [],
|
||||
totalLines: lines.length, foldedTokenEstimate: 50,
|
||||
});
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
const queryKey = getQueryKey(language);
|
||||
const queryFile = getQueryFile(queryKey);
|
||||
|
||||
// Run one batch query for all files of this language
|
||||
const absolutePaths = groupFiles.map(f => f.absolutePath);
|
||||
const batchResults = runBatchQuery(queryFile, absolutePaths, grammarPath);
|
||||
|
||||
// Build FoldedFile for each file using the batch results
|
||||
for (const file of groupFiles) {
|
||||
const lines = file.content.split("\n");
|
||||
const matches = batchResults.get(file.absolutePath) || [];
|
||||
const symbolResult = buildSymbols(matches, lines, language);
|
||||
|
||||
const folded = formatFoldedView({
|
||||
filePath: file.relativePath, language,
|
||||
symbols: symbolResult.symbols, imports: symbolResult.imports,
|
||||
totalLines: lines.length, foldedTokenEstimate: 0,
|
||||
});
|
||||
|
||||
results.set(file.relativePath, {
|
||||
filePath: file.relativePath, language,
|
||||
symbols: symbolResult.symbols, imports: symbolResult.imports,
|
||||
totalLines: lines.length,
|
||||
foldedTokenEstimate: Math.ceil(folded.length / 4),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
// --- Formatting ---
|
||||
|
||||
export function formatFoldedView(file: FoldedFile): string {
|
||||
const parts: string[] = [];
|
||||
|
||||
parts.push(`📁 ${file.filePath} (${file.language}, ${file.totalLines} lines)`);
|
||||
parts.push("");
|
||||
|
||||
if (file.imports.length > 0) {
|
||||
parts.push(` 📦 Imports: ${file.imports.length} statements`);
|
||||
for (const imp of file.imports.slice(0, 10)) {
|
||||
parts.push(` ${imp}`);
|
||||
}
|
||||
if (file.imports.length > 10) {
|
||||
parts.push(` ... +${file.imports.length - 10} more`);
|
||||
}
|
||||
parts.push("");
|
||||
}
|
||||
|
||||
for (const sym of file.symbols) {
|
||||
parts.push(formatSymbol(sym, " "));
|
||||
}
|
||||
|
||||
return parts.join("\n");
|
||||
}
|
||||
|
||||
function formatSymbol(sym: CodeSymbol, indent: string): string {
|
||||
const parts: string[] = [];
|
||||
|
||||
const icon = getSymbolIcon(sym.kind);
|
||||
const exportTag = sym.exported ? " [exported]" : "";
|
||||
const lineRange = sym.lineStart === sym.lineEnd
|
||||
? `L${sym.lineStart + 1}`
|
||||
: `L${sym.lineStart + 1}-${sym.lineEnd + 1}`;
|
||||
|
||||
parts.push(`${indent}${icon} ${sym.name}${exportTag} (${lineRange})`);
|
||||
parts.push(`${indent} ${sym.signature}`);
|
||||
|
||||
if (sym.jsdoc) {
|
||||
const jsdocLines = sym.jsdoc.split("\n");
|
||||
const firstLine = jsdocLines.find(l => {
|
||||
const t = l.replace(/^[\s*/]+/, "").replace(/^['"`]{3}/, "").trim();
|
||||
return t.length > 0 && !t.startsWith("/**");
|
||||
});
|
||||
if (firstLine) {
|
||||
const cleaned = firstLine.replace(/^[\s*/]+/, "").replace(/^['"`]{3}/, "").replace(/['"`]{3}$/, "").trim();
|
||||
if (cleaned) {
|
||||
parts.push(`${indent} 💬 ${cleaned}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (sym.children && sym.children.length > 0) {
|
||||
for (const child of sym.children) {
|
||||
parts.push(formatSymbol(child, indent + " "));
|
||||
}
|
||||
}
|
||||
|
||||
return parts.join("\n");
|
||||
}
|
||||
|
||||
function getSymbolIcon(kind: CodeSymbol["kind"]): string {
|
||||
const icons: Record<string, string> = {
|
||||
function: "ƒ", method: "ƒ", class: "◆", interface: "◇",
|
||||
type: "◇", const: "●", variable: "○", export: "→",
|
||||
struct: "◆", enum: "▣", trait: "◇", impl: "◈",
|
||||
property: "○", getter: "⇢", setter: "⇠",
|
||||
};
|
||||
return icons[kind] || "·";
|
||||
}
|
||||
|
||||
// --- Unfold ---
|
||||
|
||||
export function unfoldSymbol(content: string, filePath: string, symbolName: string): string | null {
|
||||
const file = parseFile(content, filePath);
|
||||
|
||||
const findSymbol = (symbols: CodeSymbol[]): CodeSymbol | null => {
|
||||
for (const sym of symbols) {
|
||||
if (sym.name === symbolName) return sym;
|
||||
if (sym.children) {
|
||||
const found = findSymbol(sym.children);
|
||||
if (found) return found;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
const symbol = findSymbol(file.symbols);
|
||||
if (!symbol) return null;
|
||||
|
||||
const lines = content.split("\n");
|
||||
|
||||
// Include preceding comments/decorators
|
||||
let start = symbol.lineStart;
|
||||
for (let i = symbol.lineStart - 1; i >= 0; i--) {
|
||||
const trimmed = lines[i].trim();
|
||||
if (trimmed === "" || trimmed.startsWith("*") || trimmed.startsWith("/**") ||
|
||||
trimmed.startsWith("///") || trimmed.startsWith("//") ||
|
||||
trimmed.startsWith("#") || trimmed.startsWith("@") ||
|
||||
trimmed === "*/") {
|
||||
start = i;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
const extracted = lines.slice(start, symbol.lineEnd + 1).join("\n");
|
||||
return `// 📍 ${filePath} L${start + 1}-${symbol.lineEnd + 1}\n${extracted}`;
|
||||
}
|
||||
@@ -0,0 +1,316 @@
|
||||
/**
|
||||
* Search module — finds code files and symbols matching a query.
|
||||
*
|
||||
* Two search modes:
|
||||
* 1. Grep-style: find files/lines containing the query string
|
||||
* 2. Structural: parse files and match against symbol names/signatures
|
||||
*
|
||||
* Both return folded views, not raw content.
|
||||
*
|
||||
* Uses batch parsing (one CLI call per language) for fast multi-file search.
|
||||
*/
|
||||
|
||||
import { readFile, readdir, stat } from "node:fs/promises";
|
||||
import { join, relative } from "node:path";
|
||||
import { parseFilesBatch, formatFoldedView, type FoldedFile } from "./parser.js";
|
||||
|
||||
const CODE_EXTENSIONS = new Set([
|
||||
".js", ".jsx", ".ts", ".tsx", ".mjs", ".cjs",
|
||||
".py", ".pyw",
|
||||
".go",
|
||||
".rs",
|
||||
".rb",
|
||||
".java",
|
||||
".cs",
|
||||
".cpp", ".c", ".h", ".hpp",
|
||||
".swift",
|
||||
".kt",
|
||||
".php",
|
||||
".vue", ".svelte",
|
||||
]);
|
||||
|
||||
const IGNORE_DIRS = new Set([
|
||||
"node_modules", ".git", "dist", "build", ".next", "__pycache__",
|
||||
".venv", "venv", "env", ".env", "target", "vendor",
|
||||
".cache", ".turbo", "coverage", ".nyc_output",
|
||||
".claude", ".smart-file-read",
|
||||
]);
|
||||
|
||||
const MAX_FILE_SIZE = 512 * 1024; // 512KB — skip huge files
|
||||
|
||||
export interface SearchResult {
|
||||
foldedFiles: FoldedFile[];
|
||||
matchingSymbols: SymbolMatch[];
|
||||
totalFilesScanned: number;
|
||||
totalSymbolsFound: number;
|
||||
tokenEstimate: number;
|
||||
}
|
||||
|
||||
export interface SymbolMatch {
|
||||
filePath: string;
|
||||
symbolName: string;
|
||||
kind: string;
|
||||
signature: string;
|
||||
jsdoc?: string;
|
||||
lineStart: number;
|
||||
lineEnd: number;
|
||||
matchReason: string; // why this matched
|
||||
}
|
||||
|
||||
/**
|
||||
* Walk a directory recursively, yielding file paths.
|
||||
*/
|
||||
async function* walkDir(dir: string, rootDir: string, maxDepth: number = 20): AsyncGenerator<string> {
|
||||
if (maxDepth <= 0) return;
|
||||
|
||||
let entries;
|
||||
try {
|
||||
entries = await readdir(dir, { withFileTypes: true });
|
||||
} catch {
|
||||
return; // permission denied, etc.
|
||||
}
|
||||
|
||||
for (const entry of entries) {
|
||||
if (entry.name.startsWith(".") && entry.name !== ".") continue;
|
||||
if (IGNORE_DIRS.has(entry.name)) continue;
|
||||
|
||||
const fullPath = join(dir, entry.name);
|
||||
|
||||
if (entry.isDirectory()) {
|
||||
yield* walkDir(fullPath, rootDir, maxDepth - 1);
|
||||
} else if (entry.isFile()) {
|
||||
const ext = entry.name.slice(entry.name.lastIndexOf("."));
|
||||
if (CODE_EXTENSIONS.has(ext)) {
|
||||
yield fullPath;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Read a file safely, skipping if too large or binary.
|
||||
*/
|
||||
async function safeReadFile(filePath: string): Promise<string | null> {
|
||||
try {
|
||||
const stats = await stat(filePath);
|
||||
if (stats.size > MAX_FILE_SIZE) return null;
|
||||
if (stats.size === 0) return null;
|
||||
|
||||
const content = await readFile(filePath, "utf-8");
|
||||
|
||||
// Quick binary check — if first 1000 chars have null bytes, skip
|
||||
if (content.slice(0, 1000).includes("\0")) return null;
|
||||
|
||||
return content;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Search a codebase for symbols matching a query.
|
||||
*
|
||||
* Phase 1: Collect files and read content
|
||||
* Phase 2: Batch parse all files (one CLI call per language)
|
||||
* Phase 3: Match query against parsed symbols
|
||||
*/
|
||||
export async function searchCodebase(
|
||||
rootDir: string,
|
||||
query: string,
|
||||
options: {
|
||||
maxResults?: number;
|
||||
includeImports?: boolean;
|
||||
filePattern?: string;
|
||||
} = {}
|
||||
): Promise<SearchResult> {
|
||||
const maxResults = options.maxResults || 20;
|
||||
const queryLower = query.toLowerCase();
|
||||
const queryParts = queryLower.split(/[\s_\-./]+/).filter(p => p.length > 0);
|
||||
|
||||
// Phase 1: Collect files
|
||||
const filesToParse: Array<{ absolutePath: string; relativePath: string; content: string }> = [];
|
||||
|
||||
for await (const filePath of walkDir(rootDir, rootDir)) {
|
||||
if (options.filePattern) {
|
||||
const relPath = relative(rootDir, filePath);
|
||||
if (!relPath.toLowerCase().includes(options.filePattern.toLowerCase())) continue;
|
||||
}
|
||||
|
||||
const content = await safeReadFile(filePath);
|
||||
if (!content) continue;
|
||||
|
||||
filesToParse.push({
|
||||
absolutePath: filePath,
|
||||
relativePath: relative(rootDir, filePath),
|
||||
content,
|
||||
});
|
||||
}
|
||||
|
||||
// Phase 2: Batch parse (one CLI call per language)
|
||||
const parsedFiles = parseFilesBatch(filesToParse);
|
||||
|
||||
// Phase 3: Match query against symbols
|
||||
const foldedFiles: FoldedFile[] = [];
|
||||
const matchingSymbols: SymbolMatch[] = [];
|
||||
let totalSymbolsFound = 0;
|
||||
|
||||
for (const [relPath, parsed] of parsedFiles) {
|
||||
totalSymbolsFound += countSymbols(parsed);
|
||||
|
||||
const pathMatch = matchScore(relPath.toLowerCase(), queryParts);
|
||||
let fileHasMatch = pathMatch > 0;
|
||||
const fileSymbolMatches: SymbolMatch[] = [];
|
||||
|
||||
const checkSymbols = (symbols: typeof parsed.symbols, parent?: string) => {
|
||||
for (const sym of symbols) {
|
||||
let score = 0;
|
||||
let reason = "";
|
||||
|
||||
const nameScore = matchScore(sym.name.toLowerCase(), queryParts);
|
||||
if (nameScore > 0) {
|
||||
score += nameScore * 3;
|
||||
reason = "name match";
|
||||
}
|
||||
|
||||
if (sym.signature.toLowerCase().includes(queryLower)) {
|
||||
score += 2;
|
||||
reason = reason ? `${reason} + signature` : "signature match";
|
||||
}
|
||||
|
||||
if (sym.jsdoc && sym.jsdoc.toLowerCase().includes(queryLower)) {
|
||||
score += 1;
|
||||
reason = reason ? `${reason} + jsdoc` : "jsdoc match";
|
||||
}
|
||||
|
||||
if (score > 0) {
|
||||
fileHasMatch = true;
|
||||
fileSymbolMatches.push({
|
||||
filePath: relPath,
|
||||
symbolName: parent ? `${parent}.${sym.name}` : sym.name,
|
||||
kind: sym.kind,
|
||||
signature: sym.signature,
|
||||
jsdoc: sym.jsdoc,
|
||||
lineStart: sym.lineStart,
|
||||
lineEnd: sym.lineEnd,
|
||||
matchReason: reason,
|
||||
});
|
||||
}
|
||||
|
||||
if (sym.children) {
|
||||
checkSymbols(sym.children, sym.name);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
checkSymbols(parsed.symbols);
|
||||
|
||||
if (fileHasMatch) {
|
||||
foldedFiles.push(parsed);
|
||||
matchingSymbols.push(...fileSymbolMatches);
|
||||
}
|
||||
}
|
||||
|
||||
// Sort by relevance and trim
|
||||
matchingSymbols.sort((a, b) => {
|
||||
const aScore = matchScore(a.symbolName.toLowerCase(), queryParts);
|
||||
const bScore = matchScore(b.symbolName.toLowerCase(), queryParts);
|
||||
return bScore - aScore;
|
||||
});
|
||||
|
||||
const trimmedSymbols = matchingSymbols.slice(0, maxResults);
|
||||
const relevantFiles = new Set(trimmedSymbols.map(s => s.filePath));
|
||||
const trimmedFiles = foldedFiles.filter(f => relevantFiles.has(f.filePath)).slice(0, maxResults);
|
||||
|
||||
const tokenEstimate = trimmedFiles.reduce((sum, f) => sum + f.foldedTokenEstimate, 0);
|
||||
|
||||
return {
|
||||
foldedFiles: trimmedFiles,
|
||||
matchingSymbols: trimmedSymbols,
|
||||
totalFilesScanned: filesToParse.length,
|
||||
totalSymbolsFound,
|
||||
tokenEstimate,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Score how well query parts match a string.
|
||||
* Returns 0 for no match, higher for better matches.
|
||||
*/
|
||||
function matchScore(text: string, queryParts: string[]): number {
|
||||
let score = 0;
|
||||
for (const part of queryParts) {
|
||||
if (text === part) {
|
||||
score += 10; // exact match
|
||||
} else if (text.includes(part)) {
|
||||
score += 5; // substring match
|
||||
} else {
|
||||
// Fuzzy: check if all chars appear in order
|
||||
let ti = 0;
|
||||
let matched = 0;
|
||||
for (const ch of part) {
|
||||
const idx = text.indexOf(ch, ti);
|
||||
if (idx !== -1) {
|
||||
matched++;
|
||||
ti = idx + 1;
|
||||
}
|
||||
}
|
||||
if (matched === part.length) {
|
||||
score += 1; // loose fuzzy match
|
||||
}
|
||||
}
|
||||
}
|
||||
return score;
|
||||
}
|
||||
|
||||
function countSymbols(file: FoldedFile): number {
|
||||
let count = file.symbols.length;
|
||||
for (const sym of file.symbols) {
|
||||
if (sym.children) count += sym.children.length;
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
/**
|
||||
* Format search results for LLM consumption.
|
||||
*/
|
||||
export function formatSearchResults(result: SearchResult, query: string): string {
|
||||
const parts: string[] = [];
|
||||
|
||||
parts.push(`🔍 Smart Search: "${query}"`);
|
||||
parts.push(` Scanned ${result.totalFilesScanned} files, found ${result.totalSymbolsFound} symbols`);
|
||||
parts.push(` ${result.matchingSymbols.length} matches across ${result.foldedFiles.length} files (~${result.tokenEstimate} tokens for folded view)`);
|
||||
parts.push("");
|
||||
|
||||
if (result.matchingSymbols.length === 0) {
|
||||
parts.push(" No matching symbols found.");
|
||||
return parts.join("\n");
|
||||
}
|
||||
|
||||
// Show matching symbols first (compact)
|
||||
parts.push("── Matching Symbols ──");
|
||||
parts.push("");
|
||||
for (const match of result.matchingSymbols) {
|
||||
parts.push(` ${match.kind} ${match.symbolName} (${match.filePath}:${match.lineStart + 1})`);
|
||||
parts.push(` ${match.signature}`);
|
||||
if (match.jsdoc) {
|
||||
const firstLine = match.jsdoc.split("\n").find(l => l.replace(/^[\s*/]+/, "").trim().length > 0);
|
||||
if (firstLine) {
|
||||
parts.push(` 💬 ${firstLine.replace(/^[\s*/]+/, "").trim()}`);
|
||||
}
|
||||
}
|
||||
parts.push("");
|
||||
}
|
||||
|
||||
// Show folded file views
|
||||
parts.push("── Folded File Views ──");
|
||||
parts.push("");
|
||||
for (const file of result.foldedFiles) {
|
||||
parts.push(formatFoldedView(file));
|
||||
parts.push("");
|
||||
}
|
||||
|
||||
parts.push("── Actions ──");
|
||||
parts.push(' To see full implementation: use smart_unfold with file path and symbol name');
|
||||
|
||||
return parts.join("\n");
|
||||
}
|
||||
@@ -8,7 +8,6 @@
|
||||
import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'fs';
|
||||
import { join, dirname } from 'path';
|
||||
import { homedir } from 'os';
|
||||
import { DEFAULT_OBSERVATION_TYPES_STRING, DEFAULT_OBSERVATION_CONCEPTS_STRING } from '../constants/observation-metadata.js';
|
||||
// NOTE: Do NOT import logger here - it creates a circular dependency
|
||||
// logger.ts depends on SettingsDefaultsManager for its initialization
|
||||
|
||||
@@ -41,9 +40,6 @@ export interface SettingsDefaults {
|
||||
CLAUDE_MEM_CONTEXT_SHOW_WORK_TOKENS: string;
|
||||
CLAUDE_MEM_CONTEXT_SHOW_SAVINGS_AMOUNT: string;
|
||||
CLAUDE_MEM_CONTEXT_SHOW_SAVINGS_PERCENT: string;
|
||||
// Observation Filtering
|
||||
CLAUDE_MEM_CONTEXT_OBSERVATION_TYPES: string;
|
||||
CLAUDE_MEM_CONTEXT_OBSERVATION_CONCEPTS: string;
|
||||
// Display Configuration
|
||||
CLAUDE_MEM_CONTEXT_FULL_COUNT: string;
|
||||
CLAUDE_MEM_CONTEXT_FULL_FIELD: string;
|
||||
@@ -103,9 +99,6 @@ export class SettingsDefaultsManager {
|
||||
CLAUDE_MEM_CONTEXT_SHOW_WORK_TOKENS: 'false',
|
||||
CLAUDE_MEM_CONTEXT_SHOW_SAVINGS_AMOUNT: 'false',
|
||||
CLAUDE_MEM_CONTEXT_SHOW_SAVINGS_PERCENT: 'true',
|
||||
// Observation Filtering
|
||||
CLAUDE_MEM_CONTEXT_OBSERVATION_TYPES: DEFAULT_OBSERVATION_TYPES_STRING,
|
||||
CLAUDE_MEM_CONTEXT_OBSERVATION_CONCEPTS: DEFAULT_OBSERVATION_CONCEPTS_STRING,
|
||||
// Display Configuration
|
||||
CLAUDE_MEM_CONTEXT_FULL_COUNT: '0',
|
||||
CLAUDE_MEM_CONTEXT_FULL_FIELD: 'narrative',
|
||||
|
||||
@@ -54,62 +54,6 @@ function CollapsibleSection({
|
||||
);
|
||||
}
|
||||
|
||||
// Chip group with select all/none
|
||||
function ChipGroup({
|
||||
label,
|
||||
options,
|
||||
selectedValues,
|
||||
onToggle,
|
||||
onSelectAll,
|
||||
onSelectNone
|
||||
}: {
|
||||
label: string;
|
||||
options: string[];
|
||||
selectedValues: string[];
|
||||
onToggle: (value: string) => void;
|
||||
onSelectAll: () => void;
|
||||
onSelectNone: () => void;
|
||||
}) {
|
||||
const allSelected = options.every(opt => selectedValues.includes(opt));
|
||||
const noneSelected = options.every(opt => !selectedValues.includes(opt));
|
||||
|
||||
return (
|
||||
<div className="chip-group">
|
||||
<div className="chip-group-header">
|
||||
<span className="chip-group-label">{label}</span>
|
||||
<div className="chip-group-actions">
|
||||
<button
|
||||
type="button"
|
||||
className={`chip-action ${allSelected ? 'active' : ''}`}
|
||||
onClick={onSelectAll}
|
||||
>
|
||||
All
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className={`chip-action ${noneSelected ? 'active' : ''}`}
|
||||
onClick={onSelectNone}
|
||||
>
|
||||
None
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="chips-container">
|
||||
{options.map(option => (
|
||||
<button
|
||||
key={option}
|
||||
type="button"
|
||||
className={`chip ${selectedValues.includes(option) ? 'selected' : ''}`}
|
||||
onClick={() => onToggle(option)}
|
||||
>
|
||||
{option}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Form field with optional tooltip
|
||||
function FormField({
|
||||
label,
|
||||
@@ -209,24 +153,6 @@ export function ContextSettingsModal({
|
||||
updateSetting(key, newValue);
|
||||
}, [formState, updateSetting]);
|
||||
|
||||
const toggleArrayValue = useCallback((key: keyof Settings, value: string) => {
|
||||
const currentValue = formState[key] || '';
|
||||
const currentArray = currentValue ? currentValue.split(',') : [];
|
||||
const newArray = currentArray.includes(value)
|
||||
? currentArray.filter(v => v !== value)
|
||||
: [...currentArray, value];
|
||||
updateSetting(key, newArray.join(','));
|
||||
}, [formState, updateSetting]);
|
||||
|
||||
const getArrayValues = useCallback((key: keyof Settings): string[] => {
|
||||
const currentValue = formState[key] || '';
|
||||
return currentValue ? currentValue.split(',') : [];
|
||||
}, [formState]);
|
||||
|
||||
const setAllArrayValues = useCallback((key: keyof Settings, values: string[]) => {
|
||||
updateSetting(key, values.join(','));
|
||||
}, [updateSetting]);
|
||||
|
||||
// Handle ESC key
|
||||
useEffect(() => {
|
||||
const handleEsc = (e: KeyboardEvent) => {
|
||||
@@ -240,9 +166,6 @@ export function ContextSettingsModal({
|
||||
|
||||
if (!isOpen) return null;
|
||||
|
||||
const observationTypes = ['bugfix', 'feature', 'refactor', 'discovery', 'decision', 'change'];
|
||||
const observationConcepts = ['how-it-works', 'why-it-exists', 'what-changed', 'problem-solution', 'gotcha', 'pattern', 'trade-off'];
|
||||
|
||||
return (
|
||||
<div className="modal-backdrop" onClick={onClose}>
|
||||
<div className="context-settings-modal" onClick={(e) => e.stopPropagation()}>
|
||||
@@ -322,30 +245,7 @@ export function ContextSettingsModal({
|
||||
</FormField>
|
||||
</CollapsibleSection>
|
||||
|
||||
{/* Section 2: Filters */}
|
||||
<CollapsibleSection
|
||||
title="Filters"
|
||||
description="Which observation types to include"
|
||||
>
|
||||
<ChipGroup
|
||||
label="Type"
|
||||
options={observationTypes}
|
||||
selectedValues={getArrayValues('CLAUDE_MEM_CONTEXT_OBSERVATION_TYPES')}
|
||||
onToggle={(value) => toggleArrayValue('CLAUDE_MEM_CONTEXT_OBSERVATION_TYPES', value)}
|
||||
onSelectAll={() => setAllArrayValues('CLAUDE_MEM_CONTEXT_OBSERVATION_TYPES', observationTypes)}
|
||||
onSelectNone={() => setAllArrayValues('CLAUDE_MEM_CONTEXT_OBSERVATION_TYPES', [])}
|
||||
/>
|
||||
<ChipGroup
|
||||
label="Concept"
|
||||
options={observationConcepts}
|
||||
selectedValues={getArrayValues('CLAUDE_MEM_CONTEXT_OBSERVATION_CONCEPTS')}
|
||||
onToggle={(value) => toggleArrayValue('CLAUDE_MEM_CONTEXT_OBSERVATION_CONCEPTS', value)}
|
||||
onSelectAll={() => setAllArrayValues('CLAUDE_MEM_CONTEXT_OBSERVATION_CONCEPTS', observationConcepts)}
|
||||
onSelectNone={() => setAllArrayValues('CLAUDE_MEM_CONTEXT_OBSERVATION_CONCEPTS', [])}
|
||||
/>
|
||||
</CollapsibleSection>
|
||||
|
||||
{/* Section 3: Display */}
|
||||
{/* Section 2: Display */}
|
||||
<CollapsibleSection
|
||||
title="Display"
|
||||
description="What to show in context tables"
|
||||
|
||||
@@ -24,10 +24,6 @@ export const DEFAULT_SETTINGS = {
|
||||
CLAUDE_MEM_CONTEXT_SHOW_SAVINGS_AMOUNT: 'true',
|
||||
CLAUDE_MEM_CONTEXT_SHOW_SAVINGS_PERCENT: 'true',
|
||||
|
||||
// Observation Filtering (all types and concepts)
|
||||
CLAUDE_MEM_CONTEXT_OBSERVATION_TYPES: 'bugfix,feature,refactor,discovery,decision,change',
|
||||
CLAUDE_MEM_CONTEXT_OBSERVATION_CONCEPTS: 'how-it-works,why-it-exists,what-changed,problem-solution,gotcha,pattern,trade-off',
|
||||
|
||||
// Display Configuration
|
||||
CLAUDE_MEM_CONTEXT_FULL_COUNT: '5',
|
||||
CLAUDE_MEM_CONTEXT_FULL_FIELD: 'narrative',
|
||||
|
||||
@@ -38,10 +38,6 @@ export function useSettings() {
|
||||
CLAUDE_MEM_CONTEXT_SHOW_SAVINGS_AMOUNT: data.CLAUDE_MEM_CONTEXT_SHOW_SAVINGS_AMOUNT || DEFAULT_SETTINGS.CLAUDE_MEM_CONTEXT_SHOW_SAVINGS_AMOUNT,
|
||||
CLAUDE_MEM_CONTEXT_SHOW_SAVINGS_PERCENT: data.CLAUDE_MEM_CONTEXT_SHOW_SAVINGS_PERCENT || DEFAULT_SETTINGS.CLAUDE_MEM_CONTEXT_SHOW_SAVINGS_PERCENT,
|
||||
|
||||
// Observation Filtering
|
||||
CLAUDE_MEM_CONTEXT_OBSERVATION_TYPES: data.CLAUDE_MEM_CONTEXT_OBSERVATION_TYPES || DEFAULT_SETTINGS.CLAUDE_MEM_CONTEXT_OBSERVATION_TYPES,
|
||||
CLAUDE_MEM_CONTEXT_OBSERVATION_CONCEPTS: data.CLAUDE_MEM_CONTEXT_OBSERVATION_CONCEPTS || DEFAULT_SETTINGS.CLAUDE_MEM_CONTEXT_OBSERVATION_CONCEPTS,
|
||||
|
||||
// Display Configuration
|
||||
CLAUDE_MEM_CONTEXT_FULL_COUNT: data.CLAUDE_MEM_CONTEXT_FULL_COUNT || DEFAULT_SETTINGS.CLAUDE_MEM_CONTEXT_FULL_COUNT,
|
||||
CLAUDE_MEM_CONTEXT_FULL_FIELD: data.CLAUDE_MEM_CONTEXT_FULL_FIELD || DEFAULT_SETTINGS.CLAUDE_MEM_CONTEXT_FULL_FIELD,
|
||||
|
||||
@@ -76,10 +76,6 @@ export interface Settings {
|
||||
CLAUDE_MEM_CONTEXT_SHOW_SAVINGS_AMOUNT?: string;
|
||||
CLAUDE_MEM_CONTEXT_SHOW_SAVINGS_PERCENT?: string;
|
||||
|
||||
// Observation Filtering
|
||||
CLAUDE_MEM_CONTEXT_OBSERVATION_TYPES?: string;
|
||||
CLAUDE_MEM_CONTEXT_OBSERVATION_CONCEPTS?: string;
|
||||
|
||||
// Display Configuration
|
||||
CLAUDE_MEM_CONTEXT_FULL_COUNT?: string;
|
||||
CLAUDE_MEM_CONTEXT_FULL_FIELD?: string;
|
||||
|
||||
Reference in New Issue
Block a user