Skip to content

feat: Implement CLI functionality for user query processing#15

Open
programmer-ke wants to merge 1 commit into
masterfrom
dev
Open

feat: Implement CLI functionality for user query processing#15
programmer-ke wants to merge 1 commit into
masterfrom
dev

Conversation

@programmer-ke

@programmer-ke programmer-ke commented Jul 9, 2026

Copy link
Copy Markdown
Owner

This allows the user to specify configure repository sources using a config file and use the agent to do research on any relevant queries.

Summary by CodeRabbit

  • New Features

    • Added config-file driven CLI setup with an init flow and automatic config discovery.
    • Repository syncs now track and use a selected branch.
    • Search results can now be customized with repository-specific labels and path prefixes.
  • Bug Fixes

    • Improved sync and search output handling for clearer, more accurate repository references.
    • Error messages are now more descriptive when repository updates fail.
  • Documentation

    • Updated CLI help and added a sample configuration template.

@coderabbitai

coderabbitai Bot commented Jul 9, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Adds a vendored dot-notation config loader, threads a branch parameter through repo sync commands/events/use-cases/handlers, makes repo storage adapters branch-aware with new error handling and git checkout logic, adds Whoosh index path prefixing, extends search tool/agent factories for per-repo customization, and rewrites the CLI to be config-driven (--init, --current-config, --update-sources, --query) across multiple configured repositories.

Changes

Config-driven multi-repo CLI with branch-aware sync

Layer / File(s) Summary
Vendored dotconfig module
src/docs_buddy/_vendor/dotconfig.py
New Config class provides dot-notation and item access over JSON/YAML data, with from_json/from_yaml loaders and string representations.
Branch field on sync command/event/use-case contracts
src/docs_buddy/services/commands.py, src/docs_buddy/services/events.py, src/docs_buddy/services/use_cases.py, src/docs_buddy/services/handlers.py
SyncRepo/RepositorySynced gain a branch field; RepoStorage protocol and sync_repository use case require and thread branch through; handler publishes and passes branch.
Branch-aware repo storage adapters
src/docs_buddy/adapters/__init__.py, tests/unit/test_services.py
FakeRepoStorage/FileSystemRepoStorage clone_repo/pull_repo accept branch; new FileSystemRepoStorageError; git clone/checkout commands target a specific branch; tests updated accordingly.
Per-repo search tool naming and agent model configuration
src/docs_buddy/adapters/agent.py, tests/integration/test_adapters.py, tests/unit/test_tools.py, tests/unit/test_services.py
make_search_tool accepts tool_id/content_information to customize name/docstring; make_openai_research_agent accepts model_name/base_url and reads only OPENAI_API_KEY; tests updated for new signatures.
Whoosh index file_prefix support
src/docs_buddy/adapters/whoosh_index.py, tests/integration/test_adapters.py
WhooshDocumentIndex accepts file_prefix and prepends it to returned result paths.
Python identifier sanitization helper
src/docs_buddy/common.py, tests/unit/test_common.py
New sanitize_to_python_id normalizes arbitrary strings into valid Python identifiers, with unit tests.
Bootstrap doc_extensions wiring
src/docs_buddy/entrypoints/bootstrap.py
get_message_bus accepts and threads doc_extensions into FileSystemDocsStorage.
Config-driven CLI init, sync, and query flows
src/docs_buddy/entrypoints/cli/__main__.py, src/docs_buddy/entrypoints/cli/data/docs_buddy_template.yaml, src/docs_buddy/entrypoints/cli/requirements.txt, src/docs_buddy/entrypoints/cli/README.md
CLI rewritten to add --init, --current-config, config discovery/loading, and --update-sources/--query flows iterating configured repositories; adds YAML template, pinned PyYAML deps, and updated README usage.
Repository ignore rules and todo tracking
.gitignore, todo.md
Ignores docs_buddy.yaml; reorganizes in-progress user stories for web-form and CLI query features.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant CLI as __main__.py
    participant Config as dotconfig.Config
    participant MessageBus as bootstrap/handlers
    participant RepoStorage as FileSystemRepoStorage
    participant Whoosh as WhooshDocumentIndex
    participant Agent as OpenAI research agent

    User->>CLI: run with --update-sources
    CLI->>Config: _find_config / _load_config
    Config-->>CLI: repositories, branches, extensions
    loop for each configured repo
        CLI->>MessageBus: SyncRepo(repo_url, branch)
        MessageBus->>RepoStorage: clone_repo(url, branch) or pull_repo(branch)
        RepoStorage-->>MessageBus: RepositorySynced(url, branch)
    end

    User->>CLI: run with --query
    CLI->>Config: _load_config
    loop for each configured repo
        CLI->>Whoosh: WhooshDocumentIndex(index_location, file_prefix)
        CLI->>CLI: make_search_tool(index, tool_id, content_information)
    end
    CLI->>Agent: make_openai_research_agent(prompt, model_name, base_url)
    CLI->>Agent: find_answer(query, tools)
    Agent-->>CLI: answer with references
    CLI-->>User: print answer and reference paths
Loading

Possibly related PRs

  • programmer-ke/docs-buddy#10: Builds on the same SyncRepo/RepositorySynced message-bus pattern that this PR extends with a branch field.
  • programmer-ke/docs-buddy#11: Modifies the same WhooshDocumentIndex.search/domain.QueryResult code that this PR extends with file_prefix rewriting.
  • programmer-ke/docs-buddy#12: Modifies the same src/docs_buddy/entrypoints/cli/__main__.py query flow that this PR rewrites for multi-repo config support.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 33.33% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title matches the main change: adding CLI query-processing functionality and related configuration support.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch dev

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (1)
src/docs_buddy/adapters/__init__.py (1)

139-169: 🔒 Security & Privacy | 🔵 Trivial | ⚡ Quick win

Harden git commands against branch names starting with -.

If a branch name begins with -, git may interpret it as an option flag rather than a value. Using = syntax for --branch and -- separators for positional args prevents this.

🛡️ Proposed fix
     `@staticmethod`
     def _clone_repository(url: str, target_dir: str, branch: str) -> None:
         """Clone a git repository with depth 1."""
         subprocess.run(
-            ["git", "clone", "--depth", "1", "--branch", branch, url, target_dir],
+            ["git", "clone", "--depth", "1", f"--branch={branch}", "--", url, target_dir],
             check=True,
             capture_output=True,
             text=True,
         )

     `@staticmethod`
     def _update_repository(directory: str, branch: str) -> None:
         """Pull latest changes from a git repository."""
         subprocess.run(
             [
                 "git",
                 "fetch",
                 "--depth",
                 "1",
                 "origin",
+                "--",
                 f"{branch}:refs/remotes/origin/{branch}",
             ],
             cwd=directory,
             check=True,
             capture_output=True,
             text=True,
         )

         subprocess.run(
-            ["git", "checkout", "-B", branch, f"origin/{branch}"],
+            ["git", "checkout", "-B", branch, "--", f"origin/{branch}"],
             cwd=directory,
             capture_output=True,
             check=True,
             text=True,
         )
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/docs_buddy/adapters/__init__.py` around lines 139 - 169, The git
operations in `_clone_repository` and `_update_repository` should be hardened so
branch names starting with a dash are not parsed as options. Update the
`subprocess.run` calls to use safe argument forms: pass the branch to `git
clone` with `--branch=...` and ensure `git checkout` separates options from the
branch/ref with `--` where applicable. Keep the behavior of `_clone_repository`
and `_update_repository` unchanged otherwise, but make the branch handling
unambiguous in these helper methods.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/docs_buddy/adapters/__init__.py`:
- Around line 139-169: Add text=True to the subprocess.run calls in
_clone_repository and _update_repository so captured stderr/stdout are decoded
as strings instead of bytes. This will ensure pull_repo and clone_repo surface
readable git error messages when they interpolate exc.stderr, and you should
keep the existing capture_output/check behavior unchanged while updating all git
subprocess invocations in this adapter.

In `@src/docs_buddy/common.py`:
- Around line 51-56: The sanitize_to_python_id function can crash on empty input
because it indexes sanitized[0] without checking for an empty string. Update
sanitize_to_python_id to handle empty identifiers (including the empty repo id
path from _derive_repo_id) before the digit check, and return a safe fallback
identifier when the input is blank so callers in __main__.py do not hit
IndexError.

In `@src/docs_buddy/entrypoints/cli/__main__.py`:
- Around line 175-178: The --query flow is reading optional config fields
directly, which can raise AttributeError when they are missing. In the CLI
entrypoint around the WhooshDocumentIndex setup, switch the accesses for
formatted_file_prefix and repo_content_description to getattr with a None
fallback, matching the optional handling already used elsewhere in the flow.
Keep the behavior aligned with WhooshDocumentIndex.__init__ and the existing
update-sources error handling so missing config entries do not crash the
command.

---

Nitpick comments:
In `@src/docs_buddy/adapters/__init__.py`:
- Around line 139-169: The git operations in `_clone_repository` and
`_update_repository` should be hardened so branch names starting with a dash are
not parsed as options. Update the `subprocess.run` calls to use safe argument
forms: pass the branch to `git clone` with `--branch=...` and ensure `git
checkout` separates options from the branch/ref with `--` where applicable. Keep
the behavior of `_clone_repository` and `_update_repository` unchanged
otherwise, but make the branch handling unambiguous in these helper methods.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: b8a5f3b9-8f3b-4851-82bb-25f1054fae80

📥 Commits

Reviewing files that changed from the base of the PR and between 5f8dc54 and d127527.

📒 Files selected for processing (21)
  • .gitignore
  • src/docs_buddy/_vendor/__init__.py
  • src/docs_buddy/_vendor/dotconfig.py
  • src/docs_buddy/adapters/__init__.py
  • src/docs_buddy/adapters/agent.py
  • src/docs_buddy/adapters/whoosh_index.py
  • src/docs_buddy/common.py
  • src/docs_buddy/entrypoints/bootstrap.py
  • src/docs_buddy/entrypoints/cli/README.md
  • src/docs_buddy/entrypoints/cli/__main__.py
  • src/docs_buddy/entrypoints/cli/data/docs_buddy_template.yaml
  • src/docs_buddy/entrypoints/cli/requirements.txt
  • src/docs_buddy/services/commands.py
  • src/docs_buddy/services/events.py
  • src/docs_buddy/services/handlers.py
  • src/docs_buddy/services/use_cases.py
  • tests/integration/test_adapters.py
  • tests/unit/test_common.py
  • tests/unit/test_services.py
  • tests/unit/test_tools.py
  • todo.md

Comment on lines +139 to +169
def _clone_repository(url: str, target_dir: str, branch: str) -> None:
"""Clone a git repository with depth 1."""
subprocess.run(
["git", "clone", "--depth", "1", url, target_dir],
["git", "clone", "--depth", "1", "--branch", branch, url, target_dir],
check=True,
capture_output=True,
)

@staticmethod
def _update_repository(directory: str) -> None:
def _update_repository(directory: str, branch: str) -> None:
"""Pull latest changes from a git repository."""
subprocess.run(["git", "pull"], cwd=directory, check=True, capture_output=True)
subprocess.run(
[
"git",
"fetch",
"--depth",
"1",
"origin",
f"{branch}:refs/remotes/origin/{branch}",
],
cwd=directory,
check=True,
capture_output=True,
)

subprocess.run(
["git", "checkout", "-B", branch, f"origin/{branch}"],
cwd=directory,
capture_output=True,
check=True,
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Add text=True to subprocess.run calls for clean error messages.

Without text=True, capture_output=True captures stderr as bytes. The error messages in pull_repo/clone_repo interpolate exc.stderr, so users will see b'fatal: ...' instead of fatal: ....

🔧 Proposed fix
     `@staticmethod`
     def _clone_repository(url: str, target_dir: str, branch: str) -> None:
         """Clone a git repository with depth 1."""
         subprocess.run(
             ["git", "clone", "--depth", "1", "--branch", branch, url, target_dir],
             check=True,
             capture_output=True,
+            text=True,
         )

     `@staticmethod`
     def _update_repository(directory: str, branch: str) -> None:
         """Pull latest changes from a git repository."""
         subprocess.run(
             [
                 "git",
                 "fetch",
                 "--depth",
                 "1",
                 "origin",
                 f"{branch}:refs/remotes/origin/{branch}",
             ],
             cwd=directory,
             check=True,
             capture_output=True,
+            text=True,
         )

         subprocess.run(
             ["git", "checkout", "-B", branch, f"origin/{branch}"],
             cwd=directory,
             capture_output=True,
             check=True,
+            text=True,
         )
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
def _clone_repository(url: str, target_dir: str, branch: str) -> None:
"""Clone a git repository with depth 1."""
subprocess.run(
["git", "clone", "--depth", "1", url, target_dir],
["git", "clone", "--depth", "1", "--branch", branch, url, target_dir],
check=True,
capture_output=True,
)
@staticmethod
def _update_repository(directory: str) -> None:
def _update_repository(directory: str, branch: str) -> None:
"""Pull latest changes from a git repository."""
subprocess.run(["git", "pull"], cwd=directory, check=True, capture_output=True)
subprocess.run(
[
"git",
"fetch",
"--depth",
"1",
"origin",
f"{branch}:refs/remotes/origin/{branch}",
],
cwd=directory,
check=True,
capture_output=True,
)
subprocess.run(
["git", "checkout", "-B", branch, f"origin/{branch}"],
cwd=directory,
capture_output=True,
check=True,
)
def _clone_repository(url: str, target_dir: str, branch: str) -> None:
"""Clone a git repository with depth 1."""
subprocess.run(
["git", "clone", "--depth", "1", "--branch", branch, url, target_dir],
check=True,
capture_output=True,
text=True,
)
`@staticmethod`
def _update_repository(directory: str, branch: str) -> None:
"""Pull latest changes from a git repository."""
subprocess.run(
[
"git",
"fetch",
"--depth",
"1",
"origin",
f"{branch}:refs/remotes/origin/{branch}",
],
cwd=directory,
check=True,
capture_output=True,
text=True,
)
subprocess.run(
["git", "checkout", "-B", branch, f"origin/{branch}"],
cwd=directory,
capture_output=True,
check=True,
text=True,
)
🧰 Tools
🪛 ast-grep (0.44.1)

[error] 140-144: Command coming from incoming request
Context: subprocess.run(
["git", "clone", "--depth", "1", "--branch", branch, url, target_dir],
check=True,
capture_output=True,
)
Note: [CWE-78] Improper Neutralization of Special Elements used in an OS Command ('OS Command Injection').

(subprocess-from-request)


[error] 149-161: Command coming from incoming request
Context: subprocess.run(
[
"git",
"fetch",
"--depth",
"1",
"origin",
f"{branch}:refs/remotes/origin/{branch}",
],
cwd=directory,
check=True,
capture_output=True,
)
Note: [CWE-78] Improper Neutralization of Special Elements used in an OS Command ('OS Command Injection').

(subprocess-from-request)


[error] 163-168: Command coming from incoming request
Context: subprocess.run(
["git", "checkout", "-B", branch, f"origin/{branch}"],
cwd=directory,
capture_output=True,
check=True,
)
Note: [CWE-78] Improper Neutralization of Special Elements used in an OS Command ('OS Command Injection').

(subprocess-from-request)

🪛 Ruff (0.15.20)

[error] 141-141: subprocess call: check for execution of untrusted input

(S603)


[error] 142-142: Starting a process with a partial executable path

(S607)


[error] 150-150: subprocess call: check for execution of untrusted input

(S603)


[error] 151-158: Starting a process with a partial executable path

(S607)


[error] 164-164: subprocess call: check for execution of untrusted input

(S603)


[error] 165-165: Starting a process with a partial executable path

(S607)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/docs_buddy/adapters/__init__.py` around lines 139 - 169, Add text=True to
the subprocess.run calls in _clone_repository and _update_repository so captured
stderr/stdout are decoded as strings instead of bytes. This will ensure
pull_repo and clone_repo surface readable git error messages when they
interpolate exc.stderr, and you should keep the existing capture_output/check
behavior unchanged while updating all git subprocess invocations in this
adapter.

Comment thread src/docs_buddy/common.py
Comment on lines +51 to +56
def sanitize_to_python_id(identifier: str) -> str:
"""Converts string to valid Python identifier"""
sanitized = re.sub(r"[^a-zA-Z0-9_]", "_", identifier)
if sanitized[0].isdigit():
sanitized = "name_" + sanitized
return sanitized

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Guard against empty string input in sanitize_to_python_id.

sanitized[0] raises IndexError when identifier is empty. If a config entry has an empty repo_url, _derive_repo_id returns "", which then crashes here when called from __main__.py:179.

🛡️ Proposed fix
 def sanitize_to_python_id(identifier: str) -> str:
     """Converts string to valid Python identifier"""
     sanitized = re.sub(r"[^a-zA-Z0-9_]", "_", identifier)
-    if sanitized[0].isdigit():
+    if sanitized and sanitized[0].isdigit():
         sanitized = "name_" + sanitized
     return sanitized
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
def sanitize_to_python_id(identifier: str) -> str:
"""Converts string to valid Python identifier"""
sanitized = re.sub(r"[^a-zA-Z0-9_]", "_", identifier)
if sanitized[0].isdigit():
sanitized = "name_" + sanitized
return sanitized
def sanitize_to_python_id(identifier: str) -> str:
"""Converts string to valid Python identifier"""
sanitized = re.sub(r"[^a-zA-Z0-9_]", "_", identifier)
if sanitized and sanitized[0].isdigit():
sanitized = "name_" + sanitized
return sanitized
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/docs_buddy/common.py` around lines 51 - 56, The sanitize_to_python_id
function can crash on empty input because it indexes sanitized[0] without
checking for an empty string. Update sanitize_to_python_id to handle empty
identifiers (including the empty repo id path from _derive_repo_id) before the
digit check, and return a safe fallback identifier when the input is blank so
callers in __main__.py do not hit IndexError.

Comment on lines +175 to +178
file_prefix = repo_config.formatted_file_prefix
document_index = adapters.WhooshDocumentIndex(index_path, file_prefix)

content_description = repo_config.repo_content_description

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Use getattr fallback for optional config fields in --query flow.

formatted_file_prefix and repo_content_description are accessed without a fallback, but WhooshDocumentIndex.__init__ explicitly accepts None for file_prefix. This is inconsistent with the --update-sources flow (lines 142–145) which wraps file_extensions in a try/except. A config entry missing these fields will crash with an unhandled AttributeError.

🛡️ Proposed fix
-            file_prefix = repo_config.formatted_file_prefix
+            file_prefix = getattr(repo_config, "formatted_file_prefix", None)
             document_index = adapters.WhooshDocumentIndex(index_path, file_prefix)
 
-            content_description = repo_config.repo_content_description
+            content_description = getattr(repo_config, "repo_content_description", "")
             tool_id = common.sanitize_to_python_id(repo_id)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
file_prefix = repo_config.formatted_file_prefix
document_index = adapters.WhooshDocumentIndex(index_path, file_prefix)
content_description = repo_config.repo_content_description
file_prefix = getattr(repo_config, "formatted_file_prefix", None)
document_index = adapters.WhooshDocumentIndex(index_path, file_prefix)
content_description = getattr(repo_config, "repo_content_description", "")
tool_id = common.sanitize_to_python_id(repo_id)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/docs_buddy/entrypoints/cli/__main__.py` around lines 175 - 178, The
--query flow is reading optional config fields directly, which can raise
AttributeError when they are missing. In the CLI entrypoint around the
WhooshDocumentIndex setup, switch the accesses for formatted_file_prefix and
repo_content_description to getattr with a None fallback, matching the optional
handling already used elsewhere in the flow. Keep the behavior aligned with
WhooshDocumentIndex.__init__ and the existing update-sources error handling so
missing config entries do not crash the command.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant