java-codebase-rag 0.6.6__py3-none-any.whl → 0.8.0__py3-none-any.whl
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.
- ast_java.py +8 -3
- build_ast_graph.py +72 -16
- graph_enrich.py +2 -1
- graph_types.py +133 -0
- java_codebase_rag/_fdlimit.py +10 -2
- java_codebase_rag/_stdio.py +32 -0
- java_codebase_rag/cli.py +135 -24
- java_codebase_rag/config.py +128 -9
- java_codebase_rag/install_data/agents/explorer-rag-cli.md +291 -0
- java_codebase_rag/install_data/agents/explorer-rag-enhanced.md +8 -8
- java_codebase_rag/install_data/skills/explore-codebase/SKILL.md +8 -8
- java_codebase_rag/install_data/skills/explore-codebase-cli/SKILL.md +251 -0
- java_codebase_rag/installer.py +438 -103
- java_codebase_rag/jrag.py +4300 -0
- java_codebase_rag/jrag_envelope.py +1085 -0
- java_codebase_rag/jrag_hints.py +204 -0
- java_codebase_rag/jrag_render.py +688 -0
- java_codebase_rag/pipeline.py +20 -0
- {java_codebase_rag-0.6.6.dist-info → java_codebase_rag-0.8.0.dist-info}/METADATA +137 -94
- java_codebase_rag-0.8.0.dist-info/RECORD +43 -0
- {java_codebase_rag-0.6.6.dist-info → java_codebase_rag-0.8.0.dist-info}/WHEEL +1 -1
- {java_codebase_rag-0.6.6.dist-info → java_codebase_rag-0.8.0.dist-info}/entry_points.txt +1 -0
- {java_codebase_rag-0.6.6.dist-info → java_codebase_rag-0.8.0.dist-info}/top_level.txt +2 -0
- java_index_flow_lancedb.py +34 -19
- java_ontology.py +12 -0
- ladybug_queries.py +233 -52
- mcp_hints.py +6 -6
- mcp_v2.py +205 -617
- resolve_service.py +649 -0
- search_lancedb.py +10 -1
- server.py +20 -12
- java_codebase_rag-0.6.6.dist-info/RECORD +0 -34
- {java_codebase_rag-0.6.6.dist-info → java_codebase_rag-0.8.0.dist-info}/licenses/LICENSE +0 -0
java_codebase_rag/installer.py
CHANGED
|
@@ -22,10 +22,19 @@ from typing import Literal, NamedTuple
|
|
|
22
22
|
import yaml
|
|
23
23
|
|
|
24
24
|
Scope = Literal["project", "user"]
|
|
25
|
+
Surface = Literal["mcp", "cli"]
|
|
25
26
|
|
|
26
27
|
# MCP server name constant
|
|
27
28
|
_MCP_SERVER_NAME = "java-codebase-rag"
|
|
28
29
|
|
|
30
|
+
# Marker file written at install time so a CLI-only install (no MCP entry) is
|
|
31
|
+
# still visible to ``update``. Lives at the project/source root alongside
|
|
32
|
+
# ``.java-codebase-rag.yml``. JSON shape:
|
|
33
|
+
# {"version": 1, "hosts": [{"host": "claude-code", "scope": "project",
|
|
34
|
+
# "surface": "mcp"|"cli"}, ...]}
|
|
35
|
+
_MARKER_FILE_NAME = ".java-codebase-rag.hosts"
|
|
36
|
+
_MARKER_FILE_VERSION = 1
|
|
37
|
+
|
|
29
38
|
# Exit code constants
|
|
30
39
|
EXIT_SUCCESS = 0
|
|
31
40
|
EXIT_PARTIAL = 1
|
|
@@ -40,6 +49,20 @@ class ArtifactResult(NamedTuple):
|
|
|
40
49
|
error: str | None
|
|
41
50
|
|
|
42
51
|
|
|
52
|
+
class ConfiguredHost(NamedTuple):
|
|
53
|
+
"""A host installed on this machine: which host, which scope, which surface.
|
|
54
|
+
|
|
55
|
+
Replaces the prior 2-tuple ``(HostConfig, scope)`` returned by
|
|
56
|
+
``detect_configured_hosts`` so ``update`` can route the refresh through the
|
|
57
|
+
correct ``Surface`` (an MCP-surface install refreshes MCP+skill+agent; a
|
|
58
|
+
CLI-surface install refreshes the CLI skill+agent only).
|
|
59
|
+
"""
|
|
60
|
+
|
|
61
|
+
host: "HostConfig"
|
|
62
|
+
scope: Scope
|
|
63
|
+
surface: Surface
|
|
64
|
+
|
|
65
|
+
|
|
43
66
|
@dataclass(frozen=True)
|
|
44
67
|
class HostConfig:
|
|
45
68
|
"""Configuration for an agent host."""
|
|
@@ -94,6 +117,37 @@ HOSTS: dict[str, HostConfig] = {
|
|
|
94
117
|
}
|
|
95
118
|
|
|
96
119
|
|
|
120
|
+
# ---------------------------------------------------------------------------
|
|
121
|
+
# ArtifactManifest — single source of truth for which artifacts each surface
|
|
122
|
+
# ships. Iterated by both ``deploy_artifacts`` and ``refresh_artifacts`` so
|
|
123
|
+
# adding/removing an artifact is one edit, not two.
|
|
124
|
+
#
|
|
125
|
+
# Each entry is a 3-tuple ``(kind, package_path, dest_relative)``:
|
|
126
|
+
# - ``kind``: "mcp" dispatches to ``_deploy_mcp_config`` / ``_refresh_mcp_config``
|
|
127
|
+
# (the MCP config path is host/scope-resolved inside those helpers —
|
|
128
|
+
# ``package_path`` and ``dest_relative`` are unused for this kind).
|
|
129
|
+
# - ``kind``: "skill" | "agent" dispatches to ``_deploy_file`` / ``_refresh_file``.
|
|
130
|
+
# - ``package_path``: relative path under ``install_data/``.
|
|
131
|
+
# - ``dest_relative``: relative path under ``host.scope_path(scope, cwd)``.
|
|
132
|
+
#
|
|
133
|
+
# The ``mcp`` surface carries the MCP config entry; the ``cli`` surface does
|
|
134
|
+
# NOT (a CLI install never registers an MCP server).
|
|
135
|
+
# ---------------------------------------------------------------------------
|
|
136
|
+
ArtifactManifestEntry = tuple[str, str, str]
|
|
137
|
+
|
|
138
|
+
ARTIFACT_MANIFEST: dict[Surface, list[ArtifactManifestEntry]] = {
|
|
139
|
+
"mcp": [
|
|
140
|
+
("mcp", "", ""),
|
|
141
|
+
("skill", "skills/explore-codebase/SKILL.md", "skills/explore-codebase/SKILL.md"),
|
|
142
|
+
("agent", "agents/explorer-rag-enhanced.md", "agents/explorer-rag-enhanced.md"),
|
|
143
|
+
],
|
|
144
|
+
"cli": [
|
|
145
|
+
("skill", "skills/explore-codebase-cli/SKILL.md", "skills/explore-codebase-cli/SKILL.md"),
|
|
146
|
+
("agent", "agents/explorer-rag-cli.md", "agents/explorer-rag-cli.md"),
|
|
147
|
+
],
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
|
|
97
151
|
def prompt(
|
|
98
152
|
prompt_type: str,
|
|
99
153
|
message: str,
|
|
@@ -421,36 +475,117 @@ def select_scope(*, non_interactive: bool, cli_scope: str | None) -> Scope:
|
|
|
421
475
|
return selected # type: ignore
|
|
422
476
|
|
|
423
477
|
|
|
424
|
-
def
|
|
425
|
-
|
|
478
|
+
def select_surface(
|
|
479
|
+
*,
|
|
480
|
+
non_interactive: bool,
|
|
481
|
+
cli_surface: str | None,
|
|
482
|
+
prefill: Surface | None = None,
|
|
483
|
+
) -> Surface:
|
|
484
|
+
"""Select 'mcp' or 'cli' surface (PR-JRAG-5).
|
|
426
485
|
|
|
427
|
-
|
|
486
|
+
The MCP surface registers the stdio MCP server (today's behavior). The CLI
|
|
487
|
+
surface ships the ``jrag`` console-script skill+subagent instead — no MCP
|
|
488
|
+
entry is registered.
|
|
428
489
|
|
|
429
490
|
Args:
|
|
430
|
-
non_interactive: If True,
|
|
491
|
+
non_interactive: If True, honor ``cli_surface`` (default ``"mcp"``).
|
|
492
|
+
cli_surface: Surface from the ``--surface`` CLI flag.
|
|
493
|
+
prefill: On re-run, the surface recorded in the existing marker file.
|
|
494
|
+
When set and the user does not pick otherwise, this is preserved.
|
|
431
495
|
|
|
432
496
|
Returns:
|
|
433
|
-
|
|
497
|
+
Selected surface (``"mcp"`` or ``"cli"``).
|
|
434
498
|
|
|
435
499
|
Raises:
|
|
436
|
-
SystemExit(2):
|
|
500
|
+
SystemExit(2): if ``cli_surface`` is invalid.
|
|
437
501
|
"""
|
|
438
|
-
|
|
502
|
+
if cli_surface:
|
|
503
|
+
if cli_surface not in ("mcp", "cli"):
|
|
504
|
+
print(f"Error: Invalid surface '{cli_surface}'. Must be 'mcp' or 'cli'.")
|
|
505
|
+
raise SystemExit(2)
|
|
506
|
+
return cli_surface # type: ignore
|
|
439
507
|
|
|
440
|
-
if
|
|
441
|
-
|
|
508
|
+
if non_interactive:
|
|
509
|
+
# Default to MCP for back-comat when no flag is passed.
|
|
510
|
+
return "mcp"
|
|
511
|
+
|
|
512
|
+
print(
|
|
513
|
+
"Note: 'mcp' surface registers the java-codebase-rag MCP server (5 tools: "
|
|
514
|
+
"search/find/describe/neighbors/resolve)."
|
|
515
|
+
)
|
|
516
|
+
print(
|
|
517
|
+
" 'cli' surface deploys the `jrag` console-script skill+subagent "
|
|
518
|
+
"(one command per intent, no MCP server)."
|
|
519
|
+
)
|
|
520
|
+
|
|
521
|
+
choices = ["mcp", "cli"]
|
|
522
|
+
if prefill is not None:
|
|
523
|
+
# Surface the prior choice first so the user can keep it with Enter.
|
|
524
|
+
choices = [prefill] + [c for c in ("mcp", "cli") if c != prefill]
|
|
525
|
+
default = prefill
|
|
526
|
+
else:
|
|
527
|
+
default = "mcp"
|
|
528
|
+
|
|
529
|
+
selected = prompt(
|
|
530
|
+
"select",
|
|
531
|
+
"Select agent surface:",
|
|
532
|
+
choices=choices,
|
|
533
|
+
default=default,
|
|
534
|
+
)
|
|
535
|
+
|
|
536
|
+
if not selected:
|
|
537
|
+
return default
|
|
538
|
+
return selected # type: ignore
|
|
539
|
+
|
|
540
|
+
|
|
541
|
+
def resolve_mcp_command(*, non_interactive: bool, surface: Surface = "mcp") -> str:
|
|
542
|
+
"""Resolve the absolute path to the runtime binary for the chosen surface.
|
|
543
|
+
|
|
544
|
+
- ``surface="mcp"`` (today's behavior): resolve ``java-codebase-rag-mcp``;
|
|
545
|
+
on missing + non-interactive, exit with code 2.
|
|
546
|
+
- ``surface="cli"``: resolve the ``jrag`` console script instead. The CLI
|
|
547
|
+
surface registers no MCP server, so the MCP binary is irrelevant —
|
|
548
|
+
never raise ``SystemExit(2)`` for a missing MCP binary on this surface.
|
|
549
|
+
If ``jrag`` is missing, fall through to the interactive prompt (or
|
|
550
|
+
non-interactive exit) parameterized for ``jrag``.
|
|
551
|
+
|
|
552
|
+
Args:
|
|
553
|
+
non_interactive: If True, exit with code 2 when the target binary
|
|
554
|
+
is not found.
|
|
555
|
+
surface: Which surface's binary to resolve.
|
|
556
|
+
|
|
557
|
+
Returns:
|
|
558
|
+
Absolute path to the resolved executable.
|
|
559
|
+
|
|
560
|
+
Raises:
|
|
561
|
+
SystemExit(2): If not found and non-interactive, or user aborts.
|
|
562
|
+
"""
|
|
563
|
+
binary_name, display_name = _surface_binary(surface)
|
|
564
|
+
resolved = shutil.which(binary_name)
|
|
565
|
+
|
|
566
|
+
if resolved:
|
|
567
|
+
return resolved
|
|
442
568
|
|
|
443
569
|
# Not found on PATH
|
|
444
570
|
if non_interactive:
|
|
445
|
-
print("Error: `
|
|
446
|
-
|
|
571
|
+
print(f"Error: `{display_name}` not found on PATH.")
|
|
572
|
+
if surface == "mcp":
|
|
573
|
+
print(
|
|
574
|
+
"Ensure `java-codebase-rag` is installed, then re-run with "
|
|
575
|
+
"`--non-interactive --agent <host>`."
|
|
576
|
+
)
|
|
577
|
+
else:
|
|
578
|
+
print(
|
|
579
|
+
"Ensure `java-codebase-rag` is installed (provides the `jrag` "
|
|
580
|
+
"console script), then re-run with `--non-interactive --agent <host>`."
|
|
581
|
+
)
|
|
447
582
|
raise SystemExit(2)
|
|
448
583
|
|
|
449
584
|
# Interactive: prompt user for path
|
|
450
|
-
print("Warning: `
|
|
585
|
+
print(f"Warning: `{display_name}` not found on PATH.")
|
|
451
586
|
user_path = prompt(
|
|
452
587
|
"text",
|
|
453
|
-
"Enter the full path to
|
|
588
|
+
f"Enter the full path to {display_name} (or 'abort'):",
|
|
454
589
|
default="abort",
|
|
455
590
|
)
|
|
456
591
|
|
|
@@ -466,7 +601,7 @@ def resolve_mcp_command(*, non_interactive: bool) -> str:
|
|
|
466
601
|
print(f"Error: Path {path_obj} does not exist or is not a file.")
|
|
467
602
|
user_path = prompt(
|
|
468
603
|
"text",
|
|
469
|
-
"Enter the full path to
|
|
604
|
+
f"Enter the full path to {display_name} (or 'abort'):",
|
|
470
605
|
default="abort",
|
|
471
606
|
)
|
|
472
607
|
if user_path == "abort" or not user_path:
|
|
@@ -482,6 +617,18 @@ def resolve_mcp_command(*, non_interactive: bool) -> str:
|
|
|
482
617
|
return str(path_obj.resolve())
|
|
483
618
|
|
|
484
619
|
|
|
620
|
+
def _surface_binary(surface: Surface) -> tuple[str, str]:
|
|
621
|
+
"""Return ``(shutil_which_target, user_display_name)`` for a surface.
|
|
622
|
+
|
|
623
|
+
The CLI surface resolves the ``jrag`` console script (no MCP server is
|
|
624
|
+
registered, so the MCP binary is irrelevant). The MCP surface keeps
|
|
625
|
+
today's behavior.
|
|
626
|
+
"""
|
|
627
|
+
if surface == "cli":
|
|
628
|
+
return ("jrag", "jrag")
|
|
629
|
+
return ("java-codebase-rag-mcp", "java-codebase-rag-mcp")
|
|
630
|
+
|
|
631
|
+
|
|
485
632
|
def merge_mcp_config(config_path: Path, host: HostConfig, *, mcp_command: str) -> bool:
|
|
486
633
|
"""Read, merge, write MCP config. Returns True if entry was added/updated.
|
|
487
634
|
|
|
@@ -536,7 +683,7 @@ def merge_mcp_config(config_path: Path, host: HostConfig, *, mcp_command: str) -
|
|
|
536
683
|
tmp_name = tmp.name
|
|
537
684
|
|
|
538
685
|
# Atomic rename
|
|
539
|
-
os.
|
|
686
|
+
os.replace(tmp_name, config_path)
|
|
540
687
|
return True
|
|
541
688
|
except (IOError, OSError) as e:
|
|
542
689
|
if tmp_name:
|
|
@@ -562,53 +709,52 @@ def deploy_artifacts(
|
|
|
562
709
|
*,
|
|
563
710
|
non_interactive: bool,
|
|
564
711
|
mcp_command: str,
|
|
712
|
+
surface: Surface = "mcp",
|
|
565
713
|
) -> list[ArtifactResult]:
|
|
566
714
|
"""Deploy artifacts (MCP config, skill, agent) to selected hosts.
|
|
567
715
|
|
|
716
|
+
Iterates ``ARTIFACT_MANIFEST[surface]`` so both surfaces share one source
|
|
717
|
+
of truth. The keyword-only ``surface`` defaults to ``"mcp"`` so existing
|
|
718
|
+
direct-call sites in tests keep working unchanged.
|
|
719
|
+
|
|
568
720
|
Args:
|
|
569
721
|
hosts: List of HostConfig objects to deploy to
|
|
570
722
|
scope: Installation scope ("project" or "user")
|
|
571
723
|
cwd: Current working directory
|
|
572
724
|
non_interactive: If True, skip overwrite prompts
|
|
573
|
-
mcp_command: Resolved absolute path to
|
|
725
|
+
mcp_command: Resolved absolute path to the runtime binary
|
|
726
|
+
(``java-codebase-rag-mcp`` for ``mcp`` surface; ``jrag`` for
|
|
727
|
+
``cli`` surface — unused for the latter since CLI ships no MCP
|
|
728
|
+
config).
|
|
729
|
+
surface: Which artifact set to deploy (default ``"mcp"`` for back-comat).
|
|
574
730
|
|
|
575
731
|
Returns:
|
|
576
732
|
List of ArtifactResult objects for each deployment
|
|
577
733
|
"""
|
|
578
734
|
results = []
|
|
735
|
+
manifest = ARTIFACT_MANIFEST[surface]
|
|
579
736
|
|
|
580
737
|
for host in hosts:
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
# Deploy agent
|
|
603
|
-
agents_dir = host.agents_dir(scope, cwd)
|
|
604
|
-
agent_dest = agents_dir / "explorer-rag-enhanced.md"
|
|
605
|
-
agent_result = _deploy_file(
|
|
606
|
-
agent_dest,
|
|
607
|
-
"agents/explorer-rag-enhanced.md",
|
|
608
|
-
artifact_type="agent",
|
|
609
|
-
non_interactive=non_interactive,
|
|
610
|
-
)
|
|
611
|
-
results.append(agent_result)
|
|
738
|
+
for kind, package_path, dest_relative in manifest:
|
|
739
|
+
if kind == "mcp":
|
|
740
|
+
# Only the MCP surface carries this entry; the CLI manifest
|
|
741
|
+
# has no "mcp" row by construction.
|
|
742
|
+
mcp_config_path = host.mcp_config_path(scope, cwd)
|
|
743
|
+
result = _deploy_mcp_config(
|
|
744
|
+
mcp_config_path,
|
|
745
|
+
host,
|
|
746
|
+
non_interactive=non_interactive,
|
|
747
|
+
mcp_command=mcp_command,
|
|
748
|
+
)
|
|
749
|
+
else:
|
|
750
|
+
dest_path = host.scope_path(scope, cwd) / dest_relative
|
|
751
|
+
result = _deploy_file(
|
|
752
|
+
dest_path,
|
|
753
|
+
package_path,
|
|
754
|
+
artifact_type=kind,
|
|
755
|
+
non_interactive=non_interactive,
|
|
756
|
+
)
|
|
757
|
+
results.append(result)
|
|
612
758
|
|
|
613
759
|
return results
|
|
614
760
|
|
|
@@ -847,8 +993,8 @@ def run_init_if_needed(
|
|
|
847
993
|
non_interactive: bool,
|
|
848
994
|
quiet: bool,
|
|
849
995
|
verbose: bool = False,
|
|
850
|
-
) -> bool:
|
|
851
|
-
"""Run init if index directory has no artifacts.
|
|
996
|
+
) -> bool | None:
|
|
997
|
+
"""Run init if index directory has no artifacts.
|
|
852
998
|
|
|
853
999
|
The indexing sub-step (CocoIndex update + AST graph build) renders the
|
|
854
1000
|
unified ``Vectors → Optimize → Graph`` progress on **stderr** in default
|
|
@@ -867,18 +1013,22 @@ def run_init_if_needed(
|
|
|
867
1013
|
verbose: If True, raw-relay subprocess output (no Live region)
|
|
868
1014
|
|
|
869
1015
|
Returns:
|
|
870
|
-
True if init
|
|
1016
|
+
True if init ran and succeeded; False if it ran and failed (cocoindex or
|
|
1017
|
+
graph build returned non-zero); None if skipped because the index already
|
|
1018
|
+
exists. Callers must distinguish ``False`` (failure) from ``None`` (skip)
|
|
1019
|
+
so a failed index does not report success (issue #351).
|
|
871
1020
|
"""
|
|
872
1021
|
from java_codebase_rag.config import (
|
|
873
1022
|
index_dir_has_existing_artifacts,
|
|
874
1023
|
resolve_operator_config,
|
|
1024
|
+
write_config_source_pointer,
|
|
875
1025
|
)
|
|
876
|
-
from java_codebase_rag.pipeline import run_build_ast_graph, run_cocoindex_update
|
|
1026
|
+
from java_codebase_rag.pipeline import is_cocoindex_preflight_blocker, run_build_ast_graph, run_cocoindex_update
|
|
877
1027
|
|
|
878
1028
|
has_existing, _ = index_dir_has_existing_artifacts(index_dir)
|
|
879
1029
|
if has_existing:
|
|
880
1030
|
print("Index already exists. Run `java-codebase-rag reprocess` to rebuild.")
|
|
881
|
-
return
|
|
1031
|
+
return None # skipped, not failed
|
|
882
1032
|
|
|
883
1033
|
cfg = resolve_operator_config(
|
|
884
1034
|
source_root=source_root,
|
|
@@ -912,13 +1062,23 @@ def run_init_if_needed(
|
|
|
912
1062
|
on_progress=on_progress,
|
|
913
1063
|
on_progress_console=on_progress_console,
|
|
914
1064
|
)
|
|
915
|
-
|
|
1065
|
+
# Graph-only install (cocoindex absent, e.g. macOS Intel): skip the vectors phase
|
|
1066
|
+
# and build the graph rather than failing install. A genuine non-zero cocoindex
|
|
1067
|
+
# exit still fails.
|
|
1068
|
+
vectors_skipped = is_cocoindex_preflight_blocker(coco)
|
|
1069
|
+
if coco.returncode != 0 and not vectors_skipped:
|
|
916
1070
|
print(
|
|
917
1071
|
f"Error: CocoIndex update failed with code {coco.returncode}",
|
|
918
1072
|
file=sys.stderr,
|
|
919
1073
|
)
|
|
920
1074
|
index_ok = False
|
|
921
1075
|
else:
|
|
1076
|
+
if vectors_skipped:
|
|
1077
|
+
print(
|
|
1078
|
+
"java-codebase-rag: vectors skipped — vector stack not installed on this "
|
|
1079
|
+
"platform (graph-only mode). Building graph only; semantic search is unavailable.",
|
|
1080
|
+
file=sys.stderr,
|
|
1081
|
+
)
|
|
922
1082
|
g = run_build_ast_graph(
|
|
923
1083
|
source_root=cfg.source_root,
|
|
924
1084
|
ladybug_path=cfg.ladybug_path,
|
|
@@ -944,6 +1104,12 @@ def run_init_if_needed(
|
|
|
944
1104
|
if renderer is not None:
|
|
945
1105
|
renderer.stop()
|
|
946
1106
|
_index_progress_footer("install", started, ok=index_ok)
|
|
1107
|
+
if index_ok:
|
|
1108
|
+
# Remember which YAML built this index so discovery from a sibling/cwd
|
|
1109
|
+
# can relocate the config (e.g. a config beside, not inside, the tree).
|
|
1110
|
+
write_config_source_pointer(
|
|
1111
|
+
index_dir=cfg.index_dir, yaml_config_path=cfg.yaml_config_path
|
|
1112
|
+
)
|
|
947
1113
|
return index_ok
|
|
948
1114
|
|
|
949
1115
|
|
|
@@ -998,32 +1164,137 @@ def handle_rerun(cwd: Path, *, non_interactive: bool) -> dict | None:
|
|
|
998
1164
|
return existing_config
|
|
999
1165
|
|
|
1000
1166
|
|
|
1001
|
-
def detect_configured_hosts(cwd: Path) -> list[
|
|
1002
|
-
"""
|
|
1167
|
+
def detect_configured_hosts(cwd: Path) -> list[ConfiguredHost]:
|
|
1168
|
+
"""Detect hosts installed under ``cwd`` (project) and ``$HOME`` (user).
|
|
1169
|
+
|
|
1170
|
+
Reads the marker file (``.java-codebase-rag.hosts``) written at install
|
|
1171
|
+
time. Falls back to the legacy MCP-entry scan with ``surface="mcp"`` when
|
|
1172
|
+
the marker is absent (pre-marker installs from earlier versions).
|
|
1173
|
+
|
|
1174
|
+
The marker is the single source of truth for CLI-surface installs (which
|
|
1175
|
+
register no MCP entry); without it, a CLI-only install would be invisible
|
|
1176
|
+
to ``update`` (the legacy scan only finds MCP entries).
|
|
1003
1177
|
|
|
1004
1178
|
Args:
|
|
1005
|
-
cwd: Current working directory (for project-scope configs)
|
|
1179
|
+
cwd: Current working directory (project root for project-scope configs)
|
|
1006
1180
|
|
|
1007
1181
|
Returns:
|
|
1008
|
-
List of (
|
|
1182
|
+
List of ``ConfiguredHost(host, scope, surface)`` tuples in marker order
|
|
1183
|
+
(or MCP-scan order in the legacy fallback path).
|
|
1009
1184
|
"""
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1185
|
+
marker_hosts = _read_hosts_marker(cwd)
|
|
1186
|
+
if marker_hosts is not None:
|
|
1187
|
+
return marker_hosts
|
|
1188
|
+
|
|
1189
|
+
# Legacy fallback: scan MCP entries + assume ``mcp`` surface. Pre-marker
|
|
1190
|
+
# installs only ever shipped the MCP surface, so this back-comat mapping
|
|
1191
|
+
# is exact.
|
|
1192
|
+
detected: list[ConfiguredHost] = []
|
|
1013
1193
|
for host_name, host_config in HOSTS.items():
|
|
1014
1194
|
# Check project scope
|
|
1015
1195
|
project_mcp_path = host_config.mcp_config_path("project", cwd)
|
|
1016
1196
|
if _has_java_codebase_rag_entry(project_mcp_path):
|
|
1017
|
-
detected.append((host_config, "project"))
|
|
1197
|
+
detected.append(ConfiguredHost(host_config, "project", "mcp"))
|
|
1018
1198
|
|
|
1019
1199
|
# Check user scope
|
|
1020
1200
|
user_mcp_path = host_config.mcp_config_path("user", cwd)
|
|
1021
1201
|
if _has_java_codebase_rag_entry(user_mcp_path):
|
|
1022
|
-
detected.append((host_config, "user"))
|
|
1202
|
+
detected.append(ConfiguredHost(host_config, "user", "mcp"))
|
|
1023
1203
|
|
|
1024
1204
|
return detected
|
|
1025
1205
|
|
|
1026
1206
|
|
|
1207
|
+
def _marker_path(cwd: Path) -> Path:
|
|
1208
|
+
"""Return the marker file path for a project root."""
|
|
1209
|
+
return cwd / _MARKER_FILE_NAME
|
|
1210
|
+
|
|
1211
|
+
|
|
1212
|
+
def _write_hosts_marker(
|
|
1213
|
+
project_root: Path, configured: list[ConfiguredHost]
|
|
1214
|
+
) -> None:
|
|
1215
|
+
"""Write the marker file recording the installed host/scope/surface set.
|
|
1216
|
+
|
|
1217
|
+
Round-trips with ``_read_hosts_marker``. Silently overwrites an existing
|
|
1218
|
+
marker so re-runs (install over an existing install) reflect the latest
|
|
1219
|
+
wizard answers.
|
|
1220
|
+
"""
|
|
1221
|
+
payload = {
|
|
1222
|
+
"version": _MARKER_FILE_VERSION,
|
|
1223
|
+
"hosts": [
|
|
1224
|
+
{"host": ch.host.name, "scope": ch.scope, "surface": ch.surface}
|
|
1225
|
+
for ch in configured
|
|
1226
|
+
],
|
|
1227
|
+
}
|
|
1228
|
+
tmp_name = None
|
|
1229
|
+
try:
|
|
1230
|
+
with tempfile.NamedTemporaryFile(
|
|
1231
|
+
mode="w",
|
|
1232
|
+
dir=project_root,
|
|
1233
|
+
prefix=f".{_MARKER_FILE_NAME}.",
|
|
1234
|
+
delete=False,
|
|
1235
|
+
) as tmp:
|
|
1236
|
+
json.dump(payload, tmp, indent=2)
|
|
1237
|
+
tmp.flush()
|
|
1238
|
+
os.fsync(tmp.fileno())
|
|
1239
|
+
tmp_name = tmp.name
|
|
1240
|
+
# os.replace (not os.rename): on Windows, os.rename raises when the
|
|
1241
|
+
# destination exists — the documented re-run path overwrites the prior
|
|
1242
|
+
# marker. os.replace atomically overwrites cross-platform (PR #371
|
|
1243
|
+
# fixed this same pattern elsewhere).
|
|
1244
|
+
os.replace(tmp_name, _marker_path(project_root))
|
|
1245
|
+
except (IOError, OSError) as e:
|
|
1246
|
+
if tmp_name:
|
|
1247
|
+
try:
|
|
1248
|
+
os.unlink(tmp_name)
|
|
1249
|
+
except OSError:
|
|
1250
|
+
pass
|
|
1251
|
+
# Non-fatal: ``update`` will fall back to the MCP-entry scan. Surface
|
|
1252
|
+
# a warning so the operator notices, but do not abort the install.
|
|
1253
|
+
print(f"Warning: failed to write {_marker_path(project_root)}: {e}")
|
|
1254
|
+
|
|
1255
|
+
|
|
1256
|
+
def _read_hosts_marker(cwd: Path) -> list[ConfiguredHost] | None:
|
|
1257
|
+
"""Read the marker file. Return ``None`` if missing or unparseable.
|
|
1258
|
+
|
|
1259
|
+
On parse/version errors, returns ``None`` so the caller falls back to the
|
|
1260
|
+
MCP-entry scan rather than crashing mid-update.
|
|
1261
|
+
"""
|
|
1262
|
+
marker = _marker_path(cwd)
|
|
1263
|
+
if not marker.is_file():
|
|
1264
|
+
return None
|
|
1265
|
+
try:
|
|
1266
|
+
with open(marker, "r") as f:
|
|
1267
|
+
payload = json.load(f)
|
|
1268
|
+
except (json.JSONDecodeError, IOError, OSError):
|
|
1269
|
+
return None
|
|
1270
|
+
|
|
1271
|
+
if not isinstance(payload, dict):
|
|
1272
|
+
return None
|
|
1273
|
+
|
|
1274
|
+
raw_hosts = payload.get("hosts", [])
|
|
1275
|
+
if not isinstance(raw_hosts, list):
|
|
1276
|
+
return None
|
|
1277
|
+
|
|
1278
|
+
configured: list[ConfiguredHost] = []
|
|
1279
|
+
for entry in raw_hosts:
|
|
1280
|
+
if not isinstance(entry, dict):
|
|
1281
|
+
return None
|
|
1282
|
+
host_name = entry.get("host")
|
|
1283
|
+
scope = entry.get("scope")
|
|
1284
|
+
surface = entry.get("surface", "mcp")
|
|
1285
|
+
if host_name not in HOSTS:
|
|
1286
|
+
return None
|
|
1287
|
+
if scope not in ("project", "user"):
|
|
1288
|
+
return None
|
|
1289
|
+
if surface not in ("mcp", "cli"):
|
|
1290
|
+
return None
|
|
1291
|
+
configured.append(
|
|
1292
|
+
ConfiguredHost(HOSTS[host_name], scope, surface) # type: ignore[arg-type]
|
|
1293
|
+
)
|
|
1294
|
+
|
|
1295
|
+
return configured
|
|
1296
|
+
|
|
1297
|
+
|
|
1027
1298
|
def _has_java_codebase_rag_entry(config_path: Path) -> bool:
|
|
1028
1299
|
"""Check if MCP config file has a java-codebase-rag entry.
|
|
1029
1300
|
|
|
@@ -1053,49 +1324,47 @@ def refresh_artifacts(
|
|
|
1053
1324
|
*,
|
|
1054
1325
|
force: bool,
|
|
1055
1326
|
dry_run: bool,
|
|
1327
|
+
surface: Surface = "mcp",
|
|
1056
1328
|
) -> list[ArtifactResult]:
|
|
1057
1329
|
"""Overwrite skill and agent files from package data. Skip MCP if entry is correct.
|
|
1058
1330
|
|
|
1331
|
+
Iterates ``ARTIFACT_MANIFEST[surface]`` so both surfaces share one source
|
|
1332
|
+
of truth (PR-JRAG-5). The keyword-only ``surface`` defaults to ``"mcp"``
|
|
1333
|
+
so existing direct-call sites in tests keep working unchanged.
|
|
1334
|
+
|
|
1059
1335
|
Args:
|
|
1060
1336
|
host: HostConfig for the agent host
|
|
1061
1337
|
scope: Installation scope ("project" or "user")
|
|
1062
1338
|
cwd: Current working directory
|
|
1063
1339
|
force: If True, overwrite all files even if matching
|
|
1064
1340
|
dry_run: If True, print changes without writing
|
|
1341
|
+
surface: Which artifact set to refresh (default ``"mcp"`` for back-comat).
|
|
1065
1342
|
|
|
1066
1343
|
Returns:
|
|
1067
1344
|
List of ArtifactResult objects for each artifact
|
|
1068
1345
|
"""
|
|
1069
1346
|
results = []
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
dry_run=dry_run,
|
|
1092
|
-
)
|
|
1093
|
-
results.append(agent_result)
|
|
1094
|
-
|
|
1095
|
-
# Refresh MCP config (update command path if needed)
|
|
1096
|
-
mcp_config_path = host.mcp_config_path(scope, cwd)
|
|
1097
|
-
mcp_result = _refresh_mcp_config(mcp_config_path, host, force=force, dry_run=dry_run)
|
|
1098
|
-
results.append(mcp_result)
|
|
1347
|
+
manifest = ARTIFACT_MANIFEST[surface]
|
|
1348
|
+
|
|
1349
|
+
for kind, package_path, dest_relative in manifest:
|
|
1350
|
+
if kind == "mcp":
|
|
1351
|
+
# Refresh MCP config (update command path if needed).
|
|
1352
|
+
# NOTE: only the MCP surface has a "mcp" row in its manifest —
|
|
1353
|
+
# ``_refresh_mcp_config`` (and therefore ``resolve_mcp_command``)
|
|
1354
|
+
# is NEVER reached on the CLI surface by construction. The CLI
|
|
1355
|
+
# surface ships no MCP entry, so there is nothing to refresh.
|
|
1356
|
+
mcp_config_path = host.mcp_config_path(scope, cwd)
|
|
1357
|
+
result = _refresh_mcp_config(mcp_config_path, host, force=force, dry_run=dry_run)
|
|
1358
|
+
else:
|
|
1359
|
+
dest_path = host.scope_path(scope, cwd) / dest_relative
|
|
1360
|
+
result = _refresh_file(
|
|
1361
|
+
dest_path,
|
|
1362
|
+
package_path,
|
|
1363
|
+
artifact_type=kind,
|
|
1364
|
+
force=force,
|
|
1365
|
+
dry_run=dry_run,
|
|
1366
|
+
)
|
|
1367
|
+
results.append(result)
|
|
1099
1368
|
|
|
1100
1369
|
return results
|
|
1101
1370
|
|
|
@@ -1255,7 +1524,7 @@ def _refresh_mcp_config(
|
|
|
1255
1524
|
tmp_name = tmp.name
|
|
1256
1525
|
|
|
1257
1526
|
# Atomic rename
|
|
1258
|
-
os.
|
|
1527
|
+
os.replace(tmp_name, config_path)
|
|
1259
1528
|
print(f"Updated MCP config at {config_path}")
|
|
1260
1529
|
return ArtifactResult(path=config_path, success=True, error=None)
|
|
1261
1530
|
|
|
@@ -1318,9 +1587,16 @@ def run_update(
|
|
|
1318
1587
|
|
|
1319
1588
|
# Refresh artifacts for each host
|
|
1320
1589
|
all_results = []
|
|
1321
|
-
for host_config, scope in configured_hosts:
|
|
1322
|
-
print(f"\nRefreshing {host_config.name} ({scope} scope)...")
|
|
1323
|
-
results = refresh_artifacts(
|
|
1590
|
+
for host_config, scope, surface in configured_hosts:
|
|
1591
|
+
print(f"\nRefreshing {host_config.name} ({scope} scope, surface={surface})...")
|
|
1592
|
+
results = refresh_artifacts(
|
|
1593
|
+
host_config,
|
|
1594
|
+
scope,
|
|
1595
|
+
cwd,
|
|
1596
|
+
force=force,
|
|
1597
|
+
dry_run=dry_run,
|
|
1598
|
+
surface=surface,
|
|
1599
|
+
)
|
|
1324
1600
|
all_results.extend(results)
|
|
1325
1601
|
|
|
1326
1602
|
# Check for partial failures
|
|
@@ -1336,8 +1612,9 @@ def run_update(
|
|
|
1336
1612
|
discover_project_root,
|
|
1337
1613
|
index_dir_has_existing_artifacts,
|
|
1338
1614
|
resolve_operator_config,
|
|
1615
|
+
write_config_source_pointer,
|
|
1339
1616
|
)
|
|
1340
|
-
from java_codebase_rag.pipeline import run_cocoindex_update, run_incremental_graph
|
|
1617
|
+
from java_codebase_rag.pipeline import is_cocoindex_preflight_blocker, run_cocoindex_update, run_incremental_graph
|
|
1341
1618
|
|
|
1342
1619
|
project_root = discover_project_root(cwd)
|
|
1343
1620
|
if project_root is None:
|
|
@@ -1400,13 +1677,22 @@ def run_update(
|
|
|
1400
1677
|
on_progress=on_progress,
|
|
1401
1678
|
on_progress_console=on_progress_console,
|
|
1402
1679
|
)
|
|
1403
|
-
|
|
1680
|
+
# Graph-only install (cocoindex absent): skip the vectors catch-up and run the
|
|
1681
|
+
# graph catch-up only. A genuine non-zero cocoindex exit still fails.
|
|
1682
|
+
vectors_skipped = is_cocoindex_preflight_blocker(coco)
|
|
1683
|
+
if coco.returncode != 0 and not vectors_skipped:
|
|
1404
1684
|
print(
|
|
1405
1685
|
f"Error: Lance index update failed with code {coco.returncode}",
|
|
1406
1686
|
file=sys.stderr,
|
|
1407
1687
|
)
|
|
1408
1688
|
index_ok = False
|
|
1409
1689
|
else:
|
|
1690
|
+
if vectors_skipped:
|
|
1691
|
+
print(
|
|
1692
|
+
"java-codebase-rag: vectors skipped — vector stack not installed on this "
|
|
1693
|
+
"platform (graph-only mode). Running graph catch-up only.",
|
|
1694
|
+
file=sys.stderr,
|
|
1695
|
+
)
|
|
1410
1696
|
g = run_incremental_graph(
|
|
1411
1697
|
source_root=cfg.source_root,
|
|
1412
1698
|
ladybug_path=cfg.ladybug_path,
|
|
@@ -1440,6 +1726,11 @@ def run_update(
|
|
|
1440
1726
|
_index_progress_footer("update", started, ok=index_ok)
|
|
1441
1727
|
if not index_ok:
|
|
1442
1728
|
return 1
|
|
1729
|
+
# Refresh the config pointer so a config moved/renamed since the last
|
|
1730
|
+
# index is relocated correctly by discovery from a sibling/cwd.
|
|
1731
|
+
write_config_source_pointer(
|
|
1732
|
+
index_dir=cfg.index_dir, yaml_config_path=cfg.yaml_config_path
|
|
1733
|
+
)
|
|
1443
1734
|
else:
|
|
1444
1735
|
print("\nWould run incremental index update (Lance + graph).")
|
|
1445
1736
|
|
|
@@ -1457,6 +1748,7 @@ def run_install(
|
|
|
1457
1748
|
agents: list[str] | None,
|
|
1458
1749
|
scope: str | None,
|
|
1459
1750
|
model: str | None,
|
|
1751
|
+
surface: str | None = None,
|
|
1460
1752
|
source_root: Path | None = None,
|
|
1461
1753
|
quiet: bool = False,
|
|
1462
1754
|
verbose: bool = False,
|
|
@@ -1468,6 +1760,7 @@ def run_install(
|
|
|
1468
1760
|
agents: List of agent names from CLI flags
|
|
1469
1761
|
scope: Scope from CLI flag
|
|
1470
1762
|
model: Model from CLI flag
|
|
1763
|
+
surface: Surface from CLI flag (``"mcp"`` or ``"cli"``; default ``"mcp"``)
|
|
1471
1764
|
source_root: Source root path (defaults to cwd if None)
|
|
1472
1765
|
quiet: If True, suppress output
|
|
1473
1766
|
verbose: If True, raw-relay subprocess indexing output (no Live region)
|
|
@@ -1508,21 +1801,30 @@ def run_install(
|
|
|
1508
1801
|
# Stage 2: Embedding model
|
|
1509
1802
|
resolved_model = resolve_model(model, non_interactive=non_interactive)
|
|
1510
1803
|
|
|
1511
|
-
# Stage 3-4: Agent host + scope selection
|
|
1804
|
+
# Stage 3-4: Agent host + scope + surface selection
|
|
1805
|
+
prior_surface = _prior_surface_from_marker(cwd)
|
|
1512
1806
|
try:
|
|
1513
1807
|
hosts = select_hosts(non_interactive=non_interactive, cli_agents=agents)
|
|
1514
1808
|
selected_scope = select_scope(non_interactive=non_interactive, cli_scope=scope)
|
|
1809
|
+
selected_surface = select_surface(
|
|
1810
|
+
non_interactive=non_interactive,
|
|
1811
|
+
cli_surface=surface,
|
|
1812
|
+
prefill=prior_surface,
|
|
1813
|
+
)
|
|
1515
1814
|
except SystemExit as e:
|
|
1516
1815
|
return e.code
|
|
1517
1816
|
|
|
1518
|
-
# Stage 5: Artifact deployment
|
|
1519
|
-
mcp_command = resolve_mcp_command(
|
|
1817
|
+
# Stage 5: Artifact deployment (manifest iterates the chosen surface)
|
|
1818
|
+
mcp_command = resolve_mcp_command(
|
|
1819
|
+
non_interactive=non_interactive, surface=selected_surface
|
|
1820
|
+
)
|
|
1520
1821
|
results = deploy_artifacts(
|
|
1521
1822
|
hosts,
|
|
1522
1823
|
selected_scope,
|
|
1523
1824
|
source_root,
|
|
1524
1825
|
non_interactive=non_interactive,
|
|
1525
1826
|
mcp_command=mcp_command,
|
|
1827
|
+
surface=selected_surface,
|
|
1526
1828
|
)
|
|
1527
1829
|
|
|
1528
1830
|
# Check for partial failures
|
|
@@ -1531,6 +1833,13 @@ def run_install(
|
|
|
1531
1833
|
print("Warning: Some artifacts failed to deploy:")
|
|
1532
1834
|
for r in partial_failures:
|
|
1533
1835
|
print(f" {r.path}: {r.error}")
|
|
1836
|
+
# Severity model: only MCP config (.json/.yml/.yaml) deploy failures are
|
|
1837
|
+
# critical (return 1) -- a broken MCP config means the server cannot start.
|
|
1838
|
+
# Skill/agent (.md / dir) failures are downgraded to non-critical: the
|
|
1839
|
+
# server still runs and the affected host simply lacks those hints. Issue
|
|
1840
|
+
# #351's "treat skill/agent deploy failures as critical for the affected
|
|
1841
|
+
# host" is intentionally DEFERRED here -- promoting them to critical is a
|
|
1842
|
+
# product decision (recoverable vs. fatal) best made explicitly, not bundled.
|
|
1534
1843
|
if all(
|
|
1535
1844
|
r.success
|
|
1536
1845
|
for r in results
|
|
@@ -1542,6 +1851,14 @@ def run_install(
|
|
|
1542
1851
|
# Critical failures
|
|
1543
1852
|
return 1
|
|
1544
1853
|
|
|
1854
|
+
# Record the host/scope/surface set so a later ``update`` can route the
|
|
1855
|
+
# refresh through the right surface — critical for CLI-only installs (no
|
|
1856
|
+
# MCP entry to scan).
|
|
1857
|
+
configured = [
|
|
1858
|
+
ConfiguredHost(h, selected_scope, selected_surface) for h in hosts
|
|
1859
|
+
]
|
|
1860
|
+
_write_hosts_marker(source_root, configured)
|
|
1861
|
+
|
|
1545
1862
|
# Stage 6: Index + finish
|
|
1546
1863
|
# Generate YAML config
|
|
1547
1864
|
yaml_content = generate_yaml_config(
|
|
@@ -1561,9 +1878,12 @@ def run_install(
|
|
|
1561
1878
|
if not quiet:
|
|
1562
1879
|
print("Configuration written to", config_path)
|
|
1563
1880
|
|
|
1564
|
-
# Run init if index directory is empty
|
|
1881
|
+
# Run init if index directory is empty. run_init_if_needed returns True (ran
|
|
1882
|
+
# OK), False (ran and failed — cocoindex/graph non-zero exit), or None
|
|
1883
|
+
# (skipped: index already exists). A failed index must NOT report success in
|
|
1884
|
+
# CI/automation; a skip is not a failure (issue #351).
|
|
1565
1885
|
index_dir = (source_root / ".java-codebase-rag").resolve()
|
|
1566
|
-
run_init_if_needed(
|
|
1886
|
+
init_outcome = run_init_if_needed(
|
|
1567
1887
|
source_root,
|
|
1568
1888
|
index_dir,
|
|
1569
1889
|
resolved_model,
|
|
@@ -1571,5 +1891,20 @@ def run_install(
|
|
|
1571
1891
|
quiet=quiet,
|
|
1572
1892
|
verbose=verbose,
|
|
1573
1893
|
)
|
|
1574
|
-
|
|
1894
|
+
if init_outcome is False:
|
|
1895
|
+
return 1
|
|
1575
1896
|
return 0
|
|
1897
|
+
|
|
1898
|
+
|
|
1899
|
+
def _prior_surface_from_marker(cwd: Path) -> Surface | None:
|
|
1900
|
+
"""Return the (single) surface recorded in the existing marker, if any.
|
|
1901
|
+
|
|
1902
|
+
On multi-surface installs (rare but possible across hosts), returns the
|
|
1903
|
+
first recorded surface — the wizard prefill is a UX nicety, not a contract.
|
|
1904
|
+
Returns ``None`` when no marker exists (fresh install) or the marker is
|
|
1905
|
+
unparseable.
|
|
1906
|
+
"""
|
|
1907
|
+
configured = _read_hosts_marker(cwd)
|
|
1908
|
+
if not configured:
|
|
1909
|
+
return None
|
|
1910
|
+
return configured[0].surface
|