sourcecode 1.35.36__py3-none-any.whl → 1.36.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 CHANGED
@@ -1,3 +1,3 @@
1
1
  """sourcecode — Deterministic codebase context maps for AI coding agents."""
2
2
 
3
- __version__ = "1.35.36"
3
+ __version__ = "1.36.0"
sourcecode/cli.py CHANGED
@@ -653,6 +653,38 @@ def version_callback(value: bool) -> None:
653
653
  raise typer.Exit()
654
654
 
655
655
 
656
+ def _print_welcome() -> None:
657
+ """Branded quickstart shown only on a bare invocation at a human terminal.
658
+
659
+ Agents and pipes never reach here: they either pass args/flags or stdout is
660
+ not a TTY, so the JSON machine contract is completely unchanged.
661
+ """
662
+ try:
663
+ from sourcecode import license as _lic
664
+ tier = "Pro" if _lic.is_pro else "Free"
665
+ except Exception:
666
+ tier = "Free"
667
+
668
+ lines = [
669
+ "",
670
+ f" sourcecode {__version__} · {tier}",
671
+ "",
672
+ " Structural context for AI coding agents — analyze a repo, get",
673
+ " LLM-ready JSON in milliseconds.",
674
+ "",
675
+ " Get started:",
676
+ " sourcecode --compact high-signal summary of this repo",
677
+ " sourcecode prepare-context onboard full onboarding context",
678
+ " sourcecode mcp init connect Claude / Cursor",
679
+ "",
680
+ " sourcecode --help all commands",
681
+ ]
682
+ if tier != "Pro":
683
+ lines.append(" sourcecode activate <key> unlock Pro (large repos)")
684
+ lines.append("")
685
+ typer.echo("\n".join(lines))
686
+
687
+
656
688
  @app.callback(invoke_without_command=True)
657
689
  def main(
658
690
  ctx: typer.Context,
@@ -902,6 +934,13 @@ def main(
902
934
  if ctx.invoked_subcommand is not None:
903
935
  return
904
936
 
937
+ # Bare invocation at a human terminal → branded quickstart instead of
938
+ # dumping a large JSON blob. Agents/pipes are untouched: any arg/flag, or a
939
+ # non-TTY stdout (piped), falls through to the normal analysis + JSON.
940
+ if len(sys.argv) <= 1 and sys.stdout.isatty():
941
+ _print_welcome()
942
+ raise typer.Exit()
943
+
905
944
  _t0 = time.monotonic()
906
945
  no_tree: bool = False # set True by --agent; --no-tree flag removed
907
946
 
@@ -2797,6 +2836,17 @@ def prepare_context_cmd(
2797
2836
  _cached_pctx = _pctx_cache.read(target, _pctx_cache_key)
2798
2837
  if _cached_pctx is not None:
2799
2838
  _emit_command_output(_cached_pctx, output_path, copy)
2839
+ try:
2840
+ from sourcecode import telemetry as _tel
2841
+ _tel.record(
2842
+ "execution_completed",
2843
+ cmd="prepare-context",
2844
+ feature=task,
2845
+ output_fmt=format,
2846
+ duration_s=0.0,
2847
+ )
2848
+ except Exception:
2849
+ pass
2800
2850
  return
2801
2851
 
2802
2852
  builder = TaskContextBuilder(target)
@@ -3192,6 +3242,18 @@ def prepare_context_cmd(
3192
3242
 
3193
3243
  _emit_command_output(_pc_content, output_path, copy)
3194
3244
 
3245
+ try:
3246
+ from sourcecode import telemetry as _tel
3247
+ _tel.record(
3248
+ "execution_completed",
3249
+ cmd="prepare-context",
3250
+ feature=task,
3251
+ output_fmt=format,
3252
+ duration_s=_time.perf_counter() - _t0,
3253
+ )
3254
+ except Exception:
3255
+ pass
3256
+
3195
3257
  from sourcecode.mcp_nudge import nudge_mcp_if_needed as _nudge
3196
3258
  _nudge()
3197
3259
 
sourcecode/license.py CHANGED
@@ -416,6 +416,15 @@ _init()
416
416
  # Entitlement helpers
417
417
  # ---------------------------------------------------------------------------
418
418
 
419
+ def _emit_telemetry(event: str, **kw: object) -> None:
420
+ """Best-effort telemetry emit. Respects opt-in; never raises or blocks."""
421
+ try:
422
+ from sourcecode import telemetry as _tel
423
+ _tel.record(event, **kw) # type: ignore[arg-type]
424
+ except Exception:
425
+ pass
426
+
427
+
419
428
  def can_use(feature_name: str) -> bool:
420
429
  """Return True if the current plan has access to feature_name.
421
430
 
@@ -510,6 +519,7 @@ def require_feature(
510
519
  }
511
520
  if extra_fields:
512
521
  payload.update(extra_fields)
522
+ _emit_telemetry("gate_blocked", feature=feature_name, success=False)
513
523
  _emit_upgrade_and_exit(
514
524
  f"'{display}' is a Pro feature.",
515
525
  [info.get("description", ""), info.get("value", "")],
@@ -560,6 +570,7 @@ def require_repo_or_pro(
560
570
  }
561
571
  if extra_fields:
562
572
  payload.update(extra_fields)
573
+ _emit_telemetry("gate_blocked", feature=feature_name, repo_size="large", success=False)
563
574
  _emit_upgrade_and_exit(headline, body, payload)
564
575
 
565
576
 
@@ -610,6 +621,7 @@ def _finish_device_auth(result: dict) -> None:
610
621
  _write_license_file(data)
611
622
  _license_data = data
612
623
  is_pro = plan == "pro" and plan_status != "inactive"
624
+ _emit_telemetry("activation", feature="device_flow", success=is_pro)
613
625
 
614
626
  sys.stderr.write(f"\n Authenticated as {email}. Plan: {plan}\n\n")
615
627
  sys.stderr.flush()
@@ -693,9 +705,11 @@ def activate_license(license_key: str) -> None:
693
705
  _fail("network_error", "Could not reach license server. Check your internet connection.")
694
706
 
695
707
  if not result.get("valid"):
708
+ _emit_telemetry("activation", feature="key", success=False, error_kind="InvalidLicense")
696
709
  _fail("invalid_license", result.get("error", "License key is not valid or subscription is inactive."))
697
710
 
698
711
  if result.get("plan") != "pro":
712
+ _emit_telemetry("activation", feature="key", success=False, error_kind="NotPro")
699
713
  _fail("not_pro", "This license is not a Pro license.")
700
714
 
701
715
  _LICENSE_DIR.mkdir(parents=True, exist_ok=True)
@@ -709,6 +723,7 @@ def activate_license(license_key: str) -> None:
709
723
  "validated_at": now,
710
724
  }
711
725
  _write_license_file(data)
726
+ _emit_telemetry("activation", feature="key", success=True)
712
727
 
713
728
  output = {"status": "activated", "plan": "pro", "features": data["features"]}
714
729
  sys.stdout.write(json.dumps(output, ensure_ascii=False) + "\n")
@@ -19,7 +19,7 @@ import sys
19
19
  import uuid
20
20
  from typing import Any, Optional
21
21
 
22
- from sourcecode.telemetry.config import is_enabled
22
+ from sourcecode.telemetry.config import get_install_id, is_enabled
23
23
  from sourcecode.telemetry.events import (
24
24
  TelemetryEvent,
25
25
  duration_bucket,
@@ -77,6 +77,8 @@ def record(
77
77
  duration_s: Optional[float] = None,
78
78
  success: bool = True,
79
79
  error_kind: Optional[str] = None,
80
+ feature: Optional[str] = None,
81
+ repo_size: Optional[str] = None,
80
82
  ) -> None:
81
83
  """Record a telemetry event. Fire-and-forget — never blocks or raises.
82
84
 
@@ -96,10 +98,16 @@ def record(
96
98
  cmd=cmd,
97
99
  flags=flags or [],
98
100
  output_fmt=output_fmt,
99
- repo_size=file_count_bucket(file_count) if file_count is not None else "unknown",
101
+ repo_size=(
102
+ repo_size if repo_size is not None
103
+ else file_count_bucket(file_count) if file_count is not None
104
+ else "unknown"
105
+ ),
100
106
  duration=duration_bucket(duration_s) if duration_s is not None else "unknown",
101
107
  success=success,
102
108
  error_kind=error_kind,
109
+ feature=feature,
110
+ install=get_install_id(),
103
111
  session=_SESSION,
104
112
  )
105
113
  payload = sanitize(ev)
@@ -63,5 +63,28 @@ def mark_asked() -> None:
63
63
  _save(data)
64
64
 
65
65
 
66
+ def get_install_id() -> str:
67
+ """Stable anonymous install id — a random UUID v4.
68
+
69
+ Created lazily on first opted-in event. NOT derived from hardware, email,
70
+ hostname or any identifier — it only says "the same install across runs",
71
+ which is what enables unique-user, conversion and retention metrics.
72
+ Returns "" if it cannot be persisted (telemetry then degrades to events
73
+ without a stable id, never an error).
74
+ """
75
+ data = _load()
76
+ tel = data.setdefault("telemetry", {})
77
+ iid = tel.get("install_id")
78
+ if not iid:
79
+ import uuid
80
+ iid = str(uuid.uuid4())
81
+ tel["install_id"] = iid
82
+ _save(data)
83
+ # If the write failed, re-read to avoid handing out a non-persisted id
84
+ if not _load().get("telemetry", {}).get("install_id"):
85
+ return ""
86
+ return str(iid)
87
+
88
+
66
89
  def config_file_path() -> Path:
67
90
  return _CONFIG_FILE
@@ -59,6 +59,9 @@ class TelemetryEvent:
59
59
  duration — <1s | <5s | <15s | <60s | 60s+
60
60
  success — True/False
61
61
  error_kind — exception class name only (no message, no traceback)
62
+ feature — gated feature / task name (closed categorical set) or None
63
+ install — stable anonymous install UUID (random, no PII); enables
64
+ unique-user / conversion / retention metrics
62
65
  session — 8-char random hex, ephemeral, NOT persisted
63
66
  """
64
67
 
@@ -75,4 +78,6 @@ class TelemetryEvent:
75
78
  duration: str = "unknown"
76
79
  success: bool = True
77
80
  error_kind: Optional[str] = None
81
+ feature: Optional[str] = None
82
+ install: str = ""
78
83
  session: str = ""
@@ -63,6 +63,19 @@ _SAFE_EVENTS: frozenset[str] = frozenset({
63
63
  "execution_failed",
64
64
  "telemetry_enabled",
65
65
  "telemetry_disabled",
66
+ "gate_blocked",
67
+ "activation",
68
+ })
69
+ # Closed set of gated features / task names. Used to learn which capability
70
+ # drives Pro demand. All values are fixed product identifiers — no user data.
71
+ _SAFE_FEATURES: frozenset[str] = frozenset({
72
+ # gated features (license._FEATURE_INFO keys)
73
+ "impact", "modernize", "fix-bug", "review-pr", "delta", "generate-tests",
74
+ "--full", "git-history", "multi-repo", "export-rich", "team-snapshots",
75
+ # prepare-context task names not already above
76
+ "explain", "onboard", "refactor",
77
+ # activation outcomes
78
+ "key", "device_flow",
66
79
  })
67
80
  _SAFE_SIZES: frozenset[str] = frozenset({"tiny", "small", "medium", "large", "huge", "unknown"})
68
81
  _SAFE_DURATIONS: frozenset[str] = frozenset({"<1s", "<5s", "<15s", "<60s", "60s+", "unknown"})
@@ -103,6 +116,14 @@ def _safe_session(value: str) -> str:
103
116
  return ""
104
117
 
105
118
 
119
+ _UUID_RE = re.compile(r"^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$")
120
+
121
+
122
+ def _safe_install(value: str) -> str:
123
+ """Install id must be a canonical UUID — nothing else can pass."""
124
+ return value if value and _UUID_RE.match(value) else ""
125
+
126
+
106
127
  def sanitize(event: TelemetryEvent) -> dict[str, Any]:
107
128
  """Apply privacy filter to event and return a safe dict for transmission.
108
129
 
@@ -127,6 +148,13 @@ def sanitize(event: TelemetryEvent) -> dict[str, Any]:
127
148
  if event.error_kind:
128
149
  safe["error_kind"] = _safe_error_kind(event.error_kind)
129
150
 
151
+ if event.feature:
152
+ safe["feature"] = _safe_str(event.feature, _SAFE_FEATURES, "other")
153
+
154
+ install = _safe_install(event.install)
155
+ if install:
156
+ safe["install"] = install
157
+
130
158
  session = _safe_session(event.session)
131
159
  if session:
132
160
  safe["session"] = session
@@ -15,7 +15,7 @@ import os
15
15
  import threading
16
16
  from typing import Any
17
17
 
18
- _DEFAULT_ENDPOINT = "https://t.sourcecode.dev/v1/event"
18
+ _DEFAULT_ENDPOINT = "https://qkndlmyekvujjdgthtmz.supabase.co/functions/v1/telemetry"
19
19
  _TIMEOUT_S = 3
20
20
 
21
21
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sourcecode
3
- Version: 1.35.36
3
+ Version: 1.36.0
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
@@ -40,7 +40,7 @@ Description-Content-Type: text/markdown
40
40
 
41
41
  **Persistent structural context and ultra-fast repeated analysis for AI coding agents.**
42
42
 
43
- ![Version](https://img.shields.io/badge/version-1.35.36-blue)
43
+ ![Version](https://img.shields.io/badge/version-1.36.0-blue)
44
44
  ![Python](https://img.shields.io/badge/python-3.10%2B-green)
45
45
 
46
46
  ---
@@ -114,7 +114,7 @@ pipx install sourcecode
114
114
 
115
115
  ```bash
116
116
  sourcecode version
117
- # sourcecode 1.35.36
117
+ # sourcecode 1.36.0
118
118
  ```
119
119
 
120
120
  ---
@@ -1,4 +1,4 @@
1
- sourcecode/__init__.py,sha256=ZxOECabDmAtGWQ7baAF8kAmS-2Kzj1Q5E1ciWq3lDi8,104
1
+ sourcecode/__init__.py,sha256=j2Rx292C93M1gnbvMuKrqWMZXlDm3K85SxUiFzO4zac,103
2
2
  sourcecode/adaptive_scanner.py,sha256=XffluXKzJUXrMtjEiAOnSNPZnztdIcts17T9ouHeID0,10521
3
3
  sourcecode/architecture_analyzer.py,sha256=liCwQmLgb5vplohy8arjYxs_HOIv5C9MjLh_gY6bc5Q,44115
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=c_lYTVoegg-1W2dZ34_2s3tN8L0GVx7eiDRh9ghdSD8,24178
8
8
  sourcecode/cir_graphs.py,sha256=rZi8JV4ZrAa2WSCeyNa4JIEKQ_yZzDZTsrvVz2KfuKA,8919
9
9
  sourcecode/classifier.py,sha256=hKzg-nQ47htqqIUzSGvYxv15cXrA3KgICTwJmdqal0o,8095
10
- sourcecode/cli.py,sha256=5LrX0Fwp1Wqw7yE3hrXc26TOPf_A2vod7X6SG1s_0ag,247606
10
+ sourcecode/cli.py,sha256=c8t9Jh68JjwcNyTZx9_hmoBLz1D0HOWu4snDFKtvang,249785
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
@@ -27,7 +27,7 @@ sourcecode/flow_analyzer.py,sha256=dSiuY4w49k29jW_EPXUOND9B5uVbuCA7kjnuHi-pIWA,2
27
27
  sourcecode/fqn_utils.py,sha256=XLU7zDkNBXz_RZkIUNfpPmp1nekWtqP-fxV92tDV1vg,2158
28
28
  sourcecode/git_analyzer.py,sha256=JStxTQXNjBWi_wLdwhsZs9mT-v50cSJIz4Agzn6Kh9I,13362
29
29
  sourcecode/graph_analyzer.py,sha256=DHR8fY69oU_Pi4SYaWboX6EoEFrctQKB9dsjpqwGMzw,62403
30
- sourcecode/license.py,sha256=iYtczUa4xh4-Y-2pMlfa6T7t1g-4nuld6-_PXT-Cq5Q,27299
30
+ sourcecode/license.py,sha256=gHNXu4_yLiHCJLhIryBCCmtzF6wBoWplHMS8rmu7m24,28074
31
31
  sourcecode/mcp_nudge.py,sha256=5ELU_ixzh6uA83NXLOZT8h00OhL53okfQdji3jyKOjg,2917
32
32
  sourcecode/metrics_analyzer.py,sha256=m0ENgtqKeBL17kUIK3fmGkgo7UfXBNHxCMj0H_Y5K7c,22750
33
33
  sourcecode/migrate_check.py,sha256=vowVIAxVaHU8vhZUEt-HrWrWM38m6a5INHJQGjEg5E0,55390
@@ -90,14 +90,14 @@ sourcecode/mcp/onboarding/applier.py,sha256=B9CneieWTpaDSDIyW3S5nrlRlBpvfqUcgi93
90
90
  sourcecode/mcp/onboarding/backup.py,sha256=ihqGOR8QTX8HASRSEDyfFyXr5bkXrygPHamv4p9KTmk,1452
91
91
  sourcecode/mcp/onboarding/detector.py,sha256=kDc0U6kXMuq_GivqwKrgJzIVLVeoLr3RQl63ksW10I8,3327
92
92
  sourcecode/mcp/onboarding/planner.py,sha256=Fopg5f72FDiPfldF7NOxYjcBA_w8hi_jBJpSz39lPb8,1332
93
- sourcecode/telemetry/__init__.py,sha256=M0eQZFNkmJiLbI_oNP4QEXwVju1dQ2d4P-E1-Bw8PxE,3116
94
- sourcecode/telemetry/config.py,sha256=Pir0WHp4z-9Qclnn2NDZ3vwitqsMkOAJckmwjUSxrk4,1795
93
+ sourcecode/telemetry/__init__.py,sha256=rth1GuU9Tqt6BvbOe6q6sro1yCygiDW4dN3r1OvmvQM,3375
94
+ sourcecode/telemetry/config.py,sha256=_MfMevIic1NTc8IRmCzQs96D8KPBLOWZ5cdhWrnHuwI,2639
95
95
  sourcecode/telemetry/consent.py,sha256=wLMvGNJeSSyZoNkQXpoUioY6mMv4Qdvuw7S9jAEWnII,2237
96
- sourcecode/telemetry/events.py,sha256=oEvvulfsv5GIDWG2174gSS6tNB95w38AIYiYeifGKlE,2294
97
- sourcecode/telemetry/filters.py,sha256=Asa71oRl7q3Wt_FMwuufIZJFzSYdgRNKS8LHCIyFeYE,4805
98
- sourcecode/telemetry/transport.py,sha256=QSslxIwij8YkRWcVvxykODDrkiN_GAAEu3dUP7KIWeE,1651
99
- sourcecode-1.35.36.dist-info/METADATA,sha256=I2uv7cJb0-J0u-7RNSvPYjiExMkJIwjHqlLnOCcN618,30386
100
- sourcecode-1.35.36.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
101
- sourcecode-1.35.36.dist-info/entry_points.txt,sha256=ex3F9rmbXeyDIoFQHtkEqTsKSaJow8F0LrVu8XfIktQ,57
102
- sourcecode-1.35.36.dist-info/licenses/LICENSE,sha256=7DdHrU9Z_3e7dSvq4ISijZNjnuHo5NIHNiHDouMQ9JU,10491
103
- sourcecode-1.35.36.dist-info/RECORD,,
96
+ sourcecode/telemetry/events.py,sha256=LtzYfaX9Ilckj5PTvAcTpDa9mLqDsYPDUiDkRa58piY,2580
97
+ sourcecode/telemetry/filters.py,sha256=zFJfvmE7TT5ZYsXr3mh6kTe0adzzYqFZSx61wUJ8Rew,5849
98
+ sourcecode/telemetry/transport.py,sha256=4gGHsq0WeY9VywEZXA3vUxykfiYnw9uuqfjAAec7F8o,1681
99
+ sourcecode-1.36.0.dist-info/METADATA,sha256=_SkuM5cooAYdgrd0BAhzvvKwy3f8i92t8XOMT2iwRtE,30383
100
+ sourcecode-1.36.0.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
101
+ sourcecode-1.36.0.dist-info/entry_points.txt,sha256=ex3F9rmbXeyDIoFQHtkEqTsKSaJow8F0LrVu8XfIktQ,57
102
+ sourcecode-1.36.0.dist-info/licenses/LICENSE,sha256=7DdHrU9Z_3e7dSvq4ISijZNjnuHo5NIHNiHDouMQ9JU,10491
103
+ sourcecode-1.36.0.dist-info/RECORD,,