agent-coderag 1.4.0__tar.gz → 1.4.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (109) hide show
  1. {agent_coderag-1.4.0/agent_coderag.egg-info → agent_coderag-1.4.1}/PKG-INFO +3 -3
  2. {agent_coderag-1.4.0 → agent_coderag-1.4.1}/README.md +2 -2
  3. {agent_coderag-1.4.0 → agent_coderag-1.4.1/agent_coderag.egg-info}/PKG-INFO +3 -3
  4. {agent_coderag-1.4.0 → agent_coderag-1.4.1}/agent_coderag.egg-info/SOURCES.txt +2 -0
  5. agent_coderag-1.4.1/agent_coderag.egg-info/entry_points.txt +2 -0
  6. agent_coderag-1.4.1/code_rag/__init__.py +69 -0
  7. {agent_coderag-1.4.0 → agent_coderag-1.4.1}/code_rag/core/error_codes.py +1 -0
  8. agent_coderag-1.4.1/code_rag/entry/args.py +113 -0
  9. {agent_coderag-1.4.0 → agent_coderag-1.4.1}/code_rag/entry/cli.py +25 -125
  10. {agent_coderag-1.4.0 → agent_coderag-1.4.1}/code_rag/intelligence/distiller.py +2 -1
  11. {agent_coderag-1.4.0 → agent_coderag-1.4.1}/code_rag/intelligence/openai_embedder.py +3 -1
  12. {agent_coderag-1.4.0 → agent_coderag-1.4.1}/code_rag/storage/duckdb_impl.py +14 -0
  13. {agent_coderag-1.4.0 → agent_coderag-1.4.1}/pyproject.toml +2 -2
  14. {agent_coderag-1.4.0 → agent_coderag-1.4.1}/tests/test_cli_detailed.py +40 -2
  15. {agent_coderag-1.4.0 → agent_coderag-1.4.1}/tests/test_cli_embeddings.py +2 -1
  16. agent_coderag-1.4.1/tests/test_cli_startup.py +123 -0
  17. {agent_coderag-1.4.0 → agent_coderag-1.4.1}/tests/test_distiller.py +3 -3
  18. {agent_coderag-1.4.0 → agent_coderag-1.4.1}/tests/test_error_codes.py +4 -0
  19. {agent_coderag-1.4.0 → agent_coderag-1.4.1}/tests/test_indexing_embeddings.py +1 -1
  20. {agent_coderag-1.4.0 → agent_coderag-1.4.1}/tests/test_openai_embedder.py +12 -12
  21. {agent_coderag-1.4.0 → agent_coderag-1.4.1}/tests/test_readme_lifetime_storage.py +1 -0
  22. {agent_coderag-1.4.0 → agent_coderag-1.4.1}/tests/test_storage_embeddings.py +71 -0
  23. agent_coderag-1.4.0/agent_coderag.egg-info/entry_points.txt +0 -2
  24. agent_coderag-1.4.0/code_rag/__init__.py +0 -21
  25. {agent_coderag-1.4.0 → agent_coderag-1.4.1}/LICENSE +0 -0
  26. {agent_coderag-1.4.0 → agent_coderag-1.4.1}/NOTICE +0 -0
  27. {agent_coderag-1.4.0 → agent_coderag-1.4.1}/agent_coderag.egg-info/dependency_links.txt +0 -0
  28. {agent_coderag-1.4.0 → agent_coderag-1.4.1}/agent_coderag.egg-info/requires.txt +0 -0
  29. {agent_coderag-1.4.0 → agent_coderag-1.4.1}/agent_coderag.egg-info/top_level.txt +0 -0
  30. {agent_coderag-1.4.0 → agent_coderag-1.4.1}/code_rag/api/__init__.py +0 -0
  31. {agent_coderag-1.4.0 → agent_coderag-1.4.1}/code_rag/api/client.py +0 -0
  32. {agent_coderag-1.4.0 → agent_coderag-1.4.1}/code_rag/api/models.py +0 -0
  33. {agent_coderag-1.4.0 → agent_coderag-1.4.1}/code_rag/core/__init__.py +0 -0
  34. {agent_coderag-1.4.0 → agent_coderag-1.4.1}/code_rag/core/constants.py +0 -0
  35. {agent_coderag-1.4.0 → agent_coderag-1.4.1}/code_rag/core/exceptions.py +0 -0
  36. {agent_coderag-1.4.0 → agent_coderag-1.4.1}/code_rag/core/interfaces.py +0 -0
  37. {agent_coderag-1.4.0 → agent_coderag-1.4.1}/code_rag/core/models.py +0 -0
  38. {agent_coderag-1.4.0 → agent_coderag-1.4.1}/code_rag/core/utils.py +0 -0
  39. {agent_coderag-1.4.0 → agent_coderag-1.4.1}/code_rag/discovery/__init__.py +0 -0
  40. {agent_coderag-1.4.0 → agent_coderag-1.4.1}/code_rag/discovery/dependency.py +0 -0
  41. {agent_coderag-1.4.0 → agent_coderag-1.4.1}/code_rag/discovery/java_discovery.py +0 -0
  42. {agent_coderag-1.4.0 → agent_coderag-1.4.1}/code_rag/discovery/manager.py +0 -0
  43. {agent_coderag-1.4.0 → agent_coderag-1.4.1}/code_rag/discovery/providers/__init__.py +0 -0
  44. {agent_coderag-1.4.0 → agent_coderag-1.4.1}/code_rag/discovery/providers/base.py +0 -0
  45. {agent_coderag-1.4.0 → agent_coderag-1.4.1}/code_rag/discovery/providers/csharp.py +0 -0
  46. {agent_coderag-1.4.0 → agent_coderag-1.4.1}/code_rag/discovery/providers/go.py +0 -0
  47. {agent_coderag-1.4.0 → agent_coderag-1.4.1}/code_rag/discovery/providers/java.py +0 -0
  48. {agent_coderag-1.4.0 → agent_coderag-1.4.1}/code_rag/discovery/providers/javascript.py +0 -0
  49. {agent_coderag-1.4.0 → agent_coderag-1.4.1}/code_rag/discovery/providers/python.py +0 -0
  50. {agent_coderag-1.4.0 → agent_coderag-1.4.1}/code_rag/discovery/providers/rust.py +0 -0
  51. {agent_coderag-1.4.0 → agent_coderag-1.4.1}/code_rag/entry/__init__.py +0 -0
  52. {agent_coderag-1.4.0 → agent_coderag-1.4.1}/code_rag/intelligence/__init__.py +0 -0
  53. {agent_coderag-1.4.0 → agent_coderag-1.4.1}/code_rag/intelligence/embedder.py +0 -0
  54. {agent_coderag-1.4.0 → agent_coderag-1.4.1}/code_rag/intelligence/factory.py +0 -0
  55. {agent_coderag-1.4.0 → agent_coderag-1.4.1}/code_rag/parsers/__init__.py +0 -0
  56. {agent_coderag-1.4.0 → agent_coderag-1.4.1}/code_rag/parsers/languages.py +0 -0
  57. {agent_coderag-1.4.0 → agent_coderag-1.4.1}/code_rag/parsers/multi_parser.py +0 -0
  58. {agent_coderag-1.4.0 → agent_coderag-1.4.1}/code_rag/parsers/tree_sitter.py +0 -0
  59. {agent_coderag-1.4.0 → agent_coderag-1.4.1}/code_rag/paths.py +0 -0
  60. {agent_coderag-1.4.0 → agent_coderag-1.4.1}/code_rag/services/__init__.py +0 -0
  61. {agent_coderag-1.4.0 → agent_coderag-1.4.1}/code_rag/services/config.py +0 -0
  62. {agent_coderag-1.4.0 → agent_coderag-1.4.1}/code_rag/services/dependencies.py +0 -0
  63. {agent_coderag-1.4.0 → agent_coderag-1.4.1}/code_rag/services/discovery_api.py +0 -0
  64. {agent_coderag-1.4.0 → agent_coderag-1.4.1}/code_rag/services/factory.py +0 -0
  65. {agent_coderag-1.4.0 → agent_coderag-1.4.1}/code_rag/services/indexing.py +0 -0
  66. {agent_coderag-1.4.0 → agent_coderag-1.4.1}/code_rag/services/search.py +0 -0
  67. {agent_coderag-1.4.0 → agent_coderag-1.4.1}/code_rag/services/setup.py +0 -0
  68. {agent_coderag-1.4.0 → agent_coderag-1.4.1}/code_rag/services/sync.py +0 -0
  69. {agent_coderag-1.4.0 → agent_coderag-1.4.1}/code_rag/storage/__init__.py +0 -0
  70. {agent_coderag-1.4.0 → agent_coderag-1.4.1}/code_rag/storage/db_connection.py +0 -0
  71. {agent_coderag-1.4.0 → agent_coderag-1.4.1}/setup.cfg +0 -0
  72. {agent_coderag-1.4.0 → agent_coderag-1.4.1}/tests/test_api_client.py +0 -0
  73. {agent_coderag-1.4.0 → agent_coderag-1.4.1}/tests/test_api_lazy_db.py +0 -0
  74. {agent_coderag-1.4.0 → agent_coderag-1.4.1}/tests/test_api_models.py +0 -0
  75. {agent_coderag-1.4.0 → agent_coderag-1.4.1}/tests/test_api_rebuild_wipe.py +0 -0
  76. {agent_coderag-1.4.0 → agent_coderag-1.4.1}/tests/test_cli.py +0 -0
  77. {agent_coderag-1.4.0 → agent_coderag-1.4.1}/tests/test_cli_json_parity.py +0 -0
  78. {agent_coderag-1.4.0 → agent_coderag-1.4.1}/tests/test_code_rag_simple.py +0 -0
  79. {agent_coderag-1.4.0 → agent_coderag-1.4.1}/tests/test_coderag_lifetime.py +0 -0
  80. {agent_coderag-1.4.0 → agent_coderag-1.4.1}/tests/test_csharp_discovery_detailed.py +0 -0
  81. {agent_coderag-1.4.0 → agent_coderag-1.4.1}/tests/test_db_connection.py +0 -0
  82. {agent_coderag-1.4.0 → agent_coderag-1.4.1}/tests/test_db_path.py +0 -0
  83. {agent_coderag-1.4.0 → agent_coderag-1.4.1}/tests/test_dependency_discovery.py +0 -0
  84. {agent_coderag-1.4.0 → agent_coderag-1.4.1}/tests/test_discovery_manager_detailed.py +0 -0
  85. {agent_coderag-1.4.0 → agent_coderag-1.4.1}/tests/test_discovery_providers_extra.py +0 -0
  86. {agent_coderag-1.4.0 → agent_coderag-1.4.1}/tests/test_discovery_python.py +0 -0
  87. {agent_coderag-1.4.0 → agent_coderag-1.4.1}/tests/test_distiller_config_embedding.py +0 -0
  88. {agent_coderag-1.4.0 → agent_coderag-1.4.1}/tests/test_distiller_extra.py +0 -0
  89. {agent_coderag-1.4.0 → agent_coderag-1.4.1}/tests/test_embedder.py +0 -0
  90. {agent_coderag-1.4.0 → agent_coderag-1.4.1}/tests/test_embedder_detailed.py +0 -0
  91. {agent_coderag-1.4.0 → agent_coderag-1.4.1}/tests/test_embedder_factory.py +0 -0
  92. {agent_coderag-1.4.0 → agent_coderag-1.4.1}/tests/test_embedder_interface.py +0 -0
  93. {agent_coderag-1.4.0 → agent_coderag-1.4.1}/tests/test_embedder_stubs.py +0 -0
  94. {agent_coderag-1.4.0 → agent_coderag-1.4.1}/tests/test_factory_async.py +0 -0
  95. {agent_coderag-1.4.0 → agent_coderag-1.4.1}/tests/test_indexing_detailed.py +0 -0
  96. {agent_coderag-1.4.0 → agent_coderag-1.4.1}/tests/test_indexing_extra.py +0 -0
  97. {agent_coderag-1.4.0 → agent_coderag-1.4.1}/tests/test_interfaces.py +0 -0
  98. {agent_coderag-1.4.0 → agent_coderag-1.4.1}/tests/test_js_discovery_detailed.py +0 -0
  99. {agent_coderag-1.4.0 → agent_coderag-1.4.1}/tests/test_languages.py +0 -0
  100. {agent_coderag-1.4.0 → agent_coderag-1.4.1}/tests/test_local_onnx_embedder.py +0 -0
  101. {agent_coderag-1.4.0 → agent_coderag-1.4.1}/tests/test_models.py +0 -0
  102. {agent_coderag-1.4.0 → agent_coderag-1.4.1}/tests/test_multi_parser.py +0 -0
  103. {agent_coderag-1.4.0 → agent_coderag-1.4.1}/tests/test_readme_library_usage.py +0 -0
  104. {agent_coderag-1.4.0 → agent_coderag-1.4.1}/tests/test_readme_offline_embeddings.py +0 -0
  105. {agent_coderag-1.4.0 → agent_coderag-1.4.1}/tests/test_rust_discovery_detailed.py +0 -0
  106. {agent_coderag-1.4.0 → agent_coderag-1.4.1}/tests/test_storage_detailed.py +0 -0
  107. {agent_coderag-1.4.0 → agent_coderag-1.4.1}/tests/test_sync_worker_errors.py +0 -0
  108. {agent_coderag-1.4.0 → agent_coderag-1.4.1}/tests/test_tree_sitter_parser.py +0 -0
  109. {agent_coderag-1.4.0 → agent_coderag-1.4.1}/tests/test_utils_detailed.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: agent-coderag
3
- Version: 1.4.0
3
+ Version: 1.4.1
4
4
  Summary: Lightweight semantic code search and distillation utility for AI coding agents. It solves the API knowledge gap via real-time local signature extraction and intent analysis without PyTorch. Optimized for token efficiency, it compresses codebase context into compact semantic summaries stored in a local DuckDB vector similarity index.
5
5
  Author-email: Igor Boloban <naranor@gmail.com>
6
6
  License: MIT
@@ -189,10 +189,10 @@ async def main():
189
189
  - **Explicit path:** `CodeRAG(db=...)` / `agent-coderag --db ...`. Path is a **file**, not a directory. Relative paths resolve against **process cwd**, not `root`.
190
190
  - **Sidecars:** DuckDB may write WAL sidecars (e.g. `.coderag.db.wal`) beside the index during writes; locks should not persist after an operation finishes.
191
191
  - **Connect timeout:** `connect_timeout_seconds=5` (CLI `--connect-timeout`) waits on file locks, then raises `StorageBusyError` (`ErrorCode.STORAGE_BUSY`). Pass `0` for a single attempt.
192
- - **Read-only search:** `search` (and `api` when storage is needed) opens read-only. A missing index file is an error — use `sync`/`rebuild` to create it. With `--json`, success is a hit array; errors are `{"status":"error","message":...}` (same shape as `sync`/`api` failures).
192
+ - **Read-only search:** `search` (and `api` when storage is needed) opens read-only. A missing **index file** is an error — use `sync`/`rebuild` to create it. An index file that exists but has no embeddings table (e.g. opened/written without a completed vector sync) raises `StorageError` with `ErrorCode.EMBEDDINGS_MISSING` — run `sync` (library: `CodeRAG.sync`) before search. With `--json`, success is a hit array; errors are `{"status":"error","message":...}` and include `"code"` when the exception carries an `ErrorCode`.
193
193
  - **Lifetime:** embedder/parser/distiller stay warm; DuckDB opens per operation and closes afterward. One `CodeRAG` instance serializes overlapping ops. `config()` with embedding flags / `--clear-embedding` closes the process embedder so the next op rebuilds it; distill-only `config` refreshes Distiller and keeps the embedder.
194
194
  - **`api()` without DB:** providers that do not need the index (e.g. Python) skip DuckDB entirely; Java uses a short read-only open for JAR cache lookup.
195
- - **Errors:** catch `CodeRAGError` and inspect `.code` — `STORAGE_BUSY`, `STORAGE_CORRUPT`, `EMBEDDING_MISMATCH` (`from code_rag import ErrorCode`).
195
+ - **Errors:** catch `CodeRAGError` and inspect `.code` — `STORAGE_BUSY`, `STORAGE_CORRUPT`, `EMBEDDING_MISMATCH`, `EMBEDDINGS_MISSING` (`from code_rag import ErrorCode`).
196
196
 
197
197
  ---
198
198
 
@@ -143,10 +143,10 @@ async def main():
143
143
  - **Explicit path:** `CodeRAG(db=...)` / `agent-coderag --db ...`. Path is a **file**, not a directory. Relative paths resolve against **process cwd**, not `root`.
144
144
  - **Sidecars:** DuckDB may write WAL sidecars (e.g. `.coderag.db.wal`) beside the index during writes; locks should not persist after an operation finishes.
145
145
  - **Connect timeout:** `connect_timeout_seconds=5` (CLI `--connect-timeout`) waits on file locks, then raises `StorageBusyError` (`ErrorCode.STORAGE_BUSY`). Pass `0` for a single attempt.
146
- - **Read-only search:** `search` (and `api` when storage is needed) opens read-only. A missing index file is an error — use `sync`/`rebuild` to create it. With `--json`, success is a hit array; errors are `{"status":"error","message":...}` (same shape as `sync`/`api` failures).
146
+ - **Read-only search:** `search` (and `api` when storage is needed) opens read-only. A missing **index file** is an error — use `sync`/`rebuild` to create it. An index file that exists but has no embeddings table (e.g. opened/written without a completed vector sync) raises `StorageError` with `ErrorCode.EMBEDDINGS_MISSING` — run `sync` (library: `CodeRAG.sync`) before search. With `--json`, success is a hit array; errors are `{"status":"error","message":...}` and include `"code"` when the exception carries an `ErrorCode`.
147
147
  - **Lifetime:** embedder/parser/distiller stay warm; DuckDB opens per operation and closes afterward. One `CodeRAG` instance serializes overlapping ops. `config()` with embedding flags / `--clear-embedding` closes the process embedder so the next op rebuilds it; distill-only `config` refreshes Distiller and keeps the embedder.
148
148
  - **`api()` without DB:** providers that do not need the index (e.g. Python) skip DuckDB entirely; Java uses a short read-only open for JAR cache lookup.
149
- - **Errors:** catch `CodeRAGError` and inspect `.code` — `STORAGE_BUSY`, `STORAGE_CORRUPT`, `EMBEDDING_MISMATCH` (`from code_rag import ErrorCode`).
149
+ - **Errors:** catch `CodeRAGError` and inspect `.code` — `STORAGE_BUSY`, `STORAGE_CORRUPT`, `EMBEDDING_MISMATCH`, `EMBEDDINGS_MISSING` (`from code_rag import ErrorCode`).
150
150
 
151
151
  ---
152
152
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: agent-coderag
3
- Version: 1.4.0
3
+ Version: 1.4.1
4
4
  Summary: Lightweight semantic code search and distillation utility for AI coding agents. It solves the API knowledge gap via real-time local signature extraction and intent analysis without PyTorch. Optimized for token efficiency, it compresses codebase context into compact semantic summaries stored in a local DuckDB vector similarity index.
5
5
  Author-email: Igor Boloban <naranor@gmail.com>
6
6
  License: MIT
@@ -189,10 +189,10 @@ async def main():
189
189
  - **Explicit path:** `CodeRAG(db=...)` / `agent-coderag --db ...`. Path is a **file**, not a directory. Relative paths resolve against **process cwd**, not `root`.
190
190
  - **Sidecars:** DuckDB may write WAL sidecars (e.g. `.coderag.db.wal`) beside the index during writes; locks should not persist after an operation finishes.
191
191
  - **Connect timeout:** `connect_timeout_seconds=5` (CLI `--connect-timeout`) waits on file locks, then raises `StorageBusyError` (`ErrorCode.STORAGE_BUSY`). Pass `0` for a single attempt.
192
- - **Read-only search:** `search` (and `api` when storage is needed) opens read-only. A missing index file is an error — use `sync`/`rebuild` to create it. With `--json`, success is a hit array; errors are `{"status":"error","message":...}` (same shape as `sync`/`api` failures).
192
+ - **Read-only search:** `search` (and `api` when storage is needed) opens read-only. A missing **index file** is an error — use `sync`/`rebuild` to create it. An index file that exists but has no embeddings table (e.g. opened/written without a completed vector sync) raises `StorageError` with `ErrorCode.EMBEDDINGS_MISSING` — run `sync` (library: `CodeRAG.sync`) before search. With `--json`, success is a hit array; errors are `{"status":"error","message":...}` and include `"code"` when the exception carries an `ErrorCode`.
193
193
  - **Lifetime:** embedder/parser/distiller stay warm; DuckDB opens per operation and closes afterward. One `CodeRAG` instance serializes overlapping ops. `config()` with embedding flags / `--clear-embedding` closes the process embedder so the next op rebuilds it; distill-only `config` refreshes Distiller and keeps the embedder.
194
194
  - **`api()` without DB:** providers that do not need the index (e.g. Python) skip DuckDB entirely; Java uses a short read-only open for JAR cache lookup.
195
- - **Errors:** catch `CodeRAGError` and inspect `.code` — `STORAGE_BUSY`, `STORAGE_CORRUPT`, `EMBEDDING_MISMATCH` (`from code_rag import ErrorCode`).
195
+ - **Errors:** catch `CodeRAGError` and inspect `.code` — `STORAGE_BUSY`, `STORAGE_CORRUPT`, `EMBEDDING_MISMATCH`, `EMBEDDINGS_MISSING` (`from code_rag import ErrorCode`).
196
196
 
197
197
  ---
198
198
 
@@ -33,6 +33,7 @@ code_rag/discovery/providers/javascript.py
33
33
  code_rag/discovery/providers/python.py
34
34
  code_rag/discovery/providers/rust.py
35
35
  code_rag/entry/__init__.py
36
+ code_rag/entry/args.py
36
37
  code_rag/entry/cli.py
37
38
  code_rag/intelligence/__init__.py
38
39
  code_rag/intelligence/distiller.py
@@ -63,6 +64,7 @@ tests/test_cli.py
63
64
  tests/test_cli_detailed.py
64
65
  tests/test_cli_embeddings.py
65
66
  tests/test_cli_json_parity.py
67
+ tests/test_cli_startup.py
66
68
  tests/test_code_rag_simple.py
67
69
  tests/test_coderag_lifetime.py
68
70
  tests/test_csharp_discovery_detailed.py
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ agent-coderag = code_rag.entry.args:main
@@ -0,0 +1,69 @@
1
+ """Public package surface. Imports stay lazy so ``agent-coderag --help`` does not load DuckDB, ONNX, or LiteLLM."""
2
+
3
+ import importlib
4
+
5
+ # Names are assigned so static checkers accept ``__all__``, then removed so
6
+ # attribute access goes through ``__getattr__`` and stays lazy.
7
+ CodeRAG = None
8
+ SyncResult = None
9
+ ApiReport = None
10
+ SetupResult = None
11
+ KnowledgeUnit = None
12
+ UnitKind = None
13
+ DistillerConfig = None
14
+ CodeRAGError = None
15
+ ErrorCode = None
16
+ StorageBusyError = None
17
+ default_db_path = None
18
+
19
+ __all__ = [
20
+ "CodeRAG",
21
+ "SyncResult",
22
+ "ApiReport",
23
+ "SetupResult",
24
+ "KnowledgeUnit",
25
+ "UnitKind",
26
+ "DistillerConfig",
27
+ "CodeRAGError",
28
+ "ErrorCode",
29
+ "StorageBusyError",
30
+ "default_db_path",
31
+ ]
32
+
33
+ _EXPORTS = {
34
+ "CodeRAG": ("code_rag.api.client", "CodeRAG"),
35
+ "SyncResult": ("code_rag.api.models", "SyncResult"),
36
+ "ApiReport": ("code_rag.api.models", "ApiReport"),
37
+ "SetupResult": ("code_rag.api.models", "SetupResult"),
38
+ "KnowledgeUnit": ("code_rag.core.models", "KnowledgeUnit"),
39
+ "UnitKind": ("code_rag.core.models", "UnitKind"),
40
+ "DistillerConfig": ("code_rag.intelligence.distiller", "DistillerConfig"),
41
+ "CodeRAGError": ("code_rag.core.exceptions", "CodeRAGError"),
42
+ "ErrorCode": ("code_rag.core.error_codes", "ErrorCode"),
43
+ "StorageBusyError": ("code_rag.core.exceptions", "StorageBusyError"),
44
+ "default_db_path": ("code_rag.paths", "default_db_path"),
45
+ }
46
+
47
+ del (
48
+ CodeRAG,
49
+ SyncResult,
50
+ ApiReport,
51
+ SetupResult,
52
+ KnowledgeUnit,
53
+ UnitKind,
54
+ DistillerConfig,
55
+ CodeRAGError,
56
+ ErrorCode,
57
+ StorageBusyError,
58
+ default_db_path,
59
+ )
60
+
61
+
62
+ def __getattr__(name):
63
+ target = _EXPORTS.get(name)
64
+ if target is None:
65
+ raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
66
+ module_name, attr = target
67
+ value = getattr(importlib.import_module(module_name), attr)
68
+ globals()[name] = value
69
+ return value
@@ -5,3 +5,4 @@ class ErrorCode(str, Enum):
5
5
  STORAGE_BUSY = "STORAGE_BUSY"
6
6
  STORAGE_CORRUPT = "STORAGE_CORRUPT"
7
7
  EMBEDDING_MISMATCH = "EMBEDDING_MISMATCH"
8
+ EMBEDDINGS_MISSING = "EMBEDDINGS_MISSING"
@@ -0,0 +1,113 @@
1
+ """Argparse entry. Importing this module must stay free of DuckDB, ONNX, and LiteLLM."""
2
+
3
+ import argparse
4
+ import importlib
5
+ import logging
6
+ import sys
7
+
8
+ from code_rag.core.constants import DEFAULT_CONNECT_TIMEOUT_SECONDS
9
+ from code_rag.core.exceptions import CodeRAGError
10
+
11
+
12
+ def build_parser() -> argparse.ArgumentParser:
13
+ parser = argparse.ArgumentParser(description="CodeRAG: API Knowledge Bridge.")
14
+ parser.add_argument(
15
+ "--db",
16
+ default=None,
17
+ help=(
18
+ "DuckDB index file. Default: resolve legacy code_rag.db in cwd/root, "
19
+ "else use .coderag.db under project root."
20
+ ),
21
+ )
22
+ parser.add_argument("--onnx", help="Path to local ONNX model file.")
23
+ parser.add_argument(
24
+ "--connect-timeout",
25
+ dest="connect_timeout",
26
+ type=float,
27
+ default=DEFAULT_CONNECT_TIMEOUT_SECONDS,
28
+ help=(
29
+ "Seconds to wait for a DuckDB file lock before StorageBusyError "
30
+ f"(default {DEFAULT_CONNECT_TIMEOUT_SECONDS:g}). 0 = no retry."
31
+ ),
32
+ )
33
+ parser.add_argument(
34
+ "--verbose", action="store_true", help="Enable verbose logging."
35
+ )
36
+ parser.add_argument("--json", action="store_true", help="Output results in JSON.")
37
+
38
+ subparsers = parser.add_subparsers(dest="command")
39
+
40
+ sync = subparsers.add_parser("sync", help="Index code units.")
41
+ sync.add_argument("path", nargs="?", help="File or directory to index.")
42
+ sync.add_argument("--all", action="store_true", help="Index all supported files.")
43
+ sync.add_argument("--force", action="store_true", help="Force re-distillation.")
44
+ sync.add_argument(
45
+ "--allow-build-execution",
46
+ action="store_true",
47
+ help="Execute repository Maven/Gradle build files during dependency sync (trusted projects only).",
48
+ )
49
+
50
+ search = subparsers.add_parser("search", help="Semantic search.")
51
+ search.add_argument("query", help="Natural language query.")
52
+ search.add_argument("--limit", type=int, default=5, help="Result limit.")
53
+
54
+ api = subparsers.add_parser("api", help="Discover library API.")
55
+ api.add_argument("library", help="Library name (e.g., pydantic).")
56
+ api.add_argument("--lang", help="Target language.")
57
+
58
+ cfg = subparsers.add_parser("config", help="Manage LLM configuration.")
59
+ cfg.add_argument("--url", help="API base URL.")
60
+ cfg.add_argument("--key", help="API key.")
61
+ cfg.add_argument("--model", help="Model name.")
62
+ cfg.add_argument("--provider", help="Provider name (openai, ollama).")
63
+ cfg.add_argument(
64
+ "--embedding-url", dest="embedding_url", help="Embedding API base URL."
65
+ )
66
+ cfg.add_argument("--embedding-key", dest="embedding_key", help="Embedding API key.")
67
+ cfg.add_argument(
68
+ "--embedding-model", dest="embedding_model", help="Remote embedding model id."
69
+ )
70
+ cfg.add_argument(
71
+ "--embedding-provider",
72
+ dest="embedding_provider",
73
+ help="LiteLLM custom_llm_provider for embeddings.",
74
+ )
75
+ cfg.add_argument(
76
+ "--clear-embedding",
77
+ dest="clear_embedding",
78
+ action="store_true",
79
+ help="Clear remote embedding config (local MiniLM).",
80
+ )
81
+
82
+ setup = subparsers.add_parser("setup", help="Initial setup (download models).")
83
+ setup.add_argument("--force", action="store_true", help="Force model redownload.")
84
+
85
+ subparsers.add_parser("rebuild", help="Full re-index of the project.")
86
+ return parser
87
+
88
+
89
+ def main() -> None:
90
+ parser = build_parser()
91
+ try:
92
+ args = parser.parse_args()
93
+
94
+ if args.verbose:
95
+ logging.basicConfig(level=logging.INFO)
96
+ else:
97
+ logging.basicConfig(level=logging.WARNING)
98
+
99
+ if not args.command:
100
+ parser.print_help()
101
+ return
102
+
103
+ # Heavy stack (DuckDB, ONNX, parsers) loads only for a real subcommand.
104
+ dispatch = importlib.import_module("code_rag.entry.cli").dispatch
105
+ dispatch(args)
106
+ except CodeRAGError as exc:
107
+ print(f"Error: {exc}", file=sys.stderr)
108
+ sys.exit(1)
109
+ except KeyboardInterrupt:
110
+ sys.exit(0)
111
+ except Exception as exc:
112
+ logging.getLogger(__name__).error("Unexpected error: %s", exc)
113
+ sys.exit(1)
@@ -1,6 +1,5 @@
1
1
  import logging
2
2
  import asyncio
3
- import argparse
4
3
  import sys
5
4
  import json
6
5
  from pathlib import Path
@@ -43,7 +42,6 @@ __all__ = [
43
42
  "get_global_dir",
44
43
  "requests",
45
44
  "validate_path",
46
- "main",
47
45
  ]
48
46
 
49
47
 
@@ -59,6 +57,14 @@ def _coderag_from_args(args) -> CodeRAG:
59
57
  )
60
58
 
61
59
 
60
+ def _emit_json_error(exc: Exception) -> None:
61
+ payload = {"status": "error", "message": str(exc)}
62
+ code = getattr(exc, "code", None)
63
+ if isinstance(exc, CodeRAGError) and code is not None:
64
+ payload["code"] = code.value if hasattr(code, "value") else str(code)
65
+ print(json.dumps(payload))
66
+
67
+
62
68
  def load_ignore_patterns() -> pathspec.PathSpec:
63
69
  """Loads ignore patterns from the current directory's .gitignore or defaults."""
64
70
  return sync_service.load_ignore_patterns(Path.cwd())
@@ -105,7 +111,7 @@ async def sync_cmd(args):
105
111
  except Exception as e:
106
112
  logger.error("Sync failed: %s", e)
107
113
  if args.json:
108
- print(json.dumps({"status": "error", "message": str(e)}))
114
+ _emit_json_error(e)
109
115
  else:
110
116
  print(f"Error: {e}", file=sys.stderr)
111
117
  raise SystemExit(1) from e
@@ -146,7 +152,7 @@ async def search_cmd(args):
146
152
  except Exception as e:
147
153
  logger.error("Search failed: %s", e)
148
154
  if args.json:
149
- print(json.dumps({"status": "error", "message": str(e)}))
155
+ _emit_json_error(e)
150
156
  else:
151
157
  print(f"Error: {e}", file=sys.stderr)
152
158
  raise SystemExit(1) from e
@@ -166,7 +172,7 @@ async def api_cmd(args):
166
172
  except Exception as e:
167
173
  logger.error("API discovery failed: %s", e)
168
174
  if args.json:
169
- print(json.dumps({"status": "error", "message": str(e)}))
175
+ _emit_json_error(e)
170
176
  else:
171
177
  print(f"Error: {e}", file=sys.stderr)
172
178
  finally:
@@ -277,7 +283,7 @@ async def rebuild_cmd(args):
277
283
  except Exception as exc:
278
284
  logger.error("Rebuild failed: %s", exc)
279
285
  if args.json:
280
- print(json.dumps({"status": "error", "message": str(exc)}))
286
+ _emit_json_error(exc)
281
287
  else:
282
288
  print(f"Error: {exc}", file=sys.stderr)
283
289
  raise SystemExit(1) from exc
@@ -285,122 +291,16 @@ async def rebuild_cmd(args):
285
291
  await rag.close()
286
292
 
287
293
 
288
- def _build_arg_parser() -> argparse.ArgumentParser:
289
- parser = argparse.ArgumentParser(description="CodeRAG: API Knowledge Bridge.")
290
- parser.add_argument(
291
- "--db",
292
- default=None,
293
- help=(
294
- "DuckDB index file. Default: resolve legacy code_rag.db in cwd/root, "
295
- "else use .coderag.db under project root."
296
- ),
297
- )
298
- parser.add_argument("--onnx", help="Path to local ONNX model file.")
299
- parser.add_argument(
300
- "--connect-timeout",
301
- dest="connect_timeout",
302
- type=float,
303
- default=DEFAULT_CONNECT_TIMEOUT_SECONDS,
304
- help=(
305
- "Seconds to wait for a DuckDB file lock before StorageBusyError "
306
- f"(default {DEFAULT_CONNECT_TIMEOUT_SECONDS:g}). 0 = no retry."
307
- ),
308
- )
309
- parser.add_argument(
310
- "--verbose", action="store_true", help="Enable verbose logging."
311
- )
312
- parser.add_argument("--json", action="store_true", help="Output results in JSON.")
313
-
314
- subparsers = parser.add_subparsers(dest="command")
315
-
316
- # Sync
317
- sync = subparsers.add_parser("sync", help="Index code units.")
318
- sync.add_argument("path", nargs="?", help="File or directory to index.")
319
- sync.add_argument("--all", action="store_true", help="Index all supported files.")
320
- sync.add_argument("--force", action="store_true", help="Force re-distillation.")
321
- sync.add_argument(
322
- "--allow-build-execution",
323
- action="store_true",
324
- help="Execute repository Maven/Gradle build files during dependency sync (trusted projects only).",
325
- )
326
-
327
- # Search
328
- search = subparsers.add_parser("search", help="Semantic search.")
329
- search.add_argument("query", help="Natural language query.")
330
- search.add_argument("--limit", type=int, default=5, help="Result limit.")
331
-
332
- # API
333
- api = subparsers.add_parser("api", help="Discover library API.")
334
- api.add_argument("library", help="Library name (e.g., pydantic).")
335
- api.add_argument("--lang", help="Target language.")
336
-
337
- # Config
338
- cfg = subparsers.add_parser("config", help="Manage LLM configuration.")
339
- cfg.add_argument("--url", help="API base URL.")
340
- cfg.add_argument("--key", help="API key.")
341
- cfg.add_argument("--model", help="Model name.")
342
- cfg.add_argument("--provider", help="Provider name (openai, ollama).")
343
- cfg.add_argument(
344
- "--embedding-url", dest="embedding_url", help="Embedding API base URL."
345
- )
346
- cfg.add_argument("--embedding-key", dest="embedding_key", help="Embedding API key.")
347
- cfg.add_argument(
348
- "--embedding-model", dest="embedding_model", help="Remote embedding model id."
349
- )
350
- cfg.add_argument(
351
- "--embedding-provider",
352
- dest="embedding_provider",
353
- help="LiteLLM custom_llm_provider for embeddings.",
354
- )
355
- cfg.add_argument(
356
- "--clear-embedding",
357
- dest="clear_embedding",
358
- action="store_true",
359
- help="Clear remote embedding config (local MiniLM).",
360
- )
361
-
362
- # Setup
363
- setup = subparsers.add_parser("setup", help="Initial setup (download models).")
364
- setup.add_argument("--force", action="store_true", help="Force model redownload.")
365
-
366
- # Rebuild
367
- subparsers.add_parser("rebuild", help="Full re-index of the project.")
368
- return parser
369
-
370
-
371
- def main():
372
- parser = _build_arg_parser()
373
- try:
374
- args = parser.parse_args()
375
-
376
- if args.verbose:
377
- logging.basicConfig(level=logging.INFO)
378
- else:
379
- logging.basicConfig(level=logging.WARNING)
380
-
381
- if args.command == "sync":
382
- asyncio.run(sync_cmd(args))
383
- elif args.command == "search":
384
- asyncio.run(search_cmd(args))
385
- elif args.command == "api":
386
- asyncio.run(api_cmd(args))
387
- elif args.command == "config":
388
- config_cmd(args)
389
- elif args.command == "setup":
390
- asyncio.run(setup_cmd(args))
391
- elif args.command == "rebuild":
392
- asyncio.run(rebuild_cmd(args))
393
- else:
394
- parser.print_help()
395
- except CodeRAGError as e:
396
- print(f"Error: {e}", file=sys.stderr)
397
- sys.exit(1)
398
- except KeyboardInterrupt:
399
- sys.exit(0)
400
- except Exception as e:
401
- logger.error("Unexpected error: %s", e)
402
- sys.exit(1)
403
-
404
-
405
- if __name__ == "__main__":
406
- main()
294
+ def dispatch(args) -> None:
295
+ if args.command == "sync":
296
+ asyncio.run(sync_cmd(args))
297
+ elif args.command == "search":
298
+ asyncio.run(search_cmd(args))
299
+ elif args.command == "api":
300
+ asyncio.run(api_cmd(args))
301
+ elif args.command == "config":
302
+ config_cmd(args)
303
+ elif args.command == "setup":
304
+ asyncio.run(setup_cmd(args))
305
+ elif args.command == "rebuild":
306
+ asyncio.run(rebuild_cmd(args))
@@ -1,7 +1,7 @@
1
1
  import logging
2
2
  import json
3
+ import importlib
3
4
  from typing import Optional
4
- import litellm
5
5
  from pydantic import BaseModel, ConfigDict, field_validator
6
6
  from ..core.exceptions import IntelligenceError
7
7
  from ..core.interfaces import IIntelligence
@@ -114,6 +114,7 @@ SUMMARY:
114
114
  if self.config.provider == "ollama" and not model_id.startswith("ollama/"):
115
115
  model_id = f"ollama/{model_id}"
116
116
 
117
+ litellm = importlib.import_module("litellm")
117
118
  response = await litellm.acompletion(
118
119
  model=model_id,
119
120
  messages=[{"role": "user", "content": prompt}],
@@ -1,8 +1,8 @@
1
1
  import asyncio
2
+ import importlib
2
3
  import logging
3
4
  from typing import Optional
4
5
 
5
- import litellm
6
6
  import numpy as np
7
7
 
8
8
  from ..core.constants import EMBEDDING_REQUEST_TIMEOUT
@@ -74,6 +74,8 @@ class OpenAICompatEmbedder(IEmbedder):
74
74
  }
75
75
  if self._provider is not None:
76
76
  kwargs["custom_llm_provider"] = self._provider
77
+ litellm = importlib.import_module("litellm")
78
+
77
79
  async with self._embed_lock:
78
80
  try:
79
81
  response = await litellm.aembedding(**kwargs)
@@ -18,6 +18,10 @@ from ..core.exceptions import StorageError, IntelligenceError
18
18
 
19
19
  logger = logging.getLogger(__name__)
20
20
 
21
+ EMBEDDINGS_MISSING_MSG = (
22
+ "Embeddings table is missing; run sync (CodeRAG.sync) before search."
23
+ )
24
+
21
25
 
22
26
  class AccessMode(str, Enum):
23
27
  """Whether a connection may write. Not part of the public package API."""
@@ -315,6 +319,16 @@ class DuckDBStorage(IStorage):
315
319
  self._pending_wipe = False
316
320
 
317
321
  async def _bind_embeddings(self) -> None:
322
+ if self.mode is AccessMode.READ_ONLY:
323
+ exists = await self._run_on_executor(
324
+ lambda: _table_exists(self.conn, "unit_embeddings")
325
+ )
326
+ if not exists:
327
+ raise StorageError(
328
+ EMBEDDINGS_MISSING_MSG,
329
+ code=ErrorCode.EMBEDDINGS_MISSING,
330
+ )
331
+
318
332
  dim = await bind_embedder_dimension(
319
333
  self.conn,
320
334
  self.embedder,
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "agent-coderag"
7
- version = "1.4.0"
7
+ version = "1.4.1"
8
8
  description = "Lightweight semantic code search and distillation utility for AI coding agents. It solves the API knowledge gap via real-time local signature extraction and intent analysis without PyTorch. Optimized for token efficiency, it compresses codebase context into compact semantic summaries stored in a local DuckDB vector similarity index."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -57,7 +57,7 @@ Issues = "https://github.com/naranor/agent-coderag/issues"
57
57
  Changelog = "https://github.com/naranor/agent-coderag/blob/main/CHANGELOG.md"
58
58
 
59
59
  [project.scripts]
60
- agent-coderag = "code_rag.entry.cli:main"
60
+ agent-coderag = "code_rag.entry.args:main"
61
61
 
62
62
  [tool.setuptools]
63
63
  package-data = {"*" = ["*.md"]}
@@ -6,6 +6,7 @@ from io import StringIO
6
6
  from unittest.mock import MagicMock, patch, AsyncMock
7
7
  from pathlib import Path
8
8
 
9
+ from code_rag.entry import args as cli_args
9
10
  from code_rag.entry import cli
10
11
  from code_rag.core.exceptions import CodeRAGError
11
12
  from code_rag.api.models import ApiReport
@@ -175,6 +176,43 @@ class TestCLIDetailed:
175
176
  data = json.loads(sys.stdout.getvalue())
176
177
  assert data["status"] == "error"
177
178
  assert "Database file not found" in data["message"]
179
+ assert "code" not in data
180
+ finally:
181
+ sys.stdout = old_stdout
182
+ instances[0].close.assert_awaited()
183
+
184
+ @pytest.mark.asyncio
185
+ async def test_search_cmd_json_error_includes_code(self):
186
+ from code_rag.core.error_codes import ErrorCode
187
+ from code_rag.core.exceptions import StorageError
188
+
189
+ fake_cls, instances = fake_coderag_class(
190
+ search=AsyncMock(
191
+ side_effect=StorageError(
192
+ "Embeddings table is missing; run sync (CodeRAG.sync) before search.",
193
+ code=ErrorCode.EMBEDDINGS_MISSING,
194
+ )
195
+ )
196
+ )
197
+ with patch("code_rag.entry.cli.CodeRAG", fake_cls):
198
+ args = argparse.Namespace(
199
+ db="x.db",
200
+ onnx=None,
201
+ verbose=False,
202
+ json=True,
203
+ query="auth",
204
+ limit=5,
205
+ )
206
+ old_stdout = sys.stdout
207
+ sys.stdout = StringIO()
208
+ try:
209
+ with pytest.raises(SystemExit) as exc:
210
+ await cli.search_cmd(args)
211
+ assert exc.value.code == 1
212
+ data = json.loads(sys.stdout.getvalue())
213
+ assert data["status"] == "error"
214
+ assert data["code"] == "EMBEDDINGS_MISSING"
215
+ assert "sync" in data["message"]
178
216
  finally:
179
217
  sys.stdout = old_stdout
180
218
  instances[0].close.assert_awaited()
@@ -350,7 +388,7 @@ class TestCLIDetailed:
350
388
  def test_cli_main_exception_handling(self):
351
389
  """Test main entry point handles exceptions gracefully."""
352
390
  with patch(
353
- "code_rag.entry.cli.argparse.ArgumentParser.parse_args"
391
+ "code_rag.entry.args.argparse.ArgumentParser.parse_args"
354
392
  ) as mock_parse, patch("code_rag.entry.cli.asyncio.run"):
355
393
  mock_parse.side_effect = CodeRAGError("Known Error")
356
394
 
@@ -358,7 +396,7 @@ class TestCLIDetailed:
358
396
  sys.stderr = StringIO()
359
397
  try:
360
398
  with pytest.raises(SystemExit) as exc:
361
- cli.main()
399
+ cli_args.main()
362
400
  assert exc.value.code == 1
363
401
  assert "Known Error" in sys.stderr.getvalue()
364
402
  finally:
@@ -7,6 +7,7 @@ from unittest.mock import patch
7
7
  import pytest
8
8
 
9
9
  from code_rag.core.exceptions import IntelligenceError
10
+ from code_rag.entry import args as cli_args
10
11
  from code_rag.entry import cli
11
12
  from code_rag.intelligence.distiller import DistillerConfig
12
13
  from tests.fake_coderag import fake_coderag_class
@@ -206,6 +207,6 @@ def test_argparse_embedding_dests():
206
207
  ],
207
208
  ),
208
209
  ):
209
- cli.main()
210
+ cli_args.main()
210
211
  assert captured["clear"] is True
211
212
  assert captured["eurl"] == "http://e"