codegraph-engine 2.1.1__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.
- codegraph/__init__.py +37 -0
- codegraph/agent.py +26 -0
- codegraph/architecture.py +328 -0
- codegraph/audit.py +106 -0
- codegraph/cache.py +95 -0
- codegraph/cli.py +854 -0
- codegraph/config.py +43 -0
- codegraph/constraints.py +238 -0
- codegraph/context.py +1228 -0
- codegraph/epistemic.py +90 -0
- codegraph/errors.py +275 -0
- codegraph/evidence/__init__.py +15 -0
- codegraph/evidence/citations.py +397 -0
- codegraph/frameworks.py +434 -0
- codegraph/freshness.py +295 -0
- codegraph/git.py +278 -0
- codegraph/graph/__init__.py +46 -0
- codegraph/graph/models.py +41 -0
- codegraph/graph/traversal.py +1291 -0
- codegraph/indexing/__init__.py +4 -0
- codegraph/indexing/classifier.py +274 -0
- codegraph/indexing/indexer.py +943 -0
- codegraph/indexing/models.py +338 -0
- codegraph/indexing/parser.py +1240 -0
- codegraph/indexing/scanner.py +200 -0
- codegraph/indexing/test_framework.py +116 -0
- codegraph/interrogation.py +1582 -0
- codegraph/llm/__init__.py +3 -0
- codegraph/llm/base.py +15 -0
- codegraph/llm/context.py +20 -0
- codegraph/mcp/__init__.py +3 -0
- codegraph/mcp/server.py +736 -0
- codegraph/memory/__init__.py +3 -0
- codegraph/memory/store.py +46 -0
- codegraph/models.py +289 -0
- codegraph/observability.py +151 -0
- codegraph/optimizer.py +372 -0
- codegraph/planner.py +417 -0
- codegraph/py.typed +1 -0
- codegraph/query_expansion.py +199 -0
- codegraph/ranking.py +363 -0
- codegraph/resolver.py +843 -0
- codegraph/resources/__init__.py +45 -0
- codegraph/resources/cache.py +117 -0
- codegraph/resources/coalescer.py +83 -0
- codegraph/resources/debouncer.py +98 -0
- codegraph/resources/governor.py +232 -0
- codegraph/resources/policy.py +123 -0
- codegraph/retrieval_policy.py +220 -0
- codegraph/search/__init__.py +23 -0
- codegraph/search/hybrid.py +301 -0
- codegraph/search/semantic.py +28 -0
- codegraph/security/__init__.py +3 -0
- codegraph/security/paths.py +35 -0
- codegraph/target_resolver.py +348 -0
- codegraph/task.py +637 -0
- codegraph_engine-2.1.1.dist-info/METADATA +334 -0
- codegraph_engine-2.1.1.dist-info/RECORD +62 -0
- codegraph_engine-2.1.1.dist-info/WHEEL +5 -0
- codegraph_engine-2.1.1.dist-info/entry_points.txt +2 -0
- codegraph_engine-2.1.1.dist-info/licenses/LICENSE +21 -0
- codegraph_engine-2.1.1.dist-info/top_level.txt +1 -0
codegraph/cli.py
ADDED
|
@@ -0,0 +1,854 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import sqlite3
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Annotated
|
|
7
|
+
|
|
8
|
+
import typer
|
|
9
|
+
|
|
10
|
+
from codegraph import __version__
|
|
11
|
+
from codegraph.architecture import get_architecture
|
|
12
|
+
from codegraph.config import Settings
|
|
13
|
+
from codegraph.context import get_context
|
|
14
|
+
from codegraph.errors import ErrorCode, SecurityError
|
|
15
|
+
from codegraph.freshness import check_freshness, index_generation
|
|
16
|
+
from codegraph.graph import trace_call
|
|
17
|
+
from codegraph.graph.traversal import get_focused_graph, get_graph_summary
|
|
18
|
+
from codegraph.indexing import Indexer
|
|
19
|
+
from codegraph.indexing.indexer import check_database_health
|
|
20
|
+
from codegraph.interrogation import (
|
|
21
|
+
get_dependents as interrogation_get_dependents,
|
|
22
|
+
)
|
|
23
|
+
from codegraph.interrogation import (
|
|
24
|
+
get_imports as interrogation_get_imports,
|
|
25
|
+
)
|
|
26
|
+
from codegraph.interrogation import (
|
|
27
|
+
get_symbol as interrogation_get_symbol,
|
|
28
|
+
)
|
|
29
|
+
from codegraph.interrogation import (
|
|
30
|
+
resolve_symbol as interrogation_resolve_symbol,
|
|
31
|
+
)
|
|
32
|
+
from codegraph.memory import MemoryStore
|
|
33
|
+
from codegraph.planner import build_retrieval_plan
|
|
34
|
+
from codegraph.resolver import ReferenceResolver
|
|
35
|
+
from codegraph.search import search as search_code
|
|
36
|
+
from codegraph.security import is_sensitive, safe_path
|
|
37
|
+
from codegraph.task import TargetExpressionType, classify_target_expression, normalize_task_spec
|
|
38
|
+
|
|
39
|
+
app = typer.Typer(no_args_is_help=True, help="Evidence-backed local codebase intelligence.")
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _version_callback(value: bool) -> None:
|
|
43
|
+
if value:
|
|
44
|
+
typer.echo(f"codegraph {__version__}")
|
|
45
|
+
raise typer.Exit()
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@app.callback()
|
|
49
|
+
def main(
|
|
50
|
+
version: Annotated[
|
|
51
|
+
bool,
|
|
52
|
+
typer.Option("--version", "-v", help="Show version and exit", callback=_version_callback, is_eager=True),
|
|
53
|
+
] = False,
|
|
54
|
+
) -> None:
|
|
55
|
+
"""Evidence-backed local codebase intelligence for MCP clients and AI agents."""
|
|
56
|
+
pass
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _resolve_repo(
|
|
60
|
+
path: Path | None = None,
|
|
61
|
+
repository: Path | None = None,
|
|
62
|
+
) -> Path:
|
|
63
|
+
"""Resolve repository path with priority: explicit option > positional argument > current directory."""
|
|
64
|
+
target: Path
|
|
65
|
+
if repository is not None:
|
|
66
|
+
target = repository
|
|
67
|
+
elif path is not None:
|
|
68
|
+
target = path
|
|
69
|
+
else:
|
|
70
|
+
target = Path.cwd()
|
|
71
|
+
|
|
72
|
+
if not target.exists():
|
|
73
|
+
typer.echo(
|
|
74
|
+
json.dumps(
|
|
75
|
+
{
|
|
76
|
+
"status": "error",
|
|
77
|
+
"error": {
|
|
78
|
+
"code": ErrorCode.INVALID_PATH.value,
|
|
79
|
+
"message": f"Repository path does not exist: {target}",
|
|
80
|
+
"next_action": {
|
|
81
|
+
"command": "codegraph init .",
|
|
82
|
+
"reason": "Specify an existing directory path.",
|
|
83
|
+
},
|
|
84
|
+
},
|
|
85
|
+
},
|
|
86
|
+
indent=2,
|
|
87
|
+
)
|
|
88
|
+
)
|
|
89
|
+
raise typer.Exit(code=1)
|
|
90
|
+
|
|
91
|
+
if not target.is_dir():
|
|
92
|
+
typer.echo(
|
|
93
|
+
json.dumps(
|
|
94
|
+
{
|
|
95
|
+
"status": "error",
|
|
96
|
+
"error": {
|
|
97
|
+
"code": ErrorCode.INVALID_PATH.value,
|
|
98
|
+
"message": f"Repository path is not a directory: {target}",
|
|
99
|
+
"next_action": {
|
|
100
|
+
"command": "codegraph init .",
|
|
101
|
+
"reason": "Specify a valid directory path.",
|
|
102
|
+
},
|
|
103
|
+
},
|
|
104
|
+
},
|
|
105
|
+
indent=2,
|
|
106
|
+
)
|
|
107
|
+
)
|
|
108
|
+
raise typer.Exit(code=1)
|
|
109
|
+
|
|
110
|
+
return target.resolve()
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def _ensure_indexed(repo: Path) -> bool:
|
|
114
|
+
"""Check if repository has an index database; if not, output structured INDEX_NOT_FOUND error."""
|
|
115
|
+
db = repo / ".codegraph.sqlite3"
|
|
116
|
+
if not db.exists():
|
|
117
|
+
typer.echo(
|
|
118
|
+
json.dumps(
|
|
119
|
+
{
|
|
120
|
+
"status": "error",
|
|
121
|
+
"error": {
|
|
122
|
+
"code": ErrorCode.INDEX_NOT_FOUND.value,
|
|
123
|
+
"message": "No CodeGraph index exists for this repository.",
|
|
124
|
+
"next_action": {
|
|
125
|
+
"command": "codegraph init",
|
|
126
|
+
"reason": "Initialize the repository before querying it.",
|
|
127
|
+
},
|
|
128
|
+
},
|
|
129
|
+
},
|
|
130
|
+
indent=2,
|
|
131
|
+
)
|
|
132
|
+
)
|
|
133
|
+
return False
|
|
134
|
+
return True
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def _indexer(repository: Path, db_path: Path | None = None) -> Indexer:
|
|
138
|
+
return Indexer(repository, Settings(repository=repository, db_path=db_path))
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
@app.command()
|
|
142
|
+
def index(
|
|
143
|
+
path: Annotated[Path | None, typer.Argument(help="Repository path to index (default: current directory)")] = None,
|
|
144
|
+
repository: Annotated[Path | None, typer.Option("--repository", "-r", "--repo", help="Repository path")] = None,
|
|
145
|
+
json_output: Annotated[bool, typer.Option("--json")] = False,
|
|
146
|
+
) -> None:
|
|
147
|
+
"""Securely index supported source files, skipping unchanged content."""
|
|
148
|
+
repo = _resolve_repo(path, repository)
|
|
149
|
+
result = _indexer(repo).index()
|
|
150
|
+
typer.echo(json.dumps(result) if json_output else " ".join(f"{k}={v}" for k, v in result.items()))
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
@app.command()
|
|
154
|
+
def init(
|
|
155
|
+
path: Annotated[Path | None, typer.Argument(help="Repository path to initialize (default: current directory)")] = None,
|
|
156
|
+
repository: Annotated[Path | None, typer.Option("--repository", "-r", "--repo", help="Repository path")] = None,
|
|
157
|
+
json_output: Annotated[bool, typer.Option("--json")] = False,
|
|
158
|
+
) -> None:
|
|
159
|
+
"""Initialize repository indexing and configuration."""
|
|
160
|
+
target_repo = _resolve_repo(path, repository)
|
|
161
|
+
result = _indexer(target_repo).index()
|
|
162
|
+
if json_output:
|
|
163
|
+
typer.echo(json.dumps({"status": "initialized", "repository": str(target_repo.resolve()), "indexing": result}, indent=2))
|
|
164
|
+
else:
|
|
165
|
+
typer.echo(f"Initialized CodeGraph repository at {target_repo.resolve()} (indexed {result.get('indexed_files', 0)} files)")
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
@app.command()
|
|
169
|
+
def search(
|
|
170
|
+
query: str,
|
|
171
|
+
repository: Annotated[Path | None, typer.Option("--repository", "-r", "--repo", help="Repository path")] = None,
|
|
172
|
+
top_k: int = 10,
|
|
173
|
+
json_output: Annotated[bool, typer.Option("--json")] = False,
|
|
174
|
+
) -> None:
|
|
175
|
+
"""Search indexed code with exact evidence."""
|
|
176
|
+
repo = _resolve_repo(repository=repository)
|
|
177
|
+
if not _ensure_indexed(repo):
|
|
178
|
+
return
|
|
179
|
+
if not query.strip():
|
|
180
|
+
typer.echo(
|
|
181
|
+
json.dumps(
|
|
182
|
+
{
|
|
183
|
+
"status": "error",
|
|
184
|
+
"error": {
|
|
185
|
+
"code": ErrorCode.INVALID_ARGUMENT.value,
|
|
186
|
+
"message": "Search query must be non-empty.",
|
|
187
|
+
"next_action": {
|
|
188
|
+
"command": "codegraph search <query>",
|
|
189
|
+
"reason": "Provide a non-empty search query.",
|
|
190
|
+
},
|
|
191
|
+
},
|
|
192
|
+
},
|
|
193
|
+
indent=2,
|
|
194
|
+
)
|
|
195
|
+
)
|
|
196
|
+
return
|
|
197
|
+
with _indexer(repo).session() as con:
|
|
198
|
+
result = [r.as_dict() for r in search_code(con, query, top_k)]
|
|
199
|
+
typer.echo(
|
|
200
|
+
json.dumps(result, indent=2)
|
|
201
|
+
if json_output
|
|
202
|
+
else "\n\n".join(
|
|
203
|
+
f"{r['file']}:{r['start_line']}-{r['end_line']} {r['symbol'] or ''} score={r['score']}\n{r['snippet']}"
|
|
204
|
+
for r in result
|
|
205
|
+
)
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
@app.command()
|
|
210
|
+
def symbols(
|
|
211
|
+
path: str,
|
|
212
|
+
repository: Annotated[Path | None, typer.Option("--repository", "-r", "--repo", help="Repository path")] = None,
|
|
213
|
+
) -> None:
|
|
214
|
+
"""Show extracted symbols for an indexed file."""
|
|
215
|
+
repo = _resolve_repo(repository=repository)
|
|
216
|
+
if not _ensure_indexed(repo):
|
|
217
|
+
return
|
|
218
|
+
try:
|
|
219
|
+
safe_path(repo, path)
|
|
220
|
+
except SecurityError:
|
|
221
|
+
typer.echo(
|
|
222
|
+
json.dumps(
|
|
223
|
+
{
|
|
224
|
+
"status": "error",
|
|
225
|
+
"error": {
|
|
226
|
+
"code": ErrorCode.PATH_OUTSIDE_REPOSITORY.value,
|
|
227
|
+
"message": f"Path traversal blocked: '{path}' resides outside repository boundary.",
|
|
228
|
+
"next_action": {
|
|
229
|
+
"command": "codegraph status",
|
|
230
|
+
"reason": "Ensure all queried paths reside within the repository root.",
|
|
231
|
+
},
|
|
232
|
+
},
|
|
233
|
+
},
|
|
234
|
+
indent=2,
|
|
235
|
+
)
|
|
236
|
+
)
|
|
237
|
+
return
|
|
238
|
+
with _indexer(repo).session() as con:
|
|
239
|
+
rows = con.execute(
|
|
240
|
+
"SELECT canonical_id, qualified_name, kind, start_line, end_line FROM symbols WHERE path=? ORDER BY start_line",
|
|
241
|
+
(path,),
|
|
242
|
+
).fetchall()
|
|
243
|
+
typer.echo(json.dumps([dict(r) for r in rows], indent=2))
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
@app.command()
|
|
247
|
+
def trace(
|
|
248
|
+
symbol: str,
|
|
249
|
+
repository: Annotated[Path | None, typer.Option("--repository", "-r", "--repo", help="Repository path")] = None,
|
|
250
|
+
callers: Annotated[bool, typer.Option("--callers", help="Trace caller hierarchy")] = False,
|
|
251
|
+
callees: Annotated[bool, typer.Option("--callees", help="Trace direct callees")] = False,
|
|
252
|
+
both: Annotated[bool, typer.Option("--both", help="Trace both callers and callees")] = False,
|
|
253
|
+
depth: Annotated[int, typer.Option("--depth", "-d", help="Max trace depth (clamped to max 5)")] = 2,
|
|
254
|
+
) -> None:
|
|
255
|
+
"""Trace a symbol definition, callers, and callees with explicit confidence labels."""
|
|
256
|
+
repo = _resolve_repo(repository=repository)
|
|
257
|
+
if not _ensure_indexed(repo):
|
|
258
|
+
return
|
|
259
|
+
if depth < 1 or depth > 10:
|
|
260
|
+
typer.echo(
|
|
261
|
+
json.dumps(
|
|
262
|
+
{
|
|
263
|
+
"status": "error",
|
|
264
|
+
"error": {
|
|
265
|
+
"code": ErrorCode.INVALID_DEPTH.value,
|
|
266
|
+
"message": f"Invalid graph traversal depth {depth}; allowed range is 1..5.",
|
|
267
|
+
"next_action": {
|
|
268
|
+
"command": "codegraph trace --depth 2 <symbol>",
|
|
269
|
+
"reason": "Specify a traversal depth between 1 and 5.",
|
|
270
|
+
},
|
|
271
|
+
},
|
|
272
|
+
},
|
|
273
|
+
indent=2,
|
|
274
|
+
)
|
|
275
|
+
)
|
|
276
|
+
return
|
|
277
|
+
bounded_depth = max(1, min(depth, 5))
|
|
278
|
+
effective_callers = callers
|
|
279
|
+
effective_callees = callees
|
|
280
|
+
effective_both = both
|
|
281
|
+
if not callers and not callees and not both:
|
|
282
|
+
effective_both = True
|
|
283
|
+
|
|
284
|
+
with _indexer(repo).session() as con:
|
|
285
|
+
res = trace_call(con, symbol, max_depth=bounded_depth, callers=effective_callers, callees=effective_callees, both=effective_both)
|
|
286
|
+
if not res:
|
|
287
|
+
sym_row = con.execute(
|
|
288
|
+
"SELECT 1 FROM symbols WHERE canonical_id=? OR qualified_name=? OR name=? LIMIT 1",
|
|
289
|
+
(symbol, symbol, symbol),
|
|
290
|
+
).fetchone()
|
|
291
|
+
if not sym_row:
|
|
292
|
+
typer.echo(
|
|
293
|
+
json.dumps(
|
|
294
|
+
{
|
|
295
|
+
"status": "unknown",
|
|
296
|
+
"error": {
|
|
297
|
+
"code": ErrorCode.SYMBOL_NOT_FOUND.value,
|
|
298
|
+
"message": f"No matching symbol was found for '{symbol}'.",
|
|
299
|
+
"next_action": {
|
|
300
|
+
"command": f"codegraph search {symbol}",
|
|
301
|
+
"reason": "Search with a broader query term.",
|
|
302
|
+
},
|
|
303
|
+
},
|
|
304
|
+
},
|
|
305
|
+
indent=2,
|
|
306
|
+
)
|
|
307
|
+
)
|
|
308
|
+
return
|
|
309
|
+
typer.echo(json.dumps(res, indent=2))
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
@app.command("get-symbol")
|
|
313
|
+
def get_symbol_cmd(
|
|
314
|
+
symbol: str,
|
|
315
|
+
repository: Annotated[Path | None, typer.Option("--repository", "-r", "--repo", help="Repository path")] = None,
|
|
316
|
+
json_output: Annotated[bool, typer.Option("--json")] = True,
|
|
317
|
+
) -> None:
|
|
318
|
+
"""Get authoritative AST details for a single symbol by name or canonical ID."""
|
|
319
|
+
repo = _resolve_repo(repository=repository)
|
|
320
|
+
if not _ensure_indexed(repo):
|
|
321
|
+
return
|
|
322
|
+
with _indexer(repo).session() as con:
|
|
323
|
+
res = interrogation_get_symbol(con, repo, symbol)
|
|
324
|
+
typer.echo(json.dumps(res, indent=2))
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
@app.command("resolve-symbol")
|
|
328
|
+
def resolve_symbol_cmd(
|
|
329
|
+
query: str,
|
|
330
|
+
repository: Annotated[Path | None, typer.Option("--repository", "-r", "--repo", help="Repository path")] = None,
|
|
331
|
+
json_output: Annotated[bool, typer.Option("--json")] = True,
|
|
332
|
+
) -> None:
|
|
333
|
+
"""Determine whether an exact/canonical symbol exists or return ambiguous candidates."""
|
|
334
|
+
repo = _resolve_repo(repository=repository)
|
|
335
|
+
if not _ensure_indexed(repo):
|
|
336
|
+
return
|
|
337
|
+
with _indexer(repo).session() as con:
|
|
338
|
+
res = interrogation_resolve_symbol(con, repo, query)
|
|
339
|
+
typer.echo(json.dumps(res, indent=2))
|
|
340
|
+
|
|
341
|
+
|
|
342
|
+
@app.command()
|
|
343
|
+
def graph(
|
|
344
|
+
path: Annotated[Path | None, typer.Argument(help="Repository path")] = None,
|
|
345
|
+
repository: Annotated[Path | None, typer.Option("--repository", "-r", "--repo", help="Repository path")] = None,
|
|
346
|
+
module: Annotated[str | None, typer.Option("--module", "-m", help="Filter focused graph by module")] = None,
|
|
347
|
+
symbol: Annotated[str | None, typer.Option("--symbol", "-s", help="Filter focused graph by symbol")] = None,
|
|
348
|
+
depth: Annotated[int, typer.Option("--depth", "-d", help="Traversal depth for focused graph (max 5)")] = 2,
|
|
349
|
+
) -> None:
|
|
350
|
+
"""Summarize indexed graph nodes, edges, modules, or inspect focused subgraphs."""
|
|
351
|
+
repo = _resolve_repo(path, repository)
|
|
352
|
+
if not _ensure_indexed(repo):
|
|
353
|
+
return
|
|
354
|
+
bounded_depth = max(1, min(depth, 5))
|
|
355
|
+
with _indexer(repo).session() as con:
|
|
356
|
+
if module or symbol:
|
|
357
|
+
result = get_focused_graph(con, module=module, symbol=symbol, depth=bounded_depth)
|
|
358
|
+
else:
|
|
359
|
+
result = get_graph_summary(con)
|
|
360
|
+
typer.echo(json.dumps(result, indent=2))
|
|
361
|
+
|
|
362
|
+
|
|
363
|
+
@app.command()
|
|
364
|
+
def routes(
|
|
365
|
+
path: Annotated[Path | None, typer.Argument(help="Repository path (default: current directory)")] = None,
|
|
366
|
+
repository: Annotated[Path | None, typer.Option("--repository", "-r", "--repo", help="Repository path")] = None,
|
|
367
|
+
framework: Annotated[str | None, typer.Option("--framework", "-f", help="Filter by framework (e.g. flask, fastapi)")] = None,
|
|
368
|
+
method: Annotated[str | None, typer.Option("--method", "-m", help="Filter by HTTP method (e.g. GET, POST)")] = None,
|
|
369
|
+
route_path: Annotated[str | None, typer.Option("--path", "-p", help="Filter by route path substring")] = None,
|
|
370
|
+
json_output: Annotated[bool, typer.Option("--json", help="Output raw JSON array")] = False,
|
|
371
|
+
) -> None:
|
|
372
|
+
"""List and inspect indexed framework routes."""
|
|
373
|
+
repo = _resolve_repo(path, repository)
|
|
374
|
+
if not _ensure_indexed(repo):
|
|
375
|
+
return
|
|
376
|
+
with _indexer(repo).session() as con:
|
|
377
|
+
query = (
|
|
378
|
+
"SELECT endpoint_id, framework, http_method, route_path, handler_name, "
|
|
379
|
+
"handler_canonical_id, file_path, line, evidence, confidence "
|
|
380
|
+
"FROM framework_routes WHERE 1=1 "
|
|
381
|
+
)
|
|
382
|
+
params: list[str] = []
|
|
383
|
+
if framework:
|
|
384
|
+
query += "AND framework=? "
|
|
385
|
+
params.append(framework.lower())
|
|
386
|
+
if method:
|
|
387
|
+
query += "AND UPPER(http_method)=? "
|
|
388
|
+
params.append(method.upper())
|
|
389
|
+
if route_path:
|
|
390
|
+
query += "AND route_path LIKE ? "
|
|
391
|
+
params.append(f"%{route_path}%")
|
|
392
|
+
query += "ORDER BY route_path, http_method"
|
|
393
|
+
|
|
394
|
+
try:
|
|
395
|
+
rows = con.execute(query, params).fetchall()
|
|
396
|
+
except sqlite3.OperationalError:
|
|
397
|
+
rows = []
|
|
398
|
+
|
|
399
|
+
route_list = [
|
|
400
|
+
{
|
|
401
|
+
"endpoint_id": r["endpoint_id"],
|
|
402
|
+
"http_method": r["http_method"],
|
|
403
|
+
"route_path": r["route_path"],
|
|
404
|
+
"handler": r["handler_name"],
|
|
405
|
+
"canonical_id": r["handler_canonical_id"],
|
|
406
|
+
"file": r["file_path"],
|
|
407
|
+
"line": r["line"],
|
|
408
|
+
"framework": r["framework"],
|
|
409
|
+
"evidence": r["evidence"],
|
|
410
|
+
"confidence": r["confidence"],
|
|
411
|
+
}
|
|
412
|
+
for r in rows
|
|
413
|
+
]
|
|
414
|
+
|
|
415
|
+
if json_output:
|
|
416
|
+
typer.echo(json.dumps(route_list, indent=2))
|
|
417
|
+
else:
|
|
418
|
+
if not route_list:
|
|
419
|
+
typer.echo("No framework routes indexed or matching criteria.")
|
|
420
|
+
return
|
|
421
|
+
typer.echo(f"Found {len(route_list)} route(s):")
|
|
422
|
+
for r in route_list:
|
|
423
|
+
typer.echo(f" [{r['http_method']}] {r['route_path']} -> {r['handler']} ({r['file']}:{r['line']}) [{r['framework']}]")
|
|
424
|
+
|
|
425
|
+
|
|
426
|
+
@app.command()
|
|
427
|
+
def imports(
|
|
428
|
+
path: Annotated[str | None, typer.Argument(help="Repository-relative file path to inspect imports for (default: inspect all imports)")] = None,
|
|
429
|
+
repository: Annotated[Path | None, typer.Option("--repository", "-r", "--repo", help="Repository path")] = None,
|
|
430
|
+
json_output: Annotated[bool, typer.Option("--json", help="Output raw JSON array")] = True,
|
|
431
|
+
) -> None:
|
|
432
|
+
"""Show what a file or module imports."""
|
|
433
|
+
repo = _resolve_repo(repository=repository)
|
|
434
|
+
if not _ensure_indexed(repo):
|
|
435
|
+
return
|
|
436
|
+
with _indexer(repo).session() as con:
|
|
437
|
+
res = interrogation_get_imports(con, repo, file=path)
|
|
438
|
+
typer.echo(json.dumps(res, indent=2))
|
|
439
|
+
|
|
440
|
+
|
|
441
|
+
@app.command()
|
|
442
|
+
def dependents(
|
|
443
|
+
target: Annotated[str, typer.Argument(help="File path or symbol canonical ID to find dependents/importers for")],
|
|
444
|
+
repository: Annotated[Path | None, typer.Option("--repository", "-r", "--repo", help="Repository path")] = None,
|
|
445
|
+
json_output: Annotated[bool, typer.Option("--json", help="Output raw JSON array")] = True,
|
|
446
|
+
) -> None:
|
|
447
|
+
"""Show what files and symbols import or depend on the target file/symbol."""
|
|
448
|
+
repo = _resolve_repo(repository=repository)
|
|
449
|
+
if not _ensure_indexed(repo):
|
|
450
|
+
return
|
|
451
|
+
with _indexer(repo).session() as con:
|
|
452
|
+
is_path = "/" in target or target.endswith((".py", ".js", ".ts", ".jsx", ".tsx"))
|
|
453
|
+
if is_path:
|
|
454
|
+
res = interrogation_get_dependents(con, repo, file=target)
|
|
455
|
+
else:
|
|
456
|
+
res = interrogation_get_dependents(con, repo, canonical_id=target)
|
|
457
|
+
typer.echo(json.dumps(res, indent=2))
|
|
458
|
+
|
|
459
|
+
|
|
460
|
+
@app.command()
|
|
461
|
+
def architecture(
|
|
462
|
+
path: Annotated[Path | None, typer.Argument(help="Repository path (default: current directory)")] = None,
|
|
463
|
+
repository: Annotated[Path | None, typer.Option("--repository", "-r", "--repo", help="Repository path")] = None,
|
|
464
|
+
json_output: Annotated[bool, typer.Option("--json", help="Output full architecture model in JSON")] = True,
|
|
465
|
+
) -> None:
|
|
466
|
+
"""Summarize repository architecture including modules, frameworks, routes, and tests."""
|
|
467
|
+
repo = _resolve_repo(path, repository)
|
|
468
|
+
if not _ensure_indexed(repo):
|
|
469
|
+
return
|
|
470
|
+
with _indexer(repo).session() as con:
|
|
471
|
+
arch = get_architecture(con, repo)
|
|
472
|
+
typer.echo(json.dumps(arch, indent=2))
|
|
473
|
+
|
|
474
|
+
|
|
475
|
+
@app.command()
|
|
476
|
+
def debug(
|
|
477
|
+
question: str,
|
|
478
|
+
repository: Annotated[Path | None, typer.Option("--repository", "-r", "--repo", help="Repository path")] = None,
|
|
479
|
+
) -> None:
|
|
480
|
+
"""Return source-backed facts and clearly-labelled debugging hypotheses."""
|
|
481
|
+
repo = _resolve_repo(repository=repository)
|
|
482
|
+
if not _ensure_indexed(repo):
|
|
483
|
+
return
|
|
484
|
+
with _indexer(repo).session() as con:
|
|
485
|
+
spec, _ = normalize_task_spec(question, con=con)
|
|
486
|
+
unknown_explicit_targets = []
|
|
487
|
+
for t in spec.targets:
|
|
488
|
+
expr_type = classify_target_expression(t)
|
|
489
|
+
if expr_type == TargetExpressionType.EXPLICIT_SYMBOL_TARGET:
|
|
490
|
+
row = con.execute(
|
|
491
|
+
"SELECT 1 FROM symbols WHERE canonical_id=? OR qualified_name=? OR name=? "
|
|
492
|
+
"UNION SELECT 1 FROM framework_routes WHERE endpoint_id=? OR route_path=? "
|
|
493
|
+
"LIMIT 1",
|
|
494
|
+
(t, t, t, t, t),
|
|
495
|
+
).fetchone()
|
|
496
|
+
if not row:
|
|
497
|
+
unknown_explicit_targets.append(t)
|
|
498
|
+
|
|
499
|
+
if unknown_explicit_targets:
|
|
500
|
+
unknown_target = unknown_explicit_targets[0]
|
|
501
|
+
lexical_evidence = [item.as_dict() for item in search_code(con, question, 10)]
|
|
502
|
+
typer.echo(
|
|
503
|
+
json.dumps(
|
|
504
|
+
{
|
|
505
|
+
"TARGET": unknown_target,
|
|
506
|
+
"STATUS": "UNKNOWN",
|
|
507
|
+
"MESSAGE": "No repository symbol/route/module matching the explicit target was found.",
|
|
508
|
+
"RELATED_LEXICAL_RESULTS": lexical_evidence,
|
|
509
|
+
},
|
|
510
|
+
indent=2,
|
|
511
|
+
)
|
|
512
|
+
)
|
|
513
|
+
return
|
|
514
|
+
|
|
515
|
+
evidence = [item.as_dict() for item in search_code(con, question, 10)]
|
|
516
|
+
typer.echo(
|
|
517
|
+
json.dumps(
|
|
518
|
+
{
|
|
519
|
+
"CONFIRMED FACT": evidence,
|
|
520
|
+
"LIKELY CAUSE": [
|
|
521
|
+
"A cited branch or raised exception may explain the symptom; this is not confirmed."
|
|
522
|
+
],
|
|
523
|
+
"POSSIBLE CAUSE": [
|
|
524
|
+
"Other uncited runtime configuration or dependency behavior may contribute."
|
|
525
|
+
],
|
|
526
|
+
"RECOMMENDATION": ["Reproduce with input that exercises each cited branch."],
|
|
527
|
+
},
|
|
528
|
+
indent=2,
|
|
529
|
+
)
|
|
530
|
+
)
|
|
531
|
+
|
|
532
|
+
|
|
533
|
+
@app.command()
|
|
534
|
+
def serve(
|
|
535
|
+
path: Annotated[Path | None, typer.Argument(help="Repository path (default: current directory)")] = None,
|
|
536
|
+
repository: Annotated[Path | None, typer.Option("--repository", "-r", "--repo", help="Repository path")] = None,
|
|
537
|
+
profile: Annotated[str, typer.Option("--profile", help="Tool profile: core | minimal | developer | full")] = "full",
|
|
538
|
+
) -> None:
|
|
539
|
+
"""Run the stdio MCP server (requires the optional mcp extra)."""
|
|
540
|
+
repo = _resolve_repo(path, repository)
|
|
541
|
+
from codegraph.mcp import create_server
|
|
542
|
+
|
|
543
|
+
create_server(repo, profile=profile).run()
|
|
544
|
+
|
|
545
|
+
|
|
546
|
+
mcp_app = typer.Typer(help="MCP server commands.")
|
|
547
|
+
app.add_typer(mcp_app, name="mcp")
|
|
548
|
+
|
|
549
|
+
|
|
550
|
+
@mcp_app.command("serve")
|
|
551
|
+
def mcp_serve(
|
|
552
|
+
path: Annotated[Path | None, typer.Argument(help="Repository path (default: current directory)")] = None,
|
|
553
|
+
repository: Annotated[Path | None, typer.Option("--repository", "-r", "--repo", help="Repository path")] = None,
|
|
554
|
+
profile: Annotated[str, typer.Option("--profile", help="Tool profile: core | minimal | developer | full")] = "full",
|
|
555
|
+
) -> None:
|
|
556
|
+
"""Run the stdio MCP server (requires the optional mcp extra)."""
|
|
557
|
+
repo = _resolve_repo(path, repository)
|
|
558
|
+
from codegraph.mcp import create_server
|
|
559
|
+
|
|
560
|
+
create_server(repo, profile=profile).run()
|
|
561
|
+
|
|
562
|
+
|
|
563
|
+
@app.command()
|
|
564
|
+
def doctor(
|
|
565
|
+
path: Annotated[Path | None, typer.Argument(help="Repository path (default: current directory)")] = None,
|
|
566
|
+
repository: Annotated[Path | None, typer.Option("--repository", "-r", "--repo", help="Repository path")] = None,
|
|
567
|
+
resources: Annotated[bool, typer.Option("--resources")] = False,
|
|
568
|
+
database: Annotated[bool, typer.Option("--database", help="Run comprehensive database integrity and schema checks")] = False,
|
|
569
|
+
json_output: Annotated[bool, typer.Option("--json")] = False,
|
|
570
|
+
) -> None:
|
|
571
|
+
"""Check repository and index readiness, database integrity, and freshness."""
|
|
572
|
+
repo = _resolve_repo(path, repository)
|
|
573
|
+
indexer_inst = _indexer(repo)
|
|
574
|
+
db = repo / ".codegraph.sqlite3"
|
|
575
|
+
result: dict[str, object] = {
|
|
576
|
+
"repository": str(repo.resolve()),
|
|
577
|
+
"index_exists": db.exists(),
|
|
578
|
+
"python": "supported",
|
|
579
|
+
}
|
|
580
|
+
if db.exists():
|
|
581
|
+
with indexer_inst.session() as con:
|
|
582
|
+
health = check_database_health(con, repo)
|
|
583
|
+
freshness_rep = check_freshness(repo, con)
|
|
584
|
+
gen = index_generation(con)
|
|
585
|
+
result["health"] = health
|
|
586
|
+
result["status"] = (
|
|
587
|
+
"healthy"
|
|
588
|
+
if health.get("status") in ("healthy", "OK") and freshness_rep.status.value == "FRESH"
|
|
589
|
+
else "warning"
|
|
590
|
+
)
|
|
591
|
+
result["freshness"] = freshness_rep.status.value
|
|
592
|
+
result["freshness_detail"] = freshness_rep.detail
|
|
593
|
+
result["index_generation"] = gen
|
|
594
|
+
result["counts"] = {
|
|
595
|
+
"files": con.execute("SELECT count(*) FROM files").fetchone()[0],
|
|
596
|
+
"symbols": con.execute("SELECT count(*) FROM symbols").fetchone()[0],
|
|
597
|
+
"chunks": con.execute("SELECT count(*) FROM chunks").fetchone()[0],
|
|
598
|
+
"framework_routes": con.execute("SELECT count(*) FROM framework_routes").fetchone()[0],
|
|
599
|
+
"graph_edges": con.execute("SELECT count(*) FROM graph_edges").fetchone()[0],
|
|
600
|
+
}
|
|
601
|
+
else:
|
|
602
|
+
result["status"] = "not_indexed"
|
|
603
|
+
|
|
604
|
+
if database:
|
|
605
|
+
if not db.exists():
|
|
606
|
+
typer.echo(json.dumps({"status": "not_indexed", "error": "Database does not exist"}, indent=2))
|
|
607
|
+
return
|
|
608
|
+
with indexer_inst.session() as con:
|
|
609
|
+
health = check_database_health(con, repo)
|
|
610
|
+
typer.echo(json.dumps(health, indent=2))
|
|
611
|
+
return
|
|
612
|
+
|
|
613
|
+
result["resources"] = indexer_inst.governor.get_state().as_dict()
|
|
614
|
+
typer.echo(json.dumps(result, indent=2))
|
|
615
|
+
|
|
616
|
+
|
|
617
|
+
@app.command()
|
|
618
|
+
def resolve(
|
|
619
|
+
symbol: str,
|
|
620
|
+
repository: Annotated[Path | None, typer.Option("--repository", "-r", "--repo", help="Repository path")] = None,
|
|
621
|
+
json_output: Annotated[bool, typer.Option("--json")] = False,
|
|
622
|
+
) -> None:
|
|
623
|
+
"""Resolve a symbol with authoritative diagnostic, callers, and callees."""
|
|
624
|
+
repo = _resolve_repo(repository=repository)
|
|
625
|
+
if not _ensure_indexed(repo):
|
|
626
|
+
return
|
|
627
|
+
with _indexer(repo).session() as con:
|
|
628
|
+
diag = ReferenceResolver.resolve_symbol_diagnostic(con, symbol)
|
|
629
|
+
typer.echo(json.dumps(diag, indent=2))
|
|
630
|
+
|
|
631
|
+
|
|
632
|
+
@app.command()
|
|
633
|
+
def privacy(
|
|
634
|
+
path: Annotated[Path | None, typer.Argument(help="Repository path (default: current directory)")] = None,
|
|
635
|
+
repository: Annotated[Path | None, typer.Option("--repository", "-r", "--repo", help="Repository path")] = None,
|
|
636
|
+
json_output: Annotated[bool, typer.Option("--json")] = False,
|
|
637
|
+
) -> None:
|
|
638
|
+
"""Verify repository privacy boundaries and ensure no sensitive data is indexed."""
|
|
639
|
+
repo = _resolve_repo(path, repository)
|
|
640
|
+
db = repo / ".codegraph.sqlite3"
|
|
641
|
+
indexed_files: list[str] = []
|
|
642
|
+
if db.exists():
|
|
643
|
+
with _indexer(repo).session() as con:
|
|
644
|
+
indexed_files = [r[0] for r in con.execute("SELECT path FROM files").fetchall()]
|
|
645
|
+
sensitive_leaks = [p for p in indexed_files if is_sensitive(Path(p))]
|
|
646
|
+
report: dict[str, object] = {
|
|
647
|
+
"repository": str(repo.resolve()),
|
|
648
|
+
"sensitive_files_indexed": len(sensitive_leaks),
|
|
649
|
+
"leaks": sensitive_leaks,
|
|
650
|
+
"clean": len(sensitive_leaks) == 0,
|
|
651
|
+
"boundary_enforced": True,
|
|
652
|
+
"source_upload": "DISABLED (100% local execution)",
|
|
653
|
+
"telemetry": "DISABLED",
|
|
654
|
+
"network_access": "DISABLED",
|
|
655
|
+
"shell_execution": "DISABLED",
|
|
656
|
+
"model_api_required": False,
|
|
657
|
+
"prompt_injection_boundary": "STRICT (code and repo docs treated as passive DATA)",
|
|
658
|
+
}
|
|
659
|
+
typer.echo(json.dumps(report, indent=2))
|
|
660
|
+
|
|
661
|
+
|
|
662
|
+
@app.command()
|
|
663
|
+
def status(
|
|
664
|
+
path: Annotated[Path | None, typer.Argument(help="Repository path (default: current directory)")] = None,
|
|
665
|
+
repository: Annotated[Path | None, typer.Option("--repository", "-r", "--repo", help="Repository path")] = None,
|
|
666
|
+
json_output: Annotated[bool, typer.Option("--json")] = False,
|
|
667
|
+
) -> None:
|
|
668
|
+
"""Show repository indexing status, freshness, and graph statistics."""
|
|
669
|
+
repo = _resolve_repo(path, repository)
|
|
670
|
+
db = repo / ".codegraph.sqlite3"
|
|
671
|
+
if not db.exists():
|
|
672
|
+
typer.echo(
|
|
673
|
+
json.dumps(
|
|
674
|
+
{
|
|
675
|
+
"status": "error",
|
|
676
|
+
"repository": str(repo.resolve()),
|
|
677
|
+
"error": {
|
|
678
|
+
"code": ErrorCode.INDEX_NOT_FOUND.value,
|
|
679
|
+
"message": "No CodeGraph index exists for this repository.",
|
|
680
|
+
"next_action": {
|
|
681
|
+
"command": "codegraph init",
|
|
682
|
+
"reason": "Initialize the repository before querying it.",
|
|
683
|
+
},
|
|
684
|
+
},
|
|
685
|
+
},
|
|
686
|
+
indent=2,
|
|
687
|
+
)
|
|
688
|
+
)
|
|
689
|
+
return
|
|
690
|
+
indexer_inst = _indexer(repo)
|
|
691
|
+
with indexer_inst.session() as con:
|
|
692
|
+
freshness_rep = check_freshness(repo, con)
|
|
693
|
+
file_count = con.execute("SELECT count(*) FROM files").fetchone()[0]
|
|
694
|
+
symbol_count = con.execute("SELECT count(*) FROM symbols").fetchone()[0]
|
|
695
|
+
chunk_count = con.execute("SELECT count(*) FROM chunks").fetchone()[0]
|
|
696
|
+
route_count = con.execute("SELECT count(*) FROM framework_routes").fetchone()[0]
|
|
697
|
+
edge_count = con.execute("SELECT count(*) FROM graph_edges").fetchone()[0]
|
|
698
|
+
gov_state = indexer_inst.governor.get_state().as_dict()
|
|
699
|
+
rep = {
|
|
700
|
+
"repository": str(repo.resolve()),
|
|
701
|
+
"freshness": freshness_rep.status.value,
|
|
702
|
+
"freshness_detail": freshness_rep.detail,
|
|
703
|
+
"files": file_count,
|
|
704
|
+
"chunks": chunk_count,
|
|
705
|
+
"symbols": symbol_count,
|
|
706
|
+
"framework_routes": route_count,
|
|
707
|
+
"graph_edges": edge_count,
|
|
708
|
+
"resource_profile": gov_state["policy_profile"],
|
|
709
|
+
"pressure_level": gov_state["pressure_level"],
|
|
710
|
+
"activity_mode": gov_state["activity_mode"],
|
|
711
|
+
"estimated_memory_mb": gov_state["estimated_memory_mb"],
|
|
712
|
+
"modified_files": freshness_rep.modified_files,
|
|
713
|
+
"deleted_files": freshness_rep.deleted_files,
|
|
714
|
+
"parse_failed_files": freshness_rep.parse_failed_files,
|
|
715
|
+
}
|
|
716
|
+
typer.echo(json.dumps(rep, indent=2))
|
|
717
|
+
|
|
718
|
+
|
|
719
|
+
@app.command()
|
|
720
|
+
def context(
|
|
721
|
+
task: str,
|
|
722
|
+
repository: Annotated[Path | None, typer.Option("--repository", "-r", "--repo", help="Repository path")] = None,
|
|
723
|
+
intent: Annotated[str | None, typer.Option("--intent", "-i")] = None,
|
|
724
|
+
max_tokens: Annotated[int, typer.Option("--max-tokens")] = 20000,
|
|
725
|
+
top_k: Annotated[int, typer.Option("--top-k")] = 15,
|
|
726
|
+
mode: Annotated[str, typer.Option("--mode", "-m", help="FAST | BALANCED | DEEP")] = "BALANCED",
|
|
727
|
+
explain: Annotated[bool, typer.Option("--explain", "-e", help="Explain budget rejections and retrieval details")] = False,
|
|
728
|
+
json_output: Annotated[bool, typer.Option("--json")] = False,
|
|
729
|
+
) -> None:
|
|
730
|
+
"""Compile evidence-backed context packet for an AI agent task."""
|
|
731
|
+
repo = _resolve_repo(repository=repository)
|
|
732
|
+
if not _ensure_indexed(repo):
|
|
733
|
+
return
|
|
734
|
+
with _indexer(repo).session() as con:
|
|
735
|
+
packet = get_context(
|
|
736
|
+
con,
|
|
737
|
+
repo,
|
|
738
|
+
task=task,
|
|
739
|
+
intent=intent,
|
|
740
|
+
max_tokens=max_tokens,
|
|
741
|
+
top_k=top_k,
|
|
742
|
+
mode=mode,
|
|
743
|
+
explain=explain,
|
|
744
|
+
)
|
|
745
|
+
typer.echo(json.dumps(packet.as_dict(), indent=2))
|
|
746
|
+
|
|
747
|
+
|
|
748
|
+
@app.command("explain-context")
|
|
749
|
+
def explain_context(
|
|
750
|
+
task: str,
|
|
751
|
+
repository: Annotated[Path | None, typer.Option("--repository", "-r", "--repo", help="Repository path")] = None,
|
|
752
|
+
intent: Annotated[str | None, typer.Option("--intent", "-i")] = None,
|
|
753
|
+
max_tokens: Annotated[int, typer.Option("--max-tokens")] = 20000,
|
|
754
|
+
mode: Annotated[str, typer.Option("--mode", "-m")] = "BALANCED",
|
|
755
|
+
) -> None:
|
|
756
|
+
"""Compile context with explicit explainability and budget rejection details."""
|
|
757
|
+
repo = _resolve_repo(repository=repository)
|
|
758
|
+
if not _ensure_indexed(repo):
|
|
759
|
+
return
|
|
760
|
+
with _indexer(repo).session() as con:
|
|
761
|
+
packet = get_context(
|
|
762
|
+
con,
|
|
763
|
+
repo,
|
|
764
|
+
task=task,
|
|
765
|
+
intent=intent,
|
|
766
|
+
max_tokens=max_tokens,
|
|
767
|
+
mode=mode,
|
|
768
|
+
explain=True,
|
|
769
|
+
)
|
|
770
|
+
typer.echo(json.dumps(packet.as_dict(), indent=2))
|
|
771
|
+
|
|
772
|
+
|
|
773
|
+
@app.command()
|
|
774
|
+
def task(
|
|
775
|
+
prompt: str,
|
|
776
|
+
repository: Annotated[Path | None, typer.Option("--repository", "-r", "--repo", help="Repository path")] = None,
|
|
777
|
+
json_output: Annotated[bool, typer.Option("--json")] = False,
|
|
778
|
+
) -> None:
|
|
779
|
+
"""Normalize a prompt into a structured TaskSpec and detect ambiguities."""
|
|
780
|
+
repo = _resolve_repo(repository=repository)
|
|
781
|
+
if not _ensure_indexed(repo):
|
|
782
|
+
return
|
|
783
|
+
with _indexer(repo).session() as con:
|
|
784
|
+
spec, ambiguities = normalize_task_spec(prompt, con)
|
|
785
|
+
res = {
|
|
786
|
+
"task_spec": spec.as_dict(),
|
|
787
|
+
"ambiguities": [a.as_dict() for a in ambiguities],
|
|
788
|
+
}
|
|
789
|
+
typer.echo(json.dumps(res, indent=2))
|
|
790
|
+
|
|
791
|
+
|
|
792
|
+
@app.command()
|
|
793
|
+
def plan(
|
|
794
|
+
prompt: str,
|
|
795
|
+
repository: Annotated[Path | None, typer.Option("--repository", "-r", "--repo", help="Repository path")] = None,
|
|
796
|
+
max_tokens: Annotated[int, typer.Option("--max-tokens")] = 20000,
|
|
797
|
+
json_output: Annotated[bool, typer.Option("--json")] = False,
|
|
798
|
+
) -> None:
|
|
799
|
+
"""Construct a deterministic RetrievalPlan for a task."""
|
|
800
|
+
repo = _resolve_repo(repository=repository)
|
|
801
|
+
if not _ensure_indexed(repo):
|
|
802
|
+
return
|
|
803
|
+
with _indexer(repo).session() as con:
|
|
804
|
+
spec, ambiguities = normalize_task_spec(prompt, con)
|
|
805
|
+
retrieval_plan = build_retrieval_plan(spec, ambiguities, con, token_budget=max_tokens)
|
|
806
|
+
typer.echo(json.dumps(retrieval_plan.as_dict(), indent=2))
|
|
807
|
+
|
|
808
|
+
|
|
809
|
+
@app.command()
|
|
810
|
+
def benchmark(
|
|
811
|
+
path: Annotated[Path | None, typer.Argument(help="Repository path (default: current directory)")] = None,
|
|
812
|
+
repository: Annotated[Path | None, typer.Option("--repository", "-r", "--repo", help="Repository path")] = None,
|
|
813
|
+
suite: Annotated[Path | None, typer.Option("--suite", "-s")] = None,
|
|
814
|
+
max_tokens: Annotated[int, typer.Option("--max-tokens")] = 20000,
|
|
815
|
+
json_output: Annotated[bool, typer.Option("--json")] = False,
|
|
816
|
+
) -> None:
|
|
817
|
+
"""Run deterministic codebase intelligence benchmark suite."""
|
|
818
|
+
repo = _resolve_repo(path, repository)
|
|
819
|
+
from benchmarks.runner import run_benchmark_suite
|
|
820
|
+
|
|
821
|
+
result = run_benchmark_suite(repo, suite_file=suite, max_tokens=max_tokens)
|
|
822
|
+
typer.echo(json.dumps(result, indent=2))
|
|
823
|
+
|
|
824
|
+
|
|
825
|
+
memory_app = typer.Typer(help="Inspect or clear repository-scoped durable notes.")
|
|
826
|
+
app.add_typer(memory_app, name="memory")
|
|
827
|
+
|
|
828
|
+
|
|
829
|
+
@memory_app.command("list")
|
|
830
|
+
def memory_list(
|
|
831
|
+
path: Annotated[Path | None, typer.Argument(help="Repository path")] = None,
|
|
832
|
+
repository: Annotated[Path | None, typer.Option("--repository", "-r", "--repo", help="Repository path")] = None,
|
|
833
|
+
) -> None:
|
|
834
|
+
repo = _resolve_repo(path, repository)
|
|
835
|
+
typer.echo(json.dumps(MemoryStore(repo / ".codegraph.sqlite3").list(), indent=2))
|
|
836
|
+
|
|
837
|
+
|
|
838
|
+
@memory_app.command("clear")
|
|
839
|
+
def memory_clear(
|
|
840
|
+
path: Annotated[Path | None, typer.Argument(help="Repository path")] = None,
|
|
841
|
+
repository: Annotated[Path | None, typer.Option("--repository", "-r", "--repo", help="Repository path")] = None,
|
|
842
|
+
) -> None:
|
|
843
|
+
repo = _resolve_repo(path, repository)
|
|
844
|
+
MemoryStore(repo / ".codegraph.sqlite3").clear()
|
|
845
|
+
typer.echo("Repository memory cleared.")
|
|
846
|
+
|
|
847
|
+
|
|
848
|
+
@app.command()
|
|
849
|
+
def version() -> None:
|
|
850
|
+
typer.echo(__version__)
|
|
851
|
+
|
|
852
|
+
|
|
853
|
+
if __name__ == "__main__":
|
|
854
|
+
app()
|