sourcecode 1.30.29__py3-none-any.whl → 1.31.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.
- sourcecode/__init__.py +1 -1
- sourcecode/cli.py +313 -5
- sourcecode/mcp/__init__.py +5 -0
- sourcecode/mcp/onboarding/__init__.py +1 -0
- sourcecode/mcp/onboarding/applier.py +63 -0
- sourcecode/mcp/onboarding/backup.py +40 -0
- sourcecode/mcp/onboarding/detector.py +59 -0
- sourcecode/mcp/onboarding/planner.py +40 -0
- sourcecode/mcp/runner.py +40 -0
- sourcecode/mcp/server.py +147 -0
- sourcecode/prepare_context.py +24 -1
- sourcecode/serializer.py +24 -2
- {sourcecode-1.30.29.dist-info → sourcecode-1.31.0.dist-info}/METADATA +6 -3
- {sourcecode-1.30.29.dist-info → sourcecode-1.31.0.dist-info}/RECORD +17 -9
- {sourcecode-1.30.29.dist-info → sourcecode-1.31.0.dist-info}/WHEEL +0 -0
- {sourcecode-1.30.29.dist-info → sourcecode-1.31.0.dist-info}/entry_points.txt +0 -0
- {sourcecode-1.30.29.dist-info → sourcecode-1.31.0.dist-info}/licenses/LICENSE +0 -0
sourcecode/__init__.py
CHANGED
sourcecode/cli.py
CHANGED
|
@@ -154,6 +154,10 @@ Compressed AI-ready context for Java/Spring enterprise codebases.
|
|
|
154
154
|
|
|
155
155
|
[bold]Subcommands:[/bold]
|
|
156
156
|
prepare-context TASK [PATH] [dim]# task-specific context (onboard, delta, fix-bug, ...)[/dim]
|
|
157
|
+
mcp init [dim]# setup MCP integration (Claude Desktop, Cursor)[/dim]
|
|
158
|
+
mcp status [dim]# show MCP integration status[/dim]
|
|
159
|
+
mcp remove [dim]# remove MCP integration safely[/dim]
|
|
160
|
+
mcp serve [dim]# start MCP server for AI agent integration[/dim]
|
|
157
161
|
telemetry status|enable|disable
|
|
158
162
|
version
|
|
159
163
|
"""
|
|
@@ -161,7 +165,7 @@ Compressed AI-ready context for Java/Spring enterprise codebases.
|
|
|
161
165
|
# Known subcommand names — tokens matching these are routed as subcommands,
|
|
162
166
|
# not consumed as a repository path.
|
|
163
167
|
_SUBCOMMANDS: frozenset[str] = frozenset(
|
|
164
|
-
{"telemetry", "prepare-context", "version", "config", "analyze", "repo-ir"}
|
|
168
|
+
{"telemetry", "prepare-context", "version", "config", "analyze", "repo-ir", "mcp"}
|
|
165
169
|
)
|
|
166
170
|
|
|
167
171
|
# Mutable container holding the path extracted by _preprocess_argv().
|
|
@@ -188,6 +192,7 @@ _OPTIONS_WITH_VALUE: frozenset[str] = frozenset({
|
|
|
188
192
|
"--rank-by",
|
|
189
193
|
"--symbol",
|
|
190
194
|
"--max-importers",
|
|
195
|
+
"--exclude",
|
|
191
196
|
})
|
|
192
197
|
|
|
193
198
|
|
|
@@ -299,6 +304,9 @@ except Exception:
|
|
|
299
304
|
telemetry_app = typer.Typer(help="Manage anonymous telemetry (opt-in).", rich_markup_mode="rich")
|
|
300
305
|
app.add_typer(telemetry_app, name="telemetry")
|
|
301
306
|
|
|
307
|
+
mcp_app = typer.Typer(help="MCP integration: setup, status, serve, remove.", rich_markup_mode="rich")
|
|
308
|
+
app.add_typer(mcp_app, name="mcp")
|
|
309
|
+
|
|
302
310
|
|
|
303
311
|
def _maybe_ask_consent() -> None:
|
|
304
312
|
"""Show first-run consent prompt once, on interactive TTYs only."""
|
|
@@ -316,6 +324,26 @@ def _maybe_ask_consent() -> None:
|
|
|
316
324
|
pass
|
|
317
325
|
|
|
318
326
|
|
|
327
|
+
def _maybe_show_mcp_hint() -> None:
|
|
328
|
+
"""Show MCP integration hint once after first install, on TTY only."""
|
|
329
|
+
import sys as _sys
|
|
330
|
+
try:
|
|
331
|
+
if not _sys.stderr.isatty():
|
|
332
|
+
return
|
|
333
|
+
from sourcecode.telemetry.config import _CONFIG_FILE, _load, _save
|
|
334
|
+
data = _load()
|
|
335
|
+
if data.get("mcp", {}).get("hint_shown"):
|
|
336
|
+
return
|
|
337
|
+
typer.echo("", err=True)
|
|
338
|
+
typer.echo(" MCP integration available:", err=True)
|
|
339
|
+
typer.echo(" → sourcecode mcp init", err=True)
|
|
340
|
+
typer.echo("", err=True)
|
|
341
|
+
data.setdefault("mcp", {})["hint_shown"] = True
|
|
342
|
+
_save(data)
|
|
343
|
+
except Exception:
|
|
344
|
+
pass
|
|
345
|
+
|
|
346
|
+
|
|
319
347
|
def _active_flags(
|
|
320
348
|
dependencies: bool, graph_modules: bool, docs: bool, full_metrics: bool,
|
|
321
349
|
semantics: bool, architecture: bool, git_context: bool, env_map: bool,
|
|
@@ -615,6 +643,7 @@ def main(
|
|
|
615
643
|
# First-run consent (skip for telemetry/version/config subcommands)
|
|
616
644
|
if ctx.invoked_subcommand not in ("telemetry", "version", "config"):
|
|
617
645
|
_maybe_ask_consent()
|
|
646
|
+
_maybe_show_mcp_hint()
|
|
618
647
|
|
|
619
648
|
# When a subcommand is invoked, skip the main analysis.
|
|
620
649
|
if ctx.invoked_subcommand is not None:
|
|
@@ -851,10 +880,18 @@ def main(
|
|
|
851
880
|
_copy_to_clipboard(_cache_hit_content)
|
|
852
881
|
return
|
|
853
882
|
|
|
854
|
-
# BUG-2: parse --exclude into extra_excludes frozenset
|
|
855
883
|
_extra_excludes: Optional[frozenset[str]] = None
|
|
856
884
|
if exclude:
|
|
857
885
|
_extra_excludes = frozenset(e.strip() for e in exclude.split(",") if e.strip())
|
|
886
|
+
# IMP-2: warn if the exclude value looks like it was swallowed as a path
|
|
887
|
+
# (BUG-2 symptom in older versions: --exclude value consumed as repo path).
|
|
888
|
+
import sys as _sys_warn
|
|
889
|
+
if len(_extra_excludes) == 1 and Path(list(_extra_excludes)[0]).is_dir():
|
|
890
|
+
_sys_warn.stderr.write(
|
|
891
|
+
f"[sourcecode] Warning: --exclude value '{list(_extra_excludes)[0]}' is a directory path. "
|
|
892
|
+
"If this was meant as a pattern, use --exclude=pattern or --exclude pattern (both are supported).\n"
|
|
893
|
+
)
|
|
894
|
+
_sys_warn.stderr.flush()
|
|
858
895
|
|
|
859
896
|
_progress = Progress()
|
|
860
897
|
_progress.start("scanning files")
|
|
@@ -1807,6 +1844,11 @@ def prepare_context_cmd(
|
|
|
1807
1844
|
"--fast",
|
|
1808
1845
|
help="Skip deep analysis (content search, test gap discovery, code annotations). Uses manifest/metadata only. Target: < 6 s.",
|
|
1809
1846
|
),
|
|
1847
|
+
include_config: bool = typer.Option(
|
|
1848
|
+
False,
|
|
1849
|
+
"--include-config",
|
|
1850
|
+
help="(generate-tests) Include tooling config files (*.conf.js, .eslintrc*, etc.) in test_gaps. Excluded by default.",
|
|
1851
|
+
),
|
|
1810
1852
|
) -> None:
|
|
1811
1853
|
"""Task-specific context for AI coding agents.
|
|
1812
1854
|
|
|
@@ -1888,7 +1930,7 @@ def prepare_context_cmd(
|
|
|
1888
1930
|
_sys.stderr.flush()
|
|
1889
1931
|
_t0 = _time.perf_counter()
|
|
1890
1932
|
try:
|
|
1891
|
-
output = builder.build(task, since=since, symptom=symptom, fast=fast)
|
|
1933
|
+
output = builder.build(task, since=since, symptom=symptom, fast=fast, include_config=include_config)
|
|
1892
1934
|
finally:
|
|
1893
1935
|
_progress.finish()
|
|
1894
1936
|
_t_total = (_time.perf_counter() - _t0) * 1000
|
|
@@ -2359,8 +2401,23 @@ def repo_ir_cmd(
|
|
|
2359
2401
|
err=True,
|
|
2360
2402
|
)
|
|
2361
2403
|
else:
|
|
2362
|
-
|
|
2363
|
-
|
|
2404
|
+
try:
|
|
2405
|
+
_sys.stdout.buffer.write(output.encode("utf-8"))
|
|
2406
|
+
_sys.stdout.buffer.write(b"\n")
|
|
2407
|
+
_sys.stdout.buffer.flush()
|
|
2408
|
+
except UnicodeEncodeError as _ue:
|
|
2409
|
+
# IMP-2: emit workaround before re-raising so the user knows what to do.
|
|
2410
|
+
_sys.stderr.write(
|
|
2411
|
+
f"[sourcecode] UnicodeEncodeError on stdout ({_ue.encoding}): "
|
|
2412
|
+
"your console codec cannot encode this output.\n"
|
|
2413
|
+
"Workaround: sourcecode repo-ir --output ir.json\n"
|
|
2414
|
+
)
|
|
2415
|
+
_sys.stderr.flush()
|
|
2416
|
+
raise
|
|
2417
|
+
except AttributeError:
|
|
2418
|
+
# Fallback for wrapped stdout without buffer (e.g. some test harnesses)
|
|
2419
|
+
_sys.stdout.write(output)
|
|
2420
|
+
_sys.stdout.write("\n")
|
|
2364
2421
|
|
|
2365
2422
|
|
|
2366
2423
|
# ── version ───────────────────────────────────────────────────────────────────
|
|
@@ -2403,6 +2460,249 @@ def analyze_cmd(
|
|
|
2403
2460
|
raise typer.Exit(code=1)
|
|
2404
2461
|
|
|
2405
2462
|
|
|
2463
|
+
# ── MCP server ────────────────────────────────────────────────────────────────
|
|
2464
|
+
|
|
2465
|
+
@mcp_app.command("serve")
|
|
2466
|
+
def mcp_serve() -> None:
|
|
2467
|
+
"""Start the MCP server on stdio for AI agent integration.
|
|
2468
|
+
|
|
2469
|
+
\b
|
|
2470
|
+
Requires the 'mcp' extra:
|
|
2471
|
+
pip install sourcecode[mcp]
|
|
2472
|
+
|
|
2473
|
+
\b
|
|
2474
|
+
Configure in your MCP client (e.g. Claude Desktop):
|
|
2475
|
+
{
|
|
2476
|
+
"sourcecode": {
|
|
2477
|
+
"command": "sourcecode",
|
|
2478
|
+
"args": ["mcp", "serve"]
|
|
2479
|
+
}
|
|
2480
|
+
}
|
|
2481
|
+
"""
|
|
2482
|
+
import logging
|
|
2483
|
+
import sys as _sys
|
|
2484
|
+
|
|
2485
|
+
logging.basicConfig(
|
|
2486
|
+
stream=_sys.stderr,
|
|
2487
|
+
level=logging.INFO,
|
|
2488
|
+
format="[sourcecode-mcp] %(levelname)s %(message)s",
|
|
2489
|
+
)
|
|
2490
|
+
try:
|
|
2491
|
+
from sourcecode.mcp.server import mcp as _mcp
|
|
2492
|
+
except ImportError:
|
|
2493
|
+
typer.echo(
|
|
2494
|
+
"MCP support not available. Install with:\n"
|
|
2495
|
+
" pip install sourcecode[mcp]",
|
|
2496
|
+
err=True,
|
|
2497
|
+
)
|
|
2498
|
+
raise typer.Exit(code=1)
|
|
2499
|
+
|
|
2500
|
+
log = logging.getLogger(__name__)
|
|
2501
|
+
log.info("sourcecode-mcp starting (stdio transport)")
|
|
2502
|
+
try:
|
|
2503
|
+
_mcp.run()
|
|
2504
|
+
except KeyboardInterrupt:
|
|
2505
|
+
log.info("sourcecode-mcp stopped")
|
|
2506
|
+
except Exception as exc:
|
|
2507
|
+
log.critical("sourcecode-mcp fatal error: %s", exc, exc_info=True)
|
|
2508
|
+
raise typer.Exit(code=1)
|
|
2509
|
+
|
|
2510
|
+
|
|
2511
|
+
# ── MCP onboarding ────────────────────────────────────────────────────────────
|
|
2512
|
+
|
|
2513
|
+
@mcp_app.command("init")
|
|
2514
|
+
def mcp_init(
|
|
2515
|
+
yes: bool = typer.Option(False, "--yes", "-y", help="Skip confirmation prompt."),
|
|
2516
|
+
) -> None:
|
|
2517
|
+
"""Setup MCP integration for Claude Desktop, Cursor, and other clients.
|
|
2518
|
+
|
|
2519
|
+
\b
|
|
2520
|
+
Detects installed MCP clients, backs up their config files, and safely
|
|
2521
|
+
inserts the sourcecode server entry. Fully idempotent — safe to re-run.
|
|
2522
|
+
"""
|
|
2523
|
+
from sourcecode.mcp.onboarding.detector import detect_clients
|
|
2524
|
+
from sourcecode.mcp.onboarding.planner import build_install_plan
|
|
2525
|
+
from sourcecode.mcp.onboarding import backup, applier
|
|
2526
|
+
|
|
2527
|
+
typer.echo("Detecting MCP clients...")
|
|
2528
|
+
typer.echo("")
|
|
2529
|
+
|
|
2530
|
+
clients = detect_clients()
|
|
2531
|
+
if not clients:
|
|
2532
|
+
typer.echo("No MCP clients found on this system.")
|
|
2533
|
+
typer.echo("")
|
|
2534
|
+
typer.echo("Manual setup — add to your MCP client config:")
|
|
2535
|
+
typer.echo(' "sourcecode": {"command": "sourcecode", "args": ["mcp", "serve"]}')
|
|
2536
|
+
raise typer.Exit(code=0)
|
|
2537
|
+
|
|
2538
|
+
# Show detection results
|
|
2539
|
+
for client in clients:
|
|
2540
|
+
mark = "✓" if client.app_installed else "○"
|
|
2541
|
+
note = "" if client.app_installed else " (not found)"
|
|
2542
|
+
typer.echo(f" {mark} {client.name:<18} {client.config_path}{note}")
|
|
2543
|
+
typer.echo("")
|
|
2544
|
+
|
|
2545
|
+
# Build plan
|
|
2546
|
+
plan = build_install_plan(clients)
|
|
2547
|
+
actionable = [a for a in plan if a.client.app_installed and not a.already_installed]
|
|
2548
|
+
already_done = [a for a in plan if a.client.app_installed and a.already_installed]
|
|
2549
|
+
|
|
2550
|
+
if already_done and not actionable:
|
|
2551
|
+
typer.echo("Already configured:")
|
|
2552
|
+
for a in already_done:
|
|
2553
|
+
typer.echo(f" ✓ {a.client.name} {a.client.config_path}")
|
|
2554
|
+
typer.echo("")
|
|
2555
|
+
typer.echo("Nothing to do. Remove: sourcecode mcp remove")
|
|
2556
|
+
raise typer.Exit(code=0)
|
|
2557
|
+
|
|
2558
|
+
if already_done:
|
|
2559
|
+
typer.echo("Already configured:")
|
|
2560
|
+
for a in already_done:
|
|
2561
|
+
typer.echo(f" ✓ {a.client.name} {a.client.config_path}")
|
|
2562
|
+
typer.echo("")
|
|
2563
|
+
|
|
2564
|
+
# Show plan for actionable items
|
|
2565
|
+
typer.echo("This will:")
|
|
2566
|
+
for a in actionable:
|
|
2567
|
+
verb = "Create " if a.will_create_file else "Modify "
|
|
2568
|
+
typer.echo(f" {verb} {a.client.config_path}")
|
|
2569
|
+
typer.echo(f" Backup → ~/.config/sourcecode/mcp-backups/")
|
|
2570
|
+
typer.echo("")
|
|
2571
|
+
|
|
2572
|
+
if not yes:
|
|
2573
|
+
confirmed = typer.confirm("Proceed?", default=False)
|
|
2574
|
+
if not confirmed:
|
|
2575
|
+
typer.echo("Aborted.")
|
|
2576
|
+
raise typer.Exit(code=0)
|
|
2577
|
+
typer.echo("")
|
|
2578
|
+
|
|
2579
|
+
# Apply
|
|
2580
|
+
errors: list[str] = []
|
|
2581
|
+
for a in actionable:
|
|
2582
|
+
try:
|
|
2583
|
+
config = applier.read_config(a.client.config_path)
|
|
2584
|
+
if a.client.config_path.exists():
|
|
2585
|
+
bak = backup.create(a.client.config_path)
|
|
2586
|
+
typer.echo(f" ✓ Backup {bak}")
|
|
2587
|
+
updated = applier.apply_entry(config)
|
|
2588
|
+
applier.write_config(a.client.config_path, updated)
|
|
2589
|
+
if not applier.validate(a.client.config_path):
|
|
2590
|
+
errors.append(f"{a.client.name}: JSON validation failed after write")
|
|
2591
|
+
continue
|
|
2592
|
+
typer.echo(f" ✓ Updated {a.client.config_path}")
|
|
2593
|
+
except Exception as exc:
|
|
2594
|
+
errors.append(f"{a.client.name}: {exc}")
|
|
2595
|
+
|
|
2596
|
+
typer.echo("")
|
|
2597
|
+
|
|
2598
|
+
if errors:
|
|
2599
|
+
for err in errors:
|
|
2600
|
+
typer.echo(f" ✗ {err}", err=True)
|
|
2601
|
+
raise typer.Exit(code=1)
|
|
2602
|
+
|
|
2603
|
+
typer.echo("MCP integration active.")
|
|
2604
|
+
typer.echo("")
|
|
2605
|
+
|
|
2606
|
+
restart_needed = [a.client.name for a in actionable if not a.will_create_file]
|
|
2607
|
+
if restart_needed:
|
|
2608
|
+
typer.echo(f" Restart {', '.join(restart_needed)} to apply changes.")
|
|
2609
|
+
typer.echo(" Remove: sourcecode mcp remove")
|
|
2610
|
+
|
|
2611
|
+
|
|
2612
|
+
@mcp_app.command("status")
|
|
2613
|
+
def mcp_status() -> None:
|
|
2614
|
+
"""Show MCP integration status for all detected clients."""
|
|
2615
|
+
from sourcecode.mcp.onboarding.detector import detect_clients
|
|
2616
|
+
from sourcecode.mcp.onboarding import applier
|
|
2617
|
+
|
|
2618
|
+
clients = detect_clients()
|
|
2619
|
+
typer.echo("MCP Integration Status")
|
|
2620
|
+
typer.echo("")
|
|
2621
|
+
|
|
2622
|
+
if not clients:
|
|
2623
|
+
typer.echo(" No MCP clients detected on this system.")
|
|
2624
|
+
raise typer.Exit(code=0)
|
|
2625
|
+
|
|
2626
|
+
for client in clients:
|
|
2627
|
+
if not client.app_installed:
|
|
2628
|
+
typer.echo(f" ○ {client.name:<18} not found")
|
|
2629
|
+
continue
|
|
2630
|
+
config = applier.read_config(client.config_path)
|
|
2631
|
+
if applier.is_installed(config):
|
|
2632
|
+
typer.echo(f" ✓ {client.name:<18} configured {client.config_path}")
|
|
2633
|
+
else:
|
|
2634
|
+
typer.echo(f" ✗ {client.name:<18} not configured")
|
|
2635
|
+
typer.echo("")
|
|
2636
|
+
typer.echo(" Setup: sourcecode mcp init")
|
|
2637
|
+
typer.echo(" Remove: sourcecode mcp remove")
|
|
2638
|
+
|
|
2639
|
+
|
|
2640
|
+
@mcp_app.command("remove")
|
|
2641
|
+
def mcp_remove(
|
|
2642
|
+
yes: bool = typer.Option(False, "--yes", "-y", help="Skip confirmation prompt."),
|
|
2643
|
+
) -> None:
|
|
2644
|
+
"""Remove sourcecode MCP integration from all configured clients.
|
|
2645
|
+
|
|
2646
|
+
\b
|
|
2647
|
+
Backs up config files before modifying. Restores from backup when available,
|
|
2648
|
+
otherwise removes the sourcecode entry while preserving all other config.
|
|
2649
|
+
"""
|
|
2650
|
+
from sourcecode.mcp.onboarding.detector import detect_clients
|
|
2651
|
+
from sourcecode.mcp.onboarding.planner import build_remove_plan
|
|
2652
|
+
from sourcecode.mcp.onboarding import backup, applier
|
|
2653
|
+
|
|
2654
|
+
clients = detect_clients()
|
|
2655
|
+
plan = build_remove_plan(clients)
|
|
2656
|
+
installed = [a for a in plan if a.already_installed]
|
|
2657
|
+
|
|
2658
|
+
if not installed:
|
|
2659
|
+
typer.echo("sourcecode MCP integration not found in any client config.")
|
|
2660
|
+
typer.echo(" Setup: sourcecode mcp init")
|
|
2661
|
+
raise typer.Exit(code=0)
|
|
2662
|
+
|
|
2663
|
+
typer.echo("Remove sourcecode MCP integration from:")
|
|
2664
|
+
typer.echo("")
|
|
2665
|
+
for a in installed:
|
|
2666
|
+
typer.echo(f" {a.client.name} {a.client.config_path}")
|
|
2667
|
+
bak = backup.latest(a.client.config_path)
|
|
2668
|
+
if bak:
|
|
2669
|
+
typer.echo(f" Backup available: {bak}")
|
|
2670
|
+
typer.echo("")
|
|
2671
|
+
|
|
2672
|
+
if not yes:
|
|
2673
|
+
confirmed = typer.confirm("Proceed?", default=False)
|
|
2674
|
+
if not confirmed:
|
|
2675
|
+
typer.echo("Aborted.")
|
|
2676
|
+
raise typer.Exit(code=0)
|
|
2677
|
+
typer.echo("")
|
|
2678
|
+
|
|
2679
|
+
errors: list[str] = []
|
|
2680
|
+
for a in installed:
|
|
2681
|
+
try:
|
|
2682
|
+
bak = backup.create(a.client.config_path)
|
|
2683
|
+
typer.echo(f" ✓ Backup {bak}")
|
|
2684
|
+
config = applier.read_config(a.client.config_path)
|
|
2685
|
+
updated = applier.remove_entry(config)
|
|
2686
|
+
applier.write_config(a.client.config_path, updated)
|
|
2687
|
+
if not applier.validate(a.client.config_path):
|
|
2688
|
+
errors.append(f"{a.client.name}: JSON validation failed — restoring backup")
|
|
2689
|
+
backup.restore(bak, a.client.config_path)
|
|
2690
|
+
continue
|
|
2691
|
+
typer.echo(f" ✓ Updated {a.client.config_path}")
|
|
2692
|
+
except Exception as exc:
|
|
2693
|
+
errors.append(f"{a.client.name}: {exc}")
|
|
2694
|
+
|
|
2695
|
+
typer.echo("")
|
|
2696
|
+
|
|
2697
|
+
if errors:
|
|
2698
|
+
for err in errors:
|
|
2699
|
+
typer.echo(f" ✗ {err}", err=True)
|
|
2700
|
+
raise typer.Exit(code=1)
|
|
2701
|
+
|
|
2702
|
+
typer.echo("MCP integration removed.")
|
|
2703
|
+
typer.echo(" Re-add: sourcecode mcp init")
|
|
2704
|
+
|
|
2705
|
+
|
|
2406
2706
|
# ── Entry point ───────────────────────────────────────────────────────────────
|
|
2407
2707
|
|
|
2408
2708
|
def main_entry() -> None:
|
|
@@ -2413,5 +2713,13 @@ def main_entry() -> None:
|
|
|
2413
2713
|
can consume them as positional arguments (which would prevent subcommand
|
|
2414
2714
|
routing for tokens like 'version' or 'config').
|
|
2415
2715
|
"""
|
|
2716
|
+
import sys as _sys
|
|
2717
|
+
# Force UTF-8 on stdout so Unicode characters (arrows, etc.) survive on
|
|
2718
|
+
# Windows where the default console codec is cp1252 (BUG-1).
|
|
2719
|
+
if hasattr(_sys.stdout, "reconfigure"):
|
|
2720
|
+
try:
|
|
2721
|
+
_sys.stdout.reconfigure(encoding="utf-8")
|
|
2722
|
+
except Exception:
|
|
2723
|
+
pass
|
|
2416
2724
|
_preprocess_argv()
|
|
2417
2725
|
app()
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""MCP client onboarding: detect, plan, apply, backup, remove."""
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
"""Safe JSON config applier for MCP client configuration files."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import json
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
_MCP_SERVERS_KEY = "mcpServers"
|
|
8
|
+
_ENTRY_NAME = "sourcecode"
|
|
9
|
+
_ENTRY_VALUE: dict[str, object] = {
|
|
10
|
+
"command": "sourcecode",
|
|
11
|
+
"args": ["mcp", "serve"],
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def read_config(path: Path) -> dict:
|
|
16
|
+
"""Parse JSON config from path. Returns empty dict if missing or empty."""
|
|
17
|
+
if not path.exists():
|
|
18
|
+
return {}
|
|
19
|
+
raw = path.read_text(encoding="utf-8").strip()
|
|
20
|
+
if not raw:
|
|
21
|
+
return {}
|
|
22
|
+
return json.loads(raw) # type: ignore[no-any-return]
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def is_installed(config: dict) -> bool:
|
|
26
|
+
"""True if sourcecode entry already present in mcpServers."""
|
|
27
|
+
return _ENTRY_NAME in config.get(_MCP_SERVERS_KEY, {})
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def apply_entry(config: dict) -> dict:
|
|
31
|
+
"""Return new config dict with sourcecode merged into mcpServers."""
|
|
32
|
+
config = dict(config)
|
|
33
|
+
servers: dict = dict(config.get(_MCP_SERVERS_KEY, {}))
|
|
34
|
+
servers[_ENTRY_NAME] = _ENTRY_VALUE
|
|
35
|
+
config[_MCP_SERVERS_KEY] = servers
|
|
36
|
+
return config
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def remove_entry(config: dict) -> dict:
|
|
40
|
+
"""Return new config dict with sourcecode removed from mcpServers."""
|
|
41
|
+
config = dict(config)
|
|
42
|
+
servers: dict = dict(config.get(_MCP_SERVERS_KEY, {}))
|
|
43
|
+
servers.pop(_ENTRY_NAME, None)
|
|
44
|
+
if servers:
|
|
45
|
+
config[_MCP_SERVERS_KEY] = servers
|
|
46
|
+
elif _MCP_SERVERS_KEY in config:
|
|
47
|
+
del config[_MCP_SERVERS_KEY]
|
|
48
|
+
return config
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def write_config(path: Path, config: dict) -> None:
|
|
52
|
+
"""Atomically write config as formatted JSON."""
|
|
53
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
54
|
+
path.write_text(json.dumps(config, indent=2) + "\n", encoding="utf-8")
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def validate(path: Path) -> bool:
|
|
58
|
+
"""True if path contains parseable JSON."""
|
|
59
|
+
try:
|
|
60
|
+
json.loads(path.read_text(encoding="utf-8"))
|
|
61
|
+
return True
|
|
62
|
+
except Exception:
|
|
63
|
+
return False
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"""Timestamped backup management for MCP config files."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from datetime import datetime
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
_BACKUP_DIR = Path.home() / ".config" / "sourcecode" / "mcp-backups"
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def _backup_stem(config_path: Path) -> str:
|
|
11
|
+
"""Stable prefix derived from the config path, safe for filenames."""
|
|
12
|
+
parts = config_path.parts
|
|
13
|
+
# Use last two path components to keep names readable but unique enough.
|
|
14
|
+
label = "_".join(p for p in parts[-2:] if p).replace(".", "_")
|
|
15
|
+
return label
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def create(config_path: Path) -> Path:
|
|
19
|
+
"""Copy config_path to a timestamped backup file. Returns backup path."""
|
|
20
|
+
_BACKUP_DIR.mkdir(parents=True, exist_ok=True)
|
|
21
|
+
ts = datetime.now().strftime("%Y%m%dT%H%M%S")
|
|
22
|
+
stem = _backup_stem(config_path)
|
|
23
|
+
backup_path = _BACKUP_DIR / f"{stem}.{ts}.bak"
|
|
24
|
+
backup_path.write_bytes(config_path.read_bytes())
|
|
25
|
+
return backup_path
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def restore(backup_path: Path, target_path: Path) -> None:
|
|
29
|
+
"""Overwrite target_path with contents of backup_path."""
|
|
30
|
+
target_path.parent.mkdir(parents=True, exist_ok=True)
|
|
31
|
+
target_path.write_bytes(backup_path.read_bytes())
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def latest(config_path: Path) -> Path | None:
|
|
35
|
+
"""Find the most recent backup for config_path, or None."""
|
|
36
|
+
if not _BACKUP_DIR.exists():
|
|
37
|
+
return None
|
|
38
|
+
stem = _backup_stem(config_path)
|
|
39
|
+
matches = sorted(_BACKUP_DIR.glob(f"{stem}.*.bak"))
|
|
40
|
+
return matches[-1] if matches else None
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"""Detect MCP-capable clients installed on the current machine."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import os
|
|
5
|
+
import sys
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
# Registry of known MCP clients and their config paths per platform.
|
|
11
|
+
# Keys: "darwin", "linux", "win32" — matching sys.platform values.
|
|
12
|
+
_CLIENT_REGISTRY: dict[str, dict[str, str]] = {
|
|
13
|
+
"Claude Desktop": {
|
|
14
|
+
"darwin": "~/Library/Application Support/Claude/claude_desktop_config.json",
|
|
15
|
+
"linux": "~/.config/Claude/claude_desktop_config.json",
|
|
16
|
+
"win32": "{APPDATA}/Claude/claude_desktop_config.json",
|
|
17
|
+
},
|
|
18
|
+
"Cursor": {
|
|
19
|
+
"darwin": "~/.cursor/mcp.json",
|
|
20
|
+
"linux": "~/.cursor/mcp.json",
|
|
21
|
+
"win32": "{USERPROFILE}/.cursor/mcp.json",
|
|
22
|
+
},
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@dataclass(frozen=True)
|
|
27
|
+
class MCPClient:
|
|
28
|
+
name: str
|
|
29
|
+
config_path: Path
|
|
30
|
+
app_installed: bool # True if the config file (or its parent dir) exists
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _resolve(template: str) -> Path:
|
|
34
|
+
"""Expand env vars in Windows-style {VAR} templates, then expanduser."""
|
|
35
|
+
result = template
|
|
36
|
+
for var in ("APPDATA", "LOCALAPPDATA", "USERPROFILE"):
|
|
37
|
+
val = os.environ.get(var, "")
|
|
38
|
+
if val:
|
|
39
|
+
result = result.replace(f"{{{var}}}", val)
|
|
40
|
+
return Path(result).expanduser()
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def detect_clients() -> list[MCPClient]:
|
|
44
|
+
"""Return all known MCP clients with their resolved config paths."""
|
|
45
|
+
plat = sys.platform
|
|
46
|
+
clients: list[MCPClient] = []
|
|
47
|
+
for name, paths in _CLIENT_REGISTRY.items():
|
|
48
|
+
template = paths.get(plat) or paths.get("linux")
|
|
49
|
+
if not template:
|
|
50
|
+
continue
|
|
51
|
+
config_path = _resolve(template)
|
|
52
|
+
# Consider client "installed" if its config file OR parent app dir exists.
|
|
53
|
+
app_installed = config_path.exists() or config_path.parent.exists()
|
|
54
|
+
clients.append(MCPClient(
|
|
55
|
+
name=name,
|
|
56
|
+
config_path=config_path,
|
|
57
|
+
app_installed=app_installed,
|
|
58
|
+
))
|
|
59
|
+
return clients
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"""Build an install/remove plan from detected MCP clients."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
|
|
6
|
+
from .applier import is_installed, read_config
|
|
7
|
+
from .detector import MCPClient
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@dataclass(frozen=True)
|
|
11
|
+
class ClientAction:
|
|
12
|
+
client: MCPClient
|
|
13
|
+
already_installed: bool # sourcecode entry already in config
|
|
14
|
+
will_create_file: bool # config file doesn't exist yet — will be created
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def build_install_plan(clients: list[MCPClient]) -> list[ClientAction]:
|
|
18
|
+
"""Describe what `mcp init` would do for each detected client."""
|
|
19
|
+
actions: list[ClientAction] = []
|
|
20
|
+
for client in clients:
|
|
21
|
+
config = read_config(client.config_path)
|
|
22
|
+
actions.append(ClientAction(
|
|
23
|
+
client=client,
|
|
24
|
+
already_installed=is_installed(config),
|
|
25
|
+
will_create_file=not client.config_path.exists(),
|
|
26
|
+
))
|
|
27
|
+
return actions
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def build_remove_plan(clients: list[MCPClient]) -> list[ClientAction]:
|
|
31
|
+
"""Describe what `mcp remove` would do for each detected client."""
|
|
32
|
+
actions: list[ClientAction] = []
|
|
33
|
+
for client in clients:
|
|
34
|
+
config = read_config(client.config_path)
|
|
35
|
+
actions.append(ClientAction(
|
|
36
|
+
client=client,
|
|
37
|
+
already_installed=is_installed(config),
|
|
38
|
+
will_create_file=False,
|
|
39
|
+
))
|
|
40
|
+
return actions
|
sourcecode/mcp/runner.py
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"""In-process CLI runner for MCP tool execution.
|
|
2
|
+
|
|
3
|
+
Replaces the subprocess adapter from the standalone sourcecode-mcp project.
|
|
4
|
+
Calls CLI commands directly in the same process via CliRunner — no binary
|
|
5
|
+
lookup, no process fork, no stdout encoding issues.
|
|
6
|
+
"""
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from typer.testing import CliRunner
|
|
10
|
+
|
|
11
|
+
_runner = CliRunner()
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def run_command(args: list[str]) -> str:
|
|
15
|
+
"""Invoke a sourcecode CLI command in-process and return stdout.
|
|
16
|
+
|
|
17
|
+
Raises RuntimeError on non-zero exit or empty output.
|
|
18
|
+
"""
|
|
19
|
+
from sourcecode.cli import _detected_path, _preprocess_args, app
|
|
20
|
+
|
|
21
|
+
_detected_path[0] = "."
|
|
22
|
+
processed = _preprocess_args(list(args))
|
|
23
|
+
result = _runner.invoke(app, processed)
|
|
24
|
+
|
|
25
|
+
if result.exit_code != 0:
|
|
26
|
+
snippet = (result.output or "").strip()
|
|
27
|
+
raise RuntimeError(
|
|
28
|
+
f"sourcecode command failed (exit {result.exit_code}).\n"
|
|
29
|
+
f"Args: {args}\n"
|
|
30
|
+
f"Output: {snippet or '(empty)'}"
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
output = (result.output or "").strip()
|
|
34
|
+
if not output:
|
|
35
|
+
raise RuntimeError(
|
|
36
|
+
f"sourcecode command produced no output.\n"
|
|
37
|
+
f"Args: {args}"
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
return output
|
sourcecode/mcp/server.py
ADDED
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
"""MCP server for sourcecode CLI.
|
|
2
|
+
|
|
3
|
+
Exposes sourcecode capabilities as MCP tools. Each tool maps to a CLI command
|
|
4
|
+
and delegates execution to the in-process runner — no subprocess, no binary
|
|
5
|
+
lookup, same process as the CLI.
|
|
6
|
+
|
|
7
|
+
All tools return the canonical contract:
|
|
8
|
+
{"success": bool, "data": str | None, "error": {"code": str, "message": str} | None}
|
|
9
|
+
"""
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from typing import Literal
|
|
13
|
+
|
|
14
|
+
from mcp.server.fastmcp import FastMCP
|
|
15
|
+
|
|
16
|
+
from sourcecode.mcp.runner import run_command
|
|
17
|
+
|
|
18
|
+
mcp = FastMCP("sourcecode")
|
|
19
|
+
|
|
20
|
+
_PREPARE_CONTEXT_TASKS = frozenset({
|
|
21
|
+
"delta", "review-pr", "fix-bug", "onboard",
|
|
22
|
+
"explain", "refactor", "generate-tests",
|
|
23
|
+
})
|
|
24
|
+
_TELEMETRY_ACTIONS = frozenset({"status", "enable", "disable"})
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _ok(data: str) -> dict:
|
|
28
|
+
return {"success": True, "data": data, "error": None}
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _err(message: str, code: str = "EXECUTION_FAILED") -> dict:
|
|
32
|
+
return {"success": False, "data": None, "error": {"code": code, "message": message}}
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _execute(args: list[str]) -> dict:
|
|
36
|
+
try:
|
|
37
|
+
return _ok(run_command(args))
|
|
38
|
+
except RuntimeError as exc:
|
|
39
|
+
return _err(str(exc))
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@mcp.tool()
|
|
43
|
+
def compact(path: str, git_context: bool = False) -> dict:
|
|
44
|
+
"""Compact analysis of a repository (~1000-3000 tokens).
|
|
45
|
+
|
|
46
|
+
Maps to: sourcecode <path> --compact [--git-context]
|
|
47
|
+
"""
|
|
48
|
+
if not isinstance(path, str):
|
|
49
|
+
return _err("path must be a string", "INVALID_ARGUMENT")
|
|
50
|
+
if not isinstance(git_context, bool):
|
|
51
|
+
return _err("git_context must be boolean", "INVALID_ARGUMENT")
|
|
52
|
+
args = [path, "--compact"]
|
|
53
|
+
if git_context:
|
|
54
|
+
args.append("--git-context")
|
|
55
|
+
return _execute(args)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
@mcp.tool()
|
|
59
|
+
def agent(path: str, git_context: bool = False) -> dict:
|
|
60
|
+
"""Agent-optimised analysis: identity, entry points, dependencies, gaps.
|
|
61
|
+
|
|
62
|
+
Maps to: sourcecode <path> --agent [--git-context]
|
|
63
|
+
"""
|
|
64
|
+
if not isinstance(path, str):
|
|
65
|
+
return _err("path must be a string", "INVALID_ARGUMENT")
|
|
66
|
+
if not isinstance(git_context, bool):
|
|
67
|
+
return _err("git_context must be boolean", "INVALID_ARGUMENT")
|
|
68
|
+
args = [path, "--agent"]
|
|
69
|
+
if git_context:
|
|
70
|
+
args.append("--git-context")
|
|
71
|
+
return _execute(args)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
@mcp.tool()
|
|
75
|
+
def prepare_context(
|
|
76
|
+
task: Literal[
|
|
77
|
+
"delta", "review-pr", "fix-bug", "onboard",
|
|
78
|
+
"explain", "refactor", "generate-tests",
|
|
79
|
+
],
|
|
80
|
+
path: str,
|
|
81
|
+
) -> dict:
|
|
82
|
+
"""Task-specific context for AI coding agents.
|
|
83
|
+
|
|
84
|
+
Maps to: sourcecode prepare-context <task> <path>
|
|
85
|
+
|
|
86
|
+
task must be one of:
|
|
87
|
+
explain Architecture, entry points, key dependencies
|
|
88
|
+
fix-bug Risk-ranked files, suspected areas, annotations
|
|
89
|
+
refactor Structural issues, improvement opportunities
|
|
90
|
+
generate-tests Untested source files, test gap analysis
|
|
91
|
+
onboard Full project context for new agents/developers
|
|
92
|
+
review-pr PR diff with runtime signals and security impact
|
|
93
|
+
delta Incremental context: git-changed files only
|
|
94
|
+
"""
|
|
95
|
+
if task not in _PREPARE_CONTEXT_TASKS:
|
|
96
|
+
return _err(
|
|
97
|
+
f"task must be one of {sorted(_PREPARE_CONTEXT_TASKS)}",
|
|
98
|
+
"INVALID_ARGUMENT",
|
|
99
|
+
)
|
|
100
|
+
if not isinstance(path, str):
|
|
101
|
+
return _err("path must be a string", "INVALID_ARGUMENT")
|
|
102
|
+
return _execute(["prepare-context", task, path])
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
@mcp.tool()
|
|
106
|
+
def repo_ir(path: str) -> dict:
|
|
107
|
+
"""Deterministic symbol-level IR for Java repositories.
|
|
108
|
+
|
|
109
|
+
Maps to: sourcecode repo-ir <path>
|
|
110
|
+
Output is JSON: graph{nodes,edges}, analysis, impact, subsystems, change_set.
|
|
111
|
+
"""
|
|
112
|
+
if not isinstance(path, str):
|
|
113
|
+
return _err("path must be a string", "INVALID_ARGUMENT")
|
|
114
|
+
return _execute(["repo-ir", path])
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
@mcp.tool()
|
|
118
|
+
def version() -> dict:
|
|
119
|
+
"""Return the installed sourcecode version.
|
|
120
|
+
|
|
121
|
+
Maps to: sourcecode version
|
|
122
|
+
"""
|
|
123
|
+
return _execute(["version"])
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
@mcp.tool()
|
|
127
|
+
def config() -> dict:
|
|
128
|
+
"""Show sourcecode configuration (version, telemetry state, config path).
|
|
129
|
+
|
|
130
|
+
Maps to: sourcecode config
|
|
131
|
+
"""
|
|
132
|
+
return _execute(["config"])
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
@mcp.tool()
|
|
136
|
+
def telemetry(action: Literal["status", "enable", "disable"]) -> dict:
|
|
137
|
+
"""Manage telemetry settings.
|
|
138
|
+
|
|
139
|
+
Maps to: sourcecode telemetry <status|enable|disable>
|
|
140
|
+
action must be one of: status, enable, disable
|
|
141
|
+
"""
|
|
142
|
+
if action not in _TELEMETRY_ACTIONS:
|
|
143
|
+
return _err(
|
|
144
|
+
f"action must be one of {sorted(_TELEMETRY_ACTIONS)}",
|
|
145
|
+
"INVALID_ARGUMENT",
|
|
146
|
+
)
|
|
147
|
+
return _execute(["telemetry", action])
|
sourcecode/prepare_context.py
CHANGED
|
@@ -712,7 +712,7 @@ class TaskContextBuilder:
|
|
|
712
712
|
def __init__(self, root: Path) -> None:
|
|
713
713
|
self.root = root
|
|
714
714
|
|
|
715
|
-
def build(self, task_name: str, *, since: Optional[str] = None, symptom: Optional[str] = None, fast: bool = False) -> TaskOutput:
|
|
715
|
+
def build(self, task_name: str, *, since: Optional[str] = None, symptom: Optional[str] = None, fast: bool = False, include_config: bool = False) -> TaskOutput:
|
|
716
716
|
if task_name not in TASKS:
|
|
717
717
|
raise ValueError(
|
|
718
718
|
f"Unknown task '{task_name}'. Available: {', '.join(TASKS)}"
|
|
@@ -788,6 +788,14 @@ class TaskContextBuilder:
|
|
|
788
788
|
# for behavioral_impact reverse lookups without scanning the whole repo).
|
|
789
789
|
file_tree: dict = {}
|
|
790
790
|
all_paths = self._expand_scope_for_analysis(_pr_scope_files or [])
|
|
791
|
+
elif fast and task_name == "onboard":
|
|
792
|
+
# Onboard fast: always use shallow scan so manifests and entry points
|
|
793
|
+
# are discoverable — git-changed-only mode would return only dirty files
|
|
794
|
+
# (e.g. .idea/vcs.xml) which yields no useful entry points (BUG-3).
|
|
795
|
+
scanner = AdaptiveScanner(self.root, base_depth=2)
|
|
796
|
+
file_tree = scanner.scan_tree()
|
|
797
|
+
manifests = scanner.find_manifests()
|
|
798
|
+
all_paths = [p.replace("\\", "/") for p in flatten_file_tree(file_tree)]
|
|
791
799
|
elif fast and _count_files_bounded(self.root) > MAX_FILES_FAST:
|
|
792
800
|
# Fast mode on large repo: git-index-only — only scan git-changed files.
|
|
793
801
|
# Skips full AdaptiveScanner traversal which takes 35s+ on 7k+ file repos.
|
|
@@ -1652,11 +1660,26 @@ class TaskContextBuilder:
|
|
|
1652
1660
|
# Python/JS: test_foo / foo_test
|
|
1653
1661
|
return stem.removeprefix("test_").removesuffix("_test")
|
|
1654
1662
|
|
|
1663
|
+
# Patterns excluded from test_gaps by default (IMP-1): tooling config
|
|
1664
|
+
# files have no business logic to test. --include-config overrides.
|
|
1665
|
+
_CONFIG_EXCLUDE_PATTERNS = (
|
|
1666
|
+
".eslintrc", ".prettierrc", "eslint.config",
|
|
1667
|
+
"karma.conf", "jest.config", "babel.config",
|
|
1668
|
+
"webpack.config", "vite.config", "rollup.config",
|
|
1669
|
+
"tsconfig", "angular.json", ".claude/",
|
|
1670
|
+
)
|
|
1671
|
+
|
|
1672
|
+
def _is_config_file(p: str) -> bool:
|
|
1673
|
+
name = Path(p).name.lower()
|
|
1674
|
+
norm = p.replace("\\", "/")
|
|
1675
|
+
return any(pat in name or pat in norm for pat in _CONFIG_EXCLUDE_PATTERNS)
|
|
1676
|
+
|
|
1655
1677
|
test_stems = {_normalize_test_stem(Path(p).stem) for p in test_set}
|
|
1656
1678
|
untested = [
|
|
1657
1679
|
p for p in source_set
|
|
1658
1680
|
if Path(p).stem not in test_stems
|
|
1659
1681
|
and not any(pen in p for pen in spec.ranking_penalties)
|
|
1682
|
+
and (include_config or not _is_config_file(p))
|
|
1660
1683
|
]
|
|
1661
1684
|
untested.sort(key=lambda p: (len(p.split("/")), p))
|
|
1662
1685
|
test_gaps = untested[:15]
|
sourcecode/serializer.py
CHANGED
|
@@ -1643,7 +1643,23 @@ def _angular_analysis(sm: "SourceMap") -> "Optional[dict[str, Any]]":
|
|
|
1643
1643
|
continue
|
|
1644
1644
|
component_count += content.count("@Component(")
|
|
1645
1645
|
service_count += content.count("@Injectable(")
|
|
1646
|
-
|
|
1646
|
+
# Count lazy route patterns: `loadChildren:` (property syntax used in route
|
|
1647
|
+
# configs) and `loadComponent:` (standalone component lazy loading). The old
|
|
1648
|
+
# `loadChildren(` form counted zero because Angular uses property syntax, not
|
|
1649
|
+
# a function call (BUG-5).
|
|
1650
|
+
fname_lower = rel.replace("\\", "/").split("/")[-1].lower()
|
|
1651
|
+
_is_routing_file = (
|
|
1652
|
+
"routing" in fname_lower
|
|
1653
|
+
or fname_lower in ("app.routes.ts", "app-routing.module.ts")
|
|
1654
|
+
or fname_lower.endswith(".routes.ts")
|
|
1655
|
+
)
|
|
1656
|
+
lazy_routes_count += content.count("loadChildren:")
|
|
1657
|
+
lazy_routes_count += content.count("loadComponent:")
|
|
1658
|
+
if _is_routing_file:
|
|
1659
|
+
# Also count standalone dynamic imports that aren't already caught above
|
|
1660
|
+
_lc_imports = content.count("import(") - content.count("loadChildren:") - content.count("loadComponent:")
|
|
1661
|
+
if _lc_imports > 0:
|
|
1662
|
+
lazy_routes_count += _lc_imports
|
|
1647
1663
|
akita_stores += content.count("@StoreConfig(")
|
|
1648
1664
|
if not standalone_components and "bootstrapApplication(" in content:
|
|
1649
1665
|
standalone_components = True
|
|
@@ -1661,7 +1677,13 @@ def _angular_analysis(sm: "SourceMap") -> "Optional[dict[str, Any]]":
|
|
|
1661
1677
|
if pkg_json.exists():
|
|
1662
1678
|
try:
|
|
1663
1679
|
pkg = _json.loads(pkg_json.read_text(encoding="utf-8", errors="replace"))
|
|
1664
|
-
|
|
1680
|
+
# Use `or {}` so explicit `null` values in package.json don't
|
|
1681
|
+
# raise TypeError when unpacking (BUG-4).
|
|
1682
|
+
deps = {
|
|
1683
|
+
**(pkg.get("dependencies") or {}),
|
|
1684
|
+
**(pkg.get("devDependencies") or {}),
|
|
1685
|
+
**(pkg.get("peerDependencies") or {}),
|
|
1686
|
+
}
|
|
1665
1687
|
av = deps.get("@angular/core")
|
|
1666
1688
|
if av:
|
|
1667
1689
|
angular_version = av.lstrip("^~>=")
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: sourcecode
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.31.0
|
|
4
4
|
Summary: Deterministic codebase context for AI coding agents
|
|
5
5
|
License: Apache License
|
|
6
6
|
Version 2.0, January 2004
|
|
@@ -212,16 +212,19 @@ Requires-Dist: tree-sitter-javascript>=0.21; extra == 'ast'
|
|
|
212
212
|
Requires-Dist: tree-sitter-typescript>=0.21; extra == 'ast'
|
|
213
213
|
Requires-Dist: tree-sitter>=0.21; extra == 'ast'
|
|
214
214
|
Provides-Extra: dev
|
|
215
|
+
Requires-Dist: mcp>=1.0.0; extra == 'dev'
|
|
215
216
|
Requires-Dist: mypy>=1.10; extra == 'dev'
|
|
216
217
|
Requires-Dist: pytest>=8; extra == 'dev'
|
|
217
218
|
Requires-Dist: ruff>=0.15; extra == 'dev'
|
|
219
|
+
Provides-Extra: mcp
|
|
220
|
+
Requires-Dist: mcp>=1.0.0; extra == 'mcp'
|
|
218
221
|
Description-Content-Type: text/markdown
|
|
219
222
|
|
|
220
223
|
# sourcecode
|
|
221
224
|
|
|
222
225
|
**Deterministic, behavior-aware codebase context for AI agents and PR review.**
|
|
223
226
|
|
|
224
|
-

|
|
225
228
|

|
|
226
229
|
|
|
227
230
|
---
|
|
@@ -257,7 +260,7 @@ pipx install sourcecode
|
|
|
257
260
|
|
|
258
261
|
```bash
|
|
259
262
|
sourcecode version
|
|
260
|
-
# sourcecode 1.
|
|
263
|
+
# sourcecode 1.31.0
|
|
261
264
|
```
|
|
262
265
|
|
|
263
266
|
---
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
sourcecode/__init__.py,sha256=
|
|
1
|
+
sourcecode/__init__.py,sha256=MiA5_ZlVOCsJoykaFJRoZJpA8VwhlU7ns_GdhCt64L4,103
|
|
2
2
|
sourcecode/adaptive_scanner.py,sha256=XffluXKzJUXrMtjEiAOnSNPZnztdIcts17T9ouHeID0,10521
|
|
3
3
|
sourcecode/architecture_analyzer.py,sha256=MyBa0Hf5HmkudZQDLKrjcWDKETXETXl0mQX1swtTwAA,39091
|
|
4
4
|
sourcecode/architecture_summary.py,sha256=z34_6v7cSwy98cof2UVciGho7SCrZ93tiqMmq5WNzRQ,20405
|
|
5
5
|
sourcecode/ast_extractor.py,sha256=XgrZg2DcWcUm9r87cRG3KGO7IK2TIL_N-CvhSbUmmh4,49901
|
|
6
6
|
sourcecode/classifier.py,sha256=-0t0HLc9L9UleMLfclfLM3AXhBjUb_AYyBPDbvgWtac,7755
|
|
7
|
-
sourcecode/cli.py,sha256=
|
|
7
|
+
sourcecode/cli.py,sha256=wncSKjhpDuN5t-tU7vj-kY2x6FREFyXoRetZnUensMs,110219
|
|
8
8
|
sourcecode/code_notes_analyzer.py,sha256=y1MJBnPZHYp4i6cQCXUb9ATIyifS_qMQWjw_8lPkpsU,9215
|
|
9
9
|
sourcecode/confidence_analyzer.py,sha256=H9VHYRzZhqMFlSCZffjtsMUGYLnDvrq1g5FjzyQ1hxE,16381
|
|
10
10
|
sourcecode/context_scorer.py,sha256=QpChSpsmaAYz91rXA4Ue5xzQmNz_ZboZN09YOHScq1U,14679
|
|
@@ -22,7 +22,7 @@ sourcecode/git_analyzer.py,sha256=0Gyj-vMpIIN4nfriKXVRouNYBeJ59s6pQDX2Xu9Pq-U,13
|
|
|
22
22
|
sourcecode/graph_analyzer.py,sha256=iUK-7pSV-cvGqqD2hENdYmhnm0wcXFEyK-xnu5ul8OU,62515
|
|
23
23
|
sourcecode/metrics_analyzer.py,sha256=m0ENgtqKeBL17kUIK3fmGkgo7UfXBNHxCMj0H_Y5K7c,22750
|
|
24
24
|
sourcecode/pr_comment_renderer.py,sha256=smHslxiG14lrytCkq5nFrFu-qTHgA-t-LFYfdrfjz2o,14423
|
|
25
|
-
sourcecode/prepare_context.py,sha256=
|
|
25
|
+
sourcecode/prepare_context.py,sha256=QNCl8uKk9PQpgXxPHBNSDXXkc1s2wTZwU6H0REC5Qms,173487
|
|
26
26
|
sourcecode/progress.py,sha256=qn30sWaHOkjTgXsSBmiPkz7Rsbwc5oSlIe6JNEMYp_k,3149
|
|
27
27
|
sourcecode/ranking_engine.py,sha256=ZAucq_YX2KkWUuAZf4P0lhtQ_38vEFnUhuGtSZd1S0E,12970
|
|
28
28
|
sourcecode/redactor.py,sha256=xuGcadGEHaPw4qZXlMDvzMCsr4VOkdp3oBQptHyJk8c,2884
|
|
@@ -33,7 +33,7 @@ sourcecode/runtime_classifier.py,sha256=zWX3r3HCKHc-qtIobErOa8aKMmaoPYREtJKvPcBG
|
|
|
33
33
|
sourcecode/scanner.py,sha256=WdOQ78mMzjR1NjmKTlbxdgwinnCTfAhxCVLBEFQiFHU,8899
|
|
34
34
|
sourcecode/schema.py,sha256=fj3BZ3IcnNV4j21BFIEvz8Qnw_vZoqIbzzRg-qQ-nd0,24530
|
|
35
35
|
sourcecode/semantic_analyzer.py,sha256=12TwXYkYbDcBdu0heX_EmfPM2EkO8a_r5osf0SaeQbs,88956
|
|
36
|
-
sourcecode/serializer.py,sha256=
|
|
36
|
+
sourcecode/serializer.py,sha256=FM4xklb9Ywg9KNdNpo8QXR50izuml5FkbeQgL2uS1HY,111611
|
|
37
37
|
sourcecode/summarizer.py,sha256=lPlKhMh28nueXkPo2xKeD3DUFYVGRlJMIdY-8TSM-ls,17486
|
|
38
38
|
sourcecode/tree_utils.py,sha256=8GAkIfQAsvtEudIeW1l4ooH_oRtrWR8cpJQJsEa_Pfw,2093
|
|
39
39
|
sourcecode/workspace.py,sha256=X_6NmNnitvT3_38V-JDChydo_sR68s249hLFlrQskU0,8271
|
|
@@ -58,14 +58,22 @@ sourcecode/detectors/rust.py,sha256=Tij1vz8BFZ332GEvVkL6vyMli2OMHJfHyDAppWfe66c,
|
|
|
58
58
|
sourcecode/detectors/systems.py,sha256=nYaKbGDFu0EOXFcd_1doWFT3tTUdkbxc2DjHUF5TcqQ,1627
|
|
59
59
|
sourcecode/detectors/terraform.py,sha256=cxORPR_zVLOJpHlh4e9JnFpkQsn_UnqMMom5yG65hZ4,1693
|
|
60
60
|
sourcecode/detectors/tooling.py,sha256=8CKbtxwQoABP-WyBRNmdAmHDOvAH57AR1cF4UKuWEdQ,2074
|
|
61
|
+
sourcecode/mcp/__init__.py,sha256=XU4HfRGbdid8wdUA0x_4f7uKZD1z3mv_XUY_WU_T9Mw,179
|
|
62
|
+
sourcecode/mcp/runner.py,sha256=B167PyUBUwAJC6stKRCd3KHpmrpZOMsAA3gOtIuUauU,1186
|
|
63
|
+
sourcecode/mcp/server.py,sha256=grK87-zeJ98W-DIXqaNg_PXGecFB_WzooKkyQcjJDa0,4480
|
|
64
|
+
sourcecode/mcp/onboarding/__init__.py,sha256=sj2PWqEBmMc4zBNkomg89WtL0M6S7A9yb7_wAuSWNP4,66
|
|
65
|
+
sourcecode/mcp/onboarding/applier.py,sha256=yfSMT0NKdZsjavtLkC8yQ7OtkfepOl5IXGByqg6bdEY,1894
|
|
66
|
+
sourcecode/mcp/onboarding/backup.py,sha256=ihqGOR8QTX8HASRSEDyfFyXr5bkXrygPHamv4p9KTmk,1452
|
|
67
|
+
sourcecode/mcp/onboarding/detector.py,sha256=lcBkERH1WlxTXGTKVtxKv1fvql4NgSnVxGftra-gMvE,1961
|
|
68
|
+
sourcecode/mcp/onboarding/planner.py,sha256=Fopg5f72FDiPfldF7NOxYjcBA_w8hi_jBJpSz39lPb8,1332
|
|
61
69
|
sourcecode/telemetry/__init__.py,sha256=M0eQZFNkmJiLbI_oNP4QEXwVju1dQ2d4P-E1-Bw8PxE,3116
|
|
62
70
|
sourcecode/telemetry/config.py,sha256=Pir0WHp4z-9Qclnn2NDZ3vwitqsMkOAJckmwjUSxrk4,1795
|
|
63
71
|
sourcecode/telemetry/consent.py,sha256=wLMvGNJeSSyZoNkQXpoUioY6mMv4Qdvuw7S9jAEWnII,2237
|
|
64
72
|
sourcecode/telemetry/events.py,sha256=oEvvulfsv5GIDWG2174gSS6tNB95w38AIYiYeifGKlE,2294
|
|
65
73
|
sourcecode/telemetry/filters.py,sha256=Asa71oRl7q3Wt_FMwuufIZJFzSYdgRNKS8LHCIyFeYE,4805
|
|
66
74
|
sourcecode/telemetry/transport.py,sha256=KJeIPCPWMdmbCP3ySGs2iUlia34U6vWne2dZsUezesw,1560
|
|
67
|
-
sourcecode-1.
|
|
68
|
-
sourcecode-1.
|
|
69
|
-
sourcecode-1.
|
|
70
|
-
sourcecode-1.
|
|
71
|
-
sourcecode-1.
|
|
75
|
+
sourcecode-1.31.0.dist-info/METADATA,sha256=tp3EeSNMJlNOi0f6JNoXdDsfrQDbEm703TurOFj83kw,29057
|
|
76
|
+
sourcecode-1.31.0.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
|
|
77
|
+
sourcecode-1.31.0.dist-info/entry_points.txt,sha256=ex3F9rmbXeyDIoFQHtkEqTsKSaJow8F0LrVu8XfIktQ,57
|
|
78
|
+
sourcecode-1.31.0.dist-info/licenses/LICENSE,sha256=7DdHrU9Z_3e7dSvq4ISijZNjnuHo5NIHNiHDouMQ9JU,10491
|
|
79
|
+
sourcecode-1.31.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|