contextual-engine 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- contextual/__init__.py +18 -0
- contextual/__main__.py +11 -0
- contextual/cli.py +339 -0
- contextual/cli_docs.py +685 -0
- contextual/config.py +7 -0
- contextual/core/__init__.py +11 -0
- contextual/core/errors.py +470 -0
- contextual/core/models.py +590 -0
- contextual/docs/__init__.py +66 -0
- contextual/docs/chunker.py +550 -0
- contextual/docs/pipeline.py +513 -0
- contextual/docs/retrieval.py +654 -0
- contextual/docs/watcher.py +265 -0
- contextual/embedding/__init__.py +87 -0
- contextual/embedding/cache.py +455 -0
- contextual/embedding/embedder.py +414 -0
- contextual/embedding/helpers.py +252 -0
- contextual/git/__init__.py +22 -0
- contextual/git/blame.py +334 -0
- contextual/indexing/__init__.py +20 -0
- contextual/indexing/bug_sweep.py +119 -0
- contextual/indexing/chunker.py +691 -0
- contextual/indexing/embedder.py +271 -0
- contextual/indexing/file_watcher.py +154 -0
- contextual/indexing/incremental.py +260 -0
- contextual/indexing/index_writer.py +442 -0
- contextual/indexing/pipeline.py +438 -0
- contextual/indexing/processor.py +436 -0
- contextual/indexing/queries/readme.md +22 -0
- contextual/indexing/symbol_extractor.py +426 -0
- contextual/indexing/tokenizer.py +203 -0
- contextual/integrations/__init__.py +10 -0
- contextual/mcp/__init__.py +15 -0
- contextual/mcp/__main__.py +24 -0
- contextual/mcp/docs_tools.py +286 -0
- contextual/mcp/server.py +118 -0
- contextual/mcp/tools.py +443 -0
- contextual/observability/__init__.py +21 -0
- contextual/observability/logging.py +115 -0
- contextual/py.typed +0 -0
- contextual/retrieval/__init__.py +24 -0
- contextual/retrieval/context_assembler.py +372 -0
- contextual/retrieval/ranker.py +193 -0
- contextual/retrieval/search.py +548 -0
- contextual/security/__init__.py +52 -0
- contextual/security/paths.py +347 -0
- contextual/security/sanitize.py +349 -0
- contextual/security/workspace.py +348 -0
- contextual/storage/__init__.py +36 -0
- contextual/storage/fts_manager.py +273 -0
- contextual/storage/migration_v2.py +289 -0
- contextual/storage/migrations.py +316 -0
- contextual/storage/schema.py +210 -0
- contextual/storage/sqlite_pool.py +468 -0
- contextual/storage/vec0_manager.py +421 -0
- contextual_engine-0.1.0.dist-info/METADATA +297 -0
- contextual_engine-0.1.0.dist-info/RECORD +60 -0
- contextual_engine-0.1.0.dist-info/WHEEL +4 -0
- contextual_engine-0.1.0.dist-info/entry_points.txt +2 -0
- contextual_engine-0.1.0.dist-info/licenses/LICENSE +111 -0
contextual/__init__.py
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"""Contextual: Temporal-first local AI context engine.
|
|
2
|
+
|
|
3
|
+
A tool-agnostic semantic code memory system that eliminates AI hallucinations
|
|
4
|
+
through bi-temporal fact tracking and deterministic retrieval.
|
|
5
|
+
"""
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
__version__ = "0.1.0"
|
|
10
|
+
__author__ = "Contextual Team"
|
|
11
|
+
__license__ = "BSL-1.1"
|
|
12
|
+
|
|
13
|
+
# Public API exports will be added as modules are built
|
|
14
|
+
__all__ = [
|
|
15
|
+
"__author__",
|
|
16
|
+
"__license__",
|
|
17
|
+
"__version__",
|
|
18
|
+
]
|
contextual/__main__.py
ADDED
contextual/cli.py
ADDED
|
@@ -0,0 +1,339 @@
|
|
|
1
|
+
"""CLI for Contextual - Temporal-first local code memory.
|
|
2
|
+
|
|
3
|
+
Main commands:
|
|
4
|
+
- init: Initialize configuration
|
|
5
|
+
- index: Index a code repository
|
|
6
|
+
- search: Search indexed code
|
|
7
|
+
- stats: Show index statistics
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import sys
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
from typing import Annotated, Optional
|
|
15
|
+
|
|
16
|
+
import typer
|
|
17
|
+
from rich.console import Console
|
|
18
|
+
from rich.table import Table
|
|
19
|
+
|
|
20
|
+
from contextual.embedding import embed_query_simple
|
|
21
|
+
from contextual.indexing.pipeline import IndexingPipeline
|
|
22
|
+
from contextual.observability.logging import get_logger
|
|
23
|
+
from contextual.retrieval.search import hybrid_search, SearchResult
|
|
24
|
+
from contextual.storage.sqlite_pool import SQLitePool
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
logger = get_logger(__name__)
|
|
28
|
+
console = Console()
|
|
29
|
+
app = typer.Typer(
|
|
30
|
+
name="contextual",
|
|
31
|
+
help="Temporal-first local code memory for AI tools",
|
|
32
|
+
add_completion=False,
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@app.command()
|
|
37
|
+
def init(
|
|
38
|
+
path: Annotated[
|
|
39
|
+
Optional[Path],
|
|
40
|
+
typer.Argument(help="Repository path to initialize (default: current directory)"),
|
|
41
|
+
] = None,
|
|
42
|
+
) -> None:
|
|
43
|
+
"""Initialize Contextual configuration for a repository.
|
|
44
|
+
|
|
45
|
+
Creates .contextual/ directory with database and config.
|
|
46
|
+
"""
|
|
47
|
+
repo_path = (path or Path.cwd()).resolve()
|
|
48
|
+
|
|
49
|
+
if not repo_path.exists():
|
|
50
|
+
console.print(f"[red]Error:[/red] Path does not exist: {repo_path}")
|
|
51
|
+
raise typer.Exit(1)
|
|
52
|
+
|
|
53
|
+
# Create .contextual directory
|
|
54
|
+
contextual_dir = repo_path / ".contextual"
|
|
55
|
+
contextual_dir.mkdir(exist_ok=True)
|
|
56
|
+
|
|
57
|
+
# Initialize database
|
|
58
|
+
db_path = contextual_dir / "contextual.db"
|
|
59
|
+
|
|
60
|
+
console.print(f"[bold]Initializing Contextual in:[/bold] {repo_path}")
|
|
61
|
+
|
|
62
|
+
try:
|
|
63
|
+
# Create pool and run migrations
|
|
64
|
+
from contextual.storage.migrations import run_migrations
|
|
65
|
+
|
|
66
|
+
pool = SQLitePool(db_path)
|
|
67
|
+
with pool.writer() as conn:
|
|
68
|
+
run_migrations(conn)
|
|
69
|
+
pool.close()
|
|
70
|
+
|
|
71
|
+
console.print(f"[green]✓[/green] Created database: {db_path}")
|
|
72
|
+
console.print(f"[green]✓[/green] Initialization complete")
|
|
73
|
+
console.print(f"\n[dim]Next steps:[/dim]")
|
|
74
|
+
console.print(f" contextual index {repo_path}")
|
|
75
|
+
console.print(f" contextual search 'your query'")
|
|
76
|
+
|
|
77
|
+
except Exception as e:
|
|
78
|
+
console.print(f"[red]Error:[/red] Initialization failed: {e}")
|
|
79
|
+
logger.exception("Initialization failed")
|
|
80
|
+
raise typer.Exit(1)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
@app.command()
|
|
84
|
+
def index(
|
|
85
|
+
path: Annotated[
|
|
86
|
+
Optional[Path],
|
|
87
|
+
typer.Argument(help="Repository path to index (default: current directory)"),
|
|
88
|
+
] = None,
|
|
89
|
+
force: Annotated[
|
|
90
|
+
bool,
|
|
91
|
+
typer.Option("--force", "-f", help="Force re-index even if up-to-date"),
|
|
92
|
+
] = False,
|
|
93
|
+
) -> None:
|
|
94
|
+
"""Index a code repository for semantic search.
|
|
95
|
+
|
|
96
|
+
Discovers source files, extracts symbols, generates embeddings,
|
|
97
|
+
and builds search index (BM25 + vector).
|
|
98
|
+
"""
|
|
99
|
+
repo_path = (path or Path.cwd()).resolve()
|
|
100
|
+
|
|
101
|
+
if not repo_path.exists():
|
|
102
|
+
console.print(f"[red]Error:[/red] Path does not exist: {repo_path}")
|
|
103
|
+
raise typer.Exit(1)
|
|
104
|
+
|
|
105
|
+
# Check for .contextual directory
|
|
106
|
+
contextual_dir = repo_path / ".contextual"
|
|
107
|
+
if not contextual_dir.exists():
|
|
108
|
+
console.print(f"[yellow]Warning:[/yellow] Not initialized. Running init first...")
|
|
109
|
+
init(repo_path)
|
|
110
|
+
|
|
111
|
+
db_path = contextual_dir / "contextual.db"
|
|
112
|
+
|
|
113
|
+
console.print(f"[bold]Indexing:[/bold] {repo_path}")
|
|
114
|
+
|
|
115
|
+
try:
|
|
116
|
+
# Initialize pipeline
|
|
117
|
+
pool = SQLitePool(db_path)
|
|
118
|
+
pipeline = IndexingPipeline(repo_root=repo_path, db_pool=pool)
|
|
119
|
+
|
|
120
|
+
# Run indexing
|
|
121
|
+
with console.status("[bold green]Indexing files..."):
|
|
122
|
+
result = pipeline.index_repository()
|
|
123
|
+
|
|
124
|
+
# Display results
|
|
125
|
+
console.print(f"\n[green]✓[/green] Indexing complete")
|
|
126
|
+
|
|
127
|
+
# Results table
|
|
128
|
+
table = Table(show_header=True, header_style="bold cyan")
|
|
129
|
+
table.add_column("Metric", style="dim")
|
|
130
|
+
table.add_column("Value", justify="right")
|
|
131
|
+
|
|
132
|
+
table.add_row("Files indexed", str(result.get("files", 0)))
|
|
133
|
+
table.add_row("Chunks created", str(result.get("chunks", 0)))
|
|
134
|
+
table.add_row("Embeddings generated", str(result.get("embeddings", 0)))
|
|
135
|
+
table.add_row("Symbols extracted", str(result.get("symbols", 0)))
|
|
136
|
+
table.add_row("Duration", f"{result.get('duration_ms', 0) / 1000:.2f}s")
|
|
137
|
+
|
|
138
|
+
console.print(table)
|
|
139
|
+
|
|
140
|
+
pool.close()
|
|
141
|
+
|
|
142
|
+
except Exception as e:
|
|
143
|
+
console.print(f"[red]Error:[/red] Indexing failed: {e}")
|
|
144
|
+
logger.exception("Indexing failed")
|
|
145
|
+
raise typer.Exit(1)
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
@app.command()
|
|
149
|
+
def search(
|
|
150
|
+
query: Annotated[str, typer.Argument(help="Search query")],
|
|
151
|
+
path: Annotated[
|
|
152
|
+
Optional[Path],
|
|
153
|
+
typer.Option("--path", "-p", help="Repository path (default: current directory)"),
|
|
154
|
+
] = None,
|
|
155
|
+
limit: Annotated[
|
|
156
|
+
int,
|
|
157
|
+
typer.Option("--limit", "-n", help="Number of results", min=1, max=50),
|
|
158
|
+
] = 10,
|
|
159
|
+
language: Annotated[
|
|
160
|
+
Optional[str],
|
|
161
|
+
typer.Option("--language", "-l", help="Filter by language (py, ts, js, rs, go)"),
|
|
162
|
+
] = None,
|
|
163
|
+
) -> None:
|
|
164
|
+
"""Search indexed code semantically.
|
|
165
|
+
|
|
166
|
+
Uses hybrid search (BM25 + vector embeddings) with RRF fusion.
|
|
167
|
+
"""
|
|
168
|
+
repo_path = (path or Path.cwd()).resolve()
|
|
169
|
+
contextual_dir = repo_path / ".contextual"
|
|
170
|
+
|
|
171
|
+
if not contextual_dir.exists():
|
|
172
|
+
console.print(f"[red]Error:[/red] Repository not indexed. Run 'contextual index' first.")
|
|
173
|
+
raise typer.Exit(1)
|
|
174
|
+
|
|
175
|
+
db_path = contextual_dir / "contextual.db"
|
|
176
|
+
|
|
177
|
+
console.print(f"[bold]Searching:[/bold] {query}\n")
|
|
178
|
+
|
|
179
|
+
try:
|
|
180
|
+
# Initialize database
|
|
181
|
+
pool = SQLitePool(db_path)
|
|
182
|
+
|
|
183
|
+
# Embed query
|
|
184
|
+
query_embedding = embed_query_simple(query)
|
|
185
|
+
|
|
186
|
+
# Execute search
|
|
187
|
+
with pool.reader() as conn:
|
|
188
|
+
results: list[SearchResult] = hybrid_search(
|
|
189
|
+
conn=conn,
|
|
190
|
+
query=query,
|
|
191
|
+
query_embedding=query_embedding,
|
|
192
|
+
top_k=limit,
|
|
193
|
+
language=language,
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
if not results:
|
|
197
|
+
console.print("[yellow]No results found[/yellow]")
|
|
198
|
+
pool.close()
|
|
199
|
+
return
|
|
200
|
+
|
|
201
|
+
# Display results
|
|
202
|
+
for i, result in enumerate(results, 1):
|
|
203
|
+
console.print(f"[bold cyan]{i}. {result.path}[/bold cyan]")
|
|
204
|
+
console.print(f" Lines {result.start_line}-{result.end_line} | Score: {result.score:.3f}")
|
|
205
|
+
|
|
206
|
+
if result.symbol_name:
|
|
207
|
+
console.print(f" Symbol: [green]{result.symbol_name}[/green] ({result.chunk_type})")
|
|
208
|
+
|
|
209
|
+
# Show snippet
|
|
210
|
+
console.print(f" [dim]{result.snippet}[/dim]\n")
|
|
211
|
+
|
|
212
|
+
pool.close()
|
|
213
|
+
|
|
214
|
+
except Exception as e:
|
|
215
|
+
console.print(f"[red]Error:[/red] Search failed: {e}")
|
|
216
|
+
logger.exception("Search failed")
|
|
217
|
+
raise typer.Exit(1)
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
@app.command()
|
|
221
|
+
def stats(
|
|
222
|
+
path: Annotated[
|
|
223
|
+
Optional[Path],
|
|
224
|
+
typer.Argument(help="Repository path (default: current directory)"),
|
|
225
|
+
] = None,
|
|
226
|
+
) -> None:
|
|
227
|
+
"""Show index statistics for a repository."""
|
|
228
|
+
repo_path = (path or Path.cwd()).resolve()
|
|
229
|
+
contextual_dir = repo_path / ".contextual"
|
|
230
|
+
|
|
231
|
+
if not contextual_dir.exists():
|
|
232
|
+
console.print(f"[red]Error:[/red] Repository not indexed. Run 'contextual index' first.")
|
|
233
|
+
raise typer.Exit(1)
|
|
234
|
+
|
|
235
|
+
db_path = contextual_dir / "contextual.db"
|
|
236
|
+
|
|
237
|
+
try:
|
|
238
|
+
pool = SQLitePool(db_path)
|
|
239
|
+
|
|
240
|
+
# Query statistics
|
|
241
|
+
with pool.reader() as conn:
|
|
242
|
+
# File count
|
|
243
|
+
cursor = conn.execute("SELECT COUNT(*) FROM files")
|
|
244
|
+
file_count = cursor.fetchone()[0]
|
|
245
|
+
|
|
246
|
+
# Chunk count
|
|
247
|
+
cursor = conn.execute("SELECT COUNT(*) FROM chunks")
|
|
248
|
+
chunk_count = cursor.fetchone()[0]
|
|
249
|
+
|
|
250
|
+
# Symbol count
|
|
251
|
+
cursor = conn.execute("SELECT COUNT(*) FROM code_symbols")
|
|
252
|
+
symbol_count = cursor.fetchone()[0]
|
|
253
|
+
|
|
254
|
+
# Language distribution
|
|
255
|
+
cursor = conn.execute("""
|
|
256
|
+
SELECT language, COUNT(*) as count
|
|
257
|
+
FROM files
|
|
258
|
+
GROUP BY language
|
|
259
|
+
ORDER BY count DESC
|
|
260
|
+
""")
|
|
261
|
+
languages = cursor.fetchall()
|
|
262
|
+
|
|
263
|
+
# Indexing state
|
|
264
|
+
cursor = conn.execute("""
|
|
265
|
+
SELECT last_full_index_at, total_files, total_chunks
|
|
266
|
+
FROM indexing_state
|
|
267
|
+
LIMIT 1
|
|
268
|
+
""")
|
|
269
|
+
state = cursor.fetchone()
|
|
270
|
+
|
|
271
|
+
# Display statistics
|
|
272
|
+
console.print(f"[bold]Statistics for:[/bold] {repo_path}\n")
|
|
273
|
+
|
|
274
|
+
table = Table(show_header=True, header_style="bold cyan")
|
|
275
|
+
table.add_column("Metric", style="dim")
|
|
276
|
+
table.add_column("Value", justify="right")
|
|
277
|
+
|
|
278
|
+
table.add_row("Files indexed", str(file_count))
|
|
279
|
+
table.add_row("Chunks created", str(chunk_count))
|
|
280
|
+
table.add_row("Symbols extracted", str(symbol_count))
|
|
281
|
+
|
|
282
|
+
if state:
|
|
283
|
+
table.add_row("Last indexed", str(state[0]))
|
|
284
|
+
|
|
285
|
+
console.print(table)
|
|
286
|
+
|
|
287
|
+
# Language distribution
|
|
288
|
+
if languages:
|
|
289
|
+
console.print("\n[bold]Languages:[/bold]")
|
|
290
|
+
lang_table = Table(show_header=False)
|
|
291
|
+
lang_table.add_column("Language", style="cyan")
|
|
292
|
+
lang_table.add_column("Files", justify="right")
|
|
293
|
+
|
|
294
|
+
for lang, count in languages:
|
|
295
|
+
lang_table.add_row(lang or "unknown", str(count))
|
|
296
|
+
|
|
297
|
+
console.print(lang_table)
|
|
298
|
+
|
|
299
|
+
# Database stats
|
|
300
|
+
db_stats = pool.get_db_stats()
|
|
301
|
+
console.print(f"\n[dim]Database size: {db_stats.get('db_size_bytes', 0) / 1024 / 1024:.2f} MB[/dim]")
|
|
302
|
+
|
|
303
|
+
pool.close()
|
|
304
|
+
|
|
305
|
+
except Exception as e:
|
|
306
|
+
console.print(f"[red]Error:[/red] Failed to get statistics: {e}")
|
|
307
|
+
logger.exception("Stats failed")
|
|
308
|
+
raise typer.Exit(1)
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
@app.command()
|
|
312
|
+
def version() -> None:
|
|
313
|
+
"""Show Contextual version."""
|
|
314
|
+
try:
|
|
315
|
+
from contextual import __version__
|
|
316
|
+
console.print(f"Contextual version {__version__}")
|
|
317
|
+
except ImportError:
|
|
318
|
+
console.print("Contextual version 0.1.0")
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
def main() -> None:
|
|
322
|
+
"""Entry point for CLI."""
|
|
323
|
+
try:
|
|
324
|
+
app()
|
|
325
|
+
except KeyboardInterrupt:
|
|
326
|
+
console.print("\n[yellow]Interrupted[/yellow]")
|
|
327
|
+
sys.exit(130)
|
|
328
|
+
except Exception as e:
|
|
329
|
+
console.print(f"[red]Fatal error:[/red] {e}")
|
|
330
|
+
logger.exception("Fatal error")
|
|
331
|
+
sys.exit(1)
|
|
332
|
+
|
|
333
|
+
from contextual.cli_docs import docs_app, setup_app
|
|
334
|
+
app.add_typer(docs_app, name="docs", help="Index and search documentation.")
|
|
335
|
+
app.add_typer(setup_app, name="setup", help="Configure Contextual in your IDE/AI client.")
|
|
336
|
+
|
|
337
|
+
|
|
338
|
+
if __name__ == "__main__":
|
|
339
|
+
main()
|