@@ -50,12 +50,13 @@ def list_methods(
5050 "file_pattern" : file_pattern ,
5151 "callee_pattern" : callee_pattern ,
5252 "include_external" : include_external ,
53+ "limit" : limit ,
5354 }
5455
5556 def execute_query ():
5657 codebase_info = self .codebase_tracker .get_codebase (codebase_hash )
57- if not codebase_info or not codebase_info . cpg_path :
58- raise ValidationError (f"CPG not found for codebase { codebase_hash } " )
58+ if not codebase_info :
59+ raise ValidationError (f"Codebase not found for codebase { codebase_hash } " )
5960
6061 query_parts = ["cpg.method" ]
6162 if not include_external :
@@ -71,7 +72,7 @@ def execute_query():
7172 ".map(m => (m.name, m.id, m.fullName, m.signature, m.filename, m.lineNumber.getOrElse(-1), m.isExternal))"
7273 )
7374
74- query_limit = max (limit , 10000 )
75+ query_limit = min (limit , 10000 )
7576 query = "" .join (query_parts ) + f".dedup.take({ query_limit } ).l"
7677
7778 result = self .query_executor .execute_query (
@@ -106,6 +107,9 @@ def execute_query():
106107 return full_result
107108
108109 methods = full_result .get ("methods" , [])
110+ # Respect the provided 'limit' for the returned list, independent of page_size
111+ if limit is not None and limit > 0 :
112+ methods = methods [:limit ]
109113 total = len (methods )
110114
111115 # Pagination
@@ -125,49 +129,109 @@ def execute_query():
125129 def list_files (
126130 self ,
127131 codebase_hash : str ,
132+ local_path : Optional [str ] = None ,
128133 limit : int = 1000 ,
129134 page : int = 1 ,
130135 page_size : int = 100 ,
131136 ) -> Dict [str , Any ]:
132137
133138 validate_codebase_hash (codebase_hash )
134- cache_params = {} # No filters for now
139+ cache_params = {"local_path" : local_path }
135140
136141 def execute_query ():
137142 codebase_info = self .codebase_tracker .get_codebase (codebase_hash )
138- if not codebase_info or not codebase_info .cpg_path :
139- raise ValidationError (f"CPG not found for codebase { codebase_hash } " )
140-
141- query = f"cpg.file.map(f => (f.name, f.hash.getOrElse(\" \" ))).take({ limit } ).l"
142-
143- result = self .query_executor .execute_query (
144- codebase_hash = codebase_hash ,
145- cpg_path = codebase_info .cpg_path ,
146- query = query ,
147- timeout = 30 ,
148- limit = limit ,
143+ if not codebase_info :
144+ raise ValidationError (f"Codebase not found for codebase { codebase_hash } " )
145+ # Determine the actual filesystem path to list
146+ playground_path = os .path .abspath (
147+ os .path .join (os .path .dirname (__file__ ), ".." , ".." , "playground" )
149148 )
150149
151- if not result .success :
152- return {"success" : False , "error" : {"code" : "QUERY_ERROR" , "message" : result .error }}
153-
154- files = []
155- for item in result .data :
156- if isinstance (item , dict ):
157- files .append ({
158- "name" : item .get ("_1" , "" ),
159- "hash" : item .get ("_2" , "" ),
160- })
161- return {"success" : True , "files" : files , "total" : len (files )}
150+ if codebase_info .source_type == "github" :
151+ from ..tools .core_tools import get_cpg_cache_key
152+
153+ cpg_cache_key = get_cpg_cache_key (
154+ codebase_info .source_type ,
155+ codebase_info .source_path ,
156+ codebase_info .language ,
157+ )
158+ source_dir = os .path .join (playground_path , "codebases" , cpg_cache_key )
159+ else :
160+ source_path = codebase_info .source_path
161+ if not os .path .isabs (source_path ):
162+ source_path = os .path .abspath (source_path )
163+ source_dir = source_path
164+
165+ if not os .path .exists (source_dir ) or not os .path .isdir (source_dir ):
166+ raise ValidationError (f"Source directory not found for codebase { codebase_hash } : { source_dir } " )
167+
168+ # Resolve target directory if a local_path is provided; otherwise, use source_dir
169+ if local_path :
170+ # Support both absolute and relative local_path; ensure it stays within source_dir
171+ candidate = local_path
172+ if not os .path .isabs (candidate ):
173+ candidate = os .path .join (source_dir , candidate )
174+ candidate = os .path .normpath (candidate )
175+ source_dir_norm = os .path .normpath (source_dir )
176+ if not candidate .startswith (source_dir_norm ):
177+ raise ValidationError ("local_path must be inside the codebase source directory" )
178+ target_dir = candidate
179+ else :
180+ target_dir = source_dir
181+
182+ # per-directory limits: default 20; 50 when a local_path was provided
183+ per_dir_limit = 50 if local_path else 20
184+
185+ def _list_dir_tree (root : str , base : str , per_dir_limit : int ) -> List [Dict [str , Any ]]:
186+ try :
187+ entries = sorted (os .listdir (root ))
188+ except OSError :
189+ entries = []
190+
191+ result = []
192+ for name in entries [:per_dir_limit ]:
193+ path = os .path .join (root , name )
194+ rel_path = os .path .relpath (path , base )
195+ if os .path .isdir (path ):
196+ children = _list_dir_tree (path , base , per_dir_limit )
197+ result .append ({
198+ "name" : name ,
199+ "path" : rel_path ,
200+ "type" : "dir" ,
201+ "children" : children ,
202+ })
203+ else :
204+ result .append ({
205+ "name" : name ,
206+ "path" : rel_path ,
207+ "type" : "file" ,
208+ })
209+ return result
210+
211+ tree = _list_dir_tree (target_dir , source_dir , per_dir_limit )
212+
213+ # Count total entries in the returned tree (non-recursive counting for top-level)
214+ def _count_nodes (nodes : List [Dict [str , Any ]]) -> int :
215+ total = 0
216+ for n in nodes :
217+ total += 1
218+ if n .get ("type" ) == "dir" :
219+ total += _count_nodes (n .get ("children" , []))
220+ return total
221+
222+ total_count = _count_nodes (tree )
223+ return {"success" : True , "files" : tree , "total" : total_count }
162224
163225 full_result = self ._get_cached_or_execute ("list_files" , codebase_hash , cache_params , execute_query )
164-
226+
165227 if not full_result .get ("success" ):
166228 return full_result
167229
168230 files = full_result .get ("files" , [])
169- total = len (files )
170-
231+ total = full_result .get ("total" , len (files ))
232+
233+ # The tree-based listing does not meaningfully support pagination of top-level results,
234+ # so keep backward compatibility by paginating the top-level entries only.
171235 start_idx = (page - 1 ) * page_size
172236 end_idx = start_idx + page_size
173237 paged_files = files [start_idx :end_idx ]
@@ -178,7 +242,7 @@ def execute_query():
178242 "total" : total ,
179243 "page" : page ,
180244 "page_size" : page_size ,
181- "total_pages" : (total + page_size - 1 ) // page_size if page_size > 0 else 1
245+ "total_pages" : (total + page_size - 1 ) // page_size if page_size > 0 else 1 ,
182246 }
183247
184248 def list_calls (
@@ -195,6 +259,7 @@ def list_calls(
195259 cache_params = {
196260 "caller_pattern" : caller_pattern ,
197261 "callee_pattern" : callee_pattern ,
262+ "limit" : limit ,
198263 }
199264
200265 def execute_query ():
@@ -212,7 +277,7 @@ def execute_query():
212277 ".map(c => (c.method.name, c.name, c.code, c.method.filename, c.lineNumber.getOrElse(-1)))"
213278 )
214279
215- query_limit = max (limit , 10000 )
280+ query_limit = min (limit , 10000 )
216281 query = "" .join (query_parts ) + f".dedup.take({ query_limit } ).l"
217282
218283 result = self .query_executor .execute_query (
@@ -244,6 +309,9 @@ def execute_query():
244309 return full_result
245310
246311 calls = full_result .get ("calls" , [])
312+ # Apply the provided limit to final result set
313+ if limit is not None and limit > 0 :
314+ calls = calls [:limit ]
247315 total = len (calls )
248316
249317 start_idx = (page - 1 ) * page_size
0 commit comments