sourcecode 1.35.16__py3-none-any.whl → 1.35.18__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 CHANGED
@@ -1,3 +1,3 @@
1
1
  """sourcecode — Deterministic codebase context maps for AI coding agents."""
2
2
 
3
- __version__ = "1.35.16"
3
+ __version__ = "1.35.18"
sourcecode/cli.py CHANGED
@@ -167,6 +167,11 @@ Cold scan: 2–10s depending on repo size. Warm cache: 0.3–0.6s.
167
167
  sourcecode --compact --git-context include git hotspots and uncommitted files
168
168
  sourcecode --agent full structured JSON for AI agents
169
169
 
170
+ [bold]Auth commands:[/bold]
171
+ auth login [dim]# authenticate via browser (device code)[/dim]
172
+ auth status [dim]# show current plan and auth state[/dim]
173
+ auth logout [dim]# remove local credentials[/dim]
174
+
170
175
  [bold]Cache commands:[/bold]
171
176
  cache status [dim]# cache size, hit keys, last-warmed timestamp[/dim]
172
177
  cache warm [dim]# pre-build cache ahead of an agent session[/dim]
@@ -197,9 +202,13 @@ Cold scan: 2–10s depending on repo size. Warm cache: 0.3–0.6s.
197
202
  [dim]modernize (full) dead zones, tangles, full coupling[/dim]
198
203
  [dim]fix-bug (full) complete risk-ranked file list[/dim]
199
204
  [dim]review-pr (expanded) CI-grade PR review[/dim]
200
- [dim]prepare-context delta incremental context for CI/CD[/dim]
205
+ [dim]prepare-context delta incremental context for CI/CD (30 free runs/repo)[/dim]
201
206
  [dim]prepare-context generate-tests test gap analysis[/dim]
202
- [dim]--full removes all truncation limits[/dim]
207
+ [dim]--full removes all truncation limits (free up to 500 files)[/dim]
208
+ [dim]--rank-by git-churn file volatility ranking via git history[/dim]
209
+ [dim]rich exports (HTML/PDF/CI) structured reports for CI and stakeholders[/dim]
210
+ [dim]multi-repo analysis cross-repository blast radius[/dim]
211
+ [dim]team snapshots shared org-level cache[/dim]
203
212
 
204
213
  [dim cyan]→ sourcecode activate <key>[/dim cyan]
205
214
  """
@@ -217,8 +226,8 @@ _SUBCOMMANDS: frozenset[str] = frozenset(
217
226
  "repo-ir", "mcp", "endpoints", "impact",
218
227
  # Enterprise workflow commands
219
228
  "onboard", "modernize", "fix-bug", "review-pr",
220
- # License
221
- "activate",
229
+ # License / auth
230
+ "activate", "auth",
222
231
  # Cache observability
223
232
  "cache",
224
233
  # RIS bootstrap
@@ -508,6 +517,9 @@ app.add_typer(mcp_app, name="mcp")
508
517
  cache_app = typer.Typer(help="Cache inspection and management.", rich_markup_mode="rich")
509
518
  app.add_typer(cache_app, name="cache")
510
519
 
520
+ auth_app = typer.Typer(help="Authentication: login, status, logout.", rich_markup_mode="rich")
521
+ app.add_typer(auth_app, name="auth")
522
+
511
523
 
512
524
  def _maybe_ask_consent() -> None:
513
525
  """Show first-run consent prompt once, on interactive TTYs only."""
@@ -573,7 +585,8 @@ GRAPH_EDGE_CHOICES = {"imports", "calls", "contains", "extends"}
573
585
  DOCS_DEPTH_CHOICES = ["module", "symbols", "full"]
574
586
 
575
587
  # ── Module-level constants ─────────────────────────────────────────────────────
576
- _FREE_TIER_NODE_CAP: int = 10 # semantic cap for graph nodes and semantic symbols in free tier
588
+ _FREE_TIER_NODE_CAP: int = 50 # semantic cap for graph nodes and semantic symbols in free tier
589
+ _FREE_FULL_FILE_THRESHOLD: int = 500 # Java source files; above this --full requires Pro
577
590
  _JAVA_MIN_SCAN_DEPTH: int = 12 # Maven src/main/java/<pkg>/<module>/File depth floor
578
591
  _JVM_STACKS: frozenset[str] = frozenset({"java", "kotlin", "scala", "groovy"})
579
592
  _IMPACT_PRIORITY_THRESHOLDS: list[tuple[float, str]] = [
@@ -878,6 +891,11 @@ def main(
878
891
  )
879
892
  raise typer.Exit(code=2) # FIX-P2-7: arg validation → exit 2
880
893
 
894
+ # Pro gate for --rank-by git-churn: git history analysis is a Pro feature.
895
+ if rank_by == "git-churn":
896
+ from sourcecode.license import require_feature as _req_git_history
897
+ _req_git_history("git-history")
898
+
881
899
  if symbol is not None and not symbol.strip():
882
900
  _emit_error_json(
883
901
  INVALID_INPUT_CODE,
@@ -911,10 +929,21 @@ def main(
911
929
  )
912
930
  raise typer.Exit(code=2) # FIX-P2-7: arg validation → exit 2
913
931
 
914
- # Pro gate for --full: removing truncation limits is enterprise-scale functionality.
932
+ # Pro gate for --full: free tier allowed up to _FREE_FULL_FILE_THRESHOLD Java files.
915
933
  if full:
916
- from sourcecode.license import require_feature as _req_full
917
- _req_full("--full")
934
+ from sourcecode.license import is_pro as _full_is_pro
935
+ if not _full_is_pro:
936
+ from itertools import islice as _islice
937
+ _full_check_path = Path(_get_detected_path()).resolve()
938
+ _java_count = sum(
939
+ 1 for _ in _islice(
940
+ (p for p in _full_check_path.rglob("*.java") if ".git" not in p.parts),
941
+ _FREE_FULL_FILE_THRESHOLD + 1,
942
+ )
943
+ )
944
+ if _java_count > _FREE_FULL_FILE_THRESHOLD:
945
+ from sourcecode.license import require_feature as _req_full
946
+ _req_full("--full")
918
947
 
919
948
  # P0-2 FIX: --compact and --full are mutually exclusive.
920
949
  # compact is designed to be a bounded summary; --full removes truncation limits,
@@ -2633,14 +2662,34 @@ def prepare_context_cmd(
2633
2662
  )
2634
2663
  raise typer.Exit(code=1)
2635
2664
 
2636
- # Pro gate: generate-tests and delta require an active Pro license.
2637
- _PRO_TASKS: frozenset[str] = frozenset({"generate-tests", "delta"})
2638
- if task in _PRO_TASKS:
2665
+ # Pro gate: generate-tests requires Pro. delta allows 30 free runs per repo.
2666
+ if task == "generate-tests":
2639
2667
  from sourcecode.license import require_feature as _require_feature
2640
- _extra: dict = {}
2641
- if task == "delta":
2642
- _extra["free_tier_alternative"] = "sourcecode prepare-context review-pr --since <ref>"
2643
- _require_feature(task, extra_fields=_extra if _extra else None)
2668
+ _require_feature("generate-tests")
2669
+ elif task == "delta":
2670
+ from sourcecode.license import is_pro as _delta_is_pro
2671
+ if not _delta_is_pro:
2672
+ from sourcecode.license import check_delta_free_tier as _check_delta
2673
+ _delta_allowed, _delta_used, _delta_remaining = _check_delta(str(path.resolve()))
2674
+ if not _delta_allowed:
2675
+ from sourcecode.license import require_feature as _require_feature_delta
2676
+ _require_feature_delta(
2677
+ "delta",
2678
+ extra_fields={
2679
+ "free_tier_note": (
2680
+ f"Free quota of {30} delta runs per repository exhausted."
2681
+ ),
2682
+ "free_tier_alternative": "sourcecode prepare-context review-pr --since <ref>",
2683
+ },
2684
+ )
2685
+ # Within quota: emit a header note so CI logs show remaining runs.
2686
+ elif _delta_remaining <= 5:
2687
+ import sys as _sys_delta
2688
+ _sys_delta.stderr.write(
2689
+ f"[sourcecode] delta free tier: {_delta_remaining} run(s) remaining"
2690
+ f" (used {_delta_used}/{30}). Upgrade to Pro for unlimited CI runs.\n"
2691
+ )
2692
+ _sys_delta.stderr.flush()
2644
2693
 
2645
2694
  # Validate --format: only "json" and "github-comment" are valid for prepare-context.
2646
2695
  # "yaml" is intentionally NOT supported here (use main command for yaml output).
@@ -4758,6 +4807,73 @@ def activate_cmd(
4758
4807
  _activate(license_key)
4759
4808
 
4760
4809
 
4810
+ # ---------------------------------------------------------------------------
4811
+ # Auth commands (device-flow login / status / logout)
4812
+ # ---------------------------------------------------------------------------
4813
+
4814
+ @auth_app.command("login")
4815
+ def auth_login_cmd() -> None:
4816
+ """Authenticate via browser (device code flow).
4817
+
4818
+ \b
4819
+ The CLI shows a URL. Open it in your browser, log in with your account,
4820
+ and the CLI completes authentication automatically.
4821
+ Credentials are stored in ~/.sourcecode/license.json (30-min cache; Supabase is source of truth).
4822
+
4823
+ \b
4824
+ Examples:
4825
+ sourcecode auth login
4826
+ """
4827
+ from sourcecode.license import auth_login as _auth_login
4828
+ _auth_login()
4829
+
4830
+
4831
+ @auth_app.command("status")
4832
+ def auth_status_cmd() -> None:
4833
+ """Show current authentication and plan status."""
4834
+ import json as _json
4835
+ try:
4836
+ from sourcecode.license import _license_data as _ld, is_pro as _ip
4837
+ except Exception:
4838
+ _ld = None
4839
+ _ip = False
4840
+
4841
+ if not _ld:
4842
+ out: dict = {"status": "unauthenticated", "pro": False}
4843
+ sys.stdout.write(_json.dumps(out, ensure_ascii=False) + "\n")
4844
+ sys.stdout.flush()
4845
+ return
4846
+
4847
+ out = {
4848
+ "status": "authenticated",
4849
+ "auth_method": _ld.get("auth_method", "license_key"),
4850
+ "email": _ld.get("email", ""),
4851
+ "plan": _ld.get("plan", "unknown"),
4852
+ "plan_status": _ld.get("status", "unknown"),
4853
+ "pro": _ip,
4854
+ "validated_at": _ld.get("validated_at") or _ld.get("activated_at") or "",
4855
+ }
4856
+ sys.stdout.write(_json.dumps(out, indent=2, ensure_ascii=False) + "\n")
4857
+ sys.stdout.flush()
4858
+
4859
+
4860
+ @auth_app.command("logout")
4861
+ def auth_logout_cmd() -> None:
4862
+ """Remove local credentials (does not cancel your subscription)."""
4863
+ import json as _json
4864
+ _lf = Path.home() / ".sourcecode" / "license.json"
4865
+ if _lf.exists():
4866
+ try:
4867
+ _lf.unlink()
4868
+ out: dict = {"status": "logged_out", "message": "Local credentials removed."}
4869
+ except Exception as _exc:
4870
+ out = {"status": "error", "message": str(_exc)}
4871
+ else:
4872
+ out = {"status": "logged_out", "message": "No local credentials found."}
4873
+ sys.stdout.write(_json.dumps(out, ensure_ascii=False) + "\n")
4874
+ sys.stdout.flush()
4875
+
4876
+
4761
4877
  @app.command("version")
4762
4878
  def version_cmd() -> None:
4763
4879
  """Show version and exit.
sourcecode/license.py CHANGED
@@ -41,7 +41,18 @@ if _SUPABASE_URL != _DEFAULT_SUPABASE_URL:
41
41
 
42
42
  _LICENSE_DIR: Path = Path.home() / ".sourcecode"
43
43
  _LICENSE_FILE: Path = _LICENSE_DIR / "license.json"
44
- _CACHE_TTL_SECONDS: int = 86400 # 24 hours
44
+ _DELTA_RUNS_FILE: Path = _LICENSE_DIR / "delta_runs.json"
45
+ _CACHE_TTL_SECONDS: int = 1800 # 30 minutes default; CI env overrides to 24h (see _get_cache_ttl)
46
+ _CACHE_TTL_CI_SECONDS: int = 86400 # 24 hours — CI containers must not re-validate mid-run
47
+
48
+
49
+ def _get_cache_ttl() -> int:
50
+ """Return TTL in seconds. CI containers get 24h to avoid mid-run network calls."""
51
+ return _CACHE_TTL_CI_SECONDS if os.environ.get("SOURCECODE_CI") else _CACHE_TTL_SECONDS
52
+ _DELTA_FREE_LIMIT: int = 30
53
+ _DEVICE_POLL_INTERVAL_S: float = 2.5
54
+ _DEVICE_POLL_TIMEOUT_S: float = 300.0 # 5-minute window for user to complete browser auth
55
+ _AUTH_BASE_URL: str = "https://sourcecode.dev"
45
56
  _LICENSE_KEY_RE = re.compile(r"^[A-Za-z0-9_\-]{1,200}$")
46
57
 
47
58
  # ---------------------------------------------------------------------------
@@ -83,12 +94,37 @@ _FEATURE_INFO: dict[str, dict[str, str]] = {
83
94
  "value": "Reduces test debt systematically across the entire codebase.",
84
95
  },
85
96
  "--full": {
86
- "display": "--full flag",
97
+ "display": "--full flag (large repos)",
87
98
  "description": (
88
99
  "Removes truncation limits on transactional boundaries, DTO mappers, and large result sets."
100
+ " Free tier may use --full on repositories under 500 Java source files."
89
101
  ),
90
102
  "value": "Essential for complete analysis of enterprise-scale codebases.",
91
103
  },
104
+ "git-history": {
105
+ "display": "git history analysis",
106
+ "description": (
107
+ "Churn ranking, commit frequency per file, volatility signals over 90-day window."
108
+ ),
109
+ "value": "Identifies which files change most — the highest-risk targets in any refactor.",
110
+ },
111
+ "multi-repo": {
112
+ "display": "multi-repo analysis",
113
+ "description": (
114
+ "Cross-repository dependency graphs, shared module impact, and org-level blast radius."
115
+ ),
116
+ "value": "Required for microservices and monorepo architectures.",
117
+ },
118
+ "export-rich": {
119
+ "display": "rich exports (HTML/PDF/CI)",
120
+ "description": "Structured HTML reports, PDF exports, and CI-consumable risk summaries.",
121
+ "value": "Embed analysis into your CI pipeline or share with non-CLI stakeholders.",
122
+ },
123
+ "team-snapshots": {
124
+ "display": "team snapshot sharing",
125
+ "description": "Shared org-level snapshots and multi-user cache access.",
126
+ "value": "Eliminates cold-cache overhead across the entire engineering team.",
127
+ },
92
128
  }
93
129
 
94
130
  # ---------------------------------------------------------------------------
@@ -113,6 +149,40 @@ def _write_license_file(data: dict) -> None:
113
149
  raise
114
150
 
115
151
 
152
+ def _read_delta_runs() -> dict:
153
+ try:
154
+ if _DELTA_RUNS_FILE.exists():
155
+ return json.loads(_DELTA_RUNS_FILE.read_text(encoding="utf-8"))
156
+ except Exception:
157
+ pass
158
+ return {}
159
+
160
+
161
+ def check_delta_free_tier(repo_path: str) -> "tuple[bool, int, int]":
162
+ """Check and consume one delta free-tier run for repo_path.
163
+
164
+ Returns (allowed, runs_used, runs_remaining).
165
+ When allowed=True the run count is incremented atomically.
166
+ When allowed=False the quota is exhausted — caller should gate to Pro.
167
+ """
168
+ import hashlib
169
+ key = hashlib.sha256(str(Path(repo_path).resolve()).encode()).hexdigest()[:16]
170
+ runs = _read_delta_runs()
171
+ used = int(runs.get(key, 0))
172
+ if used >= _DELTA_FREE_LIMIT:
173
+ return False, used, 0
174
+ new_used = used + 1
175
+ runs[key] = new_used
176
+ try:
177
+ _LICENSE_DIR.mkdir(parents=True, exist_ok=True)
178
+ tmp = _DELTA_RUNS_FILE.with_suffix(".tmp")
179
+ tmp.write_text(json.dumps(runs, indent=2, ensure_ascii=False), encoding="utf-8")
180
+ tmp.replace(_DELTA_RUNS_FILE)
181
+ except Exception:
182
+ pass
183
+ return True, new_used, max(0, _DELTA_FREE_LIMIT - new_used)
184
+
185
+
116
186
  def _load_license_file() -> Optional[dict]:
117
187
  """Read ~/.sourcecode/license.json. Returns parsed dict or None."""
118
188
  try:
@@ -152,6 +222,78 @@ def _call_get_license(license_key: str) -> Optional[dict]:
152
222
  return None # Network error — caller decides what to do
153
223
 
154
224
 
225
+ def _generate_device_code() -> str:
226
+ """Generate a human-readable device code: XXXX-XXXX-XXXX."""
227
+ import uuid
228
+ raw = uuid.uuid4().hex.upper()
229
+ return f"{raw[:4]}-{raw[4:8]}-{raw[8:12]}"
230
+
231
+
232
+ def _call_device_check(device_code: str) -> Optional[dict]:
233
+ """Poll /device-check edge function. Returns dict or None on network error.
234
+
235
+ Expected responses:
236
+ {"status": "pending"}
237
+ {"status": "complete", "device_token": "...", "email": "...", "plan": "pro", ...}
238
+ {"status": "error", "message": "..."}
239
+ """
240
+ import urllib.error
241
+ import urllib.request
242
+
243
+ if not _SUPABASE_ANON_KEY:
244
+ return None
245
+
246
+ url = f"{_SUPABASE_URL}/functions/v1/device-check"
247
+ body = json.dumps({"device_code": device_code}).encode("utf-8")
248
+ req = urllib.request.Request(url, data=body, method="POST")
249
+ req.add_header("apikey", _SUPABASE_ANON_KEY)
250
+ req.add_header("Authorization", f"Bearer {_SUPABASE_ANON_KEY}")
251
+ req.add_header("Content-Type", "application/json")
252
+ req.add_header("Accept", "application/json")
253
+ try:
254
+ with urllib.request.urlopen(req, timeout=8) as resp:
255
+ return json.loads(resp.read().decode("utf-8"))
256
+ except urllib.error.HTTPError as exc:
257
+ try:
258
+ return json.loads(exc.read().decode("utf-8", errors="replace"))
259
+ except Exception:
260
+ return {"status": "error", "message": f"HTTP {exc.code}"}
261
+ except Exception:
262
+ return None
263
+
264
+
265
+ def _call_get_user_plan(device_token: str) -> Optional[dict]:
266
+ """Fetch current plan/status for an authenticated device token.
267
+
268
+ Expected response:
269
+ {"valid": true, "plan": "pro", "status": "active", "features": [...], "email": "..."}
270
+ {"valid": false, "error": "token_revoked"}
271
+ """
272
+ import urllib.error
273
+ import urllib.request
274
+
275
+ if not _SUPABASE_ANON_KEY:
276
+ return None
277
+
278
+ url = f"{_SUPABASE_URL}/functions/v1/get-user-plan"
279
+ body = json.dumps({"device_token": device_token}).encode("utf-8")
280
+ req = urllib.request.Request(url, data=body, method="POST")
281
+ req.add_header("apikey", _SUPABASE_ANON_KEY)
282
+ req.add_header("Authorization", f"Bearer {_SUPABASE_ANON_KEY}")
283
+ req.add_header("Content-Type", "application/json")
284
+ req.add_header("Accept", "application/json")
285
+ try:
286
+ with urllib.request.urlopen(req, timeout=8) as resp:
287
+ return json.loads(resp.read().decode("utf-8"))
288
+ except urllib.error.HTTPError as exc:
289
+ try:
290
+ return json.loads(exc.read().decode("utf-8", errors="replace"))
291
+ except Exception:
292
+ return {"valid": False, "error": f"HTTP {exc.code}"}
293
+ except Exception:
294
+ return None
295
+
296
+
155
297
  def _maybe_revalidate() -> None:
156
298
  """Re-validate cached license if stale. Mutates globals; never raises."""
157
299
  global _license_data, is_pro
@@ -159,18 +301,55 @@ def _maybe_revalidate() -> None:
159
301
  if not _license_data:
160
302
  return
161
303
 
162
- validated_at_str = _license_data.get("validated_at") or _license_data.get("activated_at")
304
+ validated_at_str = (
305
+ _license_data.get("validated_at")
306
+ or _license_data.get("activated_at")
307
+ or _license_data.get("authenticated_at")
308
+ )
163
309
  if validated_at_str:
164
310
  try:
165
311
  validated_at = datetime.fromisoformat(validated_at_str)
166
312
  if validated_at.tzinfo is None:
167
313
  validated_at = validated_at.replace(tzinfo=timezone.utc)
168
314
  age = (datetime.now(timezone.utc) - validated_at).total_seconds()
169
- if age < _CACHE_TTL_SECONDS:
315
+ if age < _get_cache_ttl():
170
316
  return
171
317
  except Exception:
172
318
  pass
173
319
 
320
+ auth_method = _license_data.get("auth_method")
321
+
322
+ if auth_method == "device_flow":
323
+ device_token = _license_data.get("device_token")
324
+ if not device_token:
325
+ return
326
+ result = _call_get_user_plan(device_token)
327
+ if result is None:
328
+ return # Network error — keep cached (offline-first)
329
+ if not result.get("valid", True):
330
+ _license_data = None
331
+ is_pro = False
332
+ try:
333
+ if _LICENSE_FILE.exists():
334
+ _LICENSE_FILE.unlink()
335
+ except Exception:
336
+ pass
337
+ return
338
+ _license_data["plan"] = result.get("plan", "free")
339
+ _license_data["status"] = result.get("status", "active")
340
+ _license_data["features"] = result.get("features", [])
341
+ _license_data["validated_at"] = datetime.now(timezone.utc).isoformat()
342
+ is_pro = (
343
+ _license_data.get("plan") == "pro"
344
+ and _license_data.get("status", "active") != "inactive"
345
+ )
346
+ try:
347
+ _write_license_file(_license_data)
348
+ except Exception:
349
+ pass
350
+ return
351
+
352
+ # Key-based auth (existing flow / legacy)
174
353
  key = _license_data.get("license_key")
175
354
  if not key:
176
355
  return
@@ -205,6 +384,7 @@ def _init() -> None:
205
384
  is_pro = (
206
385
  _license_data is not None
207
386
  and _license_data.get("plan") == "pro"
387
+ and _license_data.get("status", "active") != "inactive"
208
388
  )
209
389
 
210
390
 
@@ -293,7 +473,105 @@ def require_pro(feature_name: str) -> None:
293
473
 
294
474
 
295
475
  # ---------------------------------------------------------------------------
296
- # Activation
476
+ # Device-flow authentication
477
+ # ---------------------------------------------------------------------------
478
+
479
+ def _finish_device_auth(result: dict) -> None:
480
+ """Persist device-flow credentials and emit success JSON. Exits on error."""
481
+ global _license_data, is_pro
482
+
483
+ device_token = result.get("device_token") or result.get("access_token") or ""
484
+ email = result.get("email", "")
485
+ plan = result.get("plan", "free")
486
+ plan_status = (
487
+ result.get("status_detail")
488
+ or result.get("user_status")
489
+ or result.get("status", "active")
490
+ )
491
+ features = result.get("features") or []
492
+
493
+ if not device_token:
494
+ sys.stderr.write("\n")
495
+ _fail("auth_error", "Authentication completed but no session token received. Contact support.")
496
+
497
+ _LICENSE_DIR.mkdir(parents=True, exist_ok=True)
498
+ now = datetime.now(timezone.utc).isoformat()
499
+ data: dict = {
500
+ "auth_method": "device_flow",
501
+ "device_token": device_token,
502
+ "email": email,
503
+ "plan": plan,
504
+ "status": plan_status,
505
+ "features": features,
506
+ "authenticated_at": now,
507
+ "validated_at": now,
508
+ }
509
+ _write_license_file(data)
510
+ _license_data = data
511
+ is_pro = plan == "pro" and plan_status != "inactive"
512
+
513
+ sys.stderr.write(f"\n Authenticated as {email}. Plan: {plan}\n\n")
514
+ sys.stderr.flush()
515
+
516
+ output: dict = {"status": "authenticated", "email": email, "plan": plan, "pro": is_pro}
517
+ if not is_pro:
518
+ output["upgrade_hint"] = "https://sourcecode.dev/pricing"
519
+ else:
520
+ output["features"] = features
521
+ sys.stdout.write(json.dumps(output, ensure_ascii=False) + "\n")
522
+ sys.stdout.flush()
523
+
524
+
525
+ def auth_login() -> None:
526
+ """Device code authentication flow.
527
+
528
+ Shows a browser URL; polls the backend every 2.5 s until the user
529
+ completes authentication or the 5-minute window expires.
530
+ Writes credentials to ~/.sourcecode/license.json on success.
531
+ Exits 0 on success, 1 on any failure.
532
+ """
533
+ import time
534
+
535
+ device_code = _generate_device_code()
536
+ activate_url = f"{_AUTH_BASE_URL}/activate?code={device_code}"
537
+
538
+ sys.stderr.write(f"\n Open this URL to authenticate:\n {activate_url}\n\n Waiting")
539
+ sys.stderr.flush()
540
+
541
+ deadline = time.monotonic() + _DEVICE_POLL_TIMEOUT_S
542
+ _tick = 0
543
+
544
+ while time.monotonic() < deadline:
545
+ time.sleep(_DEVICE_POLL_INTERVAL_S)
546
+ _tick += 1
547
+ if _tick % 4 == 0:
548
+ sys.stderr.write(".")
549
+ sys.stderr.flush()
550
+
551
+ result = _call_device_check(device_code)
552
+ if result is None:
553
+ continue # network blip — keep polling
554
+
555
+ status = result.get("status")
556
+ if status == "pending":
557
+ continue
558
+
559
+ if status == "complete":
560
+ _finish_device_auth(result)
561
+ return
562
+
563
+ if status == "error" or result.get("error"):
564
+ sys.stderr.write("\n")
565
+ _fail("auth_error", result.get("message") or result.get("error") or "Authentication failed.")
566
+
567
+ # Unknown status — keep polling
568
+
569
+ sys.stderr.write("\n")
570
+ _fail("auth_timeout", "Authentication timed out after 5 minutes. Please try again.")
571
+
572
+
573
+ # ---------------------------------------------------------------------------
574
+ # Activation (key-based — legacy / direct key entry)
297
575
  # ---------------------------------------------------------------------------
298
576
 
299
577
  def activate_license(license_key: str) -> None:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sourcecode
3
- Version: 1.35.16
3
+ Version: 1.35.18
4
4
  Summary: Persistent structural context and ultra-fast repeated analysis for AI coding agents
5
5
  License-File: LICENSE
6
6
  Keywords: agents,ai,codebase,context,developer-tools,llm
@@ -1,4 +1,4 @@
1
- sourcecode/__init__.py,sha256=jc7t99MAOVoZuqPXgeqrR2Amq4B-LnZtcO_-sTvoMSo,104
1
+ sourcecode/__init__.py,sha256=r4oQ6QPKKMHrjnfFwyEU8-Za6oZOvpxIorOgVbDG10o,104
2
2
  sourcecode/adaptive_scanner.py,sha256=XffluXKzJUXrMtjEiAOnSNPZnztdIcts17T9ouHeID0,10521
3
3
  sourcecode/architecture_analyzer.py,sha256=qh749a7ykPtGmQI1MR9y6j8TtL_jBdVYFx9YRsLqOMw,44121
4
4
  sourcecode/architecture_summary.py,sha256=z34_6v7cSwy98cof2UVciGho7SCrZ93tiqMmq5WNzRQ,20405
@@ -7,7 +7,7 @@ sourcecode/cache.py,sha256=wAyPrXN5DqiGivnMpeEuun2xHDKfBer2_oBsh6kj_vc,30447
7
7
  sourcecode/canonical_ir.py,sha256=uwpwCnJxMh_eiIVg4cOLv7-aZthvmDFcG4azCOycLkw,24281
8
8
  sourcecode/cir_graphs.py,sha256=rZi8JV4ZrAa2WSCeyNa4JIEKQ_yZzDZTsrvVz2KfuKA,8919
9
9
  sourcecode/classifier.py,sha256=2lYoSH3vOTkXZYPU7Go2WIet1-IuNzTWVhc-ULnXtgw,8024
10
- sourcecode/cli.py,sha256=uQXXwKp1qnviXG-Qgnl34_5ioPeKPuXLDh7IXXZ3aaA,224568
10
+ sourcecode/cli.py,sha256=k_MeCOfAlal3VORr6PaWr9sRbMbhKHOV0sI3O2jcxes,229485
11
11
  sourcecode/code_notes_analyzer.py,sha256=EJemNCNc9Dn-1RZYu-aNbK0ELzmsyC4s6FdHi3XyNEI,9392
12
12
  sourcecode/confidence_analyzer.py,sha256=_jckZSxksV-OU38vbkxfVNBnWCtlCq8Vwfg23x1uspA,19054
13
13
  sourcecode/context_scorer.py,sha256=QpChSpsmaAYz91rXA4Ue5xzQmNz_ZboZN09YOHScq1U,14679
@@ -26,7 +26,7 @@ sourcecode/flow_analyzer.py,sha256=dSiuY4w49k29jW_EPXUOND9B5uVbuCA7kjnuHi-pIWA,2
26
26
  sourcecode/fqn_utils.py,sha256=XLU7zDkNBXz_RZkIUNfpPmp1nekWtqP-fxV92tDV1vg,2158
27
27
  sourcecode/git_analyzer.py,sha256=JStxTQXNjBWi_wLdwhsZs9mT-v50cSJIz4Agzn6Kh9I,13362
28
28
  sourcecode/graph_analyzer.py,sha256=DHR8fY69oU_Pi4SYaWboX6EoEFrctQKB9dsjpqwGMzw,62403
29
- sourcecode/license.py,sha256=-sPY4VuSYSe5dZyy7uFoWn9tHFcKlQkhZU0kgbwypo8,12499
29
+ sourcecode/license.py,sha256=3JCV2OeTVttKrOGBguU5uZC0c02Stig-KLB0mP2lNiY,22742
30
30
  sourcecode/mcp_nudge.py,sha256=5ELU_ixzh6uA83NXLOZT8h00OhL53okfQdji3jyKOjg,2917
31
31
  sourcecode/metrics_analyzer.py,sha256=m0ENgtqKeBL17kUIK3fmGkgo7UfXBNHxCMj0H_Y5K7c,22750
32
32
  sourcecode/output_budget.py,sha256=Js9yUlfQtPhqBl9R6wn_9UHVjjJc3GtLcqyfjf5t50Q,9869
@@ -93,8 +93,8 @@ sourcecode/telemetry/consent.py,sha256=wLMvGNJeSSyZoNkQXpoUioY6mMv4Qdvuw7S9jAEWn
93
93
  sourcecode/telemetry/events.py,sha256=oEvvulfsv5GIDWG2174gSS6tNB95w38AIYiYeifGKlE,2294
94
94
  sourcecode/telemetry/filters.py,sha256=Asa71oRl7q3Wt_FMwuufIZJFzSYdgRNKS8LHCIyFeYE,4805
95
95
  sourcecode/telemetry/transport.py,sha256=QSslxIwij8YkRWcVvxykODDrkiN_GAAEu3dUP7KIWeE,1651
96
- sourcecode-1.35.16.dist-info/METADATA,sha256=LkCx5nRHlMV_wsznbY9xRh0muC06f_qtM6K9aHP2WbM,21297
97
- sourcecode-1.35.16.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
98
- sourcecode-1.35.16.dist-info/entry_points.txt,sha256=ex3F9rmbXeyDIoFQHtkEqTsKSaJow8F0LrVu8XfIktQ,57
99
- sourcecode-1.35.16.dist-info/licenses/LICENSE,sha256=7DdHrU9Z_3e7dSvq4ISijZNjnuHo5NIHNiHDouMQ9JU,10491
100
- sourcecode-1.35.16.dist-info/RECORD,,
96
+ sourcecode-1.35.18.dist-info/METADATA,sha256=A-e9C9ArfzHZ7g2CPnXdfg2YGTRk_N1QfrFKbMT6QMs,21297
97
+ sourcecode-1.35.18.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
98
+ sourcecode-1.35.18.dist-info/entry_points.txt,sha256=ex3F9rmbXeyDIoFQHtkEqTsKSaJow8F0LrVu8XfIktQ,57
99
+ sourcecode-1.35.18.dist-info/licenses/LICENSE,sha256=7DdHrU9Z_3e7dSvq4ISijZNjnuHo5NIHNiHDouMQ9JU,10491
100
+ sourcecode-1.35.18.dist-info/RECORD,,