99from typing import Optional
1010from datetime import datetime
1111
12- from db import init_db , list_analyses , delete_analysis
12+ from db import init_db , list_analyses
1313from analyzer import analyze_local_path_background , search_semantic , call_coding_model
1414from config import CFG
1515from projects import (
16- create_project , get_project , get_project_by_id , list_projects ,
16+ get_project_by_id , list_projects ,
1717 update_project_status , delete_project , get_or_create_project
1818)
1919from models import (
2424
2525logger = get_logger (__name__ )
2626
27- DATABASE = CFG .get ("database_path" , "codebase.db" )
2827MAX_FILE_SIZE = int (CFG .get ("max_file_size" , 200000 ))
2928
3029# Controls how many characters of each snippet and total context we send to coding model
3130TOTAL_CONTEXT_LIMIT = 4000
32- _ANALYSES_CACHE = []
3331
3432@asynccontextmanager
3533async def lifespan (app : FastAPI ):
36- init_db (DATABASE )
34+ # Project registry is auto-initialized when needed via create_project
35+
36+ # Auto-create default project from configured local_path if it exists
37+ local_path = CFG .get ("local_path" )
38+ if local_path and os .path .exists (local_path ):
39+ try :
40+ get_or_create_project (local_path , "Default Project" )
41+ except Exception as e :
42+ logger .warning (f"Could not create default project: { e } " )
43+
3744 yield
3845
3946app = FastAPI (lifespan = lifespan )
@@ -184,58 +191,79 @@ def api_health():
184191
185192@app .get ("/" , response_class = HTMLResponse )
186193def index (request : Request ):
187- analyses = list_analyses (DATABASE )
188194 projects_list = list_projects ()
189195 return templates .TemplateResponse ("index.html" , {
190196 "request" : request ,
191- "analyses" : analyses ,
192197 "projects" : projects_list ,
193198 "config" : CFG
194199 })
195200
196201
197- @app .get ("/analyses /status" )
198- def analyses_status ():
199- global _ANALYSES_CACHE
202+ @app .get ("/projects /status" )
203+ def projects_status ():
204+ """Get list of all projects."""
200205 try :
201- analyses = list_analyses (DATABASE )
202- # If the DB returned a non-empty list, update cache and return it.
203- if analyses :
204- _ANALYSES_CACHE = analyses
205- return JSONResponse (analyses )
206- # If DB returned empty but we have a cached non-empty list, return cache
207- if not analyses and _ANALYSES_CACHE :
208- return JSONResponse (_ANALYSES_CACHE )
209- # else return whatever (empty list) — first-run or truly empty
210- return JSONResponse (analyses )
206+ projects = list_projects ()
207+ return JSONResponse (projects )
211208 except Exception as e :
212- # On DB errors (e.g., locked) return last known cache to avoid empty responses spam.
213- if _ANALYSES_CACHE :
214- return JSONResponse (_ANALYSES_CACHE )
215- return JSONResponse ({"error" : str (e )}, status_code = 500 )
209+ logger .exception (f"Error getting projects status: { e } " )
210+ return JSONResponse ({"error" : "Failed to retrieve projects" }, status_code = 500 )
216211
217212
218- @app .get ("/analyses/{analysis_id}/delete" )
219- def delete_analysis_endpoint (analysis_id : int ):
213+ @app .delete ("/projects/{project_id}" )
214+ def delete_project_endpoint (project_id : str ):
215+ """Delete a project and its database."""
220216 try :
221- delete_analysis ( DATABASE , analysis_id )
217+ delete_project ( project_id )
222218 return JSONResponse ({"deleted" : True })
219+ except ValueError as e :
220+ logger .warning (f"Project not found for deletion: { e } " )
221+ return JSONResponse ({"deleted" : False , "error" : "Project not found" }, status_code = 404 )
223222 except Exception as e :
224- return JSONResponse ({"deleted" : False , "error" : str (e )}, status_code = 500 )
223+ logger .exception (f"Error deleting project: { e } " )
224+ return JSONResponse ({"deleted" : False , "error" : "Failed to delete project" }, status_code = 500 )
225225
226226
227- @app .post ("/analyze" )
228- def analyze (background_tasks : BackgroundTasks ):
229- local_path = CFG .get ("local_path" )
230- if not local_path or not os .path .exists (local_path ):
231- raise HTTPException (status_code = 400 , detail = "Configured LOCAL_PATH does not exist" )
232- venv_path = CFG .get ("venv_path" )
233- background_tasks .add_task (analyze_local_path_background , local_path , DATABASE , venv_path , MAX_FILE_SIZE , CFG )
234- return RedirectResponse (url = "/" , status_code = 303 )
227+ @app .post ("/index" )
228+ def index_project (background_tasks : BackgroundTasks , project_path : str = None ):
229+ """Index/re-index the default project or specified path."""
230+ try :
231+ # Use configured path or provided path
232+ path_to_index = project_path or CFG .get ("local_path" )
233+ if not path_to_index or not os .path .exists (path_to_index ):
234+ raise HTTPException (status_code = 400 , detail = "Project path does not exist" )
235+
236+ # Get or create project
237+ project = get_or_create_project (path_to_index )
238+ project_id = project ["id" ]
239+ db_path = project ["database_path" ]
240+
241+ # Update status to indexing
242+ update_project_status (project_id , "indexing" )
243+
244+ # Start background indexing
245+ venv_path = CFG .get ("venv_path" )
246+
247+ def index_callback ():
248+ try :
249+ analyze_local_path_background (path_to_index , db_path , venv_path , MAX_FILE_SIZE , CFG )
250+ update_project_status (project_id , "ready" , datetime .utcnow ().isoformat ())
251+ except Exception as e :
252+ logger .exception (f"Indexing failed: { e } " )
253+ update_project_status (project_id , "error" )
254+ raise
255+
256+ background_tasks .add_task (index_callback )
257+
258+ return RedirectResponse (url = "/" , status_code = 303 )
259+ except Exception as e :
260+ logger .exception (f"Error starting indexing: { e } " )
261+ raise HTTPException (status_code = 500 , detail = "Failed to start indexing" )
235262
236263
237264@app .post ("/code" )
238265def code_endpoint (request : Request ):
266+ """Code completion endpoint - uses project_id to find the right database."""
239267 payload = None
240268 try :
241269 payload = request .json ()
@@ -252,29 +280,33 @@ def code_endpoint(request: Request):
252280 explicit_context = payload .get ("context" , "" ) or ""
253281 use_rag = bool (payload .get ("use_rag" , True ))
254282
255- # Support both analysis_id (old) and project_id (new for plugin)
256- analysis_id = payload .get ("analysis_id" )
283+ # Get project_id - if not provided, use the first available project
257284 project_id = payload .get ("project_id" )
258285
259- # If project_id is provided, get the database path and find the first analysis
260- database_path = DATABASE # default to main database
261- if project_id and not analysis_id :
262- try :
263- project = get_project_by_id (project_id )
264- if not project :
265- return JSONResponse ({"error" : "Project not found" }, status_code = 404 )
266-
267- database_path = project ["database_path" ]
268-
269- # Get the first analysis from this project
270- analyses = list_analyses (database_path )
271- if not analyses :
272- return JSONResponse ({"error" : "Project not indexed yet" }, status_code = 400 )
273-
274- analysis_id = analyses [0 ]["id" ]
275- except Exception as e :
276- logger .exception (f"Error getting project analysis: { e } " )
277- return JSONResponse ({"error" : "Failed to get project analysis" }, status_code = 500 )
286+ if not project_id :
287+ # Try to get default project or first available
288+ projects = list_projects ()
289+ if not projects :
290+ return JSONResponse ({"error" : "No projects available. Please index a project first." }, status_code = 400 )
291+ project_id = projects [0 ]["id" ]
292+
293+ # Get project and its database
294+ try :
295+ project = get_project_by_id (project_id )
296+ if not project :
297+ return JSONResponse ({"error" : "Project not found" }, status_code = 404 )
298+
299+ database_path = project ["database_path" ]
300+
301+ # Get the first analysis from this project's database
302+ analyses = list_analyses (database_path )
303+ if not analyses :
304+ return JSONResponse ({"error" : "Project not indexed yet. Please run indexing first." }, status_code = 400 )
305+
306+ analysis_id = analyses [0 ]["id" ]
307+ except Exception as e :
308+ logger .exception (f"Error getting project: { e } " )
309+ return JSONResponse ({"error" : "Failed to retrieve project" }, status_code = 500 )
278310
279311 try :
280312 top_k = int (payload .get ("top_k" , 5 ))
@@ -284,8 +316,8 @@ def code_endpoint(request: Request):
284316 used_context = []
285317 combined_context = explicit_context or ""
286318
287- # If RAG requested and an analysis_id provided , perform semantic search and build context
288- if use_rag and analysis_id :
319+ # If RAG requested, perform semantic search and build context
320+ if use_rag :
289321 try :
290322 retrieved = search_semantic (prompt , database_path , analysis_id = int (analysis_id ), top_k = top_k )
291323 # Build context WITHOUT including snippets: only include file references and scores
0 commit comments