
Context Engine
- 94 installs
- 451 repo stars
- Updated July 21, 2026
- borghei/claude-skills
Context Engine is a Claude skill providing patterns for AI-agent context management including context-window optimization, persistent memory, and RAG retrieval for codebases.
About
Context Engine provides patterns for managing what AI coding agents know, remember, and retrieve. It covers context-window optimization with token-budget allocation, persistent memory across sessions via a three-layer memory model, retrieval and RAG chunking strategies for code, and knowledge-graph construction. A developer uses it when building agent memory systems, optimizing context windows, or designing RAG pipelines over a codebase.
- Token-budget allocation framework and context-packing strategies for agent context windows
- Three-layer memory model (working, session, knowledge base) with a promotion protocol
- Retrieval strategies and RAG chunking tuned for code
Context Engine by the numbers
- 94 all-time installs (skills.sh)
- Ranked #4,614 of 16,546 AI & Agent Building skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
context-engine capabilities & compatibility
- Capabilities
- context optimization · agent memory · rag pipeline · knowledge graph
- Use cases
- token optimization · memory · orchestration · research
- Pricing
- Free
What context-engine says it does
Context Engine provides production-grade patterns for managing what AI agents know, remember, and retrieve.
Every AI agent operates within a finite context window. Mismanaging it is the #1 cause of degraded agent performance.
Chunk by function/class boundaries (never mid-function)
npx skills add https://github.com/borghei/claude-skills --skill context-engineAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 94 |
|---|---|
| repo stars | ★ 451 |
| Last updated | July 21, 2026 |
| Repository | borghei/claude-skills ↗ |
What it does
Design agent context-window budgets, persistent memory, and RAG retrieval strategies for codebases.
Who is it for?
Developers building agent memory systems and RAG pipelines for code
Skip if: Non-AI application code or simple documentation tasks
When should I use this skill?
Building agent memory systems, optimizing context windows, or designing RAG pipelines for code
What you get
Delivers a token budget, memory architecture, and retrieval strategy that keep agents accurate across sessions.
- token-budget allocation
- context-packing strategy
- memory architecture
By the numbers
- Three-layer memory model
- Max RAG chunk size of 200 lines
- System instructions budgeted at 5-10% of context
Files
Context Engine - AI Agent Context Management
Tier: POWERFUL Category: Engineering Tags: context management, AI agents, memory systems, RAG, token optimization, knowledge graphs
Overview
Context Engine provides production-grade patterns for managing what AI agents know, remember, and retrieve. It covers the full lifecycle: ingestion of project knowledge, optimal packing of context windows, persistent memory across sessions, and retrieval-augmented generation for large codebases. The difference between a useful agent and a hallucinating one is context management.
Core Capabilities
1. Context Window Architecture
Every AI agent operates within a finite context window. Mismanaging it is the #1 cause of degraded agent performance.
Token Budget Allocation Framework
| Segment | Budget % | Purpose | Priority |
|---|---|---|---|
| System Instructions | 5-10% | Agent identity, rules, constraints | Fixed (always loaded) |
| Task Context | 20-30% | Current task description, requirements | High (per-request) |
| Relevant Code | 25-40% | Source files, dependencies, types | Dynamic (retrieved) |
| Conversation History | 10-20% | Prior turns, decisions made | Sliding window |
| Tool Results | 5-15% | Command output, search results | Ephemeral |
| Reserved Buffer | 5-10% | Output generation headroom | Protected |
Context Packing Strategies
Greedy Relevance Packing
1. Score all candidate context by relevance to current task
2. Sort by score descending
3. Pack until budget exhausted
4. Always reserve output buffer- Pros: Simple, fast, works well for focused tasks
- Cons: Misses cross-cutting context, no diversity
Tiered Loading
Tier 0 (always loaded): System prompt, project rules, active file
Tier 1 (task-specific): Related files, type definitions, tests
Tier 2 (on-demand): Documentation, examples, history
Tier 3 (retrieved): Search results, RAG chunks- Pros: Predictable, debuggable, respects fixed costs
- Cons: Requires upfront tier classification
Adaptive Compression
1. Load full context for first pass
2. Identify low-signal sections (boilerplate, repetitive code)
3. Summarize or truncate low-signal sections
4. Re-pack with compressed context
5. Preserve high-signal sections verbatim- Pros: Maximizes information density
- Cons: Risk of losing important details in compression
2. Memory Architecture
Three-Layer Memory Model
┌─────────────────────────────────────────────────┐
│ Layer 1: Working Memory (Context Window) │
│ Scope: Current conversation/task │
│ Lifetime: Single session │
│ Storage: In-context tokens │
│ Update: Every turn │
├─────────────────────────────────────────────────┤
│ Layer 2: Session Memory (Persistent Store) │
│ Scope: Project-level learnings │
│ Lifetime: Across sessions │
│ Storage: MEMORY.md, .claude/rules/, CLAUDE.md │
│ Update: End of session or on discovery │
├─────────────────────────────────────────────────┤
│ Layer 3: Knowledge Base (Indexed Corpus) │
│ Scope: Full codebase + documentation │
│ Lifetime: Persistent, versioned │
│ Storage: Vector store, graph DB, file index │
│ Update: On commit / scheduled reindex │
└─────────────────────────────────────────────────┘Memory Promotion Protocol
Knowledge flows upward through layers based on recurrence and value:
| Signal | Action | Example |
|---|---|---|
| Pattern seen 1x | Working memory only | "This file uses tabs" |
| Pattern seen 2-3x | Candidate for session memory | "Project uses pnpm everywhere" |
| Pattern confirmed across sessions | Promote to CLAUDE.md/rules | "Always use pnpm, never npm" |
| Pattern is domain knowledge | Add to knowledge base | "Auth flow uses JWT + refresh tokens" |
Staleness Detection
Context has a shelf life. Stale context causes hallucinations.
Freshness Score = f(last_verified, change_frequency, confidence)
Fresh (< 7 days, file unchanged): Use directly
Aging (7-30 days, file changed): Re-verify before using
Stale (> 30 days): Flag, re-retrieve, or discard
Unknown (never verified): Treat as low-confidence3. Retrieval Strategies for Code
File-Level Retrieval
Best for: navigating to the right file when the agent knows what it needs.
Query: "authentication middleware"
Strategy:
1. Filename pattern match: *auth*, *middleware*
2. Import graph: files that import auth modules
3. Symbol search: exported functions matching auth*
4. Content search: files containing auth-related patterns
5. Rank by: recency of edit + import centrality + name matchChunk-Level Retrieval (RAG for Code)
Best for: finding specific implementations within large files.
Chunking Strategy for Source Code:
- Chunk by function/class boundaries (never mid-function)
- Include the function signature + docstring + body as one chunk
- Attach metadata: file path, language, exports, imports
- Overlap: include 2 lines above/below for context
- Max chunk size: 200 lines (larger functions get sub-chunked by logical block)
Embedding Considerations:
- Code-specific embeddings (CodeBERT, StarCoder embeddings) outperform general text embeddings by 15-30% on code retrieval tasks
- Hybrid search (keyword + semantic) outperforms either alone
- Index function signatures separately for fast symbol lookup
Dependency-Aware Retrieval
When retrieving a function, also retrieve: 1. Its type definitions (interfaces, types it uses) 2. Its direct dependencies (imported functions it calls) 3. Its tests (to understand expected behavior) 4. Its callers (to understand usage context)
This "context neighborhood" approach prevents the agent from seeing a function in isolation.
4. Knowledge Graph Construction
Codebase Graph Schema
Nodes:
- File (path, language, size, last_modified)
- Function (name, signature, docstring, complexity)
- Class (name, methods, properties, inheritance)
- Module (name, exports, dependencies)
- Test (name, covers, assertions)
- Config (type, values, affects)
Edges:
- IMPORTS (File → File)
- CALLS (Function → Function)
- IMPLEMENTS (Class → Interface)
- TESTS (Test → Function)
- CONFIGURES (Config → Module)
- DEPENDS_ON (Module → Module)Graph Queries for Context
| Agent Question | Graph Query | Context Retrieved |
|---|---|---|
| "How does auth work?" | Subgraph around auth module, 2 hops | Auth files + dependencies + tests |
| "What breaks if I change X?" | Reverse dependency traversal from X | All callers + their tests |
| "What's the API surface?" | All exported functions from API modules | Route handlers + types + middleware |
| "How is this tested?" | TEST edges from target function | Test files + fixtures + mocks |
5. Context Window Optimization Patterns
Pattern: Sliding Window with Anchors
For long conversations, maintain fixed "anchor" messages while sliding recent history.
[System Prompt] ← Fixed anchor (never evicted)
[Task Definition] ← Fixed anchor
[Key Decision #1] ← Pinned (user marked as important)
[Key Decision #2] ← Pinned
...
[Turn N-4] ← Sliding window starts here
[Turn N-3]
[Turn N-2]
[Turn N-1]
[Current Turn]
[Output Buffer] ← ReservedPattern: Progressive Summarization
When conversation exceeds budget: 1. Summarize oldest turns into a "conversation summary" block 2. Keep the summary as a single anchor message 3. Update summary every N turns 4. Always keep: first system message, task definition, last 5 turns
Pattern: Selective Tool Result Caching
Tool outputs (file reads, search results, command output) consume the most tokens.
Strategy:
- Cache tool results keyed by (tool, args, file_hash)
- On re-request: serve from cache (0 new tokens)
- On file change: invalidate cache for that file
- Always truncate: command output > 200 lines → first 50 + last 50
- Never cache: error output (always show in full)6. Multi-Agent Context Sharing
When multiple agents collaborate, context synchronization becomes critical.
Shared Context Bus
┌──────────┐ ┌──────────────────┐ ┌──────────┐
│ Agent A │────▶│ Shared Context │◀────│ Agent B │
│ (Planner) │ │ - Task state │ │ (Coder) │
└──────────┘ │ - Decisions log │ └──────────┘
│ - File changes │
┌──────────┐ │ - Constraints │ ┌──────────┐
│ Agent C │────▶│ - Artifacts │◀────│ Agent D │
│ (Reviewer)│ └──────────────────┘ │ (Tester) │
└──────────┘ └──────────┘Context Handoff Protocol
When Agent A passes work to Agent B: 1. State Summary: What was done, decisions made, current state 2. Relevant Artifacts: Files created/modified, with paths 3. Constraints: What must not be changed, invariants 4. Open Questions: Unresolved decisions that need Agent B's input 5. Next Steps: Explicit instructions for what Agent B should do
Anti-pattern: Passing the entire conversation history. Always summarize.
Workflows
Workflow 1: Bootstrap Agent Context for a New Codebase
Step 1: Index the codebase
- Build file tree with metadata (language, size, last modified)
- Extract all exports, imports, and dependency edges
- Identify entry points (main files, route handlers, CLI commands)
Step 2: Construct initial knowledge graph
- Map module dependencies
- Identify architectural layers (API, service, data, config)
- Detect frameworks and conventions (naming, structure, patterns)
Step 3: Generate project summary
- One paragraph: what this project does
- Architecture diagram (text-based)
- Key directories and their roles
- Critical files (config, entry points, shared types)
Step 4: Configure context tiers
- Tier 0: Project summary, CLAUDE.md, active file
- Tier 1: Related files within same module
- Tier 2: Cross-module dependencies
- Tier 3: Documentation and examplesWorkflow 2: Optimize Context for a Specific Task
Step 1: Parse task requirements
- Extract entities (files, functions, features mentioned)
- Identify task type (bug fix, feature, refactor, review)
Step 2: Retrieve relevant context
- File-level: files matching entities
- Dependency-level: imports/exports of matched files
- Test-level: tests covering matched code
- History-level: recent changes to matched files
Step 3: Budget allocation
- Calculate total tokens available
- Allocate per tier (see Token Budget Framework)
- Pack context with greedy relevance
Step 4: Verify coverage
- Check: all mentioned files included?
- Check: type definitions for used types included?
- Check: test examples for expected behavior included?
- If gaps: retrieve missing context from lower tiersWorkflow 3: Session Memory Management
Step 1: During session - capture learnings
- New patterns discovered: log to working memory
- Corrections received: mark as high-confidence learning
- Errors encountered: log with resolution
Step 2: End of session - evaluate learnings
- Which learnings are project-specific vs session-specific?
- Which patterns recurred during this session?
- Which corrections should become rules?
Step 3: Promote valuable learnings
- Recurring patterns → CLAUDE.md or .claude/rules/
- Project conventions → project documentation
- Error resolutions → knowledge base
Step 4: Prune stale memory
- Remove learnings about deleted files
- Update learnings contradicted by new information
- Archive session-specific contextAnti-Patterns
| Anti-Pattern | Problem | Better Approach |
|---|---|---|
| Dumping entire files into context | Wastes tokens on irrelevant code | Retrieve specific functions/sections |
| No output buffer reservation | Agent output gets truncated | Always reserve 10-15% for output |
| Static context loading | Same context regardless of task | Dynamic retrieval based on task type |
| No staleness tracking | Using outdated information | Timestamp and verify before using |
| Full conversation replay | Older turns crowd out relevant code | Sliding window with summarization |
| Ignoring import graph | Missing type definitions, broken understanding | Always include direct dependencies |
Evaluation Metrics
| Metric | Description | Target |
|---|---|---|
| Context Relevance | % of loaded context actually used in response | > 70% |
| Retrieval Precision | % of retrieved items that are relevant | > 80% |
| Token Utilization | % of context budget used productively | > 85% |
| Staleness Rate | % of context items that are outdated | < 5% |
| Cache Hit Rate | % of tool results served from cache | > 40% |
| Handoff Completeness | % of required context passed between agents | 100% |
Integration Points
| Skill | Integration |
|---|---|
| rag-architect | Use RAG Architect for vector store design; Context Engine for retrieval strategy |
| agent-designer | Agent Designer defines agent roles; Context Engine manages what each agent knows |
| self-improving-agent | Self-Improving Agent promotes learnings; Context Engine decides when/how to load them |
| observability-designer | Monitor context utilization metrics alongside agent performance |
References
references/context-window-strategies.md- Detailed packing algorithms and benchmarksreferences/code-retrieval-patterns.md- RAG for code: chunking, embedding, and ranking strategiesreferences/memory-architecture-guide.md- Multi-layer memory system design patterns
Troubleshooting
| Problem | Cause | Solution |
|---|---|---|
| Agent responses ignore relevant files | Context retrieval missing import graph traversal | Enable dependency-aware retrieval; always include direct imports and type definitions alongside target files |
| Output truncated mid-response | No output buffer reserved in token budget | Reserve 10-15% of context window for generation; reduce Tier 2/3 content first |
| Stale context causing hallucinations | Memory layer not tracking file modification timestamps | Implement staleness detection with freshness scores; invalidate cache entries when source files change |
| RAG retrieval returns irrelevant chunks | Chunking splits functions mid-body or ignores code structure | Switch to AST-aware chunking at function/class boundaries; attach file path and export metadata to each chunk |
| Context window exceeded on large tasks | Greedy packing loads too many full files | Use adaptive compression: summarize boilerplate, load only signatures for low-priority files, keep high-signal code verbatim |
| Multi-agent handoff loses critical state | Raw conversation history passed instead of structured summary | Follow the Context Handoff Protocol: pass state summary, artifacts, constraints, open questions, and next steps |
| Knowledge graph queries return empty results | Graph not rebuilt after major refactors or branch switches | Schedule reindexing on commit hooks or branch checkout; validate node counts after rebuild |
Success Criteria
- Context Relevance above 70%: At least 70% of tokens loaded into the context window are directly referenced or used in the agent's response.
- Retrieval Precision above 80%: More than 80% of retrieved code chunks or files are relevant to the current task, measured by human evaluation or downstream task success.
- Token Utilization above 85%: Productive token usage (system instructions + task-relevant code + active conversation) exceeds 85% of the allocated budget, with less than 15% wasted on redundant or low-signal content.
- Staleness Rate below 5%: Fewer than 5% of context items are outdated (file changed since last retrieval without re-verification), validated by comparing loaded content hashes against current file state.
- Cache Hit Rate above 40%: At least 40% of repeated tool invocations (file reads, searches) are served from cache, reducing redundant token consumption and latency.
- Handoff Completeness at 100%: Every multi-agent context handoff includes all five protocol elements (state summary, artifacts, constraints, open questions, next steps) with zero information gaps.
- Session Memory Promotion Accuracy above 90%: Learnings promoted to persistent memory (CLAUDE.md, rules files) are validated as still accurate within 30 days, with fewer than 10% requiring correction or rollback.
Scope & Limitations
This skill covers:
- Context window token budget planning, allocation strategies, and packing algorithms for AI coding agents.
- Multi-layer memory architecture design (working memory, session memory, knowledge base) with promotion and staleness protocols.
- Code-specific retrieval strategies including file-level, chunk-level, and dependency-aware retrieval for RAG pipelines.
- Knowledge graph construction from codebases and graph-based context queries for agent workflows.
This skill does NOT cover:
- Vector store infrastructure setup, embedding model selection, or database deployment — see rag-architect for vector store design and embedding strategies.
- Agent role definition, personality design, or multi-agent orchestration logic — see agent-designer for agent architecture and agent-workflow-designer for orchestration patterns.
- Runtime observability, metrics dashboards, or alerting for agent systems — see observability-designer for monitoring and instrumentation.
- Prompt engineering techniques, chain-of-thought design, or instruction tuning — see prompt-engineer-toolkit for prompt construction patterns.
Integration Points
| Skill | Integration | Data Flow |
|---|---|---|
| rag-architect | Context Engine defines retrieval strategies; RAG Architect implements the vector store and embedding pipeline | Retrieval queries flow from Context Engine to RAG Architect's indexed store; ranked results flow back as context chunks |
| agent-designer | Agent Designer defines agent roles and capabilities; Context Engine manages per-agent context budgets and memory layers | Agent specifications define context requirements; Context Engine returns tailored context windows per agent role |
| self-improving-agent | Self-Improving Agent identifies recurring patterns and corrections; Context Engine decides when to promote learnings to persistent memory | Candidate learnings flow from Self-Improving Agent; promotion decisions and memory updates flow back through Context Engine's staleness and promotion protocols |
| observability-designer | Observability Designer instruments context utilization metrics (relevance, staleness, cache hits); Context Engine exposes metric endpoints | Raw metric events flow from Context Engine; Observability Designer aggregates into dashboards and alerts |
| agent-workflow-designer | Agent Workflow Designer defines multi-agent handoff sequences; Context Engine implements the shared context bus and handoff protocol | Workflow definitions specify which agents share context; Context Engine manages the context bus, serialization, and handoff payloads |
| codebase-onboarding | Codebase Onboarding generates project summaries and architecture maps; Context Engine consumes these as Tier 0 bootstrap context | Onboarding artifacts (project summary, directory map, entry points) feed into Context Engine's initial knowledge graph and context tiers |
#!/usr/bin/env python3
"""Context Analyzer - Analyze files and prompts for token usage, relevance scoring, and optimization.
Estimates token counts, scores context relevance across segments, identifies waste,
and provides actionable optimization suggestions based on the Context Engine's
Token Budget Allocation Framework.
Usage:
python context_analyzer.py path/to/file_or_dir
python context_analyzer.py --prompt "Fix the auth middleware bug"
python context_analyzer.py path/to/dir --budget 128000 --json
"""
import argparse
import json
import math
import os
import re
import sys
from collections import Counter
from pathlib import Path
# Approximate token estimation: ~4 chars per token for English/code (GPT-family heuristic)
CHARS_PER_TOKEN = 4
# Token Budget Allocation Framework (from SKILL.md)
BUDGET_SEGMENTS = {
"system_instructions": {"min": 0.05, "max": 0.10, "priority": "fixed"},
"task_context": {"min": 0.20, "max": 0.30, "priority": "high"},
"relevant_code": {"min": 0.25, "max": 0.40, "priority": "dynamic"},
"conversation_history": {"min": 0.10, "max": 0.20, "priority": "sliding"},
"tool_results": {"min": 0.05, "max": 0.15, "priority": "ephemeral"},
"reserved_buffer": {"min": 0.05, "max": 0.10, "priority": "protected"},
}
# Patterns indicating low-signal content
LOW_SIGNAL_PATTERNS = [
(r"^\s*#.*$", "comment_lines"),
(r"^\s*$", "blank_lines"),
(r"^\s*(import|from)\s+", "import_lines"),
(r"^\s*\}\s*$", "closing_braces"),
(r"^\s*pass\s*$", "pass_statements"),
(r"^\s*\.{3}\s*$", "ellipsis"),
]
# Patterns indicating high-signal content
HIGH_SIGNAL_PATTERNS = [
(r"^\s*(def|async def)\s+\w+", "function_definitions"),
(r"^\s*class\s+\w+", "class_definitions"),
(r"^\s*(return|yield)\s+", "return_statements"),
(r"(raise|except|try|finally)\s+", "error_handling"),
(r"^\s*@\w+", "decorators"),
]
VERBOSE_PATTERNS = [
(r"#{3,}\s*-+\s*$", "decorative_dividers"),
(r"^\s*#\s*={3,}", "decorative_headers"),
(r"^\s*\"\"\"[\s\S]{200,}\"\"\"", "long_docstrings"),
(r"(TODO|FIXME|HACK|XXX|NOQA)", "todo_markers"),
]
def estimate_tokens(text):
"""Estimate token count from text using character-based heuristic."""
return max(1, len(text) // CHARS_PER_TOKEN)
def analyze_file(file_path):
"""Analyze a single file for token usage and content signals."""
try:
with open(file_path, "r", encoding="utf-8", errors="replace") as f:
content = f.read()
except (OSError, IOError) as e:
return {"path": str(file_path), "error": str(e)}
lines = content.splitlines()
total_tokens = estimate_tokens(content)
total_lines = len(lines)
low_signal_counts = Counter()
high_signal_counts = Counter()
low_signal_lines = 0
high_signal_lines = 0
for line in lines:
matched_low = False
for pattern, name in LOW_SIGNAL_PATTERNS:
if re.match(pattern, line):
low_signal_counts[name] += 1
low_signal_lines += 1
matched_low = True
break
if not matched_low:
for pattern, name in HIGH_SIGNAL_PATTERNS:
if re.search(pattern, line):
high_signal_counts[name] += 1
high_signal_lines += 1
break
verbose_counts = Counter()
for pattern, name in VERBOSE_PATTERNS:
matches = re.findall(pattern, content, re.MULTILINE)
if matches:
verbose_counts[name] = len(matches)
neutral_lines = total_lines - low_signal_lines - high_signal_lines
if total_lines > 0:
relevance_score = round(
(high_signal_lines * 1.0 + neutral_lines * 0.5) / total_lines, 3
)
else:
relevance_score = 0.0
# Detect duplicate/redundant blocks (repeated sequences of 3+ identical lines)
redundant_blocks = 0
if total_lines > 6:
seen_blocks = set()
for i in range(len(lines) - 2):
block = tuple(lines[i : i + 3])
if block in seen_blocks and any(l.strip() for l in block):
redundant_blocks += 1
seen_blocks.add(block)
ext = Path(file_path).suffix.lower()
size_bytes = os.path.getsize(file_path)
return {
"path": str(file_path),
"extension": ext,
"size_bytes": size_bytes,
"total_lines": total_lines,
"estimated_tokens": total_tokens,
"relevance_score": relevance_score,
"high_signal_lines": high_signal_lines,
"low_signal_lines": low_signal_lines,
"neutral_lines": neutral_lines,
"high_signal_breakdown": dict(high_signal_counts),
"low_signal_breakdown": dict(low_signal_counts),
"verbose_patterns": dict(verbose_counts),
"redundant_blocks": redundant_blocks,
}
def compute_budget_analysis(total_tokens, budget):
"""Compute how the analyzed tokens fit within a context budget."""
segments = {}
for name, config in BUDGET_SEGMENTS.items():
min_tokens = int(budget * config["min"])
max_tokens = int(budget * config["max"])
segments[name] = {
"min_tokens": min_tokens,
"max_tokens": max_tokens,
"priority": config["priority"],
}
utilization = round(total_tokens / budget, 3) if budget > 0 else 0.0
remaining = max(0, budget - total_tokens)
return {
"budget_tokens": budget,
"used_tokens": total_tokens,
"remaining_tokens": remaining,
"utilization": utilization,
"segments": segments,
"over_budget": total_tokens > budget,
}
def generate_suggestions(file_results, budget_analysis):
"""Generate optimization suggestions from analysis results."""
suggestions = []
total_low = sum(r.get("low_signal_lines", 0) for r in file_results if "error" not in r)
total_lines = sum(r.get("total_lines", 0) for r in file_results if "error" not in r)
if total_lines > 0 and total_low / total_lines > 0.35:
suggestions.append({
"type": "reduce_low_signal",
"severity": "high",
"message": (
f"{total_low}/{total_lines} lines ({total_low * 100 // total_lines}%) are low-signal "
"(blanks, comments, closing braces). Consider pruning or summarizing."
),
})
total_redundant = sum(r.get("redundant_blocks", 0) for r in file_results if "error" not in r)
if total_redundant > 3:
suggestions.append({
"type": "remove_redundancy",
"severity": "medium",
"message": f"Detected {total_redundant} redundant 3-line blocks. Deduplicate repeated code sections.",
})
if budget_analysis and budget_analysis["over_budget"]:
overage = budget_analysis["used_tokens"] - budget_analysis["budget_tokens"]
suggestions.append({
"type": "over_budget",
"severity": "critical",
"message": (
f"Context exceeds budget by {overage:,} tokens. "
"Apply tiered loading: keep Tier 0/1, summarize Tier 2, drop Tier 3."
),
})
elif budget_analysis and budget_analysis["utilization"] < 0.5:
suggestions.append({
"type": "under_utilized",
"severity": "info",
"message": (
f"Only {budget_analysis['utilization'] * 100:.0f}% of budget used. "
"You can load additional dependency or test files for better coverage."
),
})
# Check for very large individual files
for r in file_results:
if "error" in r:
continue
if r["estimated_tokens"] > 8000:
suggestions.append({
"type": "large_file",
"severity": "medium",
"message": (
f"{r['path']} is ~{r['estimated_tokens']:,} tokens. "
"Consider loading only relevant functions instead of the full file."
),
})
if not suggestions:
suggestions.append({
"type": "ok",
"severity": "info",
"message": "Context looks well-optimized. No major issues detected.",
})
return suggestions
def collect_files(path, max_files=100):
"""Collect files from a path (file or directory)."""
p = Path(path)
if p.is_file():
return [p]
if p.is_dir():
files = []
skip_dirs = {".git", "__pycache__", "node_modules", ".venv", "venv", ".tox"}
for root, dirs, filenames in os.walk(p):
dirs[:] = [d for d in dirs if d not in skip_dirs]
for fname in filenames:
fp = Path(root) / fname
if fp.suffix.lower() in {
".py", ".js", ".ts", ".tsx", ".jsx", ".md", ".yaml", ".yml",
".json", ".toml", ".cfg", ".ini", ".sh", ".go", ".rs", ".java",
".rb", ".c", ".cpp", ".h", ".hpp", ".cs", ".swift", ".kt",
}:
files.append(fp)
if len(files) >= max_files:
return files
return files
return []
def format_human(file_results, budget_analysis, suggestions):
"""Format results for human-readable terminal output."""
lines = []
lines.append("=" * 64)
lines.append(" CONTEXT ANALYZER REPORT")
lines.append("=" * 64)
total_tokens = sum(r.get("estimated_tokens", 0) for r in file_results if "error" not in r)
total_files = sum(1 for r in file_results if "error" not in r)
lines.append(f"\n Files analyzed: {total_files}")
lines.append(f" Total estimated tokens: {total_tokens:,}")
if budget_analysis:
lines.append(f" Budget: {budget_analysis['budget_tokens']:,} tokens")
lines.append(f" Utilization: {budget_analysis['utilization'] * 100:.1f}%")
if budget_analysis["over_budget"]:
lines.append(f" ** OVER BUDGET by {total_tokens - budget_analysis['budget_tokens']:,} tokens **")
lines.append(f"\n{' File':<45} {'Tokens':>8} {'Relevance':>10}")
lines.append(" " + "-" * 62)
for r in sorted(file_results, key=lambda x: x.get("estimated_tokens", 0), reverse=True):
if "error" in r:
lines.append(f" {r['path']:<45} ERROR: {r['error']}")
continue
name = r["path"]
if len(name) > 43:
name = "..." + name[-40:]
lines.append(f" {name:<45} {r['estimated_tokens']:>8,} {r['relevance_score']:>10.2f}")
lines.append(f"\n OPTIMIZATION SUGGESTIONS")
lines.append(" " + "-" * 40)
for s in suggestions:
marker = {"critical": "!!!", "high": "!!", "medium": "!", "info": " "}
icon = marker.get(s["severity"], " ")
lines.append(f" [{icon}] {s['message']}")
lines.append("")
return "\n".join(lines)
def main():
parser = argparse.ArgumentParser(
description="Analyze files/prompts for token usage, context relevance, and optimization suggestions.",
epilog="Example: python context_analyzer.py src/ --budget 128000 --json",
)
parser.add_argument("path", nargs="?", help="File or directory to analyze")
parser.add_argument("--prompt", type=str, help="Analyze a prompt string for token estimation")
parser.add_argument("--budget", type=int, default=128000, help="Context window budget in tokens (default: 128000)")
parser.add_argument("--max-files", type=int, default=100, help="Max files to analyze in a directory (default: 100)")
parser.add_argument("--json", action="store_true", dest="json_output", help="Output results as JSON")
args = parser.parse_args()
if not args.path and not args.prompt:
parser.print_help()
sys.exit(1)
file_results = []
if args.prompt:
tokens = estimate_tokens(args.prompt)
file_results.append({
"path": "<prompt>",
"extension": "",
"size_bytes": len(args.prompt.encode("utf-8")),
"total_lines": args.prompt.count("\n") + 1,
"estimated_tokens": tokens,
"relevance_score": 1.0,
"high_signal_lines": args.prompt.count("\n") + 1,
"low_signal_lines": 0,
"neutral_lines": 0,
"high_signal_breakdown": {},
"low_signal_breakdown": {},
"verbose_patterns": {},
"redundant_blocks": 0,
})
if args.path:
files = collect_files(args.path, max_files=args.max_files)
if not files:
print(f"Error: No analyzable files found at '{args.path}'", file=sys.stderr)
sys.exit(1)
for f in files:
file_results.append(analyze_file(f))
total_tokens = sum(r.get("estimated_tokens", 0) for r in file_results if "error" not in r)
budget_analysis = compute_budget_analysis(total_tokens, args.budget)
suggestions = generate_suggestions(file_results, budget_analysis)
result = {
"summary": {
"files_analyzed": sum(1 for r in file_results if "error" not in r),
"total_estimated_tokens": total_tokens,
"budget": args.budget,
},
"files": file_results,
"budget_analysis": budget_analysis,
"suggestions": suggestions,
}
if args.json_output:
print(json.dumps(result, indent=2))
else:
print(format_human(file_results, budget_analysis, suggestions))
if __name__ == "__main__":
main()
#!/usr/bin/env python3
"""Context Pruner - Intelligently prune context by removing low-relevance content.
Removes blank lines, comment blocks, redundant sections, verbose patterns,
and boilerplate to compress context while preserving high-signal code.
Implements the Adaptive Compression pattern from the Context Engine skill.
Usage:
python context_pruner.py path/to/file.py
python context_pruner.py path/to/file.py --aggressive --json
python context_pruner.py path/to/dir --output pruned_output/
cat context.txt | python context_pruner.py --stdin
"""
import argparse
import json
import os
import re
import sys
from pathlib import Path
# Pruning levels control how aggressively we strip content
PRUNING_LEVELS = {
"light": {
"strip_blank_runs": 2, # Collapse runs of blanks to max N
"strip_comments": False, # Keep all comments
"strip_docstrings": False, # Keep all docstrings
"strip_imports": False, # Keep all imports
"strip_decorative": True, # Remove decorative dividers
"collapse_braces": False, # Keep closing brace lines
},
"moderate": {
"strip_blank_runs": 1,
"strip_comments": True, # Remove standalone comment lines
"strip_docstrings": False,
"strip_imports": False,
"strip_decorative": True,
"collapse_braces": True,
},
"aggressive": {
"strip_blank_runs": 0, # Remove ALL blank lines
"strip_comments": True,
"strip_docstrings": True, # Trim long docstrings to first line
"strip_imports": True, # Collapse import blocks to summary
"strip_decorative": True,
"collapse_braces": True,
},
}
def collapse_blank_runs(lines, max_consecutive):
"""Collapse consecutive blank lines to at most max_consecutive."""
result = []
blank_count = 0
for line in lines:
if line.strip() == "":
blank_count += 1
if blank_count <= max_consecutive:
result.append(line)
else:
blank_count = 0
result.append(line)
return result
def strip_comment_lines(lines):
"""Remove standalone comment lines (preserving inline comments and shebangs)."""
result = []
for i, line in enumerate(lines):
stripped = line.strip()
# Keep shebangs, keep inline comments (code before #)
if stripped.startswith("#") and not stripped.startswith("#!"):
# Keep type: ignore, noqa, pragma, and encoding comments
if any(kw in stripped.lower() for kw in ["type:", "noqa", "pragma", "coding", "pylint", "fmt:"]):
result.append(line)
continue
continue
# Strip // comments for JS/TS/Go/Java/C etc.
if stripped.startswith("//") and not stripped.startswith("///"):
continue
result.append(line)
return result
def trim_docstrings(lines):
"""Trim multi-line docstrings to just the first summary line."""
result = []
in_docstring = False
docstring_quote = None
docstring_indent = ""
first_line_added = False
for line in lines:
stripped = line.strip()
if not in_docstring:
# Detect docstring opening
for quote in ['"""', "'''"]:
if quote in stripped:
idx = stripped.index(quote)
after_quote = stripped[idx + 3:]
# Single-line docstring — keep as is
if quote in after_quote:
result.append(line)
break
# Multi-line docstring starts
in_docstring = True
docstring_quote = quote
docstring_indent = line[: len(line) - len(line.lstrip())]
# Keep the opening line
result.append(line)
first_line_added = True
break
else:
result.append(line)
else:
# Inside a docstring — look for closing quote
if docstring_quote in stripped:
# Close the docstring with just the closing quotes
result.append(docstring_indent + docstring_quote)
in_docstring = False
docstring_quote = None
first_line_added = False
# Skip intermediate docstring lines (they are pruned)
return result
def collapse_imports(lines):
"""Collapse consecutive import lines into a summary comment."""
result = []
import_block = []
import_modules = set()
def flush_imports():
if import_block:
count = len(import_block)
# Extract module names
for imp_line in import_block:
m = re.match(r"^\s*(?:from\s+(\S+)|import\s+(\S+))", imp_line)
if m:
import_modules.add(m.group(1) or m.group(2))
modules_str = ", ".join(sorted(import_modules)[:8])
if len(import_modules) > 8:
modules_str += f", ... (+{len(import_modules) - 8} more)"
result.append(f"# [{count} imports: {modules_str}]")
import_block.clear()
import_modules.clear()
for line in lines:
stripped = line.strip()
if re.match(r"^(import |from \S+ import )", stripped):
import_block.append(line)
else:
flush_imports()
result.append(line)
flush_imports()
return result
def strip_decorative(lines):
"""Remove decorative dividers and banner comments."""
result = []
for line in lines:
stripped = line.strip()
# Lines that are purely decorative: ####, -----, ====, /****/
if re.match(r"^[#\-=*\/\\]{4,}\s*$", stripped):
continue
# Banner-style comments like # ========== SECTION ==========
if re.match(r"^#\s*[=\-*]{3,}.*[=\-*]{3,}\s*$", stripped):
# Keep the text, strip the decoration
text = re.sub(r"[=\-*]+", "", stripped.lstrip("#")).strip()
if text:
indent = line[: len(line) - len(line.lstrip())]
result.append(f"{indent}# {text}")
continue
result.append(line)
return result
def collapse_closing_braces(lines):
"""Remove lines that are only closing braces/brackets with optional whitespace."""
result = []
for line in lines:
stripped = line.strip()
if stripped in ("}", "},", ");", "});", "]", "],"):
# Keep it but strip trailing whitespace
result.append(line.rstrip())
else:
result.append(line)
# Remove consecutive closing-only lines (keep just one)
final = []
prev_closing = False
for line in result:
stripped = line.strip()
is_closing = stripped in ("}", "},", ");", "});", "]", "],")
if is_closing and prev_closing:
# Merge onto previous line
if final:
final[-1] = final[-1] + " " + stripped
continue
final.append(line)
prev_closing = is_closing
return final
def detect_redundant_blocks(lines, min_block_size=3):
"""Detect and remove repeated blocks of identical lines."""
result = []
seen_blocks = {}
i = 0
removed = 0
while i < len(lines):
if i + min_block_size <= len(lines):
block = tuple(l.strip() for l in lines[i : i + min_block_size])
# Skip blocks that are all empty
if any(l for l in block):
if block in seen_blocks:
result.append(f"# [duplicate block removed, first seen line {seen_blocks[block] + 1}]")
i += min_block_size
removed += min_block_size
continue
seen_blocks[block] = i
result.append(lines[i])
i += 1
return result, removed
def prune_content(content, level="moderate"):
"""Apply pruning strategies to content string. Returns (pruned_content, stats)."""
config = PRUNING_LEVELS.get(level, PRUNING_LEVELS["moderate"])
lines = content.splitlines()
original_lines = len(lines)
original_tokens = max(1, len(content) // 4)
stats = {"original_lines": original_lines, "original_tokens": original_tokens, "operations": []}
# 1. Strip decorative elements
if config["strip_decorative"]:
before = len(lines)
lines = strip_decorative(lines)
removed = before - len(lines)
if removed > 0:
stats["operations"].append({"op": "strip_decorative", "lines_removed": removed})
# 2. Strip comments
if config["strip_comments"]:
before = len(lines)
lines = strip_comment_lines(lines)
removed = before - len(lines)
if removed > 0:
stats["operations"].append({"op": "strip_comments", "lines_removed": removed})
# 3. Trim docstrings
if config["strip_docstrings"]:
before = len(lines)
lines = trim_docstrings(lines)
removed = before - len(lines)
if removed > 0:
stats["operations"].append({"op": "trim_docstrings", "lines_removed": removed})
# 4. Collapse imports
if config["strip_imports"]:
before = len(lines)
lines = collapse_imports(lines)
removed = before - len(lines)
if removed > 0:
stats["operations"].append({"op": "collapse_imports", "lines_removed": removed})
# 5. Collapse closing braces
if config["collapse_braces"]:
before = len(lines)
lines = collapse_closing_braces(lines)
removed = before - len(lines)
if removed > 0:
stats["operations"].append({"op": "collapse_braces", "lines_removed": removed})
# 6. Remove redundant blocks
before = len(lines)
lines, dup_removed = detect_redundant_blocks(lines)
if dup_removed > 0:
stats["operations"].append({"op": "remove_duplicates", "lines_removed": dup_removed})
# 7. Collapse blank lines (always, last step)
before = len(lines)
lines = collapse_blank_runs(lines, config["strip_blank_runs"])
removed = before - len(lines)
if removed > 0:
stats["operations"].append({"op": "collapse_blanks", "lines_removed": removed})
pruned_content = "\n".join(lines)
pruned_tokens = max(1, len(pruned_content) // 4)
stats["pruned_lines"] = len(lines)
stats["pruned_tokens"] = pruned_tokens
stats["lines_removed"] = original_lines - len(lines)
stats["tokens_saved"] = original_tokens - pruned_tokens
stats["compression_ratio"] = round(pruned_tokens / original_tokens, 3) if original_tokens > 0 else 1.0
return pruned_content, stats
def prune_file(file_path, level, output_dir=None):
"""Prune a single file and optionally write output."""
try:
with open(file_path, "r", encoding="utf-8", errors="replace") as f:
content = f.read()
except (OSError, IOError) as e:
return {"path": str(file_path), "error": str(e)}
pruned, stats = prune_content(content, level)
stats["path"] = str(file_path)
if output_dir:
out_path = Path(output_dir) / Path(file_path).name
os.makedirs(output_dir, exist_ok=True)
with open(out_path, "w", encoding="utf-8") as f:
f.write(pruned)
stats["output_path"] = str(out_path)
stats["pruned_content"] = pruned
return stats
def collect_files(path, max_files=50):
"""Collect prunable files from a path."""
p = Path(path)
if p.is_file():
return [p]
if p.is_dir():
files = []
skip = {".git", "__pycache__", "node_modules", ".venv", "venv"}
exts = {".py", ".js", ".ts", ".tsx", ".jsx", ".md", ".yaml", ".yml",
".go", ".rs", ".java", ".rb", ".c", ".cpp", ".h", ".sh"}
for root, dirs, fnames in os.walk(p):
dirs[:] = [d for d in dirs if d not in skip]
for fn in fnames:
fp = Path(root) / fn
if fp.suffix.lower() in exts:
files.append(fp)
if len(files) >= max_files:
return files
return files
return []
def format_human(results):
"""Format results for human-readable output."""
lines = []
lines.append("=" * 60)
lines.append(" CONTEXT PRUNER REPORT")
lines.append("=" * 60)
total_saved = 0
total_original = 0
for r in results:
if "error" in r:
lines.append(f"\n {r['path']}: ERROR - {r['error']}")
continue
lines.append(f"\n File: {r['path']}")
lines.append(f" Lines: {r['original_lines']} -> {r['pruned_lines']} ({r['lines_removed']} removed)")
lines.append(f" Tokens: ~{r['original_tokens']:,} -> ~{r['pruned_tokens']:,} ({r['tokens_saved']:,} saved)")
lines.append(f" Compression: {r['compression_ratio'] * 100:.1f}%")
if r.get("operations"):
lines.append(" Operations:")
for op in r["operations"]:
lines.append(f" - {op['op']}: {op['lines_removed']} lines removed")
if "output_path" in r:
lines.append(f" Written to: {r['output_path']}")
total_saved += r.get("tokens_saved", 0)
total_original += r.get("original_tokens", 0)
lines.append("\n" + "-" * 60)
lines.append(f" Total tokens saved: ~{total_saved:,}")
if total_original > 0:
lines.append(f" Overall compression: {(total_original - total_saved) / total_original * 100:.1f}%")
lines.append("")
return "\n".join(lines)
def main():
parser = argparse.ArgumentParser(
description="Prune context by removing low-relevance content, redundancy, and verbose patterns.",
epilog="Example: python context_pruner.py src/main.py --aggressive --json",
)
parser.add_argument("path", nargs="?", help="File or directory to prune")
parser.add_argument("--stdin", action="store_true", help="Read content from stdin")
parser.add_argument("--level", choices=["light", "moderate", "aggressive"], default="moderate",
help="Pruning aggressiveness (default: moderate)")
parser.add_argument("--aggressive", action="store_true", help="Shortcut for --level aggressive")
parser.add_argument("--output", type=str, help="Output directory for pruned files")
parser.add_argument("--max-files", type=int, default=50, help="Max files to process (default: 50)")
parser.add_argument("--json", action="store_true", dest="json_output", help="Output results as JSON")
args = parser.parse_args()
level = "aggressive" if args.aggressive else args.level
if not args.path and not args.stdin:
parser.print_help()
sys.exit(1)
results = []
if args.stdin:
content = sys.stdin.read()
pruned, stats = prune_content(content, level)
stats["path"] = "<stdin>"
if not args.json_output:
print(pruned)
return
stats.pop("pruned_content", None)
results.append(stats)
elif args.path:
files = collect_files(args.path, max_files=args.max_files)
if not files:
print(f"Error: No files found at '{args.path}'", file=sys.stderr)
sys.exit(1)
for f in files:
r = prune_file(f, level, output_dir=args.output)
r.pop("pruned_content", None)
results.append(r)
if args.json_output:
print(json.dumps(results, indent=2))
else:
print(format_human(results))
if __name__ == "__main__":
main()
#!/usr/bin/env python3
"""Memory Indexer - Index and search a memory/knowledge base directory.
Scans a directory of markdown/text files, builds a term-frequency index,
and scores entries by relevance to a query using TF-IDF-inspired ranking.
Designed for the Context Engine's Session Memory and Knowledge Base layers.
Usage:
python memory_indexer.py path/to/knowledge_base --query "authentication flow"
python memory_indexer.py path/to/memory_dir --query "deployment" --top 5 --json
python memory_indexer.py path/to/dir --index-only --json
python memory_indexer.py path/to/dir --stats
"""
import argparse
import json
import math
import os
import re
import sys
from collections import Counter, defaultdict
from datetime import datetime
from pathlib import Path
# Stop words to exclude from indexing
STOP_WORDS = frozenset({
"a", "an", "the", "and", "or", "but", "in", "on", "at", "to", "for",
"of", "with", "by", "from", "is", "are", "was", "were", "be", "been",
"has", "have", "had", "do", "does", "did", "will", "would", "could",
"should", "may", "might", "can", "this", "that", "these", "those",
"it", "its", "not", "no", "if", "then", "else", "when", "which",
"what", "how", "who", "where", "why", "all", "each", "every", "both",
"as", "so", "up", "out", "about", "into", "over", "after", "before",
"between", "under", "above", "such", "only", "also", "than", "too",
"very", "just", "more", "most", "other", "some", "any", "new", "old",
"use", "used", "using", "see", "e", "g", "i", "we", "you", "they",
})
# File extensions to index
INDEXABLE_EXTENSIONS = {".md", ".txt", ".yaml", ".yml", ".json", ".toml", ".rst", ".org"}
# Boost factors for different content locations
TITLE_BOOST = 3.0
HEADING_BOOST = 2.0
FRONTMATTER_BOOST = 1.5
CODE_BLOCK_PENALTY = 0.5
def tokenize(text):
"""Split text into lowercase tokens, filtering stop words and short tokens."""
# Split on non-alphanumeric (keep underscores and hyphens for code terms)
raw_tokens = re.findall(r"[a-zA-Z_][a-zA-Z0-9_-]*", text.lower())
return [t for t in raw_tokens if t not in STOP_WORDS and len(t) > 1]
def extract_sections(content):
"""Extract structured sections from a markdown file."""
sections = []
current_heading = ""
current_body = []
in_code_block = False
in_frontmatter = False
frontmatter_text = []
lines = content.splitlines()
for i, line in enumerate(lines):
stripped = line.strip()
# Detect YAML frontmatter
if i == 0 and stripped == "---":
in_frontmatter = True
continue
if in_frontmatter:
if stripped == "---":
in_frontmatter = False
sections.append({
"type": "frontmatter",
"heading": "_frontmatter",
"content": "\n".join(frontmatter_text),
"line": 0,
})
continue
frontmatter_text.append(line)
continue
# Track code blocks
if stripped.startswith("```"):
in_code_block = not in_code_block
current_body.append(line)
continue
# Detect headings
if not in_code_block and re.match(r"^#{1,6}\s+", stripped):
# Flush previous section
if current_heading or current_body:
sections.append({
"type": "section",
"heading": current_heading,
"content": "\n".join(current_body),
"line": max(0, i - len(current_body)),
})
current_heading = re.sub(r"^#{1,6}\s+", "", stripped)
current_body = []
else:
current_body.append(line)
# Flush last section
if current_heading or current_body:
sections.append({
"type": "section",
"heading": current_heading,
"content": "\n".join(current_body),
"line": max(0, len(lines) - len(current_body)),
})
return sections
def build_file_entry(file_path):
"""Build an index entry for a single file."""
try:
with open(file_path, "r", encoding="utf-8", errors="replace") as f:
content = f.read()
except (OSError, IOError) as e:
return None
stat = os.stat(file_path)
sections = extract_sections(content)
# Build term frequency map with positional boosts
term_freq = Counter()
section_terms = {}
# Index file name tokens with title boost
name_tokens = tokenize(Path(file_path).stem.replace("-", " ").replace("_", " "))
for t in name_tokens:
term_freq[t] += TITLE_BOOST
for section in sections:
heading_tokens = tokenize(section["heading"])
body_tokens = tokenize(section["content"])
# Boost heading terms
for t in heading_tokens:
boost = FRONTMATTER_BOOST if section["type"] == "frontmatter" else HEADING_BOOST
term_freq[t] += boost
# Body terms at base weight (penalize code blocks slightly)
in_code = False
for line in section["content"].splitlines():
if line.strip().startswith("```"):
in_code = not in_code
continue
line_tokens = tokenize(line)
weight = CODE_BLOCK_PENALTY if in_code else 1.0
for t in line_tokens:
term_freq[t] += weight
section_key = section["heading"] or f"_section_{section['line']}"
section_terms[section_key] = set(heading_tokens + body_tokens)
total_tokens = sum(term_freq.values())
# Extract title from first heading or filename
title = Path(file_path).stem
for s in sections:
if s["type"] == "section" and s["heading"]:
title = s["heading"]
break
# Detect staleness based on modification time
mtime = datetime.fromtimestamp(stat.st_mtime)
days_old = (datetime.now() - mtime).days
if days_old < 7:
freshness = "fresh"
elif days_old < 30:
freshness = "aging"
else:
freshness = "stale"
return {
"path": str(file_path),
"title": title,
"size_bytes": stat.st_size,
"modified": mtime.isoformat(),
"days_since_modified": days_old,
"freshness": freshness,
"total_weighted_tokens": round(total_tokens, 1),
"unique_terms": len(term_freq),
"sections": [{"heading": s["heading"], "type": s["type"], "line": s["line"]}
for s in sections],
"term_freq": dict(term_freq),
"section_terms": {k: list(v) for k, v in section_terms.items()},
}
def build_index(directory, max_files=500):
"""Build a full index of a knowledge base directory."""
entries = []
doc_freq = Counter() # How many documents contain each term
skip_dirs = {".git", "__pycache__", "node_modules", ".venv", "venv"}
p = Path(directory)
if not p.is_dir():
return [], {}
file_count = 0
for root, dirs, fnames in os.walk(p):
dirs[:] = [d for d in dirs if d not in skip_dirs]
for fn in sorted(fnames):
fp = Path(root) / fn
if fp.suffix.lower() not in INDEXABLE_EXTENSIONS:
continue
entry = build_file_entry(fp)
if entry:
entries.append(entry)
# Track document frequency
for term in entry["term_freq"]:
doc_freq[term] += 1
file_count += 1
if file_count >= max_files:
break
if file_count >= max_files:
break
return entries, dict(doc_freq)
def score_query(query, entries, doc_freq):
"""Score all entries against a query using TF-IDF-inspired ranking."""
query_tokens = tokenize(query)
if not query_tokens:
return []
num_docs = max(len(entries), 1)
results = []
for entry in entries:
tf = entry["term_freq"]
total = max(entry["total_weighted_tokens"], 1)
score = 0.0
matched_terms = []
for qt in query_tokens:
if qt in tf:
# TF component: term frequency normalized by document length
tf_val = tf[qt] / total
# IDF component: inverse document frequency
df = doc_freq.get(qt, 1)
idf_val = math.log(num_docs / df) + 1
term_score = tf_val * idf_val
score += term_score
matched_terms.append(qt)
# Freshness bonus: fresh docs get a small boost
if entry["freshness"] == "fresh":
score *= 1.1
elif entry["freshness"] == "stale":
score *= 0.9
# Coverage bonus: matching more query terms is better
if len(query_tokens) > 1:
coverage = len(set(matched_terms)) / len(set(query_tokens))
score *= (0.5 + 0.5 * coverage)
if score > 0:
# Find which sections matched
matched_sections = []
query_set = set(query_tokens)
for sec_name, sec_terms in entry.get("section_terms", {}).items():
if query_set & set(sec_terms):
matched_sections.append(sec_name)
results.append({
"path": entry["path"],
"title": entry["title"],
"score": round(score, 4),
"matched_terms": list(set(matched_terms)),
"matched_sections": matched_sections,
"freshness": entry["freshness"],
"days_old": entry["days_since_modified"],
})
results.sort(key=lambda x: x["score"], reverse=True)
return results
def compute_stats(entries):
"""Compute summary statistics for the index."""
if not entries:
return {"total_files": 0}
total_terms = sum(e["unique_terms"] for e in entries)
total_size = sum(e["size_bytes"] for e in entries)
freshness_dist = Counter(e["freshness"] for e in entries)
# Find most common terms across corpus
global_freq = Counter()
for e in entries:
for term, freq in e["term_freq"].items():
global_freq[term] += freq
return {
"total_files": len(entries),
"total_size_bytes": total_size,
"total_unique_terms": len(global_freq),
"avg_terms_per_file": round(total_terms / len(entries), 1),
"freshness_distribution": dict(freshness_dist),
"top_terms": [{"term": t, "frequency": round(f, 1)} for t, f in global_freq.most_common(20)],
"largest_files": sorted(
[{"path": e["path"], "size": e["size_bytes"], "terms": e["unique_terms"]}
for e in entries],
key=lambda x: x["size"], reverse=True,
)[:10],
}
def format_search_results(results, query, top_n):
"""Format search results for human-readable output."""
lines = []
lines.append("=" * 60)
lines.append(f" MEMORY INDEX SEARCH: \"{query}\"")
lines.append("=" * 60)
if not results:
lines.append("\n No matching entries found.")
lines.append("")
return "\n".join(lines)
shown = results[:top_n]
lines.append(f"\n Found {len(results)} matching entries (showing top {len(shown)}):\n")
for i, r in enumerate(shown, 1):
freshness_marker = {"fresh": "+", "aging": "~", "stale": "-"}.get(r["freshness"], "?")
lines.append(f" {i}. [{freshness_marker}] {r['title']}")
lines.append(f" Path: {r['path']}")
lines.append(f" Score: {r['score']:.4f} | Age: {r['days_old']}d ({r['freshness']})")
lines.append(f" Matched: {', '.join(r['matched_terms'])}")
if r["matched_sections"]:
lines.append(f" Sections: {', '.join(r['matched_sections'][:5])}")
lines.append("")
lines.append(f" Legend: [+] fresh (<7d) [~] aging (7-30d) [-] stale (>30d)")
lines.append("")
return "\n".join(lines)
def format_stats(stats):
"""Format index statistics for human-readable output."""
lines = []
lines.append("=" * 60)
lines.append(" MEMORY INDEX STATISTICS")
lines.append("=" * 60)
lines.append(f"\n Total files indexed: {stats['total_files']}")
lines.append(f" Total size: {stats.get('total_size_bytes', 0):,} bytes")
lines.append(f" Unique terms: {stats.get('total_unique_terms', 0):,}")
lines.append(f" Avg terms/file: {stats.get('avg_terms_per_file', 0)}")
fd = stats.get("freshness_distribution", {})
if fd:
lines.append(f"\n Freshness: {fd.get('fresh', 0)} fresh, {fd.get('aging', 0)} aging, {fd.get('stale', 0)} stale")
top = stats.get("top_terms", [])
if top:
lines.append("\n Top terms:")
for t in top[:15]:
lines.append(f" {t['term']:<30} {t['frequency']:>8.0f}")
largest = stats.get("largest_files", [])
if largest:
lines.append("\n Largest files:")
for f in largest[:5]:
name = f["path"]
if len(name) > 45:
name = "..." + name[-42:]
lines.append(f" {name:<48} {f['size']:>8,} bytes")
lines.append("")
return "\n".join(lines)
def main():
parser = argparse.ArgumentParser(
description="Index and search a memory/knowledge base directory with TF-IDF relevance scoring.",
epilog="Example: python memory_indexer.py docs/ --query 'auth middleware' --top 5",
)
parser.add_argument("directory", help="Directory containing knowledge base files to index")
parser.add_argument("--query", "-q", type=str, help="Search query to score entries against")
parser.add_argument("--top", "-n", type=int, default=10, help="Number of top results to return (default: 10)")
parser.add_argument("--index-only", action="store_true", help="Build and display the index without searching")
parser.add_argument("--stats", action="store_true", help="Show index statistics")
parser.add_argument("--max-files", type=int, default=500, help="Maximum files to index (default: 500)")
parser.add_argument("--json", action="store_true", dest="json_output", help="Output results as JSON")
args = parser.parse_args()
if not os.path.isdir(args.directory):
print(f"Error: '{args.directory}' is not a directory", file=sys.stderr)
sys.exit(1)
if not args.query and not args.index_only and not args.stats:
parser.print_help()
print("\nError: Provide --query, --index-only, or --stats", file=sys.stderr)
sys.exit(1)
entries, doc_freq = build_index(args.directory, max_files=args.max_files)
if not entries:
print(f"No indexable files found in '{args.directory}'", file=sys.stderr)
sys.exit(1)
if args.stats:
stats = compute_stats(entries)
if args.json_output:
print(json.dumps(stats, indent=2))
else:
print(format_stats(stats))
return
if args.index_only:
index_data = {
"directory": args.directory,
"total_files": len(entries),
"entries": [
{
"path": e["path"],
"title": e["title"],
"freshness": e["freshness"],
"unique_terms": e["unique_terms"],
"sections": e["sections"],
}
for e in entries
],
}
if args.json_output:
print(json.dumps(index_data, indent=2))
else:
print(f"Indexed {len(entries)} files from '{args.directory}'")
for e in entries:
marker = {"fresh": "+", "aging": "~", "stale": "-"}.get(e["freshness"], "?")
print(f" [{marker}] {e['title']} ({e['unique_terms']} terms) - {e['path']}")
return
if args.query:
results = score_query(args.query, entries, doc_freq)
if args.json_output:
output = {
"query": args.query,
"total_matches": len(results),
"results": results[:args.top],
}
print(json.dumps(output, indent=2))
else:
print(format_search_results(results, args.query, args.top))
if __name__ == "__main__":
main()
Related skills
FAQ
What is the memory model?
A three-layer model: working memory, session memory, and an indexed knowledge base.
How does it chunk code for RAG?
By function or class boundaries, capped at about 200 lines with 2 lines of overlap.