diff --git a/crates/ov_cli/ovcli.conf.example b/crates/ov_cli/ovcli.conf.example new file mode 100644 index 00000000..260f3039 --- /dev/null +++ b/crates/ov_cli/ovcli.conf.example @@ -0,0 +1,7 @@ +{ + "url": "http://localhost:1933", + "api_key": "your-api-key", + "timeout": 60.0, + "output": "table", + "echo_command": true +} diff --git a/crates/ov_cli/src/client.rs b/crates/ov_cli/src/client.rs index 63c8bfa4..8b7cf492 100644 --- a/crates/ov_cli/src/client.rs +++ b/crates/ov_cli/src/client.rs @@ -322,13 +322,14 @@ impl HttpClient { self.get("/api/v1/fs/ls", ¶ms).await } - pub async fn tree(&self, uri: &str, output: &str, abs_limit: i32, show_all_hidden: bool, node_limit: i32) -> Result { + pub async fn tree(&self, uri: &str, output: &str, abs_limit: i32, show_all_hidden: bool, node_limit: i32, level_limit: i32) -> Result { let params = vec![ ("uri".to_string(), uri.to_string()), ("output".to_string(), output.to_string()), ("abs_limit".to_string(), abs_limit.to_string()), ("show_all_hidden".to_string(), show_all_hidden.to_string()), ("node_limit".to_string(), node_limit.to_string()), + ("level_limit".to_string(), level_limit.to_string()), ]; self.get("/api/v1/fs/tree", ¶ms).await } diff --git a/crates/ov_cli/src/commands/filesystem.rs b/crates/ov_cli/src/commands/filesystem.rs index b281f1c7..bd9f6499 100644 --- a/crates/ov_cli/src/commands/filesystem.rs +++ b/crates/ov_cli/src/commands/filesystem.rs @@ -26,10 +26,11 @@ pub async fn tree( abs_limit: i32, show_all_hidden: bool, node_limit: i32, + level_limit: i32, output_format: OutputFormat, compact: bool, ) -> Result<()> { - let result = client.tree(uri, output, abs_limit, show_all_hidden, node_limit).await?; + let result = client.tree(uri, output, abs_limit, show_all_hidden, node_limit, level_limit).await?; output_success(&result, output_format, compact); Ok(()) } diff --git a/crates/ov_cli/src/config.rs b/crates/ov_cli/src/config.rs index 0c9b847a..299b31eb 100644 --- a/crates/ov_cli/src/config.rs +++ b/crates/ov_cli/src/config.rs @@ -13,6 +13,8 @@ pub struct Config { pub timeout: f64, #[serde(default = "default_output_format")] pub output: String, + #[serde(default = "default_echo_command")] + pub echo_command: bool, } fn default_url() -> String { @@ -27,6 +29,10 @@ fn default_output_format() -> String { "table".to_string() } +fn default_echo_command() -> bool { + true +} + impl Default for Config { fn default() -> Self { Self { @@ -35,6 +41,7 @@ impl Default for Config { agent_id: None, timeout: 60.0, output: "table".to_string(), + echo_command: true, } } } diff --git a/crates/ov_cli/src/main.rs b/crates/ov_cli/src/main.rs index 7c32f446..1f6b59a4 100644 --- a/crates/ov_cli/src/main.rs +++ b/crates/ov_cli/src/main.rs @@ -195,7 +195,7 @@ enum Commands { #[arg(short, long)] all: bool, /// Maximum number of nodes to list - #[arg(long = "node-limit", short = 'n', default_value = "1000")] + #[arg(long = "node-limit", short = 'n', default_value = "256")] node_limit: i32, }, /// Get directory tree @@ -209,8 +209,11 @@ enum Commands { #[arg(short, long)] all: bool, /// Maximum number of nodes to list - #[arg(long = "node-limit", short = 'n', default_value = "1000")] + #[arg(long = "node-limit", short = 'n', default_value = "256")] node_limit: i32, + /// Maximum depth level to traverse (default: 3) + #[arg(short = 'L', long = "level-limit", default_value = "3")] + level_limit: i32, }, /// Create directory Mkdir { @@ -530,8 +533,8 @@ async fn main() { Commands::Ls { uri, simple, recursive, abs_limit, all, node_limit } => { handle_ls(uri, simple, recursive, abs_limit, all, node_limit, ctx).await } - Commands::Tree { uri, abs_limit, all, node_limit } => { - handle_tree(uri, abs_limit, all, node_limit, ctx).await + Commands::Tree { uri, abs_limit, all, node_limit, level_limit } => { + handle_tree(uri, abs_limit, all, node_limit, level_limit, ctx).await } Commands::Mkdir { uri } => { handle_mkdir(uri, ctx).await @@ -877,16 +880,42 @@ async fn handle_search( commands::search::search(&client, &query, &uri, session_id, limit, threshold, ctx.output_format, ctx.compact).await } +/// Print command with specified parameters for debugging +fn print_command_echo(command: &str, params: &str, echo_enabled: bool) { + if echo_enabled { + println!("cmd: {} {}", command, params); + } +} + async fn handle_ls(uri: String, simple: bool, recursive: bool, abs_limit: i32, show_all_hidden: bool, node_limit: i32, ctx: CliContext) -> Result<()> { + let mut params = vec![ + uri.clone(), + format!("-l {}", abs_limit), + format!("-n {}", node_limit), + ]; + if simple { params.push("-s".to_string()); } + if recursive { params.push("-r".to_string()); } + if show_all_hidden { params.push("-a".to_string()); } + print_command_echo("ov ls", ¶ms.join(" "), ctx.config.echo_command); + let client = ctx.get_client(); let api_output = if ctx.compact { "agent" } else { "original" }; commands::filesystem::ls(&client, &uri, simple, recursive, api_output, abs_limit, show_all_hidden, node_limit, ctx.output_format, ctx.compact).await } -async fn handle_tree(uri: String, abs_limit: i32, show_all_hidden: bool, node_limit: i32, ctx: CliContext) -> Result<()> { +async fn handle_tree(uri: String, abs_limit: i32, show_all_hidden: bool, node_limit: i32, level_limit: i32, ctx: CliContext) -> Result<()> { + let mut params = vec![ + uri.clone(), + format!("-l {}", abs_limit), + format!("-n {}", node_limit), + format!("-L {}", level_limit), + ]; + if show_all_hidden { params.push("-a".to_string()); } + print_command_echo("ov tree", ¶ms.join(" "), ctx.config.echo_command); + let client = ctx.get_client(); let api_output = if ctx.compact { "agent" } else { "original" }; - commands::filesystem::tree(&client, &uri, api_output, abs_limit, show_all_hidden, node_limit, ctx.output_format, ctx.compact).await + commands::filesystem::tree(&client, &uri, api_output, abs_limit, show_all_hidden, node_limit, level_limit, ctx.output_format, ctx.compact).await } async fn handle_mkdir(uri: String, ctx: CliContext) -> Result<()> { diff --git a/openviking/parse/tree_builder.py b/openviking/parse/tree_builder.py index d7a0209d..ce8caa95 100644 --- a/openviking/parse/tree_builder.py +++ b/openviking/parse/tree_builder.py @@ -133,10 +133,6 @@ async def finalize_from_temp( if original_name != doc_name: logger.debug(f"[TreeBuilder] Sanitized doc name: {original_name!r} -> {doc_name!r}") - # 2. Determine base_uri and final document name with org/repo for GitHub/GitLab - if base_uri is None: - base_uri = self._get_base_uri(scope, source_path, source_format) - # Check if source_path is a GitHub/GitLab URL and extract org/repo final_doc_name = doc_name if source_path and source_format == "repository": @@ -144,27 +140,32 @@ async def finalize_from_temp( if parsed_org_repo: final_doc_name = parsed_org_repo + # 2. Determine base_uri and final document name with org/repo for GitHub/GitLab + auto_base_uri = self._get_base_uri(scope, source_path, source_format) + # 3. Check if base_uri exists - if it does, use it as parent directory - try: - await viking_fs.stat(base_uri) - base_exists = True - except Exception: - base_exists = False + base_exists = False + if base_uri: + try: + await viking_fs.stat(base_uri) + base_exists = True + except Exception: + base_exists = False if base_exists: if "/" in final_doc_name: repo_name_only = final_doc_name.split("/")[-1] else: repo_name_only = final_doc_name - candidate_uri = VikingURI(base_uri).join(repo_name_only).uri + candidate_uri = VikingURI(base_uri or auto_base_uri).join(repo_name_only).uri else: if "/" in final_doc_name: parts = final_doc_name.split("/") sanitized_parts = [VikingURI.sanitize_segment(p) for p in parts if p] - base_viking_uri = VikingURI(base_uri) + base_viking_uri = VikingURI(base_uri or auto_base_uri) candidate_uri = VikingURI.build(base_viking_uri.scope, *sanitized_parts) else: - candidate_uri = VikingURI(base_uri).join(doc_name).uri + candidate_uri = VikingURI(base_uri or auto_base_uri).join(doc_name).uri final_uri = await self._resolve_unique_uri(candidate_uri, ctx=ctx) if final_uri != candidate_uri: diff --git a/openviking/server/routers/filesystem.py b/openviking/server/routers/filesystem.py index ba710c74..d4bc369d 100644 --- a/openviking/server/routers/filesystem.py +++ b/openviking/server/routers/filesystem.py @@ -48,6 +48,7 @@ async def tree( abs_limit: int = Query(256, description="Abstract limit (only for agent output)"), show_all_hidden: bool = Query(False, description="List all hidden files, like -a"), node_limit: int = Query(1000, description="Maximum number of nodes to list"), + level_limit: int = Query(3, description="Maximum depth level to traverse"), _ctx: RequestContext = Depends(get_request_context), ): """Get directory tree.""" @@ -59,6 +60,7 @@ async def tree( abs_limit=abs_limit, show_all_hidden=show_all_hidden, node_limit=node_limit, + level_limit=level_limit, ) return Response(status="ok", result=result) diff --git a/openviking/service/fs_service.py b/openviking/service/fs_service.py index 82b1f194..bec8a0da 100644 --- a/openviking/service/fs_service.py +++ b/openviking/service/fs_service.py @@ -42,6 +42,7 @@ async def ls( abs_limit: int = 256, show_all_hidden: bool = False, node_limit: int = 1000, + level_limit: int = 3, ) -> List[Any]: """List directory contents. @@ -65,6 +66,7 @@ async def ls( output="original", show_all_hidden=show_all_hidden, node_limit=node_limit, + level_limit=level_limit, ) else: entries = await viking_fs.ls( @@ -80,6 +82,7 @@ async def ls( abs_limit=abs_limit, show_all_hidden=show_all_hidden, node_limit=node_limit, + level_limit=level_limit, ) else: entries = await viking_fs.ls( @@ -114,6 +117,7 @@ async def tree( abs_limit: int = 128, show_all_hidden: bool = False, node_limit: int = 1000, + level_limit: int = 3, ) -> List[Dict[str, Any]]: """Get directory tree.""" viking_fs = self._ensure_initialized() @@ -124,6 +128,7 @@ async def tree( abs_limit=abs_limit, show_all_hidden=show_all_hidden, node_limit=node_limit, + level_limit=level_limit, ) async def stat(self, uri: str, ctx: RequestContext) -> Dict[str, Any]: diff --git a/openviking/storage/viking_fs.py b/openviking/storage/viking_fs.py index ae09efe6..dc1fc076 100644 --- a/openviking/storage/viking_fs.py +++ b/openviking/storage/viking_fs.py @@ -384,6 +384,7 @@ async def tree( abs_limit: int = 256, show_all_hidden: bool = False, node_limit: int = 1000, + level_limit: int = 3, ctx: Optional[RequestContext] = None, ) -> List[Dict[str, Any]]: """ @@ -394,6 +395,8 @@ async def tree( output: str = "original" or "agent" abs_limit: int = 256 (for agent output abstract truncation) show_all_hidden: bool = False (list all hidden files, like -a) + node_limit: int = 1000 (maximum number of nodes to list) + level_limit: int = 3 (maximum depth level to traverse) output="original" [{'name': '.abstract.md', 'size': 100, 'mode': 420, 'modTime': '2026-02-11T16:52:16.256334192+08:00', 'isDir': False, 'meta': {...}, 'rel_path': '.abstract.md', 'uri': 'viking://resources...'}] @@ -403,9 +406,11 @@ async def tree( """ self._ensure_access(uri, ctx) if output == "original": - return await self._tree_original(uri, show_all_hidden, node_limit, ctx=ctx) + return await self._tree_original(uri, show_all_hidden, node_limit, level_limit, ctx=ctx) elif output == "agent": - return await self._tree_agent(uri, abs_limit, show_all_hidden, node_limit, ctx=ctx) + return await self._tree_agent( + uri, abs_limit, show_all_hidden, node_limit, level_limit, ctx=ctx + ) else: raise ValueError(f"Invalid output format: {output}") @@ -414,6 +419,7 @@ async def _tree_original( uri: str, show_all_hidden: bool = False, node_limit: int = 1000, + level_limit: int = 3, ctx: Optional[RequestContext] = None, ) -> List[Dict[str, Any]]: """Recursively list all contents (original format).""" @@ -421,8 +427,8 @@ async def _tree_original( all_entries = [] real_ctx = self._ctx_or_default(ctx) - async def _walk(current_path: str, current_rel: str): - if len(all_entries) >= node_limit: + async def _walk(current_path: str, current_rel: str, current_depth: int): + if len(all_entries) >= node_limit or current_depth > level_limit: return for entry in self._ls_entries(current_path): if len(all_entries) >= node_limit: @@ -438,13 +444,13 @@ async def _walk(current_path: str, current_rel: str): continue if entry.get("isDir"): all_entries.append(new_entry) - await _walk(f"{current_path}/{name}", rel_path) + await _walk(f"{current_path}/{name}", rel_path, current_depth + 1) elif not name.startswith("."): all_entries.append(new_entry) elif show_all_hidden: all_entries.append(new_entry) - await _walk(path, "") + await _walk(path, "", 0) return all_entries async def _tree_agent( @@ -453,6 +459,7 @@ async def _tree_agent( abs_limit: int, show_all_hidden: bool = False, node_limit: int = 1000, + level_limit: int = 3, ctx: Optional[RequestContext] = None, ) -> List[Dict[str, Any]]: """Recursively list all contents (agent format with abstracts).""" @@ -461,8 +468,8 @@ async def _tree_agent( now = datetime.now() real_ctx = self._ctx_or_default(ctx) - async def _walk(current_path: str, current_rel: str): - if len(all_entries) >= node_limit: + async def _walk(current_path: str, current_rel: str, current_depth: int): + if len(all_entries) >= node_limit or current_depth > level_limit: return for entry in self._ls_entries(current_path): if len(all_entries) >= node_limit: @@ -482,13 +489,13 @@ async def _walk(current_path: str, current_rel: str): continue if entry.get("isDir"): all_entries.append(new_entry) - await _walk(f"{current_path}/{name}", rel_path) + await _walk(f"{current_path}/{name}", rel_path, current_depth + 1) elif not name.startswith("."): all_entries.append(new_entry) elif show_all_hidden: all_entries.append(new_entry) - await _walk(path, "") + await _walk(path, "", 0) await self._batch_fetch_abstracts(all_entries, abs_limit, ctx=ctx)