code-explore 0.1.0__tar.gz → 0.3.0__tar.gz
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.
- code_explore-0.3.0/.github/workflows/pages.yml +44 -0
- {code_explore-0.1.0 → code_explore-0.3.0}/PKG-INFO +3 -1
- {code_explore-0.1.0 → code_explore-0.3.0}/code_explore/api/main.py +93 -17
- code_explore-0.3.0/code_explore/cli/config_cmd.py +96 -0
- {code_explore-0.1.0 → code_explore-0.3.0}/code_explore/cli/main.py +123 -15
- code_explore-0.3.0/code_explore/config.py +329 -0
- {code_explore-0.1.0 → code_explore-0.3.0}/code_explore/database.py +7 -4
- {code_explore-0.1.0 → code_explore-0.3.0}/code_explore/indexer/embeddings.py +36 -18
- {code_explore-0.1.0 → code_explore-0.3.0}/code_explore/models.py +25 -0
- code_explore-0.3.0/code_explore/search/filters.py +113 -0
- {code_explore-0.1.0 → code_explore-0.3.0}/code_explore/search/hybrid.py +6 -4
- {code_explore-0.1.0 → code_explore-0.3.0}/code_explore/search/semantic.py +5 -3
- code_explore-0.3.0/code_explore/static/app.js +482 -0
- code_explore-0.3.0/code_explore/static/index.html +78 -0
- code_explore-0.3.0/code_explore/static/style.css +349 -0
- {code_explore-0.1.0 → code_explore-0.3.0}/code_explore/summarizer/ollama.py +30 -15
- code_explore-0.3.0/code_explore/tagger/__init__.py +56 -0
- code_explore-0.3.0/docs/CNAME +1 -0
- code_explore-0.3.0/docs/index.html +93 -0
- code_explore-0.3.0/install.cmd +58 -0
- code_explore-0.3.0/install.ps1 +84 -0
- code_explore-0.3.0/install.sh +110 -0
- {code_explore-0.1.0 → code_explore-0.3.0}/pyproject.toml +9 -1
- {code_explore-0.1.0 → code_explore-0.3.0}/tests/conftest.py +9 -0
- code_explore-0.3.0/tests/test_config.py +314 -0
- code_explore-0.3.0/tests/test_config_cli.py +121 -0
- {code_explore-0.1.0 → code_explore-0.3.0}/tests/test_search_hybrid.py +3 -2
- {code_explore-0.1.0 → code_explore-0.3.0}/.editorconfig +0 -0
- {code_explore-0.1.0 → code_explore-0.3.0}/.github/workflows/publish.yml +0 -0
- {code_explore-0.1.0 → code_explore-0.3.0}/.gitignore +0 -0
- {code_explore-0.1.0 → code_explore-0.3.0}/README.md +0 -0
- {code_explore-0.1.0 → code_explore-0.3.0}/code_explore/__init__.py +0 -0
- {code_explore-0.1.0 → code_explore-0.3.0}/code_explore/analyzer/__init__.py +0 -0
- {code_explore-0.1.0 → code_explore-0.3.0}/code_explore/analyzer/dependencies.py +0 -0
- {code_explore-0.1.0 → code_explore-0.3.0}/code_explore/analyzer/language.py +0 -0
- {code_explore-0.1.0 → code_explore-0.3.0}/code_explore/analyzer/metrics.py +0 -0
- {code_explore-0.1.0 → code_explore-0.3.0}/code_explore/analyzer/patterns.py +0 -0
- {code_explore-0.1.0 → code_explore-0.3.0}/code_explore/api/__init__.py +0 -0
- {code_explore-0.1.0 → code_explore-0.3.0}/code_explore/cli/__init__.py +0 -0
- {code_explore-0.1.0 → code_explore-0.3.0}/code_explore/indexer/__init__.py +0 -0
- {code_explore-0.1.0 → code_explore-0.3.0}/code_explore/scanner/__init__.py +0 -0
- {code_explore-0.1.0 → code_explore-0.3.0}/code_explore/scanner/git_info.py +0 -0
- {code_explore-0.1.0 → code_explore-0.3.0}/code_explore/scanner/local.py +0 -0
- {code_explore-0.1.0 → code_explore-0.3.0}/code_explore/scanner/readme.py +0 -0
- {code_explore-0.1.0 → code_explore-0.3.0}/code_explore/search/__init__.py +0 -0
- {code_explore-0.1.0 → code_explore-0.3.0}/code_explore/search/fulltext.py +0 -0
- {code_explore-0.1.0 → code_explore-0.3.0}/code_explore/summarizer/__init__.py +0 -0
- {code_explore-0.1.0 → code_explore-0.3.0}/tests/__init__.py +0 -0
- {code_explore-0.1.0 → code_explore-0.3.0}/tests/test_cli.py +0 -0
- {code_explore-0.1.0 → code_explore-0.3.0}/tests/test_database.py +0 -0
- {code_explore-0.1.0 → code_explore-0.3.0}/tests/test_models.py +0 -0
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
name: Deploy to GitHub Pages
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
paths:
|
|
7
|
+
- 'docs/**'
|
|
8
|
+
- 'install.*'
|
|
9
|
+
|
|
10
|
+
permissions:
|
|
11
|
+
contents: read
|
|
12
|
+
pages: write
|
|
13
|
+
id-token: write
|
|
14
|
+
|
|
15
|
+
concurrency:
|
|
16
|
+
group: pages
|
|
17
|
+
cancel-in-progress: true
|
|
18
|
+
|
|
19
|
+
jobs:
|
|
20
|
+
build:
|
|
21
|
+
runs-on: ubuntu-latest
|
|
22
|
+
steps:
|
|
23
|
+
- uses: actions/checkout@v4
|
|
24
|
+
|
|
25
|
+
- name: Prepare site
|
|
26
|
+
run: |
|
|
27
|
+
mkdir -p _site
|
|
28
|
+
cp docs/index.html _site/index.html
|
|
29
|
+
cp docs/CNAME _site/CNAME
|
|
30
|
+
cp install.sh _site/install.sh
|
|
31
|
+
cp install.ps1 _site/install.ps1
|
|
32
|
+
cp install.cmd _site/install.cmd
|
|
33
|
+
|
|
34
|
+
- uses: actions/upload-pages-artifact@v3
|
|
35
|
+
|
|
36
|
+
deploy:
|
|
37
|
+
needs: build
|
|
38
|
+
runs-on: ubuntu-latest
|
|
39
|
+
environment:
|
|
40
|
+
name: github-pages
|
|
41
|
+
url: ${{ steps.deployment.outputs.page_url }}
|
|
42
|
+
steps:
|
|
43
|
+
- id: deployment
|
|
44
|
+
uses: actions/deploy-pages@v4
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: code-explore
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Summary: Developer knowledge base CLI — scan, index, and search your programming projects
|
|
5
5
|
Project-URL: Homepage, https://github.com/aipioneers/code-explore
|
|
6
6
|
Project-URL: Repository, https://github.com/aipioneers/code-explore
|
|
@@ -24,7 +24,9 @@ Requires-Dist: httpx>=0.24.0
|
|
|
24
24
|
Requires-Dist: lancedb>=0.4.0
|
|
25
25
|
Requires-Dist: pyarrow>=14.0.0
|
|
26
26
|
Requires-Dist: pydantic>=2.0.0
|
|
27
|
+
Requires-Dist: pyyaml>=6.0.0
|
|
27
28
|
Requires-Dist: rich>=13.0.0
|
|
29
|
+
Requires-Dist: tomli-w>=1.0.0
|
|
28
30
|
Requires-Dist: typer>=0.9.0
|
|
29
31
|
Provides-Extra: api
|
|
30
32
|
Requires-Dist: fastapi>=0.100.0; extra == 'api'
|
|
@@ -1,18 +1,20 @@
|
|
|
1
1
|
"""FastAPI REST API for Code Explore."""
|
|
2
2
|
|
|
3
|
-
import asyncio
|
|
4
3
|
import hashlib
|
|
4
|
+
from collections import Counter
|
|
5
5
|
from datetime import datetime
|
|
6
6
|
from pathlib import Path
|
|
7
7
|
|
|
8
8
|
from fastapi import FastAPI, HTTPException, Query
|
|
9
9
|
from fastapi.middleware.cors import CORSMiddleware
|
|
10
|
+
from fastapi.staticfiles import StaticFiles
|
|
10
11
|
from pydantic import BaseModel
|
|
11
12
|
|
|
12
13
|
from code_explore.database import init_db, save_project, get_project, get_all_projects, get_project_count
|
|
13
|
-
from code_explore.models import Project, ProjectSource, ProjectStatus
|
|
14
|
+
from code_explore.models import Project, ProjectSource, ProjectStatus, SearchFacets
|
|
15
|
+
from code_explore.search.filters import apply_filters, filter_projects, compute_facets
|
|
14
16
|
|
|
15
|
-
app = FastAPI(title="Code Explore", version="0.
|
|
17
|
+
app = FastAPI(title="Code Explore", version="0.3.0", description="Developer knowledge base API")
|
|
16
18
|
|
|
17
19
|
app.add_middleware(
|
|
18
20
|
CORSMiddleware,
|
|
@@ -43,36 +45,38 @@ class StatsResponse(BaseModel):
|
|
|
43
45
|
languages: dict[str, int]
|
|
44
46
|
frameworks: dict[str, int]
|
|
45
47
|
patterns: dict[str, int]
|
|
48
|
+
ai_tags: dict[str, int]
|
|
46
49
|
total_files: int
|
|
47
50
|
total_lines: int
|
|
48
51
|
statuses: dict[str, int]
|
|
49
52
|
|
|
50
53
|
|
|
54
|
+
class TagInfo(BaseModel):
|
|
55
|
+
value: str
|
|
56
|
+
category: str
|
|
57
|
+
count: int
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class TagsResponse(BaseModel):
|
|
61
|
+
tags: list[TagInfo]
|
|
62
|
+
total_tags: int
|
|
63
|
+
categories: dict[str, int]
|
|
64
|
+
|
|
65
|
+
|
|
51
66
|
@app.get("/api/projects", response_model=list[Project])
|
|
52
67
|
async def list_projects(
|
|
53
68
|
language: str | None = Query(None, description="Filter by primary language"),
|
|
54
69
|
framework: str | None = Query(None, description="Filter by framework"),
|
|
55
70
|
source: str | None = Query(None, description="Filter by source (local, github, gitlab)"),
|
|
71
|
+
tag: str | None = Query(None, description="Filter by AI tag"),
|
|
56
72
|
):
|
|
57
73
|
projects = get_all_projects()
|
|
58
74
|
|
|
59
|
-
if language:
|
|
60
|
-
lang_lower = language.lower()
|
|
61
|
-
projects = [
|
|
62
|
-
p for p in projects
|
|
63
|
-
if p.primary_language and p.primary_language.lower() == lang_lower
|
|
64
|
-
]
|
|
65
|
-
|
|
66
|
-
if framework:
|
|
67
|
-
fw_lower = framework.lower()
|
|
68
|
-
projects = [
|
|
69
|
-
p for p in projects
|
|
70
|
-
if any(f.lower() == fw_lower for f in p.frameworks)
|
|
71
|
-
]
|
|
72
|
-
|
|
73
75
|
if source:
|
|
74
76
|
projects = [p for p in projects if p.source.value == source]
|
|
75
77
|
|
|
78
|
+
projects = filter_projects(projects, language=language, framework=framework, tag=tag)
|
|
79
|
+
|
|
76
80
|
return projects
|
|
77
81
|
|
|
78
82
|
|
|
@@ -89,6 +93,10 @@ async def search_projects(
|
|
|
89
93
|
q: str = Query(..., description="Search query"),
|
|
90
94
|
mode: str = Query("hybrid", description="Search mode: fulltext, semantic, or hybrid"),
|
|
91
95
|
limit: int = Query(20, ge=1, le=100, description="Maximum results"),
|
|
96
|
+
language: str | None = Query(None, description="Filter by primary language"),
|
|
97
|
+
framework: str | None = Query(None, description="Filter by framework"),
|
|
98
|
+
pattern: str | None = Query(None, description="Filter by pattern"),
|
|
99
|
+
tag: str | None = Query(None, description="Filter by AI tag"),
|
|
92
100
|
):
|
|
93
101
|
if mode == "fulltext":
|
|
94
102
|
from code_explore.search.fulltext import search as fulltext_search
|
|
@@ -100,6 +108,9 @@ async def search_projects(
|
|
|
100
108
|
from code_explore.search.hybrid import search as hybrid_search
|
|
101
109
|
results = hybrid_search(q, limit=limit)
|
|
102
110
|
|
|
111
|
+
# Apply post-filters
|
|
112
|
+
results = apply_filters(results, language=language, framework=framework, pattern=pattern, tag=tag)
|
|
113
|
+
|
|
103
114
|
return [
|
|
104
115
|
{
|
|
105
116
|
"project": r.project.model_dump(),
|
|
@@ -111,6 +122,61 @@ async def search_projects(
|
|
|
111
122
|
]
|
|
112
123
|
|
|
113
124
|
|
|
125
|
+
@app.get("/api/facets", response_model=SearchFacets)
|
|
126
|
+
async def get_facets(
|
|
127
|
+
q: str | None = Query(None, description="Search query to scope facets"),
|
|
128
|
+
language: str | None = Query(None, description="Active language filter"),
|
|
129
|
+
framework: str | None = Query(None, description="Active framework filter"),
|
|
130
|
+
pattern: str | None = Query(None, description="Active pattern filter"),
|
|
131
|
+
tag: str | None = Query(None, description="Active tag filter"),
|
|
132
|
+
):
|
|
133
|
+
if q:
|
|
134
|
+
# Get search results first, then compute facets from those
|
|
135
|
+
from code_explore.search.hybrid import search as hybrid_search
|
|
136
|
+
results = hybrid_search(q, limit=500)
|
|
137
|
+
projects = [r.project for r in results]
|
|
138
|
+
else:
|
|
139
|
+
projects = get_all_projects()
|
|
140
|
+
|
|
141
|
+
# Apply active filters to scope facets
|
|
142
|
+
projects = filter_projects(projects, language=language, framework=framework, pattern=pattern, tag=tag)
|
|
143
|
+
|
|
144
|
+
return compute_facets(projects)
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
@app.get("/api/tags", response_model=TagsResponse)
|
|
148
|
+
async def get_tags(
|
|
149
|
+
category: str | None = Query(None, description="Filter by category: domain, technology-role, maturity"),
|
|
150
|
+
):
|
|
151
|
+
projects = get_all_projects()
|
|
152
|
+
|
|
153
|
+
tag_counts: Counter[str] = Counter()
|
|
154
|
+
tag_categories: dict[str, str] = {}
|
|
155
|
+
|
|
156
|
+
for p in projects:
|
|
157
|
+
for t in p.ai_tags:
|
|
158
|
+
cat = t.category.value if hasattr(t.category, "value") else str(t.category)
|
|
159
|
+
if category and cat != category:
|
|
160
|
+
continue
|
|
161
|
+
tag_counts[t.value] += 1
|
|
162
|
+
tag_categories[t.value] = cat
|
|
163
|
+
|
|
164
|
+
tags_list = [
|
|
165
|
+
TagInfo(value=value, category=tag_categories[value], count=count)
|
|
166
|
+
for value, count in tag_counts.most_common()
|
|
167
|
+
]
|
|
168
|
+
|
|
169
|
+
category_counts: Counter[str] = Counter()
|
|
170
|
+
for t in tags_list:
|
|
171
|
+
category_counts[t.category] += 1
|
|
172
|
+
|
|
173
|
+
return TagsResponse(
|
|
174
|
+
tags=tags_list,
|
|
175
|
+
total_tags=len(tags_list),
|
|
176
|
+
categories=dict(category_counts),
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
|
|
114
180
|
@app.get("/api/stats", response_model=StatsResponse)
|
|
115
181
|
async def get_stats():
|
|
116
182
|
projects = get_all_projects()
|
|
@@ -118,6 +184,7 @@ async def get_stats():
|
|
|
118
184
|
languages: dict[str, int] = {}
|
|
119
185
|
frameworks: dict[str, int] = {}
|
|
120
186
|
patterns: dict[str, int] = {}
|
|
187
|
+
ai_tags: dict[str, int] = {}
|
|
121
188
|
statuses: dict[str, int] = {}
|
|
122
189
|
total_files = 0
|
|
123
190
|
total_lines = 0
|
|
@@ -129,6 +196,8 @@ async def get_stats():
|
|
|
129
196
|
frameworks[fw] = frameworks.get(fw, 0) + 1
|
|
130
197
|
for pat in p.patterns:
|
|
131
198
|
patterns[pat.name] = patterns.get(pat.name, 0) + 1
|
|
199
|
+
for t in p.ai_tags:
|
|
200
|
+
ai_tags[t.value] = ai_tags.get(t.value, 0) + 1
|
|
132
201
|
statuses[p.status.value] = statuses.get(p.status.value, 0) + 1
|
|
133
202
|
total_files += p.quality.total_files
|
|
134
203
|
total_lines += p.quality.total_lines
|
|
@@ -138,6 +207,7 @@ async def get_stats():
|
|
|
138
207
|
languages=dict(sorted(languages.items(), key=lambda x: x[1], reverse=True)),
|
|
139
208
|
frameworks=dict(sorted(frameworks.items(), key=lambda x: x[1], reverse=True)),
|
|
140
209
|
patterns=dict(sorted(patterns.items(), key=lambda x: x[1], reverse=True)),
|
|
210
|
+
ai_tags=dict(sorted(ai_tags.items(), key=lambda x: x[1], reverse=True)),
|
|
141
211
|
total_files=total_files,
|
|
142
212
|
total_lines=total_lines,
|
|
143
213
|
statuses=statuses,
|
|
@@ -195,3 +265,9 @@ async def trigger_scan(request: ScanRequest):
|
|
|
195
265
|
results.append(project)
|
|
196
266
|
|
|
197
267
|
return ScanResponse(scanned=len(results), projects=results)
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
# Mount static files for the dashboard (must be after API routes)
|
|
271
|
+
_static_dir = Path(__file__).parent.parent / "static"
|
|
272
|
+
if _static_dir.is_dir():
|
|
273
|
+
app.mount("/", StaticFiles(directory=str(_static_dir), html=True), name="static")
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
"""CLI commands for configuration management."""
|
|
2
|
+
|
|
3
|
+
import typer
|
|
4
|
+
from rich.console import Console
|
|
5
|
+
from rich.table import Table
|
|
6
|
+
|
|
7
|
+
from code_explore.config import (
|
|
8
|
+
_get_config_dir,
|
|
9
|
+
get_config_path,
|
|
10
|
+
get_resolved_settings,
|
|
11
|
+
reset_config,
|
|
12
|
+
write_default_config,
|
|
13
|
+
_discover_config_file,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
config_app = typer.Typer(
|
|
17
|
+
name="config",
|
|
18
|
+
help="Manage code-explore configuration.",
|
|
19
|
+
no_args_is_help=True,
|
|
20
|
+
)
|
|
21
|
+
console = Console()
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@config_app.command()
|
|
25
|
+
def show() -> None:
|
|
26
|
+
"""Display all current settings with values and sources."""
|
|
27
|
+
settings = get_resolved_settings()
|
|
28
|
+
config_path = get_config_path()
|
|
29
|
+
|
|
30
|
+
table = Table(title="Code Explore Configuration")
|
|
31
|
+
table.add_column("Setting", style="cyan", no_wrap=True)
|
|
32
|
+
table.add_column("Value", style="white")
|
|
33
|
+
table.add_column("Source", style="green")
|
|
34
|
+
|
|
35
|
+
for s in settings:
|
|
36
|
+
table.add_row(s.name, s.value, s.source.value)
|
|
37
|
+
|
|
38
|
+
table.add_section()
|
|
39
|
+
path_display = str(config_path) if config_path else "(no config file)"
|
|
40
|
+
table.add_row("Config file", path_display, "")
|
|
41
|
+
|
|
42
|
+
console.print(table)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@config_app.command()
|
|
46
|
+
def init(
|
|
47
|
+
fmt: str = typer.Option("toml", "--format", "-f", help="Config format: toml or yaml"),
|
|
48
|
+
force: bool = typer.Option(False, "--force", help="Overwrite existing config file"),
|
|
49
|
+
) -> None:
|
|
50
|
+
"""Create a configuration file with all default values."""
|
|
51
|
+
config_dir = _get_config_dir()
|
|
52
|
+
ext = "yaml" if fmt in ("yaml", "yml") else "toml"
|
|
53
|
+
path = config_dir / f"config.{ext}"
|
|
54
|
+
|
|
55
|
+
if path.exists() and not force:
|
|
56
|
+
console.print(
|
|
57
|
+
f"[yellow]Config file already exists:[/yellow] {path}\n"
|
|
58
|
+
f"Use [bold]--force[/bold] to overwrite."
|
|
59
|
+
)
|
|
60
|
+
raise typer.Exit(1)
|
|
61
|
+
|
|
62
|
+
write_default_config(path, fmt=ext)
|
|
63
|
+
console.print(f"[green]Created configuration file:[/green] {path}")
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
@config_app.command()
|
|
67
|
+
def reset(
|
|
68
|
+
yes: bool = typer.Option(False, "--yes", "-y", help="Skip confirmation prompt"),
|
|
69
|
+
) -> None:
|
|
70
|
+
"""Delete the configuration file and revert to defaults."""
|
|
71
|
+
config_path = _discover_config_file()
|
|
72
|
+
|
|
73
|
+
if not config_path:
|
|
74
|
+
console.print("[yellow]No configuration file found. Already using defaults.[/yellow]")
|
|
75
|
+
raise typer.Exit(0)
|
|
76
|
+
|
|
77
|
+
if not yes:
|
|
78
|
+
confirm = typer.confirm(f"Delete {config_path} and revert to defaults?")
|
|
79
|
+
if not confirm:
|
|
80
|
+
console.print("[dim]Cancelled.[/dim]")
|
|
81
|
+
raise typer.Exit(0)
|
|
82
|
+
|
|
83
|
+
config_path.unlink()
|
|
84
|
+
reset_config()
|
|
85
|
+
console.print(f"[green]Reset configuration to defaults.[/green] Removed: {config_path}")
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
@config_app.command()
|
|
89
|
+
def path() -> None:
|
|
90
|
+
"""Print the path to the active configuration file."""
|
|
91
|
+
config_path = _discover_config_file()
|
|
92
|
+
if config_path:
|
|
93
|
+
console.print(str(config_path))
|
|
94
|
+
else:
|
|
95
|
+
default_path = _get_config_dir() / "config.toml"
|
|
96
|
+
console.print(f"{default_path} [dim](not created yet)[/dim]")
|
|
@@ -14,12 +14,14 @@ from rich.tree import Tree
|
|
|
14
14
|
|
|
15
15
|
from code_explore.database import init_db, save_project, get_project, get_all_projects, get_project_count
|
|
16
16
|
from code_explore.models import Project, ProjectSource, ProjectStatus
|
|
17
|
+
from code_explore.cli.config_cmd import config_app
|
|
17
18
|
|
|
18
19
|
app = typer.Typer(
|
|
19
20
|
name="code-explore",
|
|
20
21
|
help="Personal developer knowledge base - index, analyze and search all your projects.",
|
|
21
22
|
no_args_is_help=True,
|
|
22
23
|
)
|
|
24
|
+
app.add_typer(config_app, name="config")
|
|
23
25
|
console = Console()
|
|
24
26
|
|
|
25
27
|
|
|
@@ -142,9 +144,18 @@ def scan(
|
|
|
142
144
|
def search(
|
|
143
145
|
query: str = typer.Argument(..., help="Search query"),
|
|
144
146
|
mode: str = typer.Option("hybrid", "--mode", "-m", help="Search mode: fulltext, semantic, or hybrid"),
|
|
145
|
-
limit: int = typer.Option(
|
|
147
|
+
limit: int = typer.Option(None, "--limit", "-l", help="Maximum results"),
|
|
148
|
+
language: str = typer.Option(None, "--language", "-L", help="Filter by primary language"),
|
|
149
|
+
framework: str = typer.Option(None, "--framework", "-F", help="Filter by framework"),
|
|
150
|
+
pattern: str = typer.Option(None, "--pattern", help="Filter by architectural pattern"),
|
|
151
|
+
tag: str = typer.Option(None, "--tag", "-t", help="Filter by AI-generated tag"),
|
|
146
152
|
) -> None:
|
|
147
153
|
"""Search across all indexed projects."""
|
|
154
|
+
from code_explore.config import get_config
|
|
155
|
+
from code_explore.search.filters import apply_filters
|
|
156
|
+
|
|
157
|
+
if limit is None:
|
|
158
|
+
limit = get_config().result_limit
|
|
148
159
|
init_db()
|
|
149
160
|
|
|
150
161
|
if mode == "fulltext":
|
|
@@ -157,6 +168,23 @@ def search(
|
|
|
157
168
|
from code_explore.search.hybrid import search as hybrid_search
|
|
158
169
|
results = hybrid_search(query, limit=limit)
|
|
159
170
|
|
|
171
|
+
# Apply post-filters
|
|
172
|
+
results = apply_filters(results, language=language, framework=framework, pattern=pattern, tag=tag)
|
|
173
|
+
|
|
174
|
+
# Show active filters
|
|
175
|
+
active_filters = []
|
|
176
|
+
if language:
|
|
177
|
+
active_filters.append(f"language={language}")
|
|
178
|
+
if framework:
|
|
179
|
+
active_filters.append(f"framework={framework}")
|
|
180
|
+
if pattern:
|
|
181
|
+
active_filters.append(f"pattern={pattern}")
|
|
182
|
+
if tag:
|
|
183
|
+
active_filters.append(f"tag={tag}")
|
|
184
|
+
|
|
185
|
+
if active_filters:
|
|
186
|
+
console.print(f"[dim]Filters: {', '.join(active_filters)}[/dim]")
|
|
187
|
+
|
|
160
188
|
if not results:
|
|
161
189
|
console.print(f"[yellow]No results found for:[/yellow] {query}")
|
|
162
190
|
raise typer.Exit(0)
|
|
@@ -276,6 +304,16 @@ def show(
|
|
|
276
304
|
if project.concepts:
|
|
277
305
|
tree.add(f"[bold]Concepts:[/bold] {', '.join(project.concepts)}")
|
|
278
306
|
|
|
307
|
+
# AI Tags grouped by category
|
|
308
|
+
if project.ai_tags:
|
|
309
|
+
ai_branch = tree.add("[bold]AI Tags[/bold]")
|
|
310
|
+
by_category: dict[str, list[str]] = {}
|
|
311
|
+
for t in project.ai_tags:
|
|
312
|
+
cat = t.category.value if hasattr(t.category, "value") else str(t.category)
|
|
313
|
+
by_category.setdefault(cat, []).append(t.value)
|
|
314
|
+
for cat, values in sorted(by_category.items()):
|
|
315
|
+
ai_branch.add(f"[magenta]{cat}:[/magenta] {', '.join(values)}")
|
|
316
|
+
|
|
279
317
|
console.print(Panel(tree, title=f"Project: {project.name}", border_style="cyan"))
|
|
280
318
|
|
|
281
319
|
|
|
@@ -303,22 +341,30 @@ def index(
|
|
|
303
341
|
MofNCompleteColumn(),
|
|
304
342
|
console=console,
|
|
305
343
|
) as progress:
|
|
306
|
-
summary_task = progress.add_task("Generating summaries...", total=len(projects))
|
|
344
|
+
summary_task = progress.add_task("Generating summaries & AI tags...", total=len(projects))
|
|
307
345
|
summarized = 0
|
|
346
|
+
tagged = 0
|
|
308
347
|
|
|
309
348
|
for project in projects:
|
|
310
|
-
|
|
349
|
+
needs_summary = not project.summary
|
|
350
|
+
needs_tags = not project.ai_tags
|
|
351
|
+
|
|
352
|
+
if needs_summary or needs_tags:
|
|
311
353
|
progress.update(summary_task, description=f"Summarizing [cyan]{project.name}[/cyan]")
|
|
312
354
|
if model:
|
|
313
|
-
summary, tags, concepts = summarize_project(project, model=model)
|
|
355
|
+
summary, tags, concepts, ai_tags = summarize_project(project, model=model)
|
|
314
356
|
else:
|
|
315
|
-
summary, tags, concepts = summarize_project(project)
|
|
316
|
-
if summary:
|
|
357
|
+
summary, tags, concepts, ai_tags = summarize_project(project)
|
|
358
|
+
if summary and needs_summary:
|
|
317
359
|
project.summary = summary
|
|
318
360
|
project.tags = tags
|
|
319
361
|
project.concepts = concepts
|
|
320
|
-
save_project(project)
|
|
321
362
|
summarized += 1
|
|
363
|
+
if ai_tags:
|
|
364
|
+
project.ai_tags = ai_tags
|
|
365
|
+
tagged += 1
|
|
366
|
+
if summary or ai_tags:
|
|
367
|
+
save_project(project)
|
|
322
368
|
progress.update(summary_task, advance=1)
|
|
323
369
|
|
|
324
370
|
embed_task = progress.add_task("Generating embeddings...", total=len(projects))
|
|
@@ -334,7 +380,62 @@ def index(
|
|
|
334
380
|
indexed += 1
|
|
335
381
|
progress.update(embed_task, advance=1)
|
|
336
382
|
|
|
337
|
-
console.print(f"[green]Summarized {summarized}
|
|
383
|
+
console.print(f"[green]Summarized {summarized}, AI-tagged {tagged}, indexed {indexed} projects.[/green]")
|
|
384
|
+
|
|
385
|
+
|
|
386
|
+
@app.command()
|
|
387
|
+
def tags(
|
|
388
|
+
category: str = typer.Option(None, "--category", "-c", help="Filter by category: domain, technology-role, maturity"),
|
|
389
|
+
) -> None:
|
|
390
|
+
"""List all unique AI tags across projects with counts."""
|
|
391
|
+
init_db()
|
|
392
|
+
projects = get_all_projects()
|
|
393
|
+
|
|
394
|
+
if not projects:
|
|
395
|
+
console.print("[yellow]No projects found. Run 'scan' first.[/yellow]")
|
|
396
|
+
raise typer.Exit(0)
|
|
397
|
+
|
|
398
|
+
# Collect all AI tags with counts
|
|
399
|
+
tag_counts: Counter[str] = Counter()
|
|
400
|
+
tag_categories: dict[str, str] = {}
|
|
401
|
+
|
|
402
|
+
for p in projects:
|
|
403
|
+
for t in p.ai_tags:
|
|
404
|
+
cat = t.category.value if hasattr(t.category, "value") else str(t.category)
|
|
405
|
+
if category and cat != category:
|
|
406
|
+
continue
|
|
407
|
+
tag_counts[t.value] += 1
|
|
408
|
+
tag_categories[t.value] = cat
|
|
409
|
+
|
|
410
|
+
if not tag_counts:
|
|
411
|
+
if category:
|
|
412
|
+
console.print(f"[yellow]No AI tags found for category '{category}'.[/yellow]")
|
|
413
|
+
else:
|
|
414
|
+
console.print("[yellow]No AI tags found. Run 'cex index' to generate tags.[/yellow]")
|
|
415
|
+
raise typer.Exit(0)
|
|
416
|
+
|
|
417
|
+
# Group by category
|
|
418
|
+
by_category: dict[str, list[tuple[str, int]]] = {}
|
|
419
|
+
for tag_value, count in tag_counts.most_common():
|
|
420
|
+
cat = tag_categories[tag_value]
|
|
421
|
+
by_category.setdefault(cat, []).append((tag_value, count))
|
|
422
|
+
|
|
423
|
+
total_tags = len(tag_counts)
|
|
424
|
+
total_projects = len(projects)
|
|
425
|
+
console.print(Panel(
|
|
426
|
+
f"[bold]{total_tags}[/bold] unique AI tags across [bold]{total_projects}[/bold] projects",
|
|
427
|
+
title="AI Tags",
|
|
428
|
+
border_style="magenta",
|
|
429
|
+
))
|
|
430
|
+
|
|
431
|
+
for cat in sorted(by_category.keys()):
|
|
432
|
+
items = by_category[cat]
|
|
433
|
+
table = Table(title=f"{cat} tags")
|
|
434
|
+
table.add_column("Tag", style="magenta")
|
|
435
|
+
table.add_column("Projects", justify="right", style="yellow")
|
|
436
|
+
for tag_value, count in items:
|
|
437
|
+
table.add_row(tag_value, str(count))
|
|
438
|
+
console.print(table)
|
|
338
439
|
|
|
339
440
|
|
|
340
441
|
@app.command()
|
|
@@ -342,6 +443,7 @@ def update(
|
|
|
342
443
|
force: bool = typer.Option(False, "--force", "-f", help="Re-analyze even if git HEAD unchanged"),
|
|
343
444
|
reindex: bool = typer.Option(False, "--reindex", help="Regenerate embeddings for updated projects"),
|
|
344
445
|
resummarize: bool = typer.Option(False, "--resummarize", help="Regenerate AI summaries for updated projects"),
|
|
446
|
+
retag: bool = typer.Option(False, "--retag", help="Regenerate AI tags for updated projects"),
|
|
345
447
|
) -> None:
|
|
346
448
|
"""Update existing projects by re-analyzing changed repositories."""
|
|
347
449
|
from datetime import datetime
|
|
@@ -427,20 +529,25 @@ def update(
|
|
|
427
529
|
updated += 1
|
|
428
530
|
progress.update(task, advance=1)
|
|
429
531
|
|
|
430
|
-
if resummarize and updated_projects:
|
|
532
|
+
if (resummarize or retag) and updated_projects:
|
|
431
533
|
from code_explore.summarizer.ollama import summarize_project
|
|
432
534
|
|
|
433
535
|
summary_task = progress.add_task("Resummarizing...", total=len(updated_projects))
|
|
434
536
|
for project in updated_projects:
|
|
435
537
|
progress.update(summary_task, description=f"Summarizing [cyan]{project.name}[/cyan]")
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
if
|
|
538
|
+
if resummarize:
|
|
539
|
+
project.summary = None
|
|
540
|
+
project.tags = []
|
|
541
|
+
project.concepts = []
|
|
542
|
+
if retag:
|
|
543
|
+
project.ai_tags = []
|
|
544
|
+
summary, tags, concepts, ai_tags = summarize_project(project)
|
|
545
|
+
if summary and resummarize:
|
|
441
546
|
project.summary = summary
|
|
442
547
|
project.tags = tags
|
|
443
548
|
project.concepts = concepts
|
|
549
|
+
if ai_tags and retag:
|
|
550
|
+
project.ai_tags = ai_tags
|
|
444
551
|
save_project(project)
|
|
445
552
|
progress.update(summary_task, advance=1)
|
|
446
553
|
|
|
@@ -541,12 +648,13 @@ def serve(
|
|
|
541
648
|
host: str = typer.Option("0.0.0.0", "--host", "-h", help="Bind host"),
|
|
542
649
|
port: int = typer.Option(8000, "--port", "-p", help="Bind port"),
|
|
543
650
|
) -> None:
|
|
544
|
-
"""Start the FastAPI server."""
|
|
651
|
+
"""Start the FastAPI server with web dashboard."""
|
|
545
652
|
import uvicorn
|
|
546
653
|
|
|
547
654
|
init_db()
|
|
548
655
|
console.print(Panel(
|
|
549
656
|
f"Starting server on [bold cyan]http://{host}:{port}[/bold cyan]\n"
|
|
657
|
+
f"Dashboard at [bold cyan]http://{host}:{port}[/bold cyan]\n"
|
|
550
658
|
f"API docs at [bold cyan]http://{host}:{port}/docs[/bold cyan]",
|
|
551
659
|
title="Code Explore API",
|
|
552
660
|
))
|