codedna 0.9.2__tar.gz → 0.9.4__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.
Files changed (94) hide show
  1. {codedna-0.9.2 → codedna-0.9.4}/PKG-INFO +1 -1
  2. {codedna-0.9.2 → codedna-0.9.4}/codedna/__init__.py +1 -1
  3. {codedna-0.9.2 → codedna-0.9.4}/codedna/cli.py +93 -4
  4. codedna-0.9.4/codedna/natureco_account.py +200 -0
  5. {codedna-0.9.2 → codedna-0.9.4}/codedna/scorer.py +4 -1
  6. {codedna-0.9.2 → codedna-0.9.4}/pyproject.toml +1 -2
  7. {codedna-0.9.2 → codedna-0.9.4}/.gitignore +0 -0
  8. {codedna-0.9.2 → codedna-0.9.4}/LICENSE +0 -0
  9. {codedna-0.9.2 → codedna-0.9.4}/README.md +0 -0
  10. {codedna-0.9.2 → codedna-0.9.4}/codedna/ai.py +0 -0
  11. {codedna-0.9.2 → codedna-0.9.4}/codedna/ai_box.py +0 -0
  12. {codedna-0.9.2 → codedna-0.9.4}/codedna/ai_fingerprint.py +0 -0
  13. {codedna-0.9.2 → codedna-0.9.4}/codedna/analyzer.py +0 -0
  14. {codedna-0.9.2 → codedna-0.9.4}/codedna/api.py +0 -0
  15. {codedna-0.9.2 → codedna-0.9.4}/codedna/auth.py +0 -0
  16. {codedna-0.9.2 → codedna-0.9.4}/codedna/bus_factor.py +0 -0
  17. {codedna-0.9.2 → codedna-0.9.4}/codedna/db.py +0 -0
  18. {codedna-0.9.2 → codedna-0.9.4}/codedna/demo.py +0 -0
  19. {codedna-0.9.2 → codedna-0.9.4}/codedna/git_hook.py +0 -0
  20. {codedna-0.9.2 → codedna-0.9.4}/codedna/integrations/__init__.py +0 -0
  21. {codedna-0.9.2 → codedna-0.9.4}/codedna/integrations/github_bot.py +0 -0
  22. {codedna-0.9.2 → codedna-0.9.4}/codedna/integrations/jira.py +0 -0
  23. {codedna-0.9.2 → codedna-0.9.4}/codedna/integrations/lemonsqueezy.py +0 -0
  24. {codedna-0.9.2 → codedna-0.9.4}/codedna/interview.py +0 -0
  25. {codedna-0.9.2 → codedna-0.9.4}/codedna/nl.py +0 -0
  26. {codedna-0.9.2 → codedna-0.9.4}/codedna/nl_tools.json +0 -0
  27. {codedna-0.9.2 → codedna-0.9.4}/codedna/onboarding.py +0 -0
  28. {codedna-0.9.2 → codedna-0.9.4}/codedna/plan.py +0 -0
  29. {codedna-0.9.2 → codedna-0.9.4}/codedna/protection.py +0 -0
  30. {codedna-0.9.2 → codedna-0.9.4}/codedna/rate_limit.py +0 -0
  31. {codedna-0.9.2 → codedna-0.9.4}/codedna/sprint_health.py +0 -0
  32. {codedna-0.9.2 → codedna-0.9.4}/codedna/survey.py +0 -0
  33. {codedna-0.9.2 → codedna-0.9.4}/codedna/tech_debt.py +0 -0
  34. {codedna-0.9.2 → codedna-0.9.4}/codedna/webhooks.py +0 -0
  35. {codedna-0.9.2 → codedna-0.9.4}/dashboard/.env.local.example +0 -0
  36. {codedna-0.9.2 → codedna-0.9.4}/dashboard/.gitignore +0 -0
  37. {codedna-0.9.2 → codedna-0.9.4}/dashboard/Dockerfile +0 -0
  38. {codedna-0.9.2 → codedna-0.9.4}/dashboard/README.md +0 -0
  39. {codedna-0.9.2 → codedna-0.9.4}/dashboard/app/ai-compare/page.tsx +0 -0
  40. {codedna-0.9.2 → codedna-0.9.4}/dashboard/app/api/auth/login/route.ts +0 -0
  41. {codedna-0.9.2 → codedna-0.9.4}/dashboard/app/api/auth/logout/route.ts +0 -0
  42. {codedna-0.9.2 → codedna-0.9.4}/dashboard/app/api/auth/me/route.ts +0 -0
  43. {codedna-0.9.2 → codedna-0.9.4}/dashboard/app/api/auth/register/route.ts +0 -0
  44. {codedna-0.9.2 → codedna-0.9.4}/dashboard/app/api/billing/checkout/route.ts +0 -0
  45. {codedna-0.9.2 → codedna-0.9.4}/dashboard/app/billing/page.tsx +0 -0
  46. {codedna-0.9.2 → codedna-0.9.4}/dashboard/app/bus-factor/page.tsx +0 -0
  47. {codedna-0.9.2 → codedna-0.9.4}/dashboard/app/commits/page.tsx +0 -0
  48. {codedna-0.9.2 → codedna-0.9.4}/dashboard/app/debt/page.tsx +0 -0
  49. {codedna-0.9.2 → codedna-0.9.4}/dashboard/app/favicon.ico +0 -0
  50. {codedna-0.9.2 → codedna-0.9.4}/dashboard/app/feedback/page.tsx +0 -0
  51. {codedna-0.9.2 → codedna-0.9.4}/dashboard/app/files/page.tsx +0 -0
  52. {codedna-0.9.2 → codedna-0.9.4}/dashboard/app/fonts/GeistMonoVF.woff +0 -0
  53. {codedna-0.9.2 → codedna-0.9.4}/dashboard/app/fonts/GeistVF.woff +0 -0
  54. {codedna-0.9.2 → codedna-0.9.4}/dashboard/app/globals.css +0 -0
  55. {codedna-0.9.2 → codedna-0.9.4}/dashboard/app/interview/page.tsx +0 -0
  56. {codedna-0.9.2 → codedna-0.9.4}/dashboard/app/layout.tsx +0 -0
  57. {codedna-0.9.2 → codedna-0.9.4}/dashboard/app/login/page.tsx +0 -0
  58. {codedna-0.9.2 → codedna-0.9.4}/dashboard/app/onboarding/page.tsx +0 -0
  59. {codedna-0.9.2 → codedna-0.9.4}/dashboard/app/page.tsx +0 -0
  60. {codedna-0.9.2 → codedna-0.9.4}/dashboard/app/pricing/page.tsx +0 -0
  61. {codedna-0.9.2 → codedna-0.9.4}/dashboard/app/protected/page.tsx +0 -0
  62. {codedna-0.9.2 → codedna-0.9.4}/dashboard/app/register/page.tsx +0 -0
  63. {codedna-0.9.2 → codedna-0.9.4}/dashboard/app/repos/page.tsx +0 -0
  64. {codedna-0.9.2 → codedna-0.9.4}/dashboard/app/settings/integrations/page.tsx +0 -0
  65. {codedna-0.9.2 → codedna-0.9.4}/dashboard/app/sprints/page.tsx +0 -0
  66. {codedna-0.9.2 → codedna-0.9.4}/dashboard/app/trends/page.tsx +0 -0
  67. {codedna-0.9.2 → codedna-0.9.4}/dashboard/components/AuthProvider.tsx +0 -0
  68. {codedna-0.9.2 → codedna-0.9.4}/dashboard/components/ClientProviders.tsx +0 -0
  69. {codedna-0.9.2 → codedna-0.9.4}/dashboard/components/CommitTable.tsx +0 -0
  70. {codedna-0.9.2 → codedna-0.9.4}/dashboard/components/CostInfoTooltip.tsx +0 -0
  71. {codedna-0.9.2 → codedna-0.9.4}/dashboard/components/ErrorBanner.tsx +0 -0
  72. {codedna-0.9.2 → codedna-0.9.4}/dashboard/components/FeatureGate.tsx +0 -0
  73. {codedna-0.9.2 → codedna-0.9.4}/dashboard/components/FileHeatmap.tsx +0 -0
  74. {codedna-0.9.2 → codedna-0.9.4}/dashboard/components/LanguageSwitcher.tsx +0 -0
  75. {codedna-0.9.2 → codedna-0.9.4}/dashboard/components/NavClient.tsx +0 -0
  76. {codedna-0.9.2 → codedna-0.9.4}/dashboard/components/OverviewTitles.tsx +0 -0
  77. {codedna-0.9.2 → codedna-0.9.4}/dashboard/components/QuickInsights.tsx +0 -0
  78. {codedna-0.9.2 → codedna-0.9.4}/dashboard/components/RiskBadge.tsx +0 -0
  79. {codedna-0.9.2 → codedna-0.9.4}/dashboard/components/SummaryCards.tsx +0 -0
  80. {codedna-0.9.2 → codedna-0.9.4}/dashboard/components/UnderstandingChart.tsx +0 -0
  81. {codedna-0.9.2 → codedna-0.9.4}/dashboard/components/UpgradeBanner.tsx +0 -0
  82. {codedna-0.9.2 → codedna-0.9.4}/dashboard/lib/api.ts +0 -0
  83. {codedna-0.9.2 → codedna-0.9.4}/dashboard/lib/auth.ts +0 -0
  84. {codedna-0.9.2 → codedna-0.9.4}/dashboard/lib/i18n/en.ts +0 -0
  85. {codedna-0.9.2 → codedna-0.9.4}/dashboard/lib/i18n/index.tsx +0 -0
  86. {codedna-0.9.2 → codedna-0.9.4}/dashboard/lib/i18n/tr.ts +0 -0
  87. {codedna-0.9.2 → codedna-0.9.4}/dashboard/lib/plan.ts +0 -0
  88. {codedna-0.9.2 → codedna-0.9.4}/dashboard/next-env.d.ts +0 -0
  89. {codedna-0.9.2 → codedna-0.9.4}/dashboard/next.config.mjs +0 -0
  90. {codedna-0.9.2 → codedna-0.9.4}/dashboard/package-lock.json +0 -0
  91. {codedna-0.9.2 → codedna-0.9.4}/dashboard/package.json +0 -0
  92. {codedna-0.9.2 → codedna-0.9.4}/dashboard/postcss.config.mjs +0 -0
  93. {codedna-0.9.2 → codedna-0.9.4}/dashboard/tailwind.config.ts +0 -0
  94. {codedna-0.9.2 → codedna-0.9.4}/dashboard/tsconfig.json +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: codedna
3
- Version: 0.9.2
3
+ Version: 0.9.4
4
4
  Summary: AI Code Transparency Tool — detect AI-written code, analyze commit quality, track understanding debt, sprint health, bus factor, and more
5
5
  Project-URL: Homepage, https://codedna.dev
6
6
  Project-URL: Repository, https://github.com/natureco-official/codedna
@@ -1,4 +1,4 @@
1
1
  """CodeDNA — AI Code Transparency Tool."""
2
2
 
3
- __version__ = "0.9.1"
3
+ __version__ = "0.9.4"
4
4
  __app_name__ = "codedna"
@@ -132,6 +132,52 @@ def init(
132
132
  console.print(Panel(info, border_style="green", padding=(1, 2)))
133
133
 
134
134
 
135
+ # ---------------------------------------------------------------------------
136
+ # codedna account (NatureCo SSO — shared with the NatureCo CLI & terminal)
137
+ # ---------------------------------------------------------------------------
138
+ @app.command()
139
+ def account(
140
+ action: str = typer.Argument("whoami", help="login | logout | whoami"),
141
+ ) -> None:
142
+ """Sign in with your NatureCo account (single account across the ecosystem)."""
143
+ from codedna import natureco_account as nc
144
+
145
+ if action == "whoami":
146
+ me = nc.whoami()
147
+ if me:
148
+ console.print(f"[green]✓[/green] Signed in as [bold]{me.get('email')}[/bold]")
149
+ else:
150
+ console.print("[yellow]Not signed in.[/yellow] Run [bold cyan]codedna account login[/bold cyan]")
151
+ return
152
+
153
+ if action == "logout":
154
+ nc.logout()
155
+ console.print("[green]✓[/green] Signed out.")
156
+ return
157
+
158
+ if action == "login":
159
+ email = typer.prompt("NatureCo email").strip()
160
+ use_pw = typer.confirm("Sign in with a password? (No = email a code/login link)", default=False)
161
+ try:
162
+ if use_pw:
163
+ password = typer.prompt("Password", hide_input=True)
164
+ res = nc.login_with_password(email, password)
165
+ else:
166
+ nc.send_otp(email)
167
+ console.print("[dim]Check your email — paste the 6-digit code or the full login link.[/dim]")
168
+ value = typer.prompt("Code or login link").strip()
169
+ res = nc.verify_link(value) if "://" in value else nc.verify_otp(email, value)
170
+ who = (res.get("user") or {}).get("email") or email
171
+ console.print(f"[green]✓ Signed in as[/green] [bold]{who}[/bold]")
172
+ except Exception as e: # noqa: BLE001
173
+ console.print(f"[red]✗ Sign-in failed:[/red] {e}")
174
+ raise typer.Exit(1) from e
175
+ return
176
+
177
+ console.print(f"[red]Unknown action:[/red] {action} [dim](use: login | logout | whoami)[/dim]")
178
+ raise typer.Exit(1)
179
+
180
+
135
181
  # ---------------------------------------------------------------------------
136
182
  # codedna scan
137
183
  # ---------------------------------------------------------------------------
@@ -140,17 +186,29 @@ def scan(
140
186
  repo: Optional[Path] = typer.Option(None, "--repo", "-r", help="Git repo directory"),
141
187
  max_files: int = typer.Option(200, "--max", "-m", help="Maximum number of files to scan"),
142
188
  min_risk: float = typer.Option(0.0, "--min-risk", help="Minimum AI probability filter (0.0-1.0)"),
189
+ json_out: bool = typer.Option(False, "--json", help="Output machine-readable JSON (for tooling)"),
143
190
  ) -> None:
144
191
  """Scan current repo and show AI risk report."""
145
- console.print()
146
- console.print("[bold cyan]🧬 CodeDNA[/bold cyan] — Scanning repo...\n")
192
+ if not json_out:
193
+ console.print()
194
+ console.print("[bold cyan]🧬 CodeDNA[/bold cyan] — Scanning repo...\n")
147
195
 
148
196
  root = repo or find_git_root() or Path.cwd()
149
197
 
150
- with console.status("[dim]Analyzing files...[/dim]"):
198
+ if json_out:
151
199
  results = scan_repository(root, max_files=max_files)
200
+ else:
201
+ with console.status("[dim]Analyzing files...[/dim]"):
202
+ results = scan_repository(root, max_files=max_files)
152
203
 
153
204
  if not results:
205
+ if json_out:
206
+ import json as _json
207
+ print(_json.dumps({
208
+ "version": __version__, "repo": str(root), "file_count": 0,
209
+ "avg_ai_probability": 0.0, "max_ai_probability": 0.0, "files": [],
210
+ }, indent=2))
211
+ raise typer.Exit(0)
154
212
  console.print("[yellow]No supported files found to scan.[/yellow]")
155
213
  console.print("[dim]Supported: .py .js .jsx .ts .tsx[/dim]")
156
214
  return
@@ -170,6 +228,30 @@ def scan(
170
228
  except Exception:
171
229
  pass
172
230
 
231
+ # --json: machine-readable output for tooling (e.g. the NatureCo CLI `dna` command)
232
+ if json_out:
233
+ import json as _json
234
+ avg_ai = (sum(s.ai_probability for s in results) / len(results)) if results else 0.0
235
+ payload = {
236
+ "version": __version__,
237
+ "repo": str(root),
238
+ "file_count": len(results),
239
+ "avg_ai_probability": round(avg_ai, 4),
240
+ "max_ai_probability": round(max((s.ai_probability for s in results), default=0.0), 4),
241
+ "files": [
242
+ {
243
+ "file": s.file_path,
244
+ "ai_probability": round(s.ai_probability, 4),
245
+ "complexity": s.complexity_label,
246
+ "lines": s.total_lines,
247
+ "understanding": understanding_scores_map.get(s.file_path),
248
+ }
249
+ for s in results
250
+ ],
251
+ }
252
+ print(_json.dumps(payload, indent=2, default=str))
253
+ raise typer.Exit(0)
254
+
173
255
  # Build table
174
256
  table = Table(
175
257
  title="",
@@ -2052,7 +2134,14 @@ def ask(
2052
2134
  dry_run: bool = typer.Option(False, "--dry-run", help="Show the command without running it."),
2053
2135
  ) -> None:
2054
2136
  """Run a CodeDNA command from a natural-language request (powered by Needle)."""
2055
- from codedna.nl import route_command
2137
+ try:
2138
+ from codedna.nl import route_command
2139
+ except ImportError as e:
2140
+ # The natural-language router needs the Needle model, which isn't part of
2141
+ # the published package. Degrade gracefully instead of crashing.
2142
+ console.print("[yellow]The natural-language [bold]ask[/bold] command needs the Needle model, which isn't available in this install.[/yellow]")
2143
+ console.print("[dim]Use the direct commands instead — e.g. [bold]codedna scan[/bold]. Run [bold]codedna --help[/bold] for the full list.[/dim]")
2144
+ raise typer.Exit(1) from e
2056
2145
  route_command(" ".join(query), dry_run=dry_run)
2057
2146
 
2058
2147
 
@@ -0,0 +1,200 @@
1
+ """NatureCo Account (SSO) — one NatureCo account, ecosystem-wide.
2
+
3
+ Shares the same session file (``~/.natureco/auth.json``) with the NatureCo CLI
4
+ and Cupertino Terminal, so signing in once works everywhere. Built on the
5
+ natureco.me Supabase Auth REST API. Dependency-free (standard-library urllib);
6
+ the anon key is public (embedded in every client, not a secret).
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import base64
12
+ import json
13
+ import os
14
+ import time
15
+ import urllib.error
16
+ import urllib.request
17
+ from pathlib import Path
18
+ from urllib.parse import parse_qs, urlparse
19
+
20
+ # natureco.me identity project — anon key is PUBLIC (shipped in clients, not secret)
21
+ SUPABASE_URL = os.environ.get("NATURECO_SUPABASE_URL", "https://mxnlehflfkesasclcldy.supabase.co")
22
+ SUPABASE_ANON = os.environ.get(
23
+ "NATURECO_SUPABASE_ANON",
24
+ "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Im14bmxlaGZsZmtlc2FzY2xjbGR5Iiwicm9sZSI6ImFub24iLCJpYXQiOjE3NzY2NDA5MzEsImV4cCI6MjA5MjIxNjkzMX0.93aPOg6bVmgFaJvsM5jVZwiX2TTuFIyAzhP6BlhBkGU",
25
+ )
26
+ AUTH_BASE = f"{SUPABASE_URL}/auth/v1"
27
+
28
+
29
+ class NatureCoAuthError(Exception):
30
+ def __init__(self, message: str, status_code: int | None = None):
31
+ super().__init__(message)
32
+ self.status_code = status_code
33
+
34
+
35
+ def _auth_file() -> Path:
36
+ return Path.home() / ".natureco" / "auth.json"
37
+
38
+
39
+ def load_session() -> dict | None:
40
+ try:
41
+ return json.loads(_auth_file().read_text(encoding="utf-8"))
42
+ except Exception:
43
+ return None
44
+
45
+
46
+ def save_session(session: dict) -> dict:
47
+ f = _auth_file()
48
+ f.parent.mkdir(parents=True, exist_ok=True)
49
+ f.write_text(json.dumps(session, indent=2), encoding="utf-8")
50
+ try:
51
+ os.chmod(f, 0o600)
52
+ except Exception:
53
+ pass
54
+ return session
55
+
56
+
57
+ def clear_session() -> None:
58
+ try:
59
+ _auth_file().unlink()
60
+ except Exception:
61
+ pass
62
+
63
+
64
+ def _post(path: str, body: dict, access_token: str | None = None) -> dict:
65
+ headers = {"apikey": SUPABASE_ANON, "Content-Type": "application/json"}
66
+ if access_token:
67
+ headers["Authorization"] = f"Bearer {access_token}"
68
+ req = urllib.request.Request(
69
+ f"{AUTH_BASE}{path}", data=json.dumps(body).encode(), headers=headers, method="POST"
70
+ )
71
+ try:
72
+ with urllib.request.urlopen(req, timeout=30) as r:
73
+ return json.loads(r.read().decode())
74
+ except urllib.error.HTTPError as e:
75
+ try:
76
+ data = json.loads(e.read().decode())
77
+ except Exception:
78
+ data = {}
79
+ msg = data.get("error_description") or data.get("msg") or data.get("error") or f"Auth error ({e.code})"
80
+ raise NatureCoAuthError(msg, e.code) from e
81
+
82
+
83
+ def _shape(s: dict) -> dict:
84
+ expires_at = s.get("expires_at") or (int(time.time()) + s["expires_in"] if s.get("expires_in") else None)
85
+ user = s.get("user")
86
+ return {
87
+ "access_token": s.get("access_token"),
88
+ "refresh_token": s.get("refresh_token"),
89
+ "token_type": s.get("token_type", "bearer"),
90
+ "expires_at": expires_at,
91
+ "user": {"id": user["id"], "email": user["email"]} if user else None,
92
+ }
93
+
94
+
95
+ def _user_from_jwt(token: str) -> dict | None:
96
+ """Decode the user out of the access token (no signature check — display only)."""
97
+ try:
98
+ part = token.split(".")[1]
99
+ part += "=" * (-len(part) % 4)
100
+ p = json.loads(base64.urlsafe_b64decode(part))
101
+ return {"id": p.get("sub"), "email": p.get("email")}
102
+ except Exception:
103
+ return None
104
+
105
+
106
+ def login_with_password(email: str, password: str) -> dict:
107
+ return save_session(_shape(_post("/token?grant_type=password", {"email": email, "password": password})))
108
+
109
+
110
+ def send_otp(email: str) -> dict:
111
+ """Passwordless: send a code / login link to the email (existing account only)."""
112
+ _post("/otp", {"email": email, "create_user": False})
113
+ return {"sent": True, "email": email}
114
+
115
+
116
+ def verify_otp(email: str, token: str) -> dict:
117
+ """Verify the OTP code. Depending on the email template the verification type
118
+ may be 'email' or 'magiclink', so both are attempted."""
119
+ code = "".join(str(token).split())
120
+ try:
121
+ return save_session(_shape(_post("/verify", {"type": "email", "email": email, "token": code})))
122
+ except NatureCoAuthError as e1:
123
+ try:
124
+ return save_session(_shape(_post("/verify", {"type": "magiclink", "email": email, "token": code})))
125
+ except Exception:
126
+ raise e1
127
+
128
+
129
+ def verify_link(link: str) -> dict:
130
+ """Sign in from a magic login-link email. Handles both the implicit flow
131
+ (access_token in the URL fragment) and the token_hash flow."""
132
+ u = urlparse(link.strip())
133
+ frag = parse_qs(u.fragment)
134
+ q = parse_qs(u.query)
135
+
136
+ def pick(k: str):
137
+ return (frag.get(k) or q.get(k) or [None])[0]
138
+
139
+ access_token = pick("access_token")
140
+ if access_token:
141
+ return save_session(_shape({
142
+ "access_token": access_token,
143
+ "refresh_token": pick("refresh_token"),
144
+ "token_type": pick("token_type") or "bearer",
145
+ "expires_at": int(pick("expires_at") or 0) or None,
146
+ "expires_in": int(pick("expires_in") or 0) or None,
147
+ "user": _user_from_jwt(access_token),
148
+ }))
149
+ token_hash = pick("token_hash") or pick("token")
150
+ typ = pick("type") or "magiclink"
151
+ if not token_hash:
152
+ raise NatureCoAuthError("No verification token found in the link")
153
+ return save_session(_shape(_post("/verify", {"type": typ, "token_hash": token_hash})))
154
+
155
+
156
+ def refresh() -> dict:
157
+ s = load_session()
158
+ if not s or not s.get("refresh_token"):
159
+ raise NatureCoAuthError("Not signed in", 401)
160
+ return save_session(_shape(_post("/token?grant_type=refresh_token", {"refresh_token": s["refresh_token"]})))
161
+
162
+
163
+ def get_access_token() -> str | None:
164
+ s = load_session()
165
+ if not s:
166
+ return None
167
+ if s.get("expires_at") and time.time() > s["expires_at"] - 60:
168
+ try:
169
+ s = refresh()
170
+ except Exception:
171
+ return None
172
+ return s.get("access_token") if s else None
173
+
174
+
175
+ def whoami() -> dict | None:
176
+ token = get_access_token()
177
+ if not token:
178
+ return None
179
+ req = urllib.request.Request(
180
+ f"{AUTH_BASE}/user", headers={"apikey": SUPABASE_ANON, "Authorization": f"Bearer {token}"}
181
+ )
182
+ try:
183
+ with urllib.request.urlopen(req, timeout=30) as r:
184
+ return json.loads(r.read().decode())
185
+ except Exception:
186
+ return None
187
+
188
+
189
+ def is_logged_in() -> bool:
190
+ s = load_session()
191
+ return bool(s and s.get("access_token"))
192
+
193
+
194
+ def current_email() -> str | None:
195
+ s = load_session()
196
+ return s.get("user", {}).get("email") if s and s.get("user") else None
197
+
198
+
199
+ def logout() -> None:
200
+ clear_session()
@@ -142,7 +142,10 @@ def scan_repository(
142
142
  if file.suffix.lower() not in supported_extensions:
143
143
  continue
144
144
 
145
- relative_path = str(file.relative_to(root))
145
+ # Use POSIX (forward-slash) paths so they match `git ls-files` output on
146
+ # Windows too (relative_to() yields backslashes there → never matched the
147
+ # tracked set → scan returned 0 files on Windows).
148
+ relative_path = file.relative_to(root).as_posix()
146
149
 
147
150
  # Key fix: when a Git repo is present, scan ONLY git-tracked files.
148
151
  # This automatically excludes all build/generated files in .gitignore.
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "codedna"
3
- version = "0.9.2"
3
+ version = "0.9.4"
4
4
  description = "AI Code Transparency Tool — detect AI-written code, analyze commit quality, track understanding debt, sprint health, bus factor, and more"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.10"
@@ -51,7 +51,6 @@ dashboard = [
51
51
  "next-cmd>=0.5.0", # Next.js process management
52
52
  ]
53
53
  nl = [
54
- # "needle @ git+https://github.com/cactus-compute/needle.git", # TODO: publish needle to PyPI first
55
54
  "huggingface_hub",
56
55
  "pyarrow>=14,<18",
57
56
  ]
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes