sourcecode 1.31.31__py3-none-any.whl → 1.31.32__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/cache.py +1 -0
- sourcecode/cli.py +43 -13
- sourcecode/env_analyzer.py +6 -1
- sourcecode/git_analyzer.py +3 -0
- sourcecode/license.py +110 -47
- sourcecode/mcp/onboarding/applier.py +14 -6
- sourcecode/mcp/runner.py +2 -2
- sourcecode/mcp/server.py +17 -14
- sourcecode/mcp_nudge.py +6 -1
- sourcecode/redactor.py +4 -0
- sourcecode/repository_ir.py +1 -1
- sourcecode/serializer.py +1 -1
- {sourcecode-1.31.31.dist-info → sourcecode-1.31.32.dist-info}/METADATA +3 -3
- {sourcecode-1.31.31.dist-info → sourcecode-1.31.32.dist-info}/RECORD +18 -18
- {sourcecode-1.31.31.dist-info → sourcecode-1.31.32.dist-info}/WHEEL +0 -0
- {sourcecode-1.31.31.dist-info → sourcecode-1.31.32.dist-info}/entry_points.txt +0 -0
- {sourcecode-1.31.31.dist-info → sourcecode-1.31.32.dist-info}/licenses/LICENSE +0 -0
sourcecode/__init__.py
CHANGED
sourcecode/cache.py
CHANGED
|
@@ -469,6 +469,7 @@ def _cas_load_blob(cache_d: Path, blob_hash: str) -> Optional[str]:
|
|
|
469
469
|
try:
|
|
470
470
|
return gzip.decompress(path.read_bytes()).decode("utf-8")
|
|
471
471
|
except Exception:
|
|
472
|
+
_safe_unlink(path) # evict corrupted blob so it doesn't block future reads
|
|
472
473
|
return None
|
|
473
474
|
|
|
474
475
|
|
sourcecode/cli.py
CHANGED
|
@@ -2,6 +2,7 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
import hashlib
|
|
4
4
|
import json
|
|
5
|
+
import threading
|
|
5
6
|
import time
|
|
6
7
|
from pathlib import Path
|
|
7
8
|
from typing import Any, Optional, cast
|
|
@@ -140,7 +141,7 @@ def _check_pipeline_coherence(sm: "SourceMap") -> list[str]: # type: ignore[nam
|
|
|
140
141
|
return issues
|
|
141
142
|
|
|
142
143
|
_HELP = """\
|
|
143
|
-
|
|
144
|
+
Deterministic codebase context for AI coding agents.
|
|
144
145
|
|
|
145
146
|
[bold]Primary usage:[/bold]
|
|
146
147
|
sourcecode --compact high-signal summary (~600-800 tokens)
|
|
@@ -176,9 +177,20 @@ _SUBCOMMANDS: frozenset[str] = frozenset(
|
|
|
176
177
|
}
|
|
177
178
|
)
|
|
178
179
|
|
|
179
|
-
#
|
|
180
|
-
#
|
|
181
|
-
|
|
180
|
+
# Thread-local storage for the path extracted by _preprocess_argv().
|
|
181
|
+
# Using threading.local() prevents concurrent MCP tool calls from clobbering
|
|
182
|
+
# each other's target path (the old module-level list was a shared mutable global).
|
|
183
|
+
_tls = threading.local()
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def _get_detected_path() -> str:
|
|
187
|
+
"""Return the thread-local detected path, defaulting to '.'."""
|
|
188
|
+
return _tls.__dict__.get("detected_path", ".")
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def _set_detected_path(value: str) -> None:
|
|
192
|
+
"""Set the thread-local detected path."""
|
|
193
|
+
_tls.detected_path = value
|
|
182
194
|
|
|
183
195
|
|
|
184
196
|
# Options that take a value token — their next arg must not be treated as a path.
|
|
@@ -214,6 +226,7 @@ def _preprocess_args(args: list[str]) -> list[str]:
|
|
|
214
226
|
"""
|
|
215
227
|
result = list(args)
|
|
216
228
|
skip_next = False
|
|
229
|
+
_path_index: int = -1
|
|
217
230
|
for i, arg in enumerate(result):
|
|
218
231
|
if skip_next:
|
|
219
232
|
skip_next = False
|
|
@@ -227,9 +240,11 @@ def _preprocess_args(args: list[str]) -> list[str]:
|
|
|
227
240
|
if arg in _SUBCOMMANDS:
|
|
228
241
|
return result # known subcommand — leave for Click to dispatch
|
|
229
242
|
# First genuine positional: treat as repository path
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
243
|
+
_set_detected_path(arg)
|
|
244
|
+
_path_index = i
|
|
245
|
+
break
|
|
246
|
+
if _path_index >= 0:
|
|
247
|
+
result.pop(_path_index)
|
|
233
248
|
return result
|
|
234
249
|
|
|
235
250
|
|
|
@@ -334,7 +349,7 @@ def _get_command_with_preprocessing(typer_instance: Any) -> Any:
|
|
|
334
349
|
def _cmd_main(args: Optional[list[str]] = None, **kwargs: Any) -> Any:
|
|
335
350
|
if args is not None:
|
|
336
351
|
# CliRunner / programmatic call: preprocess the explicit args list.
|
|
337
|
-
|
|
352
|
+
_set_detected_path(".")
|
|
338
353
|
args = _preprocess_args(list(args))
|
|
339
354
|
# args=None → Click reads sys.argv; _preprocess_argv() in main_entry handled it.
|
|
340
355
|
return _orig_cmd_main(args=args, **kwargs)
|
|
@@ -363,7 +378,7 @@ app.add_typer(mcp_app, name="mcp")
|
|
|
363
378
|
def _maybe_ask_consent() -> None:
|
|
364
379
|
"""Show first-run consent prompt once, on interactive TTYs only."""
|
|
365
380
|
try:
|
|
366
|
-
from sourcecode.telemetry.config import has_been_asked,
|
|
381
|
+
from sourcecode.telemetry.config import has_been_asked, set_enabled
|
|
367
382
|
from sourcecode.telemetry.consent import ask_for_consent
|
|
368
383
|
if not has_been_asked():
|
|
369
384
|
enabled = ask_for_consent()
|
|
@@ -833,7 +848,7 @@ def main(
|
|
|
833
848
|
# Path was extracted from argv by _preprocess_argv() before Click ran.
|
|
834
849
|
# FIX-P2-8: preserve original user input in error messages (Windows Git Bash
|
|
835
850
|
# rewrites "/nonexistent" → "C:\Program Files\Git\nonexistent" via Path.resolve()).
|
|
836
|
-
_raw_path_input =
|
|
851
|
+
_raw_path_input = _get_detected_path()
|
|
837
852
|
target = Path(_raw_path_input).resolve()
|
|
838
853
|
if not target.exists():
|
|
839
854
|
_emit_error_json(
|
|
@@ -852,6 +867,7 @@ def main(
|
|
|
852
867
|
|
|
853
868
|
# Normalize mode aliases
|
|
854
869
|
_CONTRACT_MODES = frozenset({"contract", "minimal", "standard"})
|
|
870
|
+
_user_mode_explicit = mode not in ("contract",) # track if user passed a non-default value
|
|
855
871
|
if mode == "minimal":
|
|
856
872
|
mode = "contract" # minimal is a documented alias for contract
|
|
857
873
|
elif mode not in _CONTRACT_MODES and mode != "raw":
|
|
@@ -872,6 +888,20 @@ def main(
|
|
|
872
888
|
or docs or semantics or full_metrics or architecture
|
|
873
889
|
)
|
|
874
890
|
if mode in ("contract", "standard") and _legacy_flags_active:
|
|
891
|
+
if _user_mode_explicit:
|
|
892
|
+
_overriding_flags = [
|
|
893
|
+
f for f, v in [
|
|
894
|
+
("--compact", compact), ("--tree", tree),
|
|
895
|
+
("--trace-pipeline", trace_pipeline), ("--docs", docs),
|
|
896
|
+
("--semantics", semantics), ("--full-metrics", full_metrics),
|
|
897
|
+
("--architecture", architecture),
|
|
898
|
+
] if v
|
|
899
|
+
]
|
|
900
|
+
typer.echo(
|
|
901
|
+
f"[warning] --mode {mode} was overridden to raw because legacy flags "
|
|
902
|
+
f"({', '.join(_overriding_flags)}) require raw output mode.",
|
|
903
|
+
err=True,
|
|
904
|
+
)
|
|
875
905
|
mode = "raw"
|
|
876
906
|
|
|
877
907
|
# Map mode to contract_view depth
|
|
@@ -995,7 +1025,7 @@ def main(
|
|
|
995
1025
|
f"cn={code_notes},mode={mode},"
|
|
996
1026
|
f"ex={_excl_key},depth={effective_depth}"
|
|
997
1027
|
)
|
|
998
|
-
_core_h = _hashlib.
|
|
1028
|
+
_core_h = _hashlib.sha256(_core_flags_str.encode()).hexdigest()[:8]
|
|
999
1029
|
_core_key = f"{_git_sha}-{_core_h}"
|
|
1000
1030
|
|
|
1001
1031
|
# ── View flags: output presentation only (no re-analysis needed) ──
|
|
@@ -1007,7 +1037,7 @@ def main(
|
|
|
1007
1037
|
f"mn={max_nodes},ge={graph_edges},mi={max_importers},"
|
|
1008
1038
|
f"eg={emit_graph}"
|
|
1009
1039
|
)
|
|
1010
|
-
_view_h = _hashlib.
|
|
1040
|
+
_view_h = _hashlib.sha256(_view_flags_str.encode()).hexdigest()[:8]
|
|
1011
1041
|
|
|
1012
1042
|
# ── Lookup ──────────────────────────────────────────────────────
|
|
1013
1043
|
# Step 1: try L1 to obtain the core_hash needed for L2 key
|
|
@@ -1961,7 +1991,7 @@ def main(
|
|
|
1961
1991
|
if _written_core_hash:
|
|
1962
1992
|
if not _view_key:
|
|
1963
1993
|
# _view_key not set (L1 was also a miss); compute it now
|
|
1964
|
-
_wvh = _hashlib.
|
|
1994
|
+
_wvh = _hashlib.sha256(_view_flags_str.encode()).hexdigest()[:8]
|
|
1965
1995
|
_view_key = f"{_written_core_hash}-{_wvh}"
|
|
1966
1996
|
_cache_mod.write_view(
|
|
1967
1997
|
target,
|
sourcecode/env_analyzer.py
CHANGED
|
@@ -478,8 +478,13 @@ class EnvAnalyzer:
|
|
|
478
478
|
example_files_found: list,
|
|
479
479
|
limitations: list,
|
|
480
480
|
profiles_scanned: list,
|
|
481
|
+
depth: int = 0,
|
|
482
|
+
max_depth: int = 12,
|
|
481
483
|
) -> int:
|
|
482
484
|
"""Walk the directory tree accumulating env var findings. Returns spring_candidates count."""
|
|
485
|
+
if depth >= max_depth:
|
|
486
|
+
return 0
|
|
487
|
+
|
|
483
488
|
try:
|
|
484
489
|
entries = sorted(current.iterdir())
|
|
485
490
|
except PermissionError:
|
|
@@ -496,7 +501,7 @@ class EnvAnalyzer:
|
|
|
496
501
|
continue
|
|
497
502
|
total_spring_candidates += self._walk(
|
|
498
503
|
root, entry, findings, example_entries, example_files_found,
|
|
499
|
-
limitations, profiles_scanned,
|
|
504
|
+
limitations, profiles_scanned, depth + 1, max_depth,
|
|
500
505
|
)
|
|
501
506
|
elif entry.is_file():
|
|
502
507
|
rel = entry.relative_to(root).as_posix()
|
sourcecode/git_analyzer.py
CHANGED
|
@@ -326,6 +326,9 @@ def _parse_uncommitted(output: str) -> "UncommittedChanges":
|
|
|
326
326
|
continue
|
|
327
327
|
x, y = line[0], line[1]
|
|
328
328
|
filepath = line[3:].strip()
|
|
329
|
+
# Renamed files: git porcelain v1 produces "old -> new"; keep only the new path.
|
|
330
|
+
if x == "R" and " -> " in filepath:
|
|
331
|
+
filepath = filepath.split(" -> ", 1)[1]
|
|
329
332
|
if x == "?" and y == "?":
|
|
330
333
|
untracked.append(filepath)
|
|
331
334
|
else:
|
sourcecode/license.py
CHANGED
|
@@ -5,10 +5,12 @@ Flow:
|
|
|
5
5
|
2. is_pro set globally (True when plan == "pro")
|
|
6
6
|
3. Pro commands call require_pro(feature_name) at entry — exits 1 if not Pro
|
|
7
7
|
4. `sourcecode activate <key>` calls activate_license(key) — validates via
|
|
8
|
-
|
|
8
|
+
Edge Function, writes ~/.sourcecode/license.json, exits 0 on success
|
|
9
|
+
5. Cached license is re-validated every 24 h (online); network errors keep
|
|
10
|
+
cached state (offline-first). Server-side invalidity clears cache.
|
|
9
11
|
|
|
10
|
-
Supabase credentials:
|
|
11
|
-
SOURCECODE_SUPABASE_URL — project
|
|
12
|
+
Supabase credentials (baked in; override via env vars for testing):
|
|
13
|
+
SOURCECODE_SUPABASE_URL — project Edge Function base URL
|
|
12
14
|
SOURCECODE_SUPABASE_ANON_KEY — public anon key (not a secret)
|
|
13
15
|
"""
|
|
14
16
|
from __future__ import annotations
|
|
@@ -21,19 +23,20 @@ from pathlib import Path
|
|
|
21
23
|
from typing import Optional
|
|
22
24
|
|
|
23
25
|
# ---------------------------------------------------------------------------
|
|
24
|
-
# Supabase endpoint config — override via env
|
|
26
|
+
# Supabase endpoint config — hardcoded for production; override via env for dev
|
|
25
27
|
# ---------------------------------------------------------------------------
|
|
26
28
|
_SUPABASE_URL: str = os.environ.get(
|
|
27
29
|
"SOURCECODE_SUPABASE_URL",
|
|
28
|
-
"https://
|
|
30
|
+
"https://qkndlmyekvujjdgthtmz.supabase.co",
|
|
29
31
|
)
|
|
30
32
|
_SUPABASE_ANON_KEY: str = os.environ.get(
|
|
31
33
|
"SOURCECODE_SUPABASE_ANON_KEY",
|
|
32
|
-
"",
|
|
34
|
+
"", # Set SOURCECODE_SUPABASE_ANON_KEY to your project anon key
|
|
33
35
|
)
|
|
34
36
|
|
|
35
37
|
_LICENSE_DIR: Path = Path.home() / ".sourcecode"
|
|
36
38
|
_LICENSE_FILE: Path = _LICENSE_DIR / "license.json"
|
|
39
|
+
_CACHE_TTL_SECONDS: int = 86400 # 24 hours
|
|
37
40
|
|
|
38
41
|
# ---------------------------------------------------------------------------
|
|
39
42
|
# Global license state — loaded once at import time
|
|
@@ -53,6 +56,84 @@ def _load_license_file() -> Optional[dict]:
|
|
|
53
56
|
return None
|
|
54
57
|
|
|
55
58
|
|
|
59
|
+
def _call_get_license(license_key: str) -> Optional[dict]:
|
|
60
|
+
"""POST to /get-license edge function. Returns parsed dict or None on network error."""
|
|
61
|
+
import urllib.error
|
|
62
|
+
import urllib.request
|
|
63
|
+
|
|
64
|
+
if not _SUPABASE_ANON_KEY:
|
|
65
|
+
return None
|
|
66
|
+
|
|
67
|
+
url = f"{_SUPABASE_URL}/functions/v1/get-license"
|
|
68
|
+
body = json.dumps({"license_key": license_key}).encode("utf-8")
|
|
69
|
+
req = urllib.request.Request(url, data=body, method="POST")
|
|
70
|
+
req.add_header("apikey", _SUPABASE_ANON_KEY)
|
|
71
|
+
req.add_header("Authorization", f"Bearer {_SUPABASE_ANON_KEY}")
|
|
72
|
+
req.add_header("Content-Type", "application/json")
|
|
73
|
+
req.add_header("Accept", "application/json")
|
|
74
|
+
|
|
75
|
+
try:
|
|
76
|
+
with urllib.request.urlopen(req, timeout=8) as resp:
|
|
77
|
+
return json.loads(resp.read().decode("utf-8"))
|
|
78
|
+
except urllib.error.HTTPError as exc:
|
|
79
|
+
try:
|
|
80
|
+
return json.loads(exc.read().decode("utf-8", errors="replace"))
|
|
81
|
+
except Exception:
|
|
82
|
+
return {"valid": False, "error": f"HTTP {exc.code}"}
|
|
83
|
+
except Exception:
|
|
84
|
+
return None # Network error — caller decides what to do
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _maybe_revalidate() -> None:
|
|
88
|
+
"""Re-validate cached license if stale. Mutates globals; never raises."""
|
|
89
|
+
global _license_data, is_pro
|
|
90
|
+
|
|
91
|
+
if not _license_data:
|
|
92
|
+
return
|
|
93
|
+
|
|
94
|
+
validated_at_str = _license_data.get("validated_at") or _license_data.get("activated_at")
|
|
95
|
+
if validated_at_str:
|
|
96
|
+
try:
|
|
97
|
+
validated_at = datetime.fromisoformat(validated_at_str)
|
|
98
|
+
if validated_at.tzinfo is None:
|
|
99
|
+
validated_at = validated_at.replace(tzinfo=timezone.utc)
|
|
100
|
+
age = (datetime.now(timezone.utc) - validated_at).total_seconds()
|
|
101
|
+
if age < _CACHE_TTL_SECONDS:
|
|
102
|
+
return
|
|
103
|
+
except Exception:
|
|
104
|
+
pass
|
|
105
|
+
|
|
106
|
+
key = _license_data.get("license_key")
|
|
107
|
+
if not key:
|
|
108
|
+
return
|
|
109
|
+
|
|
110
|
+
result = _call_get_license(key)
|
|
111
|
+
if result is None:
|
|
112
|
+
return # Network error — keep cached data (offline-first)
|
|
113
|
+
|
|
114
|
+
if not result.get("valid"):
|
|
115
|
+
_license_data = None
|
|
116
|
+
is_pro = False
|
|
117
|
+
try:
|
|
118
|
+
if _LICENSE_FILE.exists():
|
|
119
|
+
_LICENSE_FILE.unlink()
|
|
120
|
+
except Exception:
|
|
121
|
+
pass
|
|
122
|
+
return
|
|
123
|
+
|
|
124
|
+
_license_data["plan"] = result.get("plan", "pro")
|
|
125
|
+
_license_data["features"] = result.get("features", [])
|
|
126
|
+
_license_data["validated_at"] = datetime.now(timezone.utc).isoformat()
|
|
127
|
+
is_pro = _license_data.get("plan") == "pro"
|
|
128
|
+
try:
|
|
129
|
+
_LICENSE_FILE.write_text(
|
|
130
|
+
json.dumps(_license_data, indent=2, ensure_ascii=False),
|
|
131
|
+
encoding="utf-8",
|
|
132
|
+
)
|
|
133
|
+
except Exception:
|
|
134
|
+
pass
|
|
135
|
+
|
|
136
|
+
|
|
56
137
|
def _init() -> None:
|
|
57
138
|
global _license_data, is_pro
|
|
58
139
|
_license_data = _load_license_file()
|
|
@@ -72,17 +153,20 @@ _init()
|
|
|
72
153
|
def require_pro(feature_name: str) -> None:
|
|
73
154
|
"""Exit with structured JSON error when not Pro.
|
|
74
155
|
|
|
75
|
-
|
|
156
|
+
Re-validates stale cached license before gating (once per 24 h, online).
|
|
76
157
|
|
|
77
158
|
Example:
|
|
78
159
|
from sourcecode.license import require_pro
|
|
79
160
|
require_pro("impact")
|
|
80
161
|
"""
|
|
162
|
+
if is_pro:
|
|
163
|
+
_maybe_revalidate()
|
|
164
|
+
|
|
81
165
|
if not is_pro:
|
|
82
166
|
payload = {
|
|
83
167
|
"error": "pro_required",
|
|
84
168
|
"feature": feature_name,
|
|
85
|
-
"message": "Run sourcecode activate <license_key>",
|
|
169
|
+
"message": "Run: sourcecode activate <license_key>",
|
|
86
170
|
}
|
|
87
171
|
sys.stdout.write(json.dumps(payload, ensure_ascii=False) + "\n")
|
|
88
172
|
sys.stdout.flush()
|
|
@@ -94,63 +178,42 @@ def require_pro(feature_name: str) -> None:
|
|
|
94
178
|
# ---------------------------------------------------------------------------
|
|
95
179
|
|
|
96
180
|
def activate_license(license_key: str) -> None:
|
|
97
|
-
"""Validate license_key via
|
|
181
|
+
"""Validate license_key via Edge Function, write ~/.sourcecode/license.json.
|
|
98
182
|
|
|
99
183
|
Outputs JSON to stdout; exits 0 on success, 1 on any failure.
|
|
100
184
|
Never raises — all error paths emit JSON and call sys.exit(1).
|
|
101
185
|
"""
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
# Bail early when Supabase isn't configured yet
|
|
106
|
-
if not _SUPABASE_ANON_KEY or _SUPABASE_URL == "https://YOUR_PROJECT.supabase.co":
|
|
107
|
-
_fail("configuration_error", "SOURCECODE_SUPABASE_URL / SOURCECODE_SUPABASE_ANON_KEY not configured.")
|
|
108
|
-
|
|
109
|
-
url = (
|
|
110
|
-
f"{_SUPABASE_URL}/rest/v1/users"
|
|
111
|
-
f"?license_key=eq.{license_key}"
|
|
112
|
-
f"&select=license_key,plan,email"
|
|
113
|
-
)
|
|
114
|
-
req = urllib.request.Request(url)
|
|
115
|
-
req.add_header("apikey", _SUPABASE_ANON_KEY)
|
|
116
|
-
req.add_header("Authorization", f"Bearer {_SUPABASE_ANON_KEY}")
|
|
117
|
-
req.add_header("Accept", "application/json")
|
|
186
|
+
if not _SUPABASE_ANON_KEY:
|
|
187
|
+
_fail("configuration_error", "SOURCECODE_SUPABASE_ANON_KEY not set. Contact support.")
|
|
118
188
|
|
|
119
|
-
|
|
120
|
-
with urllib.request.urlopen(req, timeout=10) as resp:
|
|
121
|
-
body = resp.read().decode("utf-8")
|
|
122
|
-
except urllib.error.HTTPError as exc:
|
|
123
|
-
_fail("network_error", f"Supabase returned HTTP {exc.code}")
|
|
124
|
-
except Exception as exc:
|
|
125
|
-
_fail("network_error", str(exc))
|
|
189
|
+
result = _call_get_license(license_key)
|
|
126
190
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
except Exception:
|
|
130
|
-
_fail("network_error", "Invalid JSON response from Supabase")
|
|
191
|
+
if result is None:
|
|
192
|
+
_fail("network_error", "Could not reach license server. Check your internet connection.")
|
|
131
193
|
|
|
132
|
-
if not
|
|
133
|
-
_fail("invalid_license", "License key not
|
|
194
|
+
if not result.get("valid"):
|
|
195
|
+
_fail("invalid_license", result.get("error", "License key is not valid or subscription is inactive."))
|
|
134
196
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
_fail("not_pro", "This license is not Pro")
|
|
197
|
+
if result.get("plan") != "pro":
|
|
198
|
+
_fail("not_pro", "This license is not a Pro license.")
|
|
138
199
|
|
|
139
|
-
# Write license file
|
|
140
200
|
_LICENSE_DIR.mkdir(parents=True, exist_ok=True)
|
|
201
|
+
now = datetime.now(timezone.utc).isoformat()
|
|
141
202
|
data = {
|
|
142
203
|
"license_key": license_key,
|
|
143
|
-
"plan": "
|
|
144
|
-
"
|
|
145
|
-
"
|
|
204
|
+
"plan": result["plan"],
|
|
205
|
+
"features": result.get("features", []),
|
|
206
|
+
"email": result.get("email", ""),
|
|
207
|
+
"activated_at": now,
|
|
208
|
+
"validated_at": now,
|
|
146
209
|
}
|
|
147
210
|
_LICENSE_FILE.write_text(
|
|
148
211
|
json.dumps(data, indent=2, ensure_ascii=False),
|
|
149
212
|
encoding="utf-8",
|
|
150
213
|
)
|
|
151
214
|
|
|
152
|
-
|
|
153
|
-
sys.stdout.write(json.dumps(
|
|
215
|
+
output = {"status": "activated", "plan": "pro", "features": data["features"]}
|
|
216
|
+
sys.stdout.write(json.dumps(output, ensure_ascii=False) + "\n")
|
|
154
217
|
sys.stdout.flush()
|
|
155
218
|
|
|
156
219
|
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
from __future__ import annotations
|
|
3
3
|
|
|
4
4
|
import json
|
|
5
|
+
import os
|
|
5
6
|
from pathlib import Path
|
|
6
7
|
|
|
7
8
|
_MCP_SERVERS_KEY = "mcpServers"
|
|
@@ -13,13 +14,14 @@ _ENTRY_VALUE: dict[str, object] = {
|
|
|
13
14
|
|
|
14
15
|
|
|
15
16
|
def read_config(path: Path) -> dict:
|
|
16
|
-
"""Parse JSON config from path. Returns empty dict if missing or
|
|
17
|
+
"""Parse JSON config from path. Returns empty dict if missing, empty, or unreadable."""
|
|
17
18
|
if not path.exists():
|
|
18
19
|
return {}
|
|
19
|
-
|
|
20
|
-
|
|
20
|
+
try:
|
|
21
|
+
text = path.read_text(encoding="utf-8")
|
|
22
|
+
return json.loads(text) if text.strip() else {}
|
|
23
|
+
except (OSError, json.JSONDecodeError):
|
|
21
24
|
return {}
|
|
22
|
-
return json.loads(raw) # type: ignore[no-any-return]
|
|
23
25
|
|
|
24
26
|
|
|
25
27
|
def is_installed(config: dict) -> bool:
|
|
@@ -49,9 +51,15 @@ def remove_entry(config: dict) -> dict:
|
|
|
49
51
|
|
|
50
52
|
|
|
51
53
|
def write_config(path: Path, config: dict) -> None:
|
|
52
|
-
"""Atomically write config as formatted JSON."""
|
|
54
|
+
"""Atomically write config as formatted JSON using a temp file + os.replace."""
|
|
53
55
|
path.parent.mkdir(parents=True, exist_ok=True)
|
|
54
|
-
path.
|
|
56
|
+
tmp = path.with_suffix(".tmp")
|
|
57
|
+
try:
|
|
58
|
+
tmp.write_text(json.dumps(config, indent=2) + "\n", encoding="utf-8")
|
|
59
|
+
os.replace(tmp, path)
|
|
60
|
+
finally:
|
|
61
|
+
if tmp.exists():
|
|
62
|
+
tmp.unlink(missing_ok=True)
|
|
55
63
|
|
|
56
64
|
|
|
57
65
|
def validate(path: Path) -> bool:
|
sourcecode/mcp/runner.py
CHANGED
|
@@ -20,9 +20,9 @@ def run_command(args: list[str]) -> Any:
|
|
|
20
20
|
Returns parsed JSON dict when output is valid JSON, else the raw string.
|
|
21
21
|
Raises RuntimeError on non-zero exit or empty output.
|
|
22
22
|
"""
|
|
23
|
-
from sourcecode.cli import
|
|
23
|
+
from sourcecode.cli import _set_detected_path, _preprocess_args, app
|
|
24
24
|
|
|
25
|
-
|
|
25
|
+
_set_detected_path(".")
|
|
26
26
|
processed = _preprocess_args(list(args))
|
|
27
27
|
result = _runner.invoke(app, processed)
|
|
28
28
|
|
sourcecode/mcp/server.py
CHANGED
|
@@ -454,18 +454,21 @@ def generate_tests_context(repo_path: str = ".", include_all: bool = False) -> d
|
|
|
454
454
|
timeout_s = timeout_ms / 1000.0
|
|
455
455
|
|
|
456
456
|
executor = concurrent.futures.ThreadPoolExecutor(max_workers=1)
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
457
|
+
try:
|
|
458
|
+
future = executor.submit(_execute, args)
|
|
459
|
+
done, _not_done = concurrent.futures.wait([future], timeout=timeout_s)
|
|
460
|
+
if _not_done:
|
|
461
|
+
executor.shutdown(wait=False)
|
|
462
|
+
return _ok({
|
|
463
|
+
"truncated": True,
|
|
464
|
+
"truncated_reason": f"timeout_{timeout_ms // 1000}s" if timeout_ms >= 1000 else f"timeout_{timeout_ms}ms",
|
|
465
|
+
"files_analyzed": 0,
|
|
466
|
+
"results": [],
|
|
467
|
+
})
|
|
468
|
+
result = future.result()
|
|
469
|
+
finally:
|
|
470
|
+
executor.shutdown(wait=True)
|
|
471
|
+
return result
|
|
469
472
|
|
|
470
473
|
except Exception as exc:
|
|
471
474
|
return _err(
|
|
@@ -532,8 +535,8 @@ def modernize_context(repo_path: str = ".", format: str = "json") -> dict:
|
|
|
532
535
|
try:
|
|
533
536
|
if not isinstance(repo_path, str):
|
|
534
537
|
return _err("repo_path must be a string", "INVALID_ARGUMENT")
|
|
535
|
-
if not isinstance(format, str) or format
|
|
536
|
-
return _err("format must be 'json'
|
|
538
|
+
if not isinstance(format, str) or format != "json":
|
|
539
|
+
return _err("format must be 'json' — yaml is not supported for modernize output", "INVALID_ARGUMENT")
|
|
537
540
|
repo_path = _normalize_repo_path(repo_path)
|
|
538
541
|
_path_err = _check_repo_path(repo_path)
|
|
539
542
|
if _path_err is not None:
|
sourcecode/mcp_nudge.py
CHANGED
|
@@ -3,7 +3,12 @@
|
|
|
3
3
|
Fires when:
|
|
4
4
|
1. At least one known MCP client (Claude Desktop, Cursor) is installed
|
|
5
5
|
2. sourcecode is NOT yet registered in that client's config
|
|
6
|
-
3. The nudge hasn't been shown
|
|
6
|
+
3. The nudge hasn't been shown yet (~/.sourcecode/nudge_shown flag absent)
|
|
7
|
+
|
|
8
|
+
The sentinel flag (~/.sourcecode/nudge_shown) persists globally on the
|
|
9
|
+
filesystem — it is NOT session-scoped. Once written it suppresses all future
|
|
10
|
+
nudges across all terminal sessions and process invocations until it is
|
|
11
|
+
deleted (which `sourcecode mcp init` does on successful installation).
|
|
7
12
|
|
|
8
13
|
Cleared by: a successful `sourcecode mcp init` (deletes the flag so the
|
|
9
14
|
post-init detection finds is_installed=True and never nudges again).
|
sourcecode/redactor.py
CHANGED
|
@@ -23,6 +23,10 @@ _SECRET_PATTERNS: list[re.Pattern[str]] = [
|
|
|
23
23
|
re.compile(r"sk-[A-Za-z0-9]{48}"), # OpenAI legacy key
|
|
24
24
|
re.compile(r"AKIA[0-9A-Z]{16}"), # AWS Access Key ID
|
|
25
25
|
re.compile(r"Bearer\s+[A-Za-z0-9\-._~+/]+=*"), # Bearer tokens
|
|
26
|
+
re.compile(r"sk-ant-[A-Za-z0-9\-_]{32,}"), # Anthropic API key
|
|
27
|
+
re.compile(r"hf_[A-Za-z0-9]{32,}"), # Hugging Face token
|
|
28
|
+
re.compile(r"sk_live_[A-Za-z0-9]{24,}"), # Stripe live secret key
|
|
29
|
+
re.compile(r"pk_live_[A-Za-z0-9]{24,}"), # Stripe live publishable key
|
|
26
30
|
]
|
|
27
31
|
|
|
28
32
|
# Patrones de nombres de fichero que deben excluirse (SEC-02)
|
sourcecode/repository_ir.py
CHANGED
|
@@ -3246,7 +3246,7 @@ def compute_blast_radius(
|
|
|
3246
3246
|
if _hub_class_guard and direct_callers:
|
|
3247
3247
|
_n_direct = len(direct_callers)
|
|
3248
3248
|
_k = min(_HUB_SAMPLE_SIZE, _n_direct)
|
|
3249
|
-
_sample_seeds =
|
|
3249
|
+
_sample_seeds = sorted(direct_callers, key=lambda x: str(x))[:_k]
|
|
3250
3250
|
_sample_visited: set[str] = set(matched_fqns) | set(direct_callers)
|
|
3251
3251
|
_sample_queue: list[tuple[str, int]] = [(c, 1) for c in _sample_seeds]
|
|
3252
3252
|
_sample_indirect: list[str] = []
|
sourcecode/serializer.py
CHANGED
|
@@ -190,7 +190,7 @@ def _dependency_import_index(root: Path, file_paths: list[str]) -> set[str]:
|
|
|
190
190
|
r"require\(['\"]([^'\"]+)['\"]\)|from\s+['\"]([^'\"]+)['\"])",
|
|
191
191
|
re.MULTILINE,
|
|
192
192
|
)
|
|
193
|
-
for path in file_paths[:
|
|
193
|
+
for path in file_paths[:500]:
|
|
194
194
|
if Path(path).suffix.lower() not in {".py", ".js", ".ts", ".tsx", ".jsx", ".mjs", ".cjs"}:
|
|
195
195
|
continue
|
|
196
196
|
try:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: sourcecode
|
|
3
|
-
Version: 1.31.
|
|
3
|
+
Version: 1.31.32
|
|
4
4
|
Summary: Deterministic codebase context for AI coding agents
|
|
5
5
|
License: Apache License
|
|
6
6
|
Version 2.0, January 2004
|
|
@@ -225,7 +225,7 @@ Description-Content-Type: text/markdown
|
|
|
225
225
|
|
|
226
226
|
**AI-ready change intelligence for Java/Spring enterprise monoliths.**
|
|
227
227
|
|
|
228
|
-

|
|
229
229
|

|
|
230
230
|
|
|
231
231
|
---
|
|
@@ -263,7 +263,7 @@ pipx install sourcecode
|
|
|
263
263
|
|
|
264
264
|
```bash
|
|
265
265
|
sourcecode version
|
|
266
|
-
# sourcecode 1.31.
|
|
266
|
+
# sourcecode 1.31.32
|
|
267
267
|
```
|
|
268
268
|
|
|
269
269
|
---
|
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
sourcecode/__init__.py,sha256=
|
|
1
|
+
sourcecode/__init__.py,sha256=FAgcPCebMe_1K921d8TjlfM7T_GMcbLwHvBZvp_uat8,104
|
|
2
2
|
sourcecode/adaptive_scanner.py,sha256=XffluXKzJUXrMtjEiAOnSNPZnztdIcts17T9ouHeID0,10521
|
|
3
3
|
sourcecode/architecture_analyzer.py,sha256=Ry3aYT9dc7XuLmWLT5IZ93RkCf_P14Qtew0nGPvUl_8,42184
|
|
4
4
|
sourcecode/architecture_summary.py,sha256=z34_6v7cSwy98cof2UVciGho7SCrZ93tiqMmq5WNzRQ,20405
|
|
5
5
|
sourcecode/ast_extractor.py,sha256=_btmeOJIe3t-NicF94D5ZAesa2YIJ0_QNExGnbHxGFE,50578
|
|
6
|
-
sourcecode/cache.py,sha256=
|
|
6
|
+
sourcecode/cache.py,sha256=pBrPdpPrOgpXHHQO670U3aUfVf5N3A3obsTKgiZtN4I,23030
|
|
7
7
|
sourcecode/canonical_ir.py,sha256=_HM3AUmKSdna9u4dCoU6rpgSA6HdF8gzOKZykIUCNGY,23277
|
|
8
8
|
sourcecode/classifier.py,sha256=yWeq6agTjkFa3zuNa-gdVIHtjoBoPoVlJnX-b7tdVJs,7851
|
|
9
|
-
sourcecode/cli.py,sha256=
|
|
9
|
+
sourcecode/cli.py,sha256=7tkUpZEnXjU_swX1wadkMnfYS0A0p0NvSRZEBIOLLi0,161521
|
|
10
10
|
sourcecode/code_notes_analyzer.py,sha256=EJemNCNc9Dn-1RZYu-aNbK0ELzmsyC4s6FdHi3XyNEI,9392
|
|
11
11
|
sourcecode/confidence_analyzer.py,sha256=_jckZSxksV-OU38vbkxfVNBnWCtlCq8Vwfg23x1uspA,19054
|
|
12
12
|
sourcecode/context_scorer.py,sha256=QpChSpsmaAYz91rXA4Ue5xzQmNz_ZboZN09YOHScq1U,14679
|
|
@@ -17,13 +17,13 @@ sourcecode/coverage_parser.py,sha256=q0LeZJaX1bnntLu-ImksdBsMlpsVmk_iUfSaB4eaJGo
|
|
|
17
17
|
sourcecode/dependency_analyzer.py,sha256=Po7GKJnClCkXty0np1B4F1zo_bPeKAtgbehazhXuaBM,56493
|
|
18
18
|
sourcecode/doc_analyzer.py,sha256=05bjTUbDbmnbajD_cgRnACzS8T7xxBKVX4CjkJlhZg8,24411
|
|
19
19
|
sourcecode/entrypoint_classifier.py,sha256=MTa7yqbeuJ9XPbGCPuvtR9IqY-SN3hoXXyVtb3iXDhs,4316
|
|
20
|
-
sourcecode/env_analyzer.py,sha256=
|
|
20
|
+
sourcecode/env_analyzer.py,sha256=9_q_U_Q1tjiY5FSaeEImSDgToP2uHGrjBAFF7ihJn2I,22204
|
|
21
21
|
sourcecode/file_classifier.py,sha256=QrYm7MlG29HQdAR1WOfpnIIBysAz62c5coz9eQ76meo,12892
|
|
22
22
|
sourcecode/flow_analyzer.py,sha256=dSiuY4w49k29jW_EPXUOND9B5uVbuCA7kjnuHi-pIWA,28781
|
|
23
|
-
sourcecode/git_analyzer.py,sha256=
|
|
23
|
+
sourcecode/git_analyzer.py,sha256=JStxTQXNjBWi_wLdwhsZs9mT-v50cSJIz4Agzn6Kh9I,13362
|
|
24
24
|
sourcecode/graph_analyzer.py,sha256=iUK-7pSV-cvGqqD2hENdYmhnm0wcXFEyK-xnu5ul8OU,62515
|
|
25
|
-
sourcecode/license.py,sha256=
|
|
26
|
-
sourcecode/mcp_nudge.py,sha256=
|
|
25
|
+
sourcecode/license.py,sha256=oiAxBXHf1mz42rrDKQCPX28W2yaAHvWqaVZuijJGSF8,7902
|
|
26
|
+
sourcecode/mcp_nudge.py,sha256=5ELU_ixzh6uA83NXLOZT8h00OhL53okfQdji3jyKOjg,2917
|
|
27
27
|
sourcecode/metrics_analyzer.py,sha256=m0ENgtqKeBL17kUIK3fmGkgo7UfXBNHxCMj0H_Y5K7c,22750
|
|
28
28
|
sourcecode/output_budget.py,sha256=43307mJEyUPU3MI-QEQoVxrcAvNyUzdzF_SAPgisBQE,6603
|
|
29
29
|
sourcecode/path_filters.py,sha256=ROFRQ8eSLBEMiixK9f45-RO7um4VEEcjoD5AA4I427I,3739
|
|
@@ -31,15 +31,15 @@ sourcecode/pr_comment_renderer.py,sha256=smHslxiG14lrytCkq5nFrFu-qTHgA-t-LFYfdrf
|
|
|
31
31
|
sourcecode/prepare_context.py,sha256=RM7ka0rduJy8kwGHzLU9if6q7D9ST7tGjOf5LnsdTuw,201451
|
|
32
32
|
sourcecode/progress.py,sha256=qn30sWaHOkjTgXsSBmiPkz7Rsbwc5oSlIe6JNEMYp_k,3149
|
|
33
33
|
sourcecode/ranking_engine.py,sha256=ZAucq_YX2KkWUuAZf4P0lhtQ_38vEFnUhuGtSZd1S0E,12970
|
|
34
|
-
sourcecode/redactor.py,sha256=
|
|
34
|
+
sourcecode/redactor.py,sha256=SB4hwIvg8h-hvcqKcDWaZvA-aSyn-at-BIRwa0tUv5E,3227
|
|
35
35
|
sourcecode/relevance_scorer.py,sha256=MYF4FFkveAQps9SmTeTlh6ODiBz2F--_hWNeHMLtUHQ,8405
|
|
36
36
|
sourcecode/repo_classifier.py,sha256=FG1vaWKdWXsWdl-S8hjVMiTqcwgaRXkDyvK4rPcOGtQ,22681
|
|
37
|
-
sourcecode/repository_ir.py,sha256=
|
|
37
|
+
sourcecode/repository_ir.py,sha256=rga0I9L0K-3kSMTF8R8slBH4Fkwwrh-ut4Y3h6GxLvY,152757
|
|
38
38
|
sourcecode/runtime_classifier.py,sha256=uTAD6BDCiBLUZEDRfqk718kM4RTT_vAbfkcOI2_Xx58,18432
|
|
39
39
|
sourcecode/scanner.py,sha256=WdOQ78mMzjR1NjmKTlbxdgwinnCTfAhxCVLBEFQiFHU,8899
|
|
40
40
|
sourcecode/schema.py,sha256=aHNXDf8LGyUC8ZDE_VS9kiskC2-Oswhi_WnpdGy6HDw,24897
|
|
41
41
|
sourcecode/semantic_analyzer.py,sha256=TDuC3wzZR2DPm1mgrAg1YSLk2QzJoueS3TZAmyGGpCU,89417
|
|
42
|
-
sourcecode/serializer.py,sha256=
|
|
42
|
+
sourcecode/serializer.py,sha256=3JBvsMDj5pt1RXpi1zJpk5DGWUKbeb2Jl622-kmYWD4,123312
|
|
43
43
|
sourcecode/summarizer.py,sha256=BMHJA0Do4rBnabc1_BxHoETTNb5ew0VqCX_eY3_PdCg,20706
|
|
44
44
|
sourcecode/tree_utils.py,sha256=8GAkIfQAsvtEudIeW1l4ooH_oRtrWR8cpJQJsEa_Pfw,2093
|
|
45
45
|
sourcecode/workspace.py,sha256=X_6NmNnitvT3_38V-JDChydo_sR68s249hLFlrQskU0,8271
|
|
@@ -65,10 +65,10 @@ sourcecode/detectors/systems.py,sha256=nYaKbGDFu0EOXFcd_1doWFT3tTUdkbxc2DjHUF5Tc
|
|
|
65
65
|
sourcecode/detectors/terraform.py,sha256=cxORPR_zVLOJpHlh4e9JnFpkQsn_UnqMMom5yG65hZ4,1693
|
|
66
66
|
sourcecode/detectors/tooling.py,sha256=8CKbtxwQoABP-WyBRNmdAmHDOvAH57AR1cF4UKuWEdQ,2074
|
|
67
67
|
sourcecode/mcp/__init__.py,sha256=XU4HfRGbdid8wdUA0x_4f7uKZD1z3mv_XUY_WU_T9Mw,179
|
|
68
|
-
sourcecode/mcp/runner.py,sha256=
|
|
69
|
-
sourcecode/mcp/server.py,sha256=
|
|
68
|
+
sourcecode/mcp/runner.py,sha256=YSw2DXEICau6mCBr3Gfia3D_tKxMbRvIIXEh4cHC1SY,1390
|
|
69
|
+
sourcecode/mcp/server.py,sha256=zbc4G5j2VAlVnluAksoSk6zV_wwgDevaH5Q4oVsl2kA,24018
|
|
70
70
|
sourcecode/mcp/onboarding/__init__.py,sha256=sj2PWqEBmMc4zBNkomg89WtL0M6S7A9yb7_wAuSWNP4,66
|
|
71
|
-
sourcecode/mcp/onboarding/applier.py,sha256=
|
|
71
|
+
sourcecode/mcp/onboarding/applier.py,sha256=B9CneieWTpaDSDIyW3S5nrlRlBpvfqUcgi93-mm_ApQ,2135
|
|
72
72
|
sourcecode/mcp/onboarding/backup.py,sha256=ihqGOR8QTX8HASRSEDyfFyXr5bkXrygPHamv4p9KTmk,1452
|
|
73
73
|
sourcecode/mcp/onboarding/detector.py,sha256=kDc0U6kXMuq_GivqwKrgJzIVLVeoLr3RQl63ksW10I8,3327
|
|
74
74
|
sourcecode/mcp/onboarding/planner.py,sha256=Fopg5f72FDiPfldF7NOxYjcBA_w8hi_jBJpSz39lPb8,1332
|
|
@@ -78,8 +78,8 @@ sourcecode/telemetry/consent.py,sha256=wLMvGNJeSSyZoNkQXpoUioY6mMv4Qdvuw7S9jAEWn
|
|
|
78
78
|
sourcecode/telemetry/events.py,sha256=oEvvulfsv5GIDWG2174gSS6tNB95w38AIYiYeifGKlE,2294
|
|
79
79
|
sourcecode/telemetry/filters.py,sha256=Asa71oRl7q3Wt_FMwuufIZJFzSYdgRNKS8LHCIyFeYE,4805
|
|
80
80
|
sourcecode/telemetry/transport.py,sha256=KJeIPCPWMdmbCP3ySGs2iUlia34U6vWne2dZsUezesw,1560
|
|
81
|
-
sourcecode-1.31.
|
|
82
|
-
sourcecode-1.31.
|
|
83
|
-
sourcecode-1.31.
|
|
84
|
-
sourcecode-1.31.
|
|
85
|
-
sourcecode-1.31.
|
|
81
|
+
sourcecode-1.31.32.dist-info/METADATA,sha256=69Ai2xiH7kn4Zmsm-KhGAfTTXtUbmyyy9EgLSSQ9GU4,31103
|
|
82
|
+
sourcecode-1.31.32.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
|
|
83
|
+
sourcecode-1.31.32.dist-info/entry_points.txt,sha256=ex3F9rmbXeyDIoFQHtkEqTsKSaJow8F0LrVu8XfIktQ,57
|
|
84
|
+
sourcecode-1.31.32.dist-info/licenses/LICENSE,sha256=7DdHrU9Z_3e7dSvq4ISijZNjnuHo5NIHNiHDouMQ9JU,10491
|
|
85
|
+
sourcecode-1.31.32.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|