diff --git a/python/vectorless/cli/__init__.py b/python/vectorless/cli/__init__.py new file mode 100644 index 00000000..b8d9796f --- /dev/null +++ b/python/vectorless/cli/__init__.py @@ -0,0 +1,5 @@ +"""Vectorless CLI — command-line interface for document intelligence.""" + +from vectorless.cli.main import app + +__all__ = ["app"] diff --git a/python/vectorless/cli/commands/__init__.py b/python/vectorless/cli/commands/__init__.py new file mode 100644 index 00000000..8a49cf3c --- /dev/null +++ b/python/vectorless/cli/commands/__init__.py @@ -0,0 +1,25 @@ +"""CLI command modules.""" + +from vectorless.cli.commands.init import init_cmd +from vectorless.cli.commands.add import add_cmd +from vectorless.cli.commands.list_cmd import list_cmd +from vectorless.cli.commands.info import info_cmd +from vectorless.cli.commands.remove import remove_cmd +from vectorless.cli.commands.query import query_cmd +from vectorless.cli.commands.ask import ask_cmd +from vectorless.cli.commands.tree import tree_cmd +from vectorless.cli.commands.stats import stats_cmd +from vectorless.cli.commands.config_cmd import config_cmd + +__all__ = [ + "init_cmd", + "add_cmd", + "list_cmd", + "info_cmd", + "remove_cmd", + "query_cmd", + "ask_cmd", + "tree_cmd", + "stats_cmd", + "config_cmd", +] diff --git a/python/vectorless/cli/commands/add.py b/python/vectorless/cli/commands/add.py new file mode 100644 index 00000000..4b27f372 --- /dev/null +++ b/python/vectorless/cli/commands/add.py @@ -0,0 +1,32 @@ +"""add command — index documents (maps to engine.index).""" + +from typing import Optional + +import click + + +def add_cmd( + path: str, + *, + recursive: bool = False, + fmt: Optional[str] = None, + force: bool = False, + jobs: int = 1, + verbose: bool = False, +) -> None: + """Index a document or directory. + + Args: + path: File or directory path. + recursive: Index directory recursively. + fmt: Force format ("markdown" | "pdf" | None for auto-detect). + force: Force re-index existing documents. + jobs: Number of parallel indexing jobs. + verbose: Show detailed progress. + + Uses: + Engine.index(IndexContext) + IndexContext.from_path / from_paths / from_dir + IndexOptions(mode="force" if force else "default") + """ + raise NotImplementedError diff --git a/python/vectorless/cli/commands/ask.py b/python/vectorless/cli/commands/ask.py new file mode 100644 index 00000000..f494fdcf --- /dev/null +++ b/python/vectorless/cli/commands/ask.py @@ -0,0 +1,46 @@ +"""ask command — interactive REPL for multi-turn queries.""" + +from typing import Optional + +import click + + +def ask_cmd(*, doc_id: Optional[str] = None, verbose: bool = False) -> None: + """Start an interactive query REPL. + + Args: + doc_id: Limit to a specific document. + verbose: Show Agent navigation steps. + + Uses: + Engine.query() in a loop with user input. + Maintains conversation context across turns. + + Built-in commands (prefixed with .): + .help Show available commands + .tree Display current document tree + .stats Show session statistics (LLM calls, tokens, cost) + .nav-log Show navigation log for current conversation + .doc Switch query target document + .doc Show current target document + .verbose Toggle verbose mode + .quit Exit REPL + """ + raise NotImplementedError + + +def _handle_repl_command(line: str) -> bool: + """Handle a built-in REPL command (prefixed with .). + + Args: + line: Raw input line. + + Returns: + True if the command was handled, False if it's a query. + """ + raise NotImplementedError + + +def _print_welcome() -> None: + """Print REPL welcome message with available commands.""" + raise NotImplementedError diff --git a/python/vectorless/cli/commands/config_cmd.py b/python/vectorless/cli/commands/config_cmd.py new file mode 100644 index 00000000..8a119702 --- /dev/null +++ b/python/vectorless/cli/commands/config_cmd.py @@ -0,0 +1,63 @@ +"""config command — view and modify configuration.""" + +from typing import Optional + +import click + + +def config_cmd( + key: Optional[str] = None, + value: Optional[str] = None, + *, + init_config: bool = False, +) -> None: + """View or modify workspace configuration. + + Args: + key: Config key (dot-separated, e.g. "llm.model"). + value: New value to set. If None, prints current value. + init_config: Reset config to defaults. + + Usage: + vectorless-cli config # show all + vectorless-cli config llm.model # show one key + vectorless-cli config llm.model gpt-4o # set value + vectorless-cli config --init # reset defaults + + Config keys (in .vectorless/config.toml): + llm.model LLM model name + llm.api_key API key (or env VECTORLESS_API_KEY) + llm.endpoint API endpoint + retrieval.strategy agent | pipeline + retrieval.max_rounds navigation budget + index.summary full | selective | lazy | navigation + index.compact_mode true | false + """ + raise NotImplementedError + + +def _load_config(workspace: str) -> dict: + """Load config.toml from workspace. + + Args: + workspace: Path to .vectorless/ directory. + + Returns: + Parsed config dict. + """ + raise NotImplementedError + + +def _save_config(workspace: str, config: dict) -> None: + """Save config dict to config.toml. + + Args: + workspace: Path to .vectorless/ directory. + config: Config dict to serialize. + """ + raise NotImplementedError + + +def _default_config() -> dict: + """Return default configuration values.""" + raise NotImplementedError diff --git a/python/vectorless/cli/commands/info.py b/python/vectorless/cli/commands/info.py new file mode 100644 index 00000000..cee109c7 --- /dev/null +++ b/python/vectorless/cli/commands/info.py @@ -0,0 +1,30 @@ +"""info command — show document index details.""" + +import click + + +def info_cmd(doc_id: str) -> None: + """Show detailed information about an indexed document. + + Args: + doc_id: Document identifier. + + Uses: + Engine.list() -> filter by doc_id + Display: title, source, format, node count, depth, leaf count, + total tokens, routing keywords, top-level sections, + indexed timestamp. + + Example output: + Document: API Guide (a1b2c3) + Source: ./docs/api-guide.md + Format: Markdown + Tree: 45 nodes, depth 4, 12 leaves + Total tokens: 8,234 + Routing keywords: api, authentication, endpoints, rate-limit + Top-level sections: + 1. Overview (12 leaves) + 2. Authentication (8 leaves) + 3. Endpoints (18 leaves) + """ + raise NotImplementedError diff --git a/python/vectorless/cli/commands/init.py b/python/vectorless/cli/commands/init.py new file mode 100644 index 00000000..8ddf8324 --- /dev/null +++ b/python/vectorless/cli/commands/init.py @@ -0,0 +1,18 @@ +"""init command — initialize .vectorless/ workspace.""" + +import click + + +def init_cmd(workspace: str) -> None: + """Create .vectorless/ directory structure with default config. + + Creates: + .vectorless/ + ├── config.toml # LLM key/model/endpoint, retrieval strategy + ├── data/ # Index data (DocumentTree, ReasoningIndex) + └── cache/ # Memo cache + + Args: + workspace: Parent directory to create .vectorless/ in. + """ + raise NotImplementedError diff --git a/python/vectorless/cli/commands/list_cmd.py b/python/vectorless/cli/commands/list_cmd.py new file mode 100644 index 00000000..4c479bc7 --- /dev/null +++ b/python/vectorless/cli/commands/list_cmd.py @@ -0,0 +1,18 @@ +"""list command — list indexed documents.""" + +import click + + +def list_cmd(*, fmt: str = "table") -> None: + """List all indexed documents in the workspace. + + Args: + fmt: Output format — "table" or "json". + + Uses: + Engine.list() -> List[DocumentInfo] + + Table output: + Doc ID | Title | Format | Nodes | Pages | Indexed At + """ + raise NotImplementedError diff --git a/python/vectorless/cli/commands/query.py b/python/vectorless/cli/commands/query.py new file mode 100644 index 00000000..f928ab46 --- /dev/null +++ b/python/vectorless/cli/commands/query.py @@ -0,0 +1,40 @@ +"""query command — single query (maps to engine.query).""" + +from typing import Optional + +import click + + +def query_cmd( + question: str, + *, + doc_ids: tuple[str, ...] = (), + workspace_scope: bool = False, + fmt: str = "text", + verbose: bool = False, + max_tokens: Optional[int] = None, +) -> None: + """Execute a single query against indexed documents. + + Args: + question: Natural-language question. + doc_ids: Limit to specific document IDs. + workspace_scope: Query across all documents. + fmt: Output format — "text" or "json". + verbose: Show Agent navigation steps. + max_tokens: Max result tokens. + + Uses: + Engine.query(QueryContext(question) + .with_doc_ids([...]) or .with_workspace() + .with_max_tokens(n)) + -> QueryResult + + Verbose mode prints Agent navigation: + [1/8] Bird's-eye: 3 top-level branches + [2/8] Descend → payment-configuration + [3/8] GetContent → doc 29139b + [4/8] Evaluate → sufficient + → Answer: ... + """ + raise NotImplementedError diff --git a/python/vectorless/cli/commands/remove.py b/python/vectorless/cli/commands/remove.py new file mode 100644 index 00000000..b4285b60 --- /dev/null +++ b/python/vectorless/cli/commands/remove.py @@ -0,0 +1,15 @@ +"""remove command — remove document index.""" + +import click + + +def remove_cmd(doc_id: str) -> None: + """Remove a document from the index. + + Args: + doc_id: Document identifier to remove. + + Uses: + Engine.remove(doc_id) + """ + raise NotImplementedError diff --git a/python/vectorless/cli/commands/stats.py b/python/vectorless/cli/commands/stats.py new file mode 100644 index 00000000..be2d9060 --- /dev/null +++ b/python/vectorless/cli/commands/stats.py @@ -0,0 +1,22 @@ +"""stats command — workspace statistics.""" + +import click + + +def stats_cmd() -> None: + """Show workspace statistics. + + Displays: + - Workspace path + - Number of indexed documents + - Total nodes / leaves / tokens + - Index size on disk + - DocumentGraph info (edges, connected components) + - Last indexed timestamp + + Uses: + Engine.list() -> count documents + Engine.metrics_report() + Filesystem scan for size info + """ + raise NotImplementedError diff --git a/python/vectorless/cli/commands/tree.py b/python/vectorless/cli/commands/tree.py new file mode 100644 index 00000000..581f8baf --- /dev/null +++ b/python/vectorless/cli/commands/tree.py @@ -0,0 +1,32 @@ +"""tree command — visualize document tree structure.""" + +from typing import Optional + +import click + + +def tree_cmd( + doc_id: str, + *, + depth: Optional[int] = None, + show_summary: bool = False, + show_keywords: bool = False, +) -> None: + """Visualize the hierarchical tree structure of an indexed document. + + Args: + doc_id: Document identifier. + depth: Max depth to display (None = full tree). + show_summary: Include node summaries in output. + show_keywords: Include routing keywords in output. + + Example output: + API Guide (a1b2c3) — 45 nodes, 12 leaves + 1. Overview [routing: api-overview] (12 leaves) + ├── 1.1 Introduction + ├── 1.2 Authentication [keywords: auth, token, api-key] + │ ├── 1.2.1 API Key Setup + │ └── 1.2.2 OAuth Flow + └── 1.3 Endpoints (18 leaves) + """ + raise NotImplementedError diff --git a/python/vectorless/cli/main.py b/python/vectorless/cli/main.py new file mode 100644 index 00000000..844d3803 --- /dev/null +++ b/python/vectorless/cli/main.py @@ -0,0 +1,138 @@ +"""CLI application definition and command routing.""" + +import asyncio +from typing import Optional + +import click + + +@click.group() +@click.version_option(package_name="vectorless") +@click.option("--workspace", "-w", default=".vectorless", help="Workspace directory path.") +@click.pass_context +def app(ctx: click.Context, workspace: str) -> None: + """Vectorless — reasoning-native document intelligence engine.""" + ctx.ensure_object(dict) + ctx.obj["workspace"] = workspace + + +# ── Index commands ────────────────────────────────────────── + +@app.command() +@click.option("--workspace", "-w", default=".", help="Directory to initialize.") +def init(workspace: str) -> None: + """Initialize a .vectorless/ workspace.""" + raise NotImplementedError + + +@app.command() +@click.argument("path", type=click.Path(exists=True)) +@click.option("--recursive", "-r", is_flag=True, help="Index directory recursively.") +@click.option("--format", "fmt", type=click.Choice(["markdown", "pdf"]), help="Force document format.") +@click.option("--force", is_flag=True, help="Force re-index existing documents.") +@click.option("--jobs", "-j", default=1, type=int, help="Parallel indexing jobs.") +@click.option("--verbose", "-v", is_flag=True, help="Show detailed progress.") +def add( + path: str, + recursive: bool, + fmt: Optional[str], + force: bool, + jobs: int, + verbose: bool, +) -> None: + """Index a document or directory. + + PATH can be a file (.md, .pdf) or a directory. + """ + raise NotImplementedError + + +@app.command("list") +@click.option("--format", "fmt", type=click.Choice(["table", "json"]), default="table") +def list_cmd(fmt: str) -> None: + """List all indexed documents.""" + raise NotImplementedError + + +@app.command() +@click.argument("doc_id") +def info(doc_id: str) -> None: + """Show details of an indexed document.""" + raise NotImplementedError + + +@app.command() +@click.argument("doc_id") +@click.confirmation_option(prompt="Remove this document index?") +def remove(doc_id: str) -> None: + """Remove a document from the index.""" + raise NotImplementedError + + +# ── Query commands ────────────────────────────────────────── + +@app.command() +@click.argument("question") +@click.option("--doc", "-d", multiple=True, help="Limit query to specific document IDs.") +@click.option("--workspace-scope", is_flag=True, help="Query across all documents.") +@click.option("--format", "fmt", type=click.Choice(["text", "json"]), default="text") +@click.option("--verbose", "-v", is_flag=True, help="Show Agent navigation steps.") +@click.option("--max-tokens", type=int, help="Max result tokens.") +def query( + question: str, + doc: tuple[str, ...], + workspace_scope: bool, + fmt: str, + verbose: bool, + max_tokens: Optional[int], +) -> None: + """Query indexed documents. + + QUESTION is the natural-language question to ask. + """ + raise NotImplementedError + + +@app.command() +@click.option("--doc", "-d", help="Limit to a specific document ID.") +@click.option("--verbose", "-v", is_flag=True, help="Show Agent navigation steps.") +def ask(doc: Optional[str], verbose: bool) -> None: + """Interactive query REPL. + + Start a multi-turn conversation with your documents. + """ + raise NotImplementedError + + +# ── Debug / tool commands ─────────────────────────────────── + +@app.command() +@click.argument("doc_id") +@click.option("--depth", "-d", type=int, help="Max depth to display.") +@click.option("--show-summary", is_flag=True, help="Show node summaries.") +@click.option("--show-keywords", is_flag=True, help="Show routing keywords.") +def tree(doc_id: str, depth: Optional[int], show_summary: bool, show_keywords: bool) -> None: + """Visualize document tree structure.""" + raise NotImplementedError + + +@app.command() +def stats() -> None: + """Show workspace statistics.""" + raise NotImplementedError + + +@app.command("config") +@click.argument("key", required=False) +@click.argument("value", required=False) +@click.option("--init", "init_config", is_flag=True, help="Re-initialize default config.") +def config_cmd(key: Optional[str], value: Optional[str], init_config: bool) -> None: + """View or modify configuration. + + \b + vectorless-cli config Show all config + vectorless-cli config llm.model Show specific key + vectorless-cli config llm.model gpt-4o Set a value + vectorless-cli config --init Reset to defaults + """ + raise NotImplementedError diff --git a/python/vectorless/cli/output.py b/python/vectorless/cli/output.py new file mode 100644 index 00000000..531058a8 --- /dev/null +++ b/python/vectorless/cli/output.py @@ -0,0 +1,75 @@ +"""Output formatting — text, json, table.""" + +from typing import Any, Optional +from enum import Enum + + +class OutputFormat(Enum): + TEXT = "text" + JSON = "json" + TABLE = "table" + + +def format_result(data: Any, fmt: OutputFormat) -> str: + """Format a result dict for terminal output. + + Args: + data: Structured data to format. + fmt: Target output format. + + Returns: + Formatted string ready to print. + """ + raise NotImplementedError + + +def format_documents_table(documents: list[dict]) -> str: + """Format a list of documents as a table. + + Columns: Doc ID | Title | Format | Nodes | Pages | Indexed At + + Args: + documents: List of document info dicts. + + Returns: + Formatted table string (uses comfy-table or rich). + """ + raise NotImplementedError + + +def format_tree( + nodes: list[dict], + *, + max_depth: Optional[int] = None, + show_summary: bool = False, + show_keywords: bool = False, +) -> str: + """Format document tree as indented tree view. + + Args: + nodes: Flat list of tree nodes with parent references. + max_depth: Max depth to display. + show_summary: Include summaries. + show_keywords: Include routing keywords. + + Returns: + Indented tree string. + """ + raise NotImplementedError + + +def format_navigation_steps(steps: list[dict]) -> str: + """Format Agent navigation steps for verbose mode. + + Args: + steps: List of navigation step dicts with action, target, reasoning. + + Returns: + Step-by-step navigation log string. + """ + raise NotImplementedError + + +def format_json(data: Any) -> str: + """Format data as indented JSON.""" + raise NotImplementedError