sourcecode 1.35.36__tar.gz → 1.36.1__tar.gz
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-1.35.36 → sourcecode-1.36.1}/PKG-INFO +4 -5
- {sourcecode-1.35.36 → sourcecode-1.36.1}/README.md +3 -4
- {sourcecode-1.35.36 → sourcecode-1.36.1}/pyproject.toml +1 -1
- {sourcecode-1.35.36 → sourcecode-1.36.1}/src/sourcecode/__init__.py +1 -1
- {sourcecode-1.35.36 → sourcecode-1.36.1}/src/sourcecode/cache.py +37 -11
- {sourcecode-1.35.36 → sourcecode-1.36.1}/src/sourcecode/cli.py +135 -20
- {sourcecode-1.35.36 → sourcecode-1.36.1}/src/sourcecode/license.py +16 -207
- {sourcecode-1.35.36 → sourcecode-1.36.1}/src/sourcecode/telemetry/__init__.py +10 -2
- {sourcecode-1.35.36 → sourcecode-1.36.1}/src/sourcecode/telemetry/config.py +23 -0
- {sourcecode-1.35.36 → sourcecode-1.36.1}/src/sourcecode/telemetry/events.py +5 -0
- {sourcecode-1.35.36 → sourcecode-1.36.1}/src/sourcecode/telemetry/filters.py +28 -0
- {sourcecode-1.35.36 → sourcecode-1.36.1}/src/sourcecode/telemetry/transport.py +1 -1
- {sourcecode-1.35.36 → sourcecode-1.36.1}/supabase/functions/README.md +6 -2
- sourcecode-1.36.1/supabase/functions/telemetry/index.ts +72 -0
- sourcecode-1.36.1/supabase/sql/telemetry_events.sql +36 -0
- {sourcecode-1.35.36 → sourcecode-1.36.1}/.github/workflows/build-windows.yml +0 -0
- {sourcecode-1.35.36 → sourcecode-1.36.1}/.gitignore +0 -0
- {sourcecode-1.35.36 → sourcecode-1.36.1}/.ruff.toml +0 -0
- {sourcecode-1.35.36 → sourcecode-1.36.1}/CHANGELOG.md +0 -0
- {sourcecode-1.35.36 → sourcecode-1.36.1}/CONTRIBUTING.md +0 -0
- {sourcecode-1.35.36 → sourcecode-1.36.1}/LICENSE +0 -0
- {sourcecode-1.35.36 → sourcecode-1.36.1}/SECURITY.md +0 -0
- {sourcecode-1.35.36 → sourcecode-1.36.1}/raw +0 -0
- {sourcecode-1.35.36 → sourcecode-1.36.1}/src/sourcecode/adaptive_scanner.py +0 -0
- {sourcecode-1.35.36 → sourcecode-1.36.1}/src/sourcecode/architecture_analyzer.py +0 -0
- {sourcecode-1.35.36 → sourcecode-1.36.1}/src/sourcecode/architecture_summary.py +0 -0
- {sourcecode-1.35.36 → sourcecode-1.36.1}/src/sourcecode/ast_extractor.py +0 -0
- {sourcecode-1.35.36 → sourcecode-1.36.1}/src/sourcecode/canonical_ir.py +0 -0
- {sourcecode-1.35.36 → sourcecode-1.36.1}/src/sourcecode/cir_graphs.py +0 -0
- {sourcecode-1.35.36 → sourcecode-1.36.1}/src/sourcecode/classifier.py +0 -0
- {sourcecode-1.35.36 → sourcecode-1.36.1}/src/sourcecode/code_notes_analyzer.py +0 -0
- {sourcecode-1.35.36 → sourcecode-1.36.1}/src/sourcecode/confidence_analyzer.py +0 -0
- {sourcecode-1.35.36 → sourcecode-1.36.1}/src/sourcecode/context_scorer.py +0 -0
- {sourcecode-1.35.36 → sourcecode-1.36.1}/src/sourcecode/context_summarizer.py +0 -0
- {sourcecode-1.35.36 → sourcecode-1.36.1}/src/sourcecode/contract_model.py +0 -0
- {sourcecode-1.35.36 → sourcecode-1.36.1}/src/sourcecode/contract_pipeline.py +0 -0
- {sourcecode-1.35.36 → sourcecode-1.36.1}/src/sourcecode/coverage_parser.py +0 -0
- {sourcecode-1.35.36 → sourcecode-1.36.1}/src/sourcecode/dependency_analyzer.py +0 -0
- {sourcecode-1.35.36 → sourcecode-1.36.1}/src/sourcecode/detectors/__init__.py +0 -0
- {sourcecode-1.35.36 → sourcecode-1.36.1}/src/sourcecode/detectors/base.py +0 -0
- {sourcecode-1.35.36 → sourcecode-1.36.1}/src/sourcecode/detectors/csproj_parser.py +0 -0
- {sourcecode-1.35.36 → sourcecode-1.36.1}/src/sourcecode/detectors/dart.py +0 -0
- {sourcecode-1.35.36 → sourcecode-1.36.1}/src/sourcecode/detectors/dotnet.py +0 -0
- {sourcecode-1.35.36 → sourcecode-1.36.1}/src/sourcecode/detectors/elixir.py +0 -0
- {sourcecode-1.35.36 → sourcecode-1.36.1}/src/sourcecode/detectors/go.py +0 -0
- {sourcecode-1.35.36 → sourcecode-1.36.1}/src/sourcecode/detectors/heuristic.py +0 -0
- {sourcecode-1.35.36 → sourcecode-1.36.1}/src/sourcecode/detectors/hybrid.py +0 -0
- {sourcecode-1.35.36 → sourcecode-1.36.1}/src/sourcecode/detectors/java.py +0 -0
- {sourcecode-1.35.36 → sourcecode-1.36.1}/src/sourcecode/detectors/jvm_ext.py +0 -0
- {sourcecode-1.35.36 → sourcecode-1.36.1}/src/sourcecode/detectors/nodejs.py +0 -0
- {sourcecode-1.35.36 → sourcecode-1.36.1}/src/sourcecode/detectors/parsers.py +0 -0
- {sourcecode-1.35.36 → sourcecode-1.36.1}/src/sourcecode/detectors/php.py +0 -0
- {sourcecode-1.35.36 → sourcecode-1.36.1}/src/sourcecode/detectors/project.py +0 -0
- {sourcecode-1.35.36 → sourcecode-1.36.1}/src/sourcecode/detectors/python.py +0 -0
- {sourcecode-1.35.36 → sourcecode-1.36.1}/src/sourcecode/detectors/ruby.py +0 -0
- {sourcecode-1.35.36 → sourcecode-1.36.1}/src/sourcecode/detectors/rust.py +0 -0
- {sourcecode-1.35.36 → sourcecode-1.36.1}/src/sourcecode/detectors/systems.py +0 -0
- {sourcecode-1.35.36 → sourcecode-1.36.1}/src/sourcecode/detectors/terraform.py +0 -0
- {sourcecode-1.35.36 → sourcecode-1.36.1}/src/sourcecode/detectors/tooling.py +0 -0
- {sourcecode-1.35.36 → sourcecode-1.36.1}/src/sourcecode/doc_analyzer.py +0 -0
- {sourcecode-1.35.36 → sourcecode-1.36.1}/src/sourcecode/entrypoint_classifier.py +0 -0
- {sourcecode-1.35.36 → sourcecode-1.36.1}/src/sourcecode/env_analyzer.py +0 -0
- {sourcecode-1.35.36 → sourcecode-1.36.1}/src/sourcecode/error_schema.py +0 -0
- {sourcecode-1.35.36 → sourcecode-1.36.1}/src/sourcecode/explain.py +0 -0
- {sourcecode-1.35.36 → sourcecode-1.36.1}/src/sourcecode/file_chunker.py +0 -0
- {sourcecode-1.35.36 → sourcecode-1.36.1}/src/sourcecode/file_classifier.py +0 -0
- {sourcecode-1.35.36 → sourcecode-1.36.1}/src/sourcecode/flow_analyzer.py +0 -0
- {sourcecode-1.35.36 → sourcecode-1.36.1}/src/sourcecode/fqn_utils.py +0 -0
- {sourcecode-1.35.36 → sourcecode-1.36.1}/src/sourcecode/git_analyzer.py +0 -0
- {sourcecode-1.35.36 → sourcecode-1.36.1}/src/sourcecode/graph_analyzer.py +0 -0
- {sourcecode-1.35.36 → sourcecode-1.36.1}/src/sourcecode/mcp/__init__.py +0 -0
- {sourcecode-1.35.36 → sourcecode-1.36.1}/src/sourcecode/mcp/onboarding/__init__.py +0 -0
- {sourcecode-1.35.36 → sourcecode-1.36.1}/src/sourcecode/mcp/onboarding/applier.py +0 -0
- {sourcecode-1.35.36 → sourcecode-1.36.1}/src/sourcecode/mcp/onboarding/backup.py +0 -0
- {sourcecode-1.35.36 → sourcecode-1.36.1}/src/sourcecode/mcp/onboarding/detector.py +0 -0
- {sourcecode-1.35.36 → sourcecode-1.36.1}/src/sourcecode/mcp/onboarding/planner.py +0 -0
- {sourcecode-1.35.36 → sourcecode-1.36.1}/src/sourcecode/mcp/orchestrator.py +0 -0
- {sourcecode-1.35.36 → sourcecode-1.36.1}/src/sourcecode/mcp/registry.py +0 -0
- {sourcecode-1.35.36 → sourcecode-1.36.1}/src/sourcecode/mcp/runner.py +0 -0
- {sourcecode-1.35.36 → sourcecode-1.36.1}/src/sourcecode/mcp/server.py +0 -0
- {sourcecode-1.35.36 → sourcecode-1.36.1}/src/sourcecode/mcp_nudge.py +0 -0
- {sourcecode-1.35.36 → sourcecode-1.36.1}/src/sourcecode/metrics_analyzer.py +0 -0
- {sourcecode-1.35.36 → sourcecode-1.36.1}/src/sourcecode/migrate_check.py +0 -0
- {sourcecode-1.35.36 → sourcecode-1.36.1}/src/sourcecode/output_budget.py +0 -0
- {sourcecode-1.35.36 → sourcecode-1.36.1}/src/sourcecode/path_filters.py +0 -0
- {sourcecode-1.35.36 → sourcecode-1.36.1}/src/sourcecode/pr_comment_renderer.py +0 -0
- {sourcecode-1.35.36 → sourcecode-1.36.1}/src/sourcecode/pr_impact.py +0 -0
- {sourcecode-1.35.36 → sourcecode-1.36.1}/src/sourcecode/prepare_context.py +0 -0
- {sourcecode-1.35.36 → sourcecode-1.36.1}/src/sourcecode/progress.py +0 -0
- {sourcecode-1.35.36 → sourcecode-1.36.1}/src/sourcecode/ranking_engine.py +0 -0
- {sourcecode-1.35.36 → sourcecode-1.36.1}/src/sourcecode/redactor.py +0 -0
- {sourcecode-1.35.36 → sourcecode-1.36.1}/src/sourcecode/relevance_scorer.py +0 -0
- {sourcecode-1.35.36 → sourcecode-1.36.1}/src/sourcecode/rename_refactor.py +0 -0
- {sourcecode-1.35.36 → sourcecode-1.36.1}/src/sourcecode/repo_classifier.py +0 -0
- {sourcecode-1.35.36 → sourcecode-1.36.1}/src/sourcecode/repository_ir.py +0 -0
- {sourcecode-1.35.36 → sourcecode-1.36.1}/src/sourcecode/ris.py +0 -0
- {sourcecode-1.35.36 → sourcecode-1.36.1}/src/sourcecode/runtime_classifier.py +0 -0
- {sourcecode-1.35.36 → sourcecode-1.36.1}/src/sourcecode/scanner.py +0 -0
- {sourcecode-1.35.36 → sourcecode-1.36.1}/src/sourcecode/schema.py +0 -0
- {sourcecode-1.35.36 → sourcecode-1.36.1}/src/sourcecode/semantic_analyzer.py +0 -0
- {sourcecode-1.35.36 → sourcecode-1.36.1}/src/sourcecode/serializer.py +0 -0
- {sourcecode-1.35.36 → sourcecode-1.36.1}/src/sourcecode/spring_event_topology.py +0 -0
- {sourcecode-1.35.36 → sourcecode-1.36.1}/src/sourcecode/spring_findings.py +0 -0
- {sourcecode-1.35.36 → sourcecode-1.36.1}/src/sourcecode/spring_impact.py +0 -0
- {sourcecode-1.35.36 → sourcecode-1.36.1}/src/sourcecode/spring_model.py +0 -0
- {sourcecode-1.35.36 → sourcecode-1.36.1}/src/sourcecode/spring_security_audit.py +0 -0
- {sourcecode-1.35.36 → sourcecode-1.36.1}/src/sourcecode/spring_semantic.py +0 -0
- {sourcecode-1.35.36 → sourcecode-1.36.1}/src/sourcecode/spring_tx_analyzer.py +0 -0
- {sourcecode-1.35.36 → sourcecode-1.36.1}/src/sourcecode/summarizer.py +0 -0
- {sourcecode-1.35.36 → sourcecode-1.36.1}/src/sourcecode/telemetry/consent.py +0 -0
- {sourcecode-1.35.36 → sourcecode-1.36.1}/src/sourcecode/tree_utils.py +0 -0
- {sourcecode-1.35.36 → sourcecode-1.36.1}/src/sourcecode/workspace.py +0 -0
- {sourcecode-1.35.36 → sourcecode-1.36.1}/supabase/functions/get-license/index.ts +0 -0
- {sourcecode-1.35.36 → sourcecode-1.36.1}/supabase/functions/lemonsqueezy-webhook/index.ts +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: sourcecode
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.36.1
|
|
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
|
-

|
|
44
44
|

|
|
45
45
|
|
|
46
46
|
---
|
|
@@ -114,7 +114,7 @@ pipx install sourcecode
|
|
|
114
114
|
|
|
115
115
|
```bash
|
|
116
116
|
sourcecode version
|
|
117
|
-
# sourcecode 1.
|
|
117
|
+
# sourcecode 1.36.1
|
|
118
118
|
```
|
|
119
119
|
|
|
120
120
|
---
|
|
@@ -311,8 +311,7 @@ when the work gets bigger or automated.
|
|
|
311
311
|
files only, by design. sourcecode monetises enterprise Java monoliths.
|
|
312
312
|
|
|
313
313
|
```bash
|
|
314
|
-
sourcecode
|
|
315
|
-
sourcecode activate <key> # or activate a license key directly
|
|
314
|
+
sourcecode activate <key> # activate a license key
|
|
316
315
|
```
|
|
317
316
|
|
|
318
317
|
Full breakdown: [docs/PRODUCT_TIERS.md](docs/PRODUCT_TIERS.md).
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
**Persistent structural context and ultra-fast repeated analysis for AI coding agents.**
|
|
4
4
|
|
|
5
|
-

|
|
6
6
|

|
|
7
7
|
|
|
8
8
|
---
|
|
@@ -76,7 +76,7 @@ pipx install sourcecode
|
|
|
76
76
|
|
|
77
77
|
```bash
|
|
78
78
|
sourcecode version
|
|
79
|
-
# sourcecode 1.
|
|
79
|
+
# sourcecode 1.36.1
|
|
80
80
|
```
|
|
81
81
|
|
|
82
82
|
---
|
|
@@ -273,8 +273,7 @@ when the work gets bigger or automated.
|
|
|
273
273
|
files only, by design. sourcecode monetises enterprise Java monoliths.
|
|
274
274
|
|
|
275
275
|
```bash
|
|
276
|
-
sourcecode
|
|
277
|
-
sourcecode activate <key> # or activate a license key directly
|
|
276
|
+
sourcecode activate <key> # activate a license key
|
|
278
277
|
```
|
|
279
278
|
|
|
280
279
|
Full breakdown: [docs/PRODUCT_TIERS.md](docs/PRODUCT_TIERS.md).
|
|
@@ -58,6 +58,8 @@ import json
|
|
|
58
58
|
import os
|
|
59
59
|
import re
|
|
60
60
|
import subprocess
|
|
61
|
+
import time
|
|
62
|
+
import uuid
|
|
61
63
|
from datetime import datetime, timezone
|
|
62
64
|
from pathlib import Path
|
|
63
65
|
from typing import Any, Optional
|
|
@@ -101,6 +103,12 @@ _DEFAULT_KEEP_COMMITS: int = 5
|
|
|
101
103
|
_DEFAULT_MAX_CORES: int = 20
|
|
102
104
|
_DEFAULT_MAX_SIZE_MB: int = 50
|
|
103
105
|
|
|
106
|
+
#: Windows hardening for _atomic_write: os.replace can raise PermissionError
|
|
107
|
+
#: (WinError 5/32) when an antivirus scanner, search indexer, or concurrent
|
|
108
|
+
#: reader transiently holds the destination open. Retry briefly to ride it out.
|
|
109
|
+
_REPLACE_RETRIES: int = 5
|
|
110
|
+
_REPLACE_BACKOFF_S: float = 0.05
|
|
111
|
+
|
|
104
112
|
# Matches "snapshot-<hex_commit>-<hex_flags>.json.gz"
|
|
105
113
|
_SNAPSHOT_RE = re.compile(r"^snapshot-([0-9a-f]+)-[0-9a-f]+\.json\.gz$")
|
|
106
114
|
|
|
@@ -808,20 +816,38 @@ def _gc_cas(cache_d: Path, surviving_snapshots: list[Path]) -> None:
|
|
|
808
816
|
# ---------------------------------------------------------------------------
|
|
809
817
|
|
|
810
818
|
def _atomic_write(dest: Path, data: bytes) -> None:
|
|
811
|
-
"""Write *data* to *dest* atomically via a
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
destination either
|
|
815
|
-
write. The
|
|
816
|
-
by the cache reader and GC.
|
|
819
|
+
"""Write *data* to *dest* atomically via a unique temp file + rename.
|
|
820
|
+
|
|
821
|
+
``os.replace`` is an atomic overwrite on both POSIX and Windows: the
|
|
822
|
+
destination ends up with either the old or the new content, never a partial
|
|
823
|
+
write. The trailing ``.tmp`` suffix keeps partial files out of the
|
|
824
|
+
``*.json.gz`` / ``*.gz`` glob patterns used by the cache reader and GC.
|
|
825
|
+
|
|
826
|
+
Windows hardening (no-ops on POSIX):
|
|
827
|
+
* The temp name is made *unique* (pid + random token) so concurrent
|
|
828
|
+
writers of the same destination never collide on a shared temp file —
|
|
829
|
+
on Windows that collision raises ``PermissionError`` (WinError 32,
|
|
830
|
+
sharing violation), whereas POSIX tolerates it.
|
|
831
|
+
* ``os.replace`` is retried with a short backoff: on Windows it raises
|
|
832
|
+
``PermissionError`` (WinError 5/32) when an antivirus scanner, the
|
|
833
|
+
search indexer, or a concurrent reader holds a transient handle on the
|
|
834
|
+
destination. POSIX ``rename(2)`` has no such restriction.
|
|
817
835
|
"""
|
|
818
|
-
tmp = dest.
|
|
836
|
+
tmp = dest.parent / f"{dest.name}.{os.getpid()}.{uuid.uuid4().hex[:8]}.tmp"
|
|
819
837
|
try:
|
|
820
838
|
tmp.write_bytes(data)
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
839
|
+
last_exc: Optional[BaseException] = None
|
|
840
|
+
for attempt in range(_REPLACE_RETRIES):
|
|
841
|
+
try:
|
|
842
|
+
os.replace(tmp, dest)
|
|
843
|
+
return
|
|
844
|
+
except PermissionError as exc: # transient lock on Windows — retry
|
|
845
|
+
last_exc = exc
|
|
846
|
+
time.sleep(_REPLACE_BACKOFF_S * (attempt + 1))
|
|
847
|
+
if last_exc is not None:
|
|
848
|
+
raise last_exc
|
|
849
|
+
finally:
|
|
850
|
+
_safe_unlink(tmp) # remove temp on failure; no-op after a successful replace
|
|
825
851
|
|
|
826
852
|
|
|
827
853
|
def _safe_unlink(path: Path) -> None:
|
|
@@ -168,7 +168,6 @@ Cold scan: 2–10s depending on repo size. Warm cache: 0.3–0.6s.
|
|
|
168
168
|
sourcecode --agent full structured JSON for AI agents
|
|
169
169
|
|
|
170
170
|
[bold]Auth commands:[/bold]
|
|
171
|
-
auth login [dim]# authenticate via browser (device code)[/dim]
|
|
172
171
|
auth status [dim]# show current plan and auth state[/dim]
|
|
173
172
|
auth logout [dim]# remove local credentials[/dim]
|
|
174
173
|
|
|
@@ -570,7 +569,7 @@ app.add_typer(mcp_app, name="mcp")
|
|
|
570
569
|
cache_app = typer.Typer(help="Cache inspection and management.", rich_markup_mode="rich")
|
|
571
570
|
app.add_typer(cache_app, name="cache")
|
|
572
571
|
|
|
573
|
-
auth_app = typer.Typer(help="Authentication:
|
|
572
|
+
auth_app = typer.Typer(help="Authentication: status, logout.", rich_markup_mode="rich")
|
|
574
573
|
app.add_typer(auth_app, name="auth")
|
|
575
574
|
|
|
576
575
|
|
|
@@ -653,6 +652,109 @@ def version_callback(value: bool) -> None:
|
|
|
653
652
|
raise typer.Exit()
|
|
654
653
|
|
|
655
654
|
|
|
655
|
+
# ANSI Shadow block wordmark, stacked "source" / "code" so it fits an 80-col
|
|
656
|
+
# terminal (the single-line "sourcecode" is 83 wide and would wrap).
|
|
657
|
+
_WELCOME_SOURCE = (
|
|
658
|
+
"███████╗ ██████╗ ██╗ ██╗██████╗ ██████╗███████╗",
|
|
659
|
+
"██╔════╝██╔═══██╗██║ ██║██╔══██╗██╔════╝██╔════╝",
|
|
660
|
+
"███████╗██║ ██║██║ ██║██████╔╝██║ █████╗ ",
|
|
661
|
+
"╚════██║██║ ██║██║ ██║██╔══██╗██║ ██╔══╝ ",
|
|
662
|
+
"███████║╚██████╔╝╚██████╔╝██║ ██║╚██████╗███████╗",
|
|
663
|
+
"╚══════╝ ╚═════╝ ╚═════╝ ╚═╝ ╚═╝ ╚═════╝╚══════╝",
|
|
664
|
+
)
|
|
665
|
+
_WELCOME_CODE = (
|
|
666
|
+
" ██████╗ ██████╗ ██████╗ ███████╗",
|
|
667
|
+
"██╔════╝██╔═══██╗██╔══██╗██╔════╝",
|
|
668
|
+
"██║ ██║ ██║██║ ██║█████╗ ",
|
|
669
|
+
"██║ ██║ ██║██║ ██║██╔══╝ ",
|
|
670
|
+
"╚██████╗╚██████╔╝██████╔╝███████╗",
|
|
671
|
+
" ╚═════╝ ╚═════╝ ╚═════╝ ╚══════╝",
|
|
672
|
+
)
|
|
673
|
+
|
|
674
|
+
_WELCOME_CMDS = (
|
|
675
|
+
("sourcecode --compact", "repo summary"),
|
|
676
|
+
("sourcecode prepare-context onboard", "onboarding"),
|
|
677
|
+
("sourcecode mcp init", "connect IDE"),
|
|
678
|
+
)
|
|
679
|
+
|
|
680
|
+
|
|
681
|
+
def _print_welcome_plain(tier: str) -> None:
|
|
682
|
+
"""Plain-text welcome — fallback when rich is unavailable."""
|
|
683
|
+
lines = ["", f" sourcecode {__version__} · {tier}", "",
|
|
684
|
+
" AI coding-agent context, instant.", "", " Get started:"]
|
|
685
|
+
for cmd, desc in _WELCOME_CMDS:
|
|
686
|
+
lines.append(f" {cmd.ljust(34)}{desc}")
|
|
687
|
+
lines.append("")
|
|
688
|
+
lines.append(" sourcecode --help all commands")
|
|
689
|
+
if tier != "Pro":
|
|
690
|
+
lines.append(" sourcecode activate <key> unlock Pro")
|
|
691
|
+
lines.append("")
|
|
692
|
+
typer.echo("\n".join(lines))
|
|
693
|
+
|
|
694
|
+
|
|
695
|
+
def _print_welcome() -> None:
|
|
696
|
+
"""Branded quickstart shown only on a bare invocation at a human terminal.
|
|
697
|
+
|
|
698
|
+
Agents and pipes never reach here: they either pass args/flags or stdout is
|
|
699
|
+
not a TTY, so the JSON machine contract is completely unchanged.
|
|
700
|
+
"""
|
|
701
|
+
try:
|
|
702
|
+
from sourcecode import license as _lic
|
|
703
|
+
tier = "Pro" if _lic.is_pro else "Free"
|
|
704
|
+
except Exception:
|
|
705
|
+
tier = "Free"
|
|
706
|
+
|
|
707
|
+
try:
|
|
708
|
+
from rich import box
|
|
709
|
+
from rich.console import Console
|
|
710
|
+
from rich.panel import Panel
|
|
711
|
+
from rich.text import Text
|
|
712
|
+
except Exception:
|
|
713
|
+
_print_welcome_plain(tier)
|
|
714
|
+
return
|
|
715
|
+
|
|
716
|
+
pad = max(len(c) for c, _ in _WELCOME_CMDS) + 2
|
|
717
|
+
tier_style = "green" if tier != "Pro" else "magenta"
|
|
718
|
+
code_w = max(len(s) for s in _WELCOME_CODE)
|
|
719
|
+
|
|
720
|
+
t = Text()
|
|
721
|
+
for ln in _WELCOME_SOURCE:
|
|
722
|
+
t.append(ln + "\n", style="bold cyan")
|
|
723
|
+
# Append version + tier to the right of the "code" block's middle rows.
|
|
724
|
+
for i, ln in enumerate(_WELCOME_CODE):
|
|
725
|
+
t.append(ln.ljust(code_w), style="bold cyan")
|
|
726
|
+
if i == 2:
|
|
727
|
+
t.append(f" {__version__}", style="dim")
|
|
728
|
+
elif i == 3:
|
|
729
|
+
t.append(" ")
|
|
730
|
+
t.append(tier, style=tier_style)
|
|
731
|
+
t.append("\n")
|
|
732
|
+
|
|
733
|
+
t.append("\nAI coding-agent context, instant.\n\n", style="white")
|
|
734
|
+
|
|
735
|
+
for cmd, desc in _WELCOME_CMDS:
|
|
736
|
+
t.append("▸ ", style="cyan")
|
|
737
|
+
t.append(cmd.ljust(pad), style="bold")
|
|
738
|
+
t.append(desc + "\n", style="dim")
|
|
739
|
+
|
|
740
|
+
t.append("\n--help", style="bold")
|
|
741
|
+
t.append(" · ", style="dim")
|
|
742
|
+
if tier != "Pro":
|
|
743
|
+
t.append("activate <key>", style="bold")
|
|
744
|
+
t.append(" for Pro", style="dim")
|
|
745
|
+
else:
|
|
746
|
+
t.append("you're on Pro ✓", style="green")
|
|
747
|
+
|
|
748
|
+
panel = Panel(
|
|
749
|
+
t,
|
|
750
|
+
box=box.ROUNDED,
|
|
751
|
+
border_style="cyan",
|
|
752
|
+
padding=(1, 3),
|
|
753
|
+
expand=False,
|
|
754
|
+
)
|
|
755
|
+
Console().print(panel)
|
|
756
|
+
|
|
757
|
+
|
|
656
758
|
@app.callback(invoke_without_command=True)
|
|
657
759
|
def main(
|
|
658
760
|
ctx: typer.Context,
|
|
@@ -902,6 +1004,13 @@ def main(
|
|
|
902
1004
|
if ctx.invoked_subcommand is not None:
|
|
903
1005
|
return
|
|
904
1006
|
|
|
1007
|
+
# Bare invocation at a human terminal → branded quickstart instead of
|
|
1008
|
+
# dumping a large JSON blob. Agents/pipes are untouched: any arg/flag, or a
|
|
1009
|
+
# non-TTY stdout (piped), falls through to the normal analysis + JSON.
|
|
1010
|
+
if len(sys.argv) <= 1 and sys.stdout.isatty():
|
|
1011
|
+
_print_welcome()
|
|
1012
|
+
raise typer.Exit()
|
|
1013
|
+
|
|
905
1014
|
_t0 = time.monotonic()
|
|
906
1015
|
no_tree: bool = False # set True by --agent; --no-tree flag removed
|
|
907
1016
|
|
|
@@ -2797,6 +2906,17 @@ def prepare_context_cmd(
|
|
|
2797
2906
|
_cached_pctx = _pctx_cache.read(target, _pctx_cache_key)
|
|
2798
2907
|
if _cached_pctx is not None:
|
|
2799
2908
|
_emit_command_output(_cached_pctx, output_path, copy)
|
|
2909
|
+
try:
|
|
2910
|
+
from sourcecode import telemetry as _tel
|
|
2911
|
+
_tel.record(
|
|
2912
|
+
"execution_completed",
|
|
2913
|
+
cmd="prepare-context",
|
|
2914
|
+
feature=task,
|
|
2915
|
+
output_fmt=format,
|
|
2916
|
+
duration_s=0.0,
|
|
2917
|
+
)
|
|
2918
|
+
except Exception:
|
|
2919
|
+
pass
|
|
2800
2920
|
return
|
|
2801
2921
|
|
|
2802
2922
|
builder = TaskContextBuilder(target)
|
|
@@ -3192,6 +3312,18 @@ def prepare_context_cmd(
|
|
|
3192
3312
|
|
|
3193
3313
|
_emit_command_output(_pc_content, output_path, copy)
|
|
3194
3314
|
|
|
3315
|
+
try:
|
|
3316
|
+
from sourcecode import telemetry as _tel
|
|
3317
|
+
_tel.record(
|
|
3318
|
+
"execution_completed",
|
|
3319
|
+
cmd="prepare-context",
|
|
3320
|
+
feature=task,
|
|
3321
|
+
output_fmt=format,
|
|
3322
|
+
duration_s=_time.perf_counter() - _t0,
|
|
3323
|
+
)
|
|
3324
|
+
except Exception:
|
|
3325
|
+
pass
|
|
3326
|
+
|
|
3195
3327
|
from sourcecode.mcp_nudge import nudge_mcp_if_needed as _nudge
|
|
3196
3328
|
_nudge()
|
|
3197
3329
|
|
|
@@ -5282,26 +5414,9 @@ def activate_cmd(
|
|
|
5282
5414
|
|
|
5283
5415
|
|
|
5284
5416
|
# ---------------------------------------------------------------------------
|
|
5285
|
-
# Auth commands (
|
|
5417
|
+
# Auth commands (status / logout)
|
|
5286
5418
|
# ---------------------------------------------------------------------------
|
|
5287
5419
|
|
|
5288
|
-
@auth_app.command("login")
|
|
5289
|
-
def auth_login_cmd() -> None:
|
|
5290
|
-
"""Authenticate via browser (device code flow).
|
|
5291
|
-
|
|
5292
|
-
\b
|
|
5293
|
-
The CLI shows a URL. Open it in your browser, log in with your account,
|
|
5294
|
-
and the CLI completes authentication automatically.
|
|
5295
|
-
Credentials are stored in ~/.sourcecode/license.json (30-min cache; Supabase is source of truth).
|
|
5296
|
-
|
|
5297
|
-
\b
|
|
5298
|
-
Examples:
|
|
5299
|
-
sourcecode auth login
|
|
5300
|
-
"""
|
|
5301
|
-
from sourcecode.license import auth_login as _auth_login
|
|
5302
|
-
_auth_login()
|
|
5303
|
-
|
|
5304
|
-
|
|
5305
5420
|
@auth_app.command("status")
|
|
5306
5421
|
def auth_status_cmd() -> None:
|
|
5307
5422
|
"""Show current authentication and plan status."""
|
|
@@ -71,9 +71,6 @@ _DELTA_FREE_LIMIT: int = 30
|
|
|
71
71
|
# Hybrid model size limit: repos at/under this many Java source files are fully
|
|
72
72
|
# free (every command, no caps). Above it = enterprise-scale monolith = Pro.
|
|
73
73
|
_FREE_REPO_JAVA_FILE_LIMIT: int = 500
|
|
74
|
-
_DEVICE_POLL_INTERVAL_S: float = 2.5
|
|
75
|
-
_DEVICE_POLL_TIMEOUT_S: float = 300.0 # 5-minute window for user to complete browser auth
|
|
76
|
-
_AUTH_BASE_URL: str = "https://sourcecode.dev"
|
|
77
74
|
_LICENSE_KEY_RE = re.compile(r"^[A-Za-z0-9_\-]{1,200}$")
|
|
78
75
|
|
|
79
76
|
# ---------------------------------------------------------------------------
|
|
@@ -243,78 +240,6 @@ def _call_get_license(license_key: str) -> Optional[dict]:
|
|
|
243
240
|
return None # Network error — caller decides what to do
|
|
244
241
|
|
|
245
242
|
|
|
246
|
-
def _generate_device_code() -> str:
|
|
247
|
-
"""Generate a human-readable device code: XXXX-XXXX-XXXX."""
|
|
248
|
-
import uuid
|
|
249
|
-
raw = uuid.uuid4().hex.upper()
|
|
250
|
-
return f"{raw[:4]}-{raw[4:8]}-{raw[8:12]}"
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
def _call_device_check(device_code: str) -> Optional[dict]:
|
|
254
|
-
"""Poll /device-check edge function. Returns dict or None on network error.
|
|
255
|
-
|
|
256
|
-
Expected responses:
|
|
257
|
-
{"status": "pending"}
|
|
258
|
-
{"status": "complete", "device_token": "...", "email": "...", "plan": "pro", ...}
|
|
259
|
-
{"status": "error", "message": "..."}
|
|
260
|
-
"""
|
|
261
|
-
import urllib.error
|
|
262
|
-
import urllib.request
|
|
263
|
-
|
|
264
|
-
if not _SUPABASE_ANON_KEY:
|
|
265
|
-
return None
|
|
266
|
-
|
|
267
|
-
url = f"{_SUPABASE_URL}/functions/v1/device-check"
|
|
268
|
-
body = json.dumps({"device_code": device_code}).encode("utf-8")
|
|
269
|
-
req = urllib.request.Request(url, data=body, method="POST")
|
|
270
|
-
req.add_header("apikey", _SUPABASE_ANON_KEY)
|
|
271
|
-
req.add_header("Authorization", f"Bearer {_SUPABASE_ANON_KEY}")
|
|
272
|
-
req.add_header("Content-Type", "application/json")
|
|
273
|
-
req.add_header("Accept", "application/json")
|
|
274
|
-
try:
|
|
275
|
-
with urllib.request.urlopen(req, timeout=8) as resp:
|
|
276
|
-
return json.loads(resp.read().decode("utf-8"))
|
|
277
|
-
except urllib.error.HTTPError as exc:
|
|
278
|
-
try:
|
|
279
|
-
return json.loads(exc.read().decode("utf-8", errors="replace"))
|
|
280
|
-
except Exception:
|
|
281
|
-
return {"status": "error", "message": f"HTTP {exc.code}"}
|
|
282
|
-
except Exception:
|
|
283
|
-
return None
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
def _call_get_user_plan(device_token: str) -> Optional[dict]:
|
|
287
|
-
"""Fetch current plan/status for an authenticated device token.
|
|
288
|
-
|
|
289
|
-
Expected response:
|
|
290
|
-
{"valid": true, "plan": "pro", "status": "active", "features": [...], "email": "..."}
|
|
291
|
-
{"valid": false, "error": "token_revoked"}
|
|
292
|
-
"""
|
|
293
|
-
import urllib.error
|
|
294
|
-
import urllib.request
|
|
295
|
-
|
|
296
|
-
if not _SUPABASE_ANON_KEY:
|
|
297
|
-
return None
|
|
298
|
-
|
|
299
|
-
url = f"{_SUPABASE_URL}/functions/v1/get-user-plan"
|
|
300
|
-
body = json.dumps({"device_token": device_token}).encode("utf-8")
|
|
301
|
-
req = urllib.request.Request(url, data=body, method="POST")
|
|
302
|
-
req.add_header("apikey", _SUPABASE_ANON_KEY)
|
|
303
|
-
req.add_header("Authorization", f"Bearer {_SUPABASE_ANON_KEY}")
|
|
304
|
-
req.add_header("Content-Type", "application/json")
|
|
305
|
-
req.add_header("Accept", "application/json")
|
|
306
|
-
try:
|
|
307
|
-
with urllib.request.urlopen(req, timeout=8) as resp:
|
|
308
|
-
return json.loads(resp.read().decode("utf-8"))
|
|
309
|
-
except urllib.error.HTTPError as exc:
|
|
310
|
-
try:
|
|
311
|
-
return json.loads(exc.read().decode("utf-8", errors="replace"))
|
|
312
|
-
except Exception:
|
|
313
|
-
return {"valid": False, "error": f"HTTP {exc.code}"}
|
|
314
|
-
except Exception:
|
|
315
|
-
return None
|
|
316
|
-
|
|
317
|
-
|
|
318
243
|
def _maybe_revalidate() -> None:
|
|
319
244
|
"""Re-validate cached license if stale. Mutates globals; never raises."""
|
|
320
245
|
global _license_data, is_pro
|
|
@@ -338,39 +263,7 @@ def _maybe_revalidate() -> None:
|
|
|
338
263
|
except Exception:
|
|
339
264
|
pass
|
|
340
265
|
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
if auth_method == "device_flow":
|
|
344
|
-
device_token = _license_data.get("device_token")
|
|
345
|
-
if not device_token:
|
|
346
|
-
return
|
|
347
|
-
result = _call_get_user_plan(device_token)
|
|
348
|
-
if result is None:
|
|
349
|
-
return # Network error — keep cached (offline-first)
|
|
350
|
-
if not result.get("valid", True):
|
|
351
|
-
_license_data = None
|
|
352
|
-
is_pro = False
|
|
353
|
-
try:
|
|
354
|
-
if _LICENSE_FILE.exists():
|
|
355
|
-
_LICENSE_FILE.unlink()
|
|
356
|
-
except Exception:
|
|
357
|
-
pass
|
|
358
|
-
return
|
|
359
|
-
_license_data["plan"] = result.get("plan", "free")
|
|
360
|
-
_license_data["status"] = result.get("status", "active")
|
|
361
|
-
_license_data["features"] = result.get("features", [])
|
|
362
|
-
_license_data["validated_at"] = datetime.now(timezone.utc).isoformat()
|
|
363
|
-
is_pro = (
|
|
364
|
-
_license_data.get("plan") == "pro"
|
|
365
|
-
and _license_data.get("status", "active") != "inactive"
|
|
366
|
-
)
|
|
367
|
-
try:
|
|
368
|
-
_write_license_file(_license_data)
|
|
369
|
-
except Exception:
|
|
370
|
-
pass
|
|
371
|
-
return
|
|
372
|
-
|
|
373
|
-
# Key-based auth (existing flow / legacy)
|
|
266
|
+
# Key-based auth
|
|
374
267
|
key = _license_data.get("license_key")
|
|
375
268
|
if not key:
|
|
376
269
|
return
|
|
@@ -416,6 +309,15 @@ _init()
|
|
|
416
309
|
# Entitlement helpers
|
|
417
310
|
# ---------------------------------------------------------------------------
|
|
418
311
|
|
|
312
|
+
def _emit_telemetry(event: str, **kw: object) -> None:
|
|
313
|
+
"""Best-effort telemetry emit. Respects opt-in; never raises or blocks."""
|
|
314
|
+
try:
|
|
315
|
+
from sourcecode import telemetry as _tel
|
|
316
|
+
_tel.record(event, **kw) # type: ignore[arg-type]
|
|
317
|
+
except Exception:
|
|
318
|
+
pass
|
|
319
|
+
|
|
320
|
+
|
|
419
321
|
def can_use(feature_name: str) -> bool:
|
|
420
322
|
"""Return True if the current plan has access to feature_name.
|
|
421
323
|
|
|
@@ -510,6 +412,7 @@ def require_feature(
|
|
|
510
412
|
}
|
|
511
413
|
if extra_fields:
|
|
512
414
|
payload.update(extra_fields)
|
|
415
|
+
_emit_telemetry("gate_blocked", feature=feature_name, success=False)
|
|
513
416
|
_emit_upgrade_and_exit(
|
|
514
417
|
f"'{display}' is a Pro feature.",
|
|
515
418
|
[info.get("description", ""), info.get("value", "")],
|
|
@@ -560,6 +463,7 @@ def require_repo_or_pro(
|
|
|
560
463
|
}
|
|
561
464
|
if extra_fields:
|
|
562
465
|
payload.update(extra_fields)
|
|
466
|
+
_emit_telemetry("gate_blocked", feature=feature_name, repo_size="large", success=False)
|
|
563
467
|
_emit_upgrade_and_exit(headline, body, payload)
|
|
564
468
|
|
|
565
469
|
|
|
@@ -574,105 +478,7 @@ def require_pro(feature_name: str) -> None:
|
|
|
574
478
|
|
|
575
479
|
|
|
576
480
|
# ---------------------------------------------------------------------------
|
|
577
|
-
#
|
|
578
|
-
# ---------------------------------------------------------------------------
|
|
579
|
-
|
|
580
|
-
def _finish_device_auth(result: dict) -> None:
|
|
581
|
-
"""Persist device-flow credentials and emit success JSON. Exits on error."""
|
|
582
|
-
global _license_data, is_pro
|
|
583
|
-
|
|
584
|
-
device_token = result.get("device_token") or result.get("access_token") or ""
|
|
585
|
-
email = result.get("email", "")
|
|
586
|
-
plan = result.get("plan", "free")
|
|
587
|
-
plan_status = (
|
|
588
|
-
result.get("status_detail")
|
|
589
|
-
or result.get("user_status")
|
|
590
|
-
or result.get("status", "active")
|
|
591
|
-
)
|
|
592
|
-
features = result.get("features") or []
|
|
593
|
-
|
|
594
|
-
if not device_token:
|
|
595
|
-
sys.stderr.write("\n")
|
|
596
|
-
_fail("auth_error", "Authentication completed but no session token received. Contact support.")
|
|
597
|
-
|
|
598
|
-
_LICENSE_DIR.mkdir(parents=True, exist_ok=True)
|
|
599
|
-
now = datetime.now(timezone.utc).isoformat()
|
|
600
|
-
data: dict = {
|
|
601
|
-
"auth_method": "device_flow",
|
|
602
|
-
"device_token": device_token,
|
|
603
|
-
"email": email,
|
|
604
|
-
"plan": plan,
|
|
605
|
-
"status": plan_status,
|
|
606
|
-
"features": features,
|
|
607
|
-
"authenticated_at": now,
|
|
608
|
-
"validated_at": now,
|
|
609
|
-
}
|
|
610
|
-
_write_license_file(data)
|
|
611
|
-
_license_data = data
|
|
612
|
-
is_pro = plan == "pro" and plan_status != "inactive"
|
|
613
|
-
|
|
614
|
-
sys.stderr.write(f"\n Authenticated as {email}. Plan: {plan}\n\n")
|
|
615
|
-
sys.stderr.flush()
|
|
616
|
-
|
|
617
|
-
output: dict = {"status": "authenticated", "email": email, "plan": plan, "pro": is_pro}
|
|
618
|
-
if not is_pro:
|
|
619
|
-
output["upgrade_hint"] = "https://sourcecode.dev/pricing"
|
|
620
|
-
else:
|
|
621
|
-
output["features"] = features
|
|
622
|
-
sys.stdout.write(json.dumps(output, ensure_ascii=False) + "\n")
|
|
623
|
-
sys.stdout.flush()
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
def auth_login() -> None:
|
|
627
|
-
"""Device code authentication flow.
|
|
628
|
-
|
|
629
|
-
Shows a browser URL; polls the backend every 2.5 s until the user
|
|
630
|
-
completes authentication or the 5-minute window expires.
|
|
631
|
-
Writes credentials to ~/.sourcecode/license.json on success.
|
|
632
|
-
Exits 0 on success, 1 on any failure.
|
|
633
|
-
"""
|
|
634
|
-
import time
|
|
635
|
-
|
|
636
|
-
device_code = _generate_device_code()
|
|
637
|
-
activate_url = f"{_AUTH_BASE_URL}/activate?code={device_code}"
|
|
638
|
-
|
|
639
|
-
sys.stderr.write(f"\n Open this URL to authenticate:\n {activate_url}\n\n Waiting")
|
|
640
|
-
sys.stderr.flush()
|
|
641
|
-
|
|
642
|
-
deadline = time.monotonic() + _DEVICE_POLL_TIMEOUT_S
|
|
643
|
-
_tick = 0
|
|
644
|
-
|
|
645
|
-
while time.monotonic() < deadline:
|
|
646
|
-
time.sleep(_DEVICE_POLL_INTERVAL_S)
|
|
647
|
-
_tick += 1
|
|
648
|
-
if _tick % 4 == 0:
|
|
649
|
-
sys.stderr.write(".")
|
|
650
|
-
sys.stderr.flush()
|
|
651
|
-
|
|
652
|
-
result = _call_device_check(device_code)
|
|
653
|
-
if result is None:
|
|
654
|
-
continue # network blip — keep polling
|
|
655
|
-
|
|
656
|
-
status = result.get("status")
|
|
657
|
-
if status == "pending":
|
|
658
|
-
continue
|
|
659
|
-
|
|
660
|
-
if status == "complete":
|
|
661
|
-
_finish_device_auth(result)
|
|
662
|
-
return
|
|
663
|
-
|
|
664
|
-
if status == "error" or result.get("error"):
|
|
665
|
-
sys.stderr.write("\n")
|
|
666
|
-
_fail("auth_error", result.get("message") or result.get("error") or "Authentication failed.")
|
|
667
|
-
|
|
668
|
-
# Unknown status — keep polling
|
|
669
|
-
|
|
670
|
-
sys.stderr.write("\n")
|
|
671
|
-
_fail("auth_timeout", "Authentication timed out after 5 minutes. Please try again.")
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
# ---------------------------------------------------------------------------
|
|
675
|
-
# Activation (key-based — legacy / direct key entry)
|
|
481
|
+
# Activation (key-based — direct key entry)
|
|
676
482
|
# ---------------------------------------------------------------------------
|
|
677
483
|
|
|
678
484
|
def activate_license(license_key: str) -> None:
|
|
@@ -693,9 +499,11 @@ def activate_license(license_key: str) -> None:
|
|
|
693
499
|
_fail("network_error", "Could not reach license server. Check your internet connection.")
|
|
694
500
|
|
|
695
501
|
if not result.get("valid"):
|
|
502
|
+
_emit_telemetry("activation", feature="key", success=False, error_kind="InvalidLicense")
|
|
696
503
|
_fail("invalid_license", result.get("error", "License key is not valid or subscription is inactive."))
|
|
697
504
|
|
|
698
505
|
if result.get("plan") != "pro":
|
|
506
|
+
_emit_telemetry("activation", feature="key", success=False, error_kind="NotPro")
|
|
699
507
|
_fail("not_pro", "This license is not a Pro license.")
|
|
700
508
|
|
|
701
509
|
_LICENSE_DIR.mkdir(parents=True, exist_ok=True)
|
|
@@ -709,6 +517,7 @@ def activate_license(license_key: str) -> None:
|
|
|
709
517
|
"validated_at": now,
|
|
710
518
|
}
|
|
711
519
|
_write_license_file(data)
|
|
520
|
+
_emit_telemetry("activation", feature="key", success=True)
|
|
712
521
|
|
|
713
522
|
output = {"status": "activated", "plan": "pro", "features": data["features"]}
|
|
714
523
|
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=
|
|
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)
|