java-codebase-rag 0.5.0__tar.gz → 0.5.2__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 (84) hide show
  1. {java_codebase_rag-0.5.0/java_codebase_rag.egg-info → java_codebase_rag-0.5.2}/PKG-INFO +31 -2
  2. {java_codebase_rag-0.5.0 → java_codebase_rag-0.5.2}/README.md +30 -1
  3. {java_codebase_rag-0.5.0 → java_codebase_rag-0.5.2}/build_ast_graph.py +3 -6
  4. {java_codebase_rag-0.5.0 → java_codebase_rag-0.5.2}/java_codebase_rag/cli.py +31 -0
  5. {java_codebase_rag-0.5.0 → java_codebase_rag-0.5.2}/java_codebase_rag/installer.py +418 -9
  6. {java_codebase_rag-0.5.0 → java_codebase_rag-0.5.2/java_codebase_rag.egg-info}/PKG-INFO +31 -2
  7. {java_codebase_rag-0.5.0 → java_codebase_rag-0.5.2}/pyproject.toml +1 -1
  8. {java_codebase_rag-0.5.0 → java_codebase_rag-0.5.2}/tests/test_installer.py +413 -0
  9. {java_codebase_rag-0.5.0 → java_codebase_rag-0.5.2}/LICENSE +0 -0
  10. {java_codebase_rag-0.5.0 → java_codebase_rag-0.5.2}/ast_java.py +0 -0
  11. {java_codebase_rag-0.5.0 → java_codebase_rag-0.5.2}/brownfield_events.py +0 -0
  12. {java_codebase_rag-0.5.0 → java_codebase_rag-0.5.2}/chunk_heuristics.py +0 -0
  13. {java_codebase_rag-0.5.0 → java_codebase_rag-0.5.2}/graph_enrich.py +0 -0
  14. {java_codebase_rag-0.5.0 → java_codebase_rag-0.5.2}/index_common.py +0 -0
  15. {java_codebase_rag-0.5.0 → java_codebase_rag-0.5.2}/java_codebase_rag/__init__.py +0 -0
  16. {java_codebase_rag-0.5.0 → java_codebase_rag-0.5.2}/java_codebase_rag/cli_format.py +0 -0
  17. {java_codebase_rag-0.5.0 → java_codebase_rag-0.5.2}/java_codebase_rag/cli_progress.py +0 -0
  18. {java_codebase_rag-0.5.0 → java_codebase_rag-0.5.2}/java_codebase_rag/config.py +0 -0
  19. {java_codebase_rag-0.5.0 → java_codebase_rag-0.5.2}/java_codebase_rag/install_data/agents/explorer-rag-enhanced.md +0 -0
  20. {java_codebase_rag-0.5.0 → java_codebase_rag-0.5.2}/java_codebase_rag/install_data/skills/explore-codebase/SKILL.md +0 -0
  21. {java_codebase_rag-0.5.0 → java_codebase_rag-0.5.2}/java_codebase_rag/pipeline.py +0 -0
  22. {java_codebase_rag-0.5.0 → java_codebase_rag-0.5.2}/java_codebase_rag.egg-info/SOURCES.txt +0 -0
  23. {java_codebase_rag-0.5.0 → java_codebase_rag-0.5.2}/java_codebase_rag.egg-info/dependency_links.txt +0 -0
  24. {java_codebase_rag-0.5.0 → java_codebase_rag-0.5.2}/java_codebase_rag.egg-info/entry_points.txt +0 -0
  25. {java_codebase_rag-0.5.0 → java_codebase_rag-0.5.2}/java_codebase_rag.egg-info/requires.txt +0 -0
  26. {java_codebase_rag-0.5.0 → java_codebase_rag-0.5.2}/java_codebase_rag.egg-info/top_level.txt +0 -0
  27. {java_codebase_rag-0.5.0 → java_codebase_rag-0.5.2}/java_index_flow_lancedb.py +0 -0
  28. {java_codebase_rag-0.5.0 → java_codebase_rag-0.5.2}/java_index_v1_common.py +0 -0
  29. {java_codebase_rag-0.5.0 → java_codebase_rag-0.5.2}/java_ontology.py +0 -0
  30. {java_codebase_rag-0.5.0 → java_codebase_rag-0.5.2}/kuzu_queries.py +0 -0
  31. {java_codebase_rag-0.5.0 → java_codebase_rag-0.5.2}/mcp_hints.py +0 -0
  32. {java_codebase_rag-0.5.0 → java_codebase_rag-0.5.2}/mcp_v2.py +0 -0
  33. {java_codebase_rag-0.5.0 → java_codebase_rag-0.5.2}/path_filtering.py +0 -0
  34. {java_codebase_rag-0.5.0 → java_codebase_rag-0.5.2}/pr_analysis.py +0 -0
  35. {java_codebase_rag-0.5.0 → java_codebase_rag-0.5.2}/search_lancedb.py +0 -0
  36. {java_codebase_rag-0.5.0 → java_codebase_rag-0.5.2}/server.py +0 -0
  37. {java_codebase_rag-0.5.0 → java_codebase_rag-0.5.2}/setup.cfg +0 -0
  38. {java_codebase_rag-0.5.0 → java_codebase_rag-0.5.2}/tests/test_agent_skills_static.py +0 -0
  39. {java_codebase_rag-0.5.0 → java_codebase_rag-0.5.2}/tests/test_assign_endpoint_client_extraction.py +0 -0
  40. {java_codebase_rag-0.5.0 → java_codebase_rag-0.5.2}/tests/test_ast_graph_build.py +0 -0
  41. {java_codebase_rag-0.5.0 → java_codebase_rag-0.5.2}/tests/test_ast_java_calls.py +0 -0
  42. {java_codebase_rag-0.5.0 → java_codebase_rag-0.5.2}/tests/test_ast_java_capabilities.py +0 -0
  43. {java_codebase_rag-0.5.0 → java_codebase_rag-0.5.2}/tests/test_bank_chat_brownfield_integration.py +0 -0
  44. {java_codebase_rag-0.5.0 → java_codebase_rag-0.5.2}/tests/test_brownfield_clients.py +0 -0
  45. {java_codebase_rag-0.5.0 → java_codebase_rag-0.5.2}/tests/test_brownfield_events.py +0 -0
  46. {java_codebase_rag-0.5.0 → java_codebase_rag-0.5.2}/tests/test_brownfield_overrides.py +0 -0
  47. {java_codebase_rag-0.5.0 → java_codebase_rag-0.5.2}/tests/test_brownfield_routes.py +0 -0
  48. {java_codebase_rag-0.5.0 → java_codebase_rag-0.5.2}/tests/test_call_edge_matching.py +0 -0
  49. {java_codebase_rag-0.5.0 → java_codebase_rag-0.5.2}/tests/test_call_edges_e2e.py +0 -0
  50. {java_codebase_rag-0.5.0 → java_codebase_rag-0.5.2}/tests/test_call_graph_receiver_resolution.py +0 -0
  51. {java_codebase_rag-0.5.0 → java_codebase_rag-0.5.2}/tests/test_call_graph_smoke_roundtrip.py +0 -0
  52. {java_codebase_rag-0.5.0 → java_codebase_rag-0.5.2}/tests/test_call_invariant.py +0 -0
  53. {java_codebase_rag-0.5.0 → java_codebase_rag-0.5.2}/tests/test_cli_progress_stdout_invariant.py +0 -0
  54. {java_codebase_rag-0.5.0 → java_codebase_rag-0.5.2}/tests/test_cli_quiet_parity.py +0 -0
  55. {java_codebase_rag-0.5.0 → java_codebase_rag-0.5.2}/tests/test_client_hint_recovery.py +0 -0
  56. {java_codebase_rag-0.5.0 → java_codebase_rag-0.5.2}/tests/test_client_node_extraction.py +0 -0
  57. {java_codebase_rag-0.5.0 → java_codebase_rag-0.5.2}/tests/test_client_role_rename.py +0 -0
  58. {java_codebase_rag-0.5.0 → java_codebase_rag-0.5.2}/tests/test_config.py +0 -0
  59. {java_codebase_rag-0.5.0 → java_codebase_rag-0.5.2}/tests/test_cross_service_resolution_flag.py +0 -0
  60. {java_codebase_rag-0.5.0 → java_codebase_rag-0.5.2}/tests/test_edge_navigation_doc.py +0 -0
  61. {java_codebase_rag-0.5.0 → java_codebase_rag-0.5.2}/tests/test_feign_not_exposer.py +0 -0
  62. {java_codebase_rag-0.5.0 → java_codebase_rag-0.5.2}/tests/test_graph_enrich.py +0 -0
  63. {java_codebase_rag-0.5.0 → java_codebase_rag-0.5.2}/tests/test_incremental_graph.py +0 -0
  64. {java_codebase_rag-0.5.0 → java_codebase_rag-0.5.2}/tests/test_installer_integration.py +0 -0
  65. {java_codebase_rag-0.5.0 → java_codebase_rag-0.5.2}/tests/test_java_codebase_rag_cli.py +0 -0
  66. {java_codebase_rag-0.5.0 → java_codebase_rag-0.5.2}/tests/test_kuzu_queries.py +0 -0
  67. {java_codebase_rag-0.5.0 → java_codebase_rag-0.5.2}/tests/test_lancedb_e2e.py +0 -0
  68. {java_codebase_rag-0.5.0 → java_codebase_rag-0.5.2}/tests/test_mcp_hints.py +0 -0
  69. {java_codebase_rag-0.5.0 → java_codebase_rag-0.5.2}/tests/test_mcp_server_project_root.py +0 -0
  70. {java_codebase_rag-0.5.0 → java_codebase_rag-0.5.2}/tests/test_mcp_tools.py +0 -0
  71. {java_codebase_rag-0.5.0 → java_codebase_rag-0.5.2}/tests/test_mcp_v2.py +0 -0
  72. {java_codebase_rag-0.5.0 → java_codebase_rag-0.5.2}/tests/test_mcp_v2_compose.py +0 -0
  73. {java_codebase_rag-0.5.0 → java_codebase_rag-0.5.2}/tests/test_meta_chain_core.py +0 -0
  74. {java_codebase_rag-0.5.0 → java_codebase_rag-0.5.2}/tests/test_microservice_scope.py +0 -0
  75. {java_codebase_rag-0.5.0 → java_codebase_rag-0.5.2}/tests/test_outgoing_call_extraction.py +0 -0
  76. {java_codebase_rag-0.5.0 → java_codebase_rag-0.5.2}/tests/test_packaging_metadata.py +0 -0
  77. {java_codebase_rag-0.5.0 → java_codebase_rag-0.5.2}/tests/test_path_filtering.py +0 -0
  78. {java_codebase_rag-0.5.0 → java_codebase_rag-0.5.2}/tests/test_pr_analysis.py +0 -0
  79. {java_codebase_rag-0.5.0 → java_codebase_rag-0.5.2}/tests/test_resolve_routes_messaging_layer_c.py +0 -0
  80. {java_codebase_rag-0.5.0 → java_codebase_rag-0.5.2}/tests/test_route_extraction.py +0 -0
  81. {java_codebase_rag-0.5.0 → java_codebase_rag-0.5.2}/tests/test_schema_consistency.py +0 -0
  82. {java_codebase_rag-0.5.0 → java_codebase_rag-0.5.2}/tests/test_search_lancedb.py +0 -0
  83. {java_codebase_rag-0.5.0 → java_codebase_rag-0.5.2}/tests/test_search_lancedb_capability.py +0 -0
  84. {java_codebase_rag-0.5.0 → java_codebase_rag-0.5.2}/tests/test_string_value_atoms.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: java-codebase-rag
3
- Version: 0.5.0
3
+ Version: 0.5.2
4
4
  Summary: MCP server for semantic + structural search over Java codebases
5
5
  Author: HumanBean17
6
6
  License-Expression: MIT
@@ -84,6 +84,31 @@ pip install java-codebase-rag
84
84
  Python **3.11+** required. After install, `java-codebase-rag --help` should print the CLI groups.
85
85
  The package includes the CocoIndex lifecycle dependency used by `init`, `increment`, `reprocess`, and `erase`.
86
86
 
87
+ ### Interactive setup (recommended)
88
+
89
+ Run `java-codebase-rag install` from your Java project root to launch an interactive setup wizard that:
90
+
91
+ 1. Detects Java source directories (Maven/Gradle modules)
92
+ 2. Configures the embedding model (auto-downloads ~90MB or uses a local path)
93
+ 3. Selects agent hosts (Claude Code, Qwen Code, GigaCode)
94
+ 4. Deploys MCP registration, skill, and agent artifacts
95
+ 5. Generates `.java-codebase-rag.yml` configuration
96
+ 6. Runs `init` to build the index
97
+
98
+ ```bash
99
+ # Interactive mode
100
+ java-codebase-rag install
101
+
102
+ # Non-interactive mode (for CI/automation)
103
+ java-codebase-rag install --non-interactive --agent claude-code
104
+ ```
105
+
106
+ After `pip install --upgrade java-codebase-rag`, run `java-codebase-rag update` to refresh shipped artifacts.
107
+
108
+ ### Manual registration
109
+
110
+ If you prefer manual configuration, see [`docs/JAVA-CODEBASE-RAG-CLI.md`](./docs/JAVA-CODEBASE-RAG-CLI.md) for the full CLI reference.
111
+
87
112
  > **Stability disclaimer.** This package does **not** promise backward compatibility. MCP tool contracts, env vars, Lance/Kuzu schemas, config files, and Python APIs may change without a deprecation period. Track `main` and rebuild indexes when ontology or embedding settings change.
88
113
 
89
114
  ---
@@ -124,7 +149,9 @@ If vector hits come back and graph expansion adds neighbor symbols, the install
124
149
 
125
150
  ## Wire into an MCP host
126
151
 
127
- ### Claude Code
152
+ > **Quick setup:** Run `java-codebase-rag install` from your Java project root. The interactive wizard handles MCP registration, skill deployment, and configuration for Claude Code, Qwen Code, and GigaCode in one step.
153
+
154
+ ### Claude Code (manual)
128
155
 
129
156
  With the package installed, the console script `java-codebase-rag-mcp` is on your `PATH`. Register it project-scoped:
130
157
 
@@ -207,6 +234,8 @@ Run `java-codebase-rag --help` to list grouped subcommands. Operator playbook wi
207
234
 
208
235
  | Group | Subcommand | What it does |
209
236
  |---|---|---|
237
+ | Setup | `install` | Interactive setup wizard: config, MCP registration, skill/agent deployment, indexing. |
238
+ | Setup | `update` | Refresh shipped artifacts (skill, agent, MCP entry) after pip upgrade. |
210
239
  | Lifecycle | `init` | First-time index. Refuses if artifacts already exist. |
211
240
  | Lifecycle | `increment` | CocoIndex catch-up + incremental Kuzu update. `--vectors-only` for Lance only. |
212
241
  | Lifecycle | `reprocess` | Full Lance + Kuzu rebuild. `--vectors-only` / `--graph-only` for a single phase. |
@@ -44,6 +44,31 @@ pip install java-codebase-rag
44
44
  Python **3.11+** required. After install, `java-codebase-rag --help` should print the CLI groups.
45
45
  The package includes the CocoIndex lifecycle dependency used by `init`, `increment`, `reprocess`, and `erase`.
46
46
 
47
+ ### Interactive setup (recommended)
48
+
49
+ Run `java-codebase-rag install` from your Java project root to launch an interactive setup wizard that:
50
+
51
+ 1. Detects Java source directories (Maven/Gradle modules)
52
+ 2. Configures the embedding model (auto-downloads ~90MB or uses a local path)
53
+ 3. Selects agent hosts (Claude Code, Qwen Code, GigaCode)
54
+ 4. Deploys MCP registration, skill, and agent artifacts
55
+ 5. Generates `.java-codebase-rag.yml` configuration
56
+ 6. Runs `init` to build the index
57
+
58
+ ```bash
59
+ # Interactive mode
60
+ java-codebase-rag install
61
+
62
+ # Non-interactive mode (for CI/automation)
63
+ java-codebase-rag install --non-interactive --agent claude-code
64
+ ```
65
+
66
+ After `pip install --upgrade java-codebase-rag`, run `java-codebase-rag update` to refresh shipped artifacts.
67
+
68
+ ### Manual registration
69
+
70
+ If you prefer manual configuration, see [`docs/JAVA-CODEBASE-RAG-CLI.md`](./docs/JAVA-CODEBASE-RAG-CLI.md) for the full CLI reference.
71
+
47
72
  > **Stability disclaimer.** This package does **not** promise backward compatibility. MCP tool contracts, env vars, Lance/Kuzu schemas, config files, and Python APIs may change without a deprecation period. Track `main` and rebuild indexes when ontology or embedding settings change.
48
73
 
49
74
  ---
@@ -84,7 +109,9 @@ If vector hits come back and graph expansion adds neighbor symbols, the install
84
109
 
85
110
  ## Wire into an MCP host
86
111
 
87
- ### Claude Code
112
+ > **Quick setup:** Run `java-codebase-rag install` from your Java project root. The interactive wizard handles MCP registration, skill deployment, and configuration for Claude Code, Qwen Code, and GigaCode in one step.
113
+
114
+ ### Claude Code (manual)
88
115
 
89
116
  With the package installed, the console script `java-codebase-rag-mcp` is on your `PATH`. Register it project-scoped:
90
117
 
@@ -167,6 +194,8 @@ Run `java-codebase-rag --help` to list grouped subcommands. Operator playbook wi
167
194
 
168
195
  | Group | Subcommand | What it does |
169
196
  |---|---|---|
197
+ | Setup | `install` | Interactive setup wizard: config, MCP registration, skill/agent deployment, indexing. |
198
+ | Setup | `update` | Refresh shipped artifacts (skill, agent, MCP entry) after pip upgrade. |
170
199
  | Lifecycle | `init` | First-time index. Refuses if artifacts already exist. |
171
200
  | Lifecycle | `increment` | CocoIndex catch-up + incremental Kuzu update. `--vectors-only` for Lance only. |
172
201
  | Lifecycle | `reprocess` | Full Lance + Kuzu rebuild. `--vectors-only` / `--graph-only` for a single phase. |
@@ -3422,12 +3422,10 @@ def incremental_rebuild(
3422
3422
  pass6_match_edges(tables, verbose=verbose)
3423
3423
  write_kuzu(kuzu_path, tables, source_root=source_root, verbose=verbose)
3424
3424
 
3425
- n_files = _init_hash_tracker(source_root, kuzu_path)
3426
-
3427
3425
  return IncrementalResult(
3428
3426
  mode="full_fallback",
3429
3427
  files_changed=0,
3430
- files_added=n_files,
3428
+ files_added=0,
3431
3429
  files_removed=0,
3432
3430
  dependents_reprocessed=0,
3433
3431
  elapsed_sec=time.time() - t_start,
@@ -3648,12 +3646,10 @@ def _fallback_to_full(source_root: Path, kuzu_path: Path, verbose: bool, t_start
3648
3646
  pass6_match_edges(tables, verbose=verbose)
3649
3647
  write_kuzu(kuzu_path, tables, source_root=source_root, verbose=verbose)
3650
3648
 
3651
- n_files = _init_hash_tracker(source_root, kuzu_path)
3652
-
3653
3649
  return IncrementalResult(
3654
3650
  mode="full_fallback",
3655
3651
  files_changed=0,
3656
- files_added=n_files,
3652
+ files_added=0,
3657
3653
  files_removed=0,
3658
3654
  dependents_reprocessed=0,
3659
3655
  elapsed_sec=time.time() - t_start,
@@ -3785,6 +3781,7 @@ def write_kuzu(
3785
3781
  _verbose_stderr_line(f"[graph] writing · routes/exposes written in {time.time() - t2:.2f}s")
3786
3782
  _write_meta(conn, tables, source_root)
3787
3783
  conn.close()
3784
+ _init_hash_tracker(source_root, db_path)
3788
3785
 
3789
3786
 
3790
3787
  # ---------- CLI ----------
@@ -496,6 +496,15 @@ def _cmd_install(args: argparse.Namespace) -> int:
496
496
  )
497
497
 
498
498
 
499
+ def _cmd_update(args: argparse.Namespace) -> int:
500
+ from java_codebase_rag.installer import run_update
501
+
502
+ return run_update(
503
+ force=bool(args.force),
504
+ dry_run=bool(args.dry_run),
505
+ )
506
+
507
+
499
508
  def _cmd_erase(args: argparse.Namespace) -> int:
500
509
  cfg = _resolved_from_ns(args)
501
510
  _startup_hints(cfg)
@@ -760,6 +769,28 @@ def build_parser() -> argparse.ArgumentParser:
760
769
  _add_verbosity_flags(install)
761
770
  install.set_defaults(handler=_cmd_install)
762
771
 
772
+ update = subparsers.add_parser(
773
+ "update",
774
+ help="Refresh shipped artifacts (skill, agent, MCP entry) after pip upgrade.",
775
+ description=(
776
+ "Post-upgrade refresh: overwrites skill and agent files with the latest "
777
+ "shipped versions and updates the MCP command path. Use --dry-run to "
778
+ "preview changes without writing. Requires a prior `install` run."
779
+ ),
780
+ )
781
+ update.add_argument(
782
+ "--force",
783
+ action="store_true",
784
+ help="Overwrite all artifacts even if content matches.",
785
+ )
786
+ update.add_argument(
787
+ "--dry-run",
788
+ action="store_true",
789
+ help="Print changes without writing files.",
790
+ )
791
+ _add_verbosity_flags(update)
792
+ update.set_defaults(handler=_cmd_update)
793
+
763
794
  increment = subparsers.add_parser(
764
795
  "increment",
765
796
  help="Pick up changes since the last index update.",
@@ -22,6 +22,14 @@ import yaml
22
22
 
23
23
  Scope = Literal["project", "user"]
24
24
 
25
+ # MCP server name constant
26
+ _MCP_SERVER_NAME = "java-codebase-rag"
27
+
28
+ # Exit code constants
29
+ EXIT_SUCCESS = 0
30
+ EXIT_PARTIAL = 1
31
+ EXIT_FATAL = 2
32
+
25
33
 
26
34
  class ArtifactResult(NamedTuple):
27
35
  """Result of deploying a single artifact."""
@@ -111,16 +119,27 @@ def prompt(
111
119
 
112
120
  # Lazy import questionary only when needed (TTY)
113
121
  import questionary
122
+ from prompt_toolkit.styles import Style
123
+
124
+ # Strip default ANSI colors — rely on ●/○ indicators only, no fg/bg highlights
125
+ # noinherit prevents prompt_toolkit from merging in questionary's default fg colors
126
+ no_color_style = Style(
127
+ [
128
+ ("highlighted", "noinherit"),
129
+ ("selected", "noinherit"),
130
+ ("pointer", "noinherit bold"),
131
+ ]
132
+ )
114
133
 
115
134
  try:
116
135
  if prompt_type == "checkbox":
117
- return questionary.checkbox(message, choices=choices).ask()
136
+ return questionary.checkbox(message, choices=choices, style=no_color_style).ask()
118
137
  elif prompt_type == "select":
119
- return questionary.select(message, choices=choices).ask()
138
+ return questionary.select(message, choices=choices, style=no_color_style).ask()
120
139
  elif prompt_type == "text":
121
- return questionary.text(message, default=default).ask()
140
+ return questionary.text(message, default=default, style=no_color_style).ask()
122
141
  elif prompt_type == "confirm":
123
- return questionary.confirm(message).ask()
142
+ return questionary.confirm(message, style=no_color_style).ask()
124
143
  else:
125
144
  raise ValueError(f"Unknown prompt_type: {prompt_type}")
126
145
  except KeyboardInterrupt:
@@ -279,10 +298,15 @@ def select_hosts(*, non_interactive: bool, cli_agents: list[str] | None) -> list
279
298
  print(f"Valid agents: {', '.join(HOSTS.keys())}")
280
299
  raise SystemExit(2)
281
300
 
282
- # Interactive: show checkbox with all hosts pre-selected
301
+ # Interactive: show checkbox with claude-code pre-selected (most common)
302
+ # Changed from all pre-selected to avoid confusion
283
303
  host_names = list(HOSTS.keys())
284
- choices = [{"name": name, "value": name, "checked": True} for name in host_names]
304
+ choices = [
305
+ {"name": name, "value": name, "checked": (name == "claude-code")}
306
+ for name in host_names
307
+ ]
285
308
 
309
+ print("Note: You can select multiple agent hosts with Space. Navigate with arrow keys.")
286
310
  selected = prompt("checkbox", "Select agent hosts to configure:", choices=choices)
287
311
 
288
312
  if not selected:
@@ -296,6 +320,8 @@ def select_hosts(*, non_interactive: bool, cli_agents: list[str] | None) -> list
296
320
  else:
297
321
  raise SystemExit(2)
298
322
 
323
+ # Show confirmation of what will be deployed
324
+ print(f"Will deploy to: {', '.join(selected)}")
299
325
  return [HOSTS[name] for name in selected]
300
326
 
301
327
 
@@ -319,6 +345,8 @@ def select_scope(*, non_interactive: bool, cli_scope: str | None) -> Scope:
319
345
  return "project"
320
346
 
321
347
  # Interactive: prompt for scope
348
+ print("Note: 'project' scope stores configs in the project directory.")
349
+ print(" 'user' scope stores configs in your home directory.")
322
350
  selected = prompt(
323
351
  "select",
324
352
  "Select installation scope:",
@@ -328,6 +356,7 @@ def select_scope(*, non_interactive: bool, cli_scope: str | None) -> Scope:
328
356
  if not selected:
329
357
  return "project"
330
358
 
359
+ print(f"Selected scope: {selected}")
331
360
  return selected # type: ignore
332
361
 
333
362
 
@@ -422,14 +451,14 @@ def merge_mcp_config(config_path: Path, host: HostConfig, *, mcp_command: str) -
422
451
 
423
452
  # Prepare new entry
424
453
  new_entry = {"command": mcp_command, "type": "stdio"}
425
- existing_entry = config["mcpServers"].get("java-codebase-rag")
454
+ existing_entry = config["mcpServers"].get(_MCP_SERVER_NAME)
426
455
 
427
456
  # Check if entry already exists with same config
428
457
  if existing_entry == new_entry:
429
458
  return False
430
459
 
431
460
  # Merge/update entry
432
- config["mcpServers"]["java-codebase-rag"] = new_entry
461
+ config["mcpServers"][_MCP_SERVER_NAME] = new_entry
433
462
 
434
463
  # Write atomically (write to tmp, then rename)
435
464
  tmp_name = None
@@ -738,7 +767,8 @@ def run_init_if_needed(
738
767
  )
739
768
  from java_codebase_rag.pipeline import run_build_ast_graph, run_cocoindex_update
740
769
 
741
- if index_dir_has_existing_artifacts(index_dir):
770
+ has_existing, _ = index_dir_has_existing_artifacts(index_dir)
771
+ if has_existing:
742
772
  print("Index already exists. Run `java-codebase-rag reprocess` to rebuild.")
743
773
  return False
744
774
 
@@ -762,6 +792,8 @@ def run_init_if_needed(
762
792
  g = run_build_ast_graph(
763
793
  source_root=cfg.source_root,
764
794
  kuzu_path=cfg.kuzu_path,
795
+ verbose=not quiet,
796
+ quiet=quiet,
765
797
  env=env,
766
798
  )
767
799
  if g.returncode != 0:
@@ -823,6 +855,383 @@ def handle_rerun(cwd: Path, *, non_interactive: bool) -> dict | None:
823
855
  return existing_config
824
856
 
825
857
 
858
+ def detect_configured_hosts(cwd: Path) -> list[tuple[HostConfig, str]]:
859
+ """Scan project + user config files for java-codebase-rag MCP entries.
860
+
861
+ Args:
862
+ cwd: Current working directory (for project-scope configs)
863
+
864
+ Returns:
865
+ List of (host_config, scope) tuples where scope is "project" or "user"
866
+ """
867
+ detected = []
868
+
869
+ # Check all hosts in both project and user scopes
870
+ for host_name, host_config in HOSTS.items():
871
+ # Check project scope
872
+ project_mcp_path = host_config.mcp_config_path("project", cwd)
873
+ if _has_java_codebase_rag_entry(project_mcp_path):
874
+ detected.append((host_config, "project"))
875
+
876
+ # Check user scope
877
+ user_mcp_path = host_config.mcp_config_path("user", cwd)
878
+ if _has_java_codebase_rag_entry(user_mcp_path):
879
+ detected.append((host_config, "user"))
880
+
881
+ return detected
882
+
883
+
884
+ def _has_java_codebase_rag_entry(config_path: Path) -> bool:
885
+ """Check if MCP config file has a java-codebase-rag entry.
886
+
887
+ Args:
888
+ config_path: Path to MCP config file
889
+
890
+ Returns:
891
+ True if file exists and contains java-codebase-rag in mcpServers
892
+ """
893
+ if not config_path.is_file():
894
+ return False
895
+
896
+ try:
897
+ with open(config_path, "r") as f:
898
+ config = json.load(f)
899
+ except (json.JSONDecodeError, IOError, OSError):
900
+ return False
901
+
902
+ mcp_servers = config.get("mcpServers", {})
903
+ return _MCP_SERVER_NAME in mcp_servers
904
+
905
+
906
+ def refresh_artifacts(
907
+ host: HostConfig,
908
+ scope: str,
909
+ cwd: Path,
910
+ *,
911
+ force: bool,
912
+ dry_run: bool,
913
+ ) -> list[ArtifactResult]:
914
+ """Overwrite skill and agent files from package data. Skip MCP if entry is correct.
915
+
916
+ Args:
917
+ host: HostConfig for the agent host
918
+ scope: Installation scope ("project" or "user")
919
+ cwd: Current working directory
920
+ force: If True, overwrite all files even if matching
921
+ dry_run: If True, print changes without writing
922
+
923
+ Returns:
924
+ List of ArtifactResult objects for each artifact
925
+ """
926
+ results = []
927
+
928
+ # Refresh skill file
929
+ skills_dir = host.skills_dir(scope, cwd)
930
+ skill_dest = skills_dir / "explore-codebase" / "SKILL.md"
931
+ skill_result = _refresh_file(
932
+ skill_dest,
933
+ "skills/explore-codebase/SKILL.md",
934
+ artifact_type="skill",
935
+ force=force,
936
+ dry_run=dry_run,
937
+ )
938
+ results.append(skill_result)
939
+
940
+ # Refresh agent file
941
+ agents_dir = host.agents_dir(scope, cwd)
942
+ agent_dest = agents_dir / "explorer-rag-enhanced.md"
943
+ agent_result = _refresh_file(
944
+ agent_dest,
945
+ "agents/explorer-rag-enhanced.md",
946
+ artifact_type="agent",
947
+ force=force,
948
+ dry_run=dry_run,
949
+ )
950
+ results.append(agent_result)
951
+
952
+ # Refresh MCP config (update command path if needed)
953
+ mcp_config_path = host.mcp_config_path(scope, cwd)
954
+ mcp_result = _refresh_mcp_config(mcp_config_path, host, force=force, dry_run=dry_run)
955
+ results.append(mcp_result)
956
+
957
+ return results
958
+
959
+
960
+ def _refresh_file(
961
+ dest_path: Path,
962
+ package_relative_path: str,
963
+ *,
964
+ artifact_type: str,
965
+ force: bool,
966
+ dry_run: bool,
967
+ ) -> ArtifactResult:
968
+ """Refresh a single file from package data.
969
+
970
+ Args:
971
+ dest_path: Destination file path
972
+ package_relative_path: Path relative to install_data
973
+ artifact_type: Type of artifact (for error messages)
974
+ force: If True, overwrite even if matching
975
+ dry_run: If True, print without writing
976
+
977
+ Returns:
978
+ ArtifactResult with success status
979
+ """
980
+ try:
981
+ # Read package data
982
+ package_content = _read_package_artifact(package_relative_path)
983
+
984
+ # Check if file exists
985
+ if dest_path.is_file():
986
+ existing_content = dest_path.read_text(encoding="utf-8")
987
+
988
+ # Skip if content matches and not forcing
989
+ if package_content == existing_content and not force:
990
+ return ArtifactResult(path=dest_path, success=True, error=None)
991
+
992
+ # Content differs or force mode
993
+ if dry_run:
994
+ print(f"Would update {artifact_type} file at {dest_path}")
995
+ return ArtifactResult(path=dest_path, success=True, error=None)
996
+
997
+ elif dry_run:
998
+ print(f"Would create {artifact_type} file at {dest_path}")
999
+ return ArtifactResult(path=dest_path, success=True, error=None)
1000
+
1001
+ # Ensure parent directory exists
1002
+ if not dry_run:
1003
+ dest_path.parent.mkdir(parents=True, exist_ok=True)
1004
+
1005
+ # Check writability
1006
+ if not _is_writable(dest_path.parent):
1007
+ return ArtifactResult(
1008
+ path=dest_path,
1009
+ success=False,
1010
+ error=f"Directory not writable: {dest_path.parent}",
1011
+ )
1012
+
1013
+ # Write file (skip in dry_run mode)
1014
+ if not dry_run:
1015
+ dest_path.write_text(package_content, encoding="utf-8")
1016
+ print(f"Updated {artifact_type} file at {dest_path}")
1017
+
1018
+ return ArtifactResult(path=dest_path, success=True, error=None)
1019
+
1020
+ except Exception as e:
1021
+ return ArtifactResult(path=dest_path, success=False, error=str(e))
1022
+
1023
+
1024
+ def _refresh_mcp_config(
1025
+ config_path: Path,
1026
+ host: HostConfig,
1027
+ *,
1028
+ force: bool,
1029
+ dry_run: bool,
1030
+ ) -> ArtifactResult:
1031
+ """Refresh MCP config entry (update command path if needed).
1032
+
1033
+ Args:
1034
+ config_path: Path to MCP config file
1035
+ host: HostConfig for the agent host
1036
+ force: If True, update even if matching
1037
+ dry_run: If True, print without writing
1038
+
1039
+ Returns:
1040
+ ArtifactResult with success status
1041
+ """
1042
+ try:
1043
+ # Resolve current MCP command path
1044
+ # Catch SystemExit because resolve_mcp_command raises it when binary not found
1045
+ try:
1046
+ mcp_command = resolve_mcp_command(non_interactive=True)
1047
+ except SystemExit:
1048
+ return ArtifactResult(
1049
+ path=config_path,
1050
+ success=False,
1051
+ error="java-codebase-rag-mcp not found on PATH",
1052
+ )
1053
+
1054
+ # Prepare new entry
1055
+ new_entry = {"command": mcp_command, "type": "stdio"}
1056
+
1057
+ # Read existing config
1058
+ if config_path.is_file():
1059
+ try:
1060
+ with open(config_path, "r") as f:
1061
+ config = json.load(f)
1062
+ except json.JSONDecodeError as e:
1063
+ return ArtifactResult(
1064
+ path=config_path,
1065
+ success=False,
1066
+ error=f"Failed to parse {config_path}: {e}",
1067
+ )
1068
+ else:
1069
+ config = {}
1070
+
1071
+ # Ensure mcpServers key exists
1072
+ if "mcpServers" not in config:
1073
+ config["mcpServers"] = {}
1074
+
1075
+ existing_entry = config["mcpServers"].get(_MCP_SERVER_NAME)
1076
+
1077
+ # Check if entry already matches (skip unless force)
1078
+ if existing_entry == new_entry and not force:
1079
+ return ArtifactResult(path=config_path, success=True, error=None)
1080
+
1081
+ # Entry differs or force mode
1082
+ if dry_run:
1083
+ print(f"Would update MCP config at {config_path}")
1084
+ return ArtifactResult(path=config_path, success=True, error=None)
1085
+
1086
+ # Merge/update entry
1087
+ config["mcpServers"][_MCP_SERVER_NAME] = new_entry
1088
+
1089
+ # Ensure parent directory exists
1090
+ config_path.parent.mkdir(parents=True, exist_ok=True)
1091
+
1092
+ # Check writability
1093
+ if not _is_writable(config_path.parent):
1094
+ return ArtifactResult(
1095
+ path=config_path,
1096
+ success=False,
1097
+ error=f"Directory not writable: {config_path.parent}",
1098
+ )
1099
+
1100
+ # Write atomically
1101
+ tmp_name = None
1102
+ try:
1103
+ with tempfile.NamedTemporaryFile(
1104
+ mode="w",
1105
+ dir=config_path.parent,
1106
+ prefix=f".{config_path.name}.",
1107
+ delete=False,
1108
+ ) as tmp:
1109
+ json.dump(config, tmp, indent=2)
1110
+ tmp.flush()
1111
+ os.fsync(tmp.fileno())
1112
+ tmp_name = tmp.name
1113
+
1114
+ # Atomic rename
1115
+ os.rename(tmp_name, config_path)
1116
+ print(f"Updated MCP config at {config_path}")
1117
+ return ArtifactResult(path=config_path, success=True, error=None)
1118
+
1119
+ except (IOError, OSError) as e:
1120
+ if tmp_name:
1121
+ try:
1122
+ os.unlink(tmp_name)
1123
+ except OSError:
1124
+ pass
1125
+ raise RuntimeError(f"Failed to write {config_path}: {e}") from e
1126
+
1127
+ except SystemExit as e:
1128
+ # Catch SystemExit from resolve_mcp_command and other exits
1129
+ return ArtifactResult(path=config_path, success=False, error=f"Command failed: {e.code}")
1130
+ except Exception as e:
1131
+ return ArtifactResult(path=config_path, success=False, error=str(e))
1132
+
1133
+
1134
+ def run_update(
1135
+ *,
1136
+ force: bool,
1137
+ dry_run: bool,
1138
+ cwd: Path | None = None,
1139
+ ) -> int:
1140
+ """Run the update pipeline. Returns exit code.
1141
+
1142
+ Args:
1143
+ force: If True, overwrite all artifacts even if matching
1144
+ dry_run: If True, print changes without writing
1145
+ cwd: Current working directory (defaults to Path.cwd())
1146
+
1147
+ Returns:
1148
+ Exit code (0=success, 1=partial, 2=fatal)
1149
+ """
1150
+ if cwd is None:
1151
+ cwd = Path.cwd()
1152
+ cwd = cwd.resolve()
1153
+
1154
+ # Detect configured hosts
1155
+ configured_hosts = detect_configured_hosts(cwd)
1156
+
1157
+ if not configured_hosts:
1158
+ print("No configured agent hosts found.")
1159
+ print("Run `java-codebase-rag install` first.")
1160
+ return EXIT_FATAL
1161
+
1162
+ print(f"Found {len(configured_hosts)} configured host(s).")
1163
+
1164
+ # Refresh artifacts for each host
1165
+ all_results = []
1166
+ for host_config, scope in configured_hosts:
1167
+ print(f"\nRefreshing {host_config.name} ({scope} scope)...")
1168
+ results = refresh_artifacts(host_config, scope, cwd, force=force, dry_run=dry_run)
1169
+ all_results.extend(results)
1170
+
1171
+ # Check for partial failures
1172
+ partial_failures = [r for r in all_results if not r.success]
1173
+ has_artifact_failures = len(partial_failures) > 0
1174
+ if partial_failures:
1175
+ print("\nWarning: Some artifacts failed to update:")
1176
+ for r in partial_failures:
1177
+ print(f" {r.path}: {r.error}")
1178
+
1179
+ # Check if index exists
1180
+ from java_codebase_rag.config import (
1181
+ discover_project_root,
1182
+ index_dir_has_existing_artifacts,
1183
+ resolve_operator_config,
1184
+ )
1185
+ from java_codebase_rag.pipeline import run_cocoindex_update
1186
+
1187
+ project_root = discover_project_root(cwd)
1188
+ if project_root is None:
1189
+ print("\nNo project configuration found (.java-codebase-rag.yml).")
1190
+ print("Skipping index update.")
1191
+ return EXIT_PARTIAL if has_artifact_failures else EXIT_SUCCESS
1192
+
1193
+ # Resolve configuration
1194
+ try:
1195
+ cfg = resolve_operator_config(source_root=project_root, cli_index_dir=None)
1196
+ index_dir = cfg.index_dir
1197
+ except Exception as e:
1198
+ print(f"\nWarning: Failed to resolve configuration: {e}")
1199
+ print("Skipping index update.")
1200
+ return EXIT_PARTIAL if has_artifact_failures else EXIT_SUCCESS
1201
+
1202
+ # Check if index has existing artifacts
1203
+ index_exists, _ = index_dir_has_existing_artifacts(index_dir)
1204
+
1205
+ if not index_exists:
1206
+ print("\nNo index found.")
1207
+ print("Run `java-codebase-rag install` to create one.")
1208
+ return EXIT_PARTIAL if has_artifact_failures else EXIT_SUCCESS
1209
+
1210
+ # Run increment (LanceDB catch-up)
1211
+ if not dry_run:
1212
+ print("\nUpdating index (incremental LanceDB update)...")
1213
+ cfg.apply_to_os_environ()
1214
+ env = cfg.subprocess_env()
1215
+
1216
+ coco = run_cocoindex_update(env, full_reprocess=False, quiet=True)
1217
+ if coco.returncode != 0:
1218
+ print(f"Error: Index update failed with code {coco.returncode}")
1219
+ return 1
1220
+
1221
+ # Print graph staleness warning
1222
+ from java_codebase_rag.cli import _INCREMENT_WARNING_LINES
1223
+ print("\n" + "\n".join(_INCREMENT_WARNING_LINES))
1224
+ else:
1225
+ print("\nWould run incremental index update.")
1226
+
1227
+ # Print summary
1228
+ print("\nUpdate complete.")
1229
+ successful = [r for r in all_results if r.success]
1230
+ print(f"Updated {len(successful)} artifact(s).")
1231
+
1232
+ return 1 if has_artifact_failures else 0
1233
+
1234
+
826
1235
  def run_install(
827
1236
  *,
828
1237
  non_interactive: bool,