codedna 0.9.2__tar.gz → 0.9.3__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.
- {codedna-0.9.2 → codedna-0.9.3}/PKG-INFO +1 -1
- {codedna-0.9.2 → codedna-0.9.3}/codedna/__init__.py +1 -1
- {codedna-0.9.2 → codedna-0.9.3}/codedna/cli.py +85 -3
- codedna-0.9.3/codedna/natureco_account.py +200 -0
- {codedna-0.9.2 → codedna-0.9.3}/codedna/scorer.py +4 -1
- {codedna-0.9.2 → codedna-0.9.3}/pyproject.toml +1 -2
- {codedna-0.9.2 → codedna-0.9.3}/.gitignore +0 -0
- {codedna-0.9.2 → codedna-0.9.3}/LICENSE +0 -0
- {codedna-0.9.2 → codedna-0.9.3}/README.md +0 -0
- {codedna-0.9.2 → codedna-0.9.3}/codedna/ai.py +0 -0
- {codedna-0.9.2 → codedna-0.9.3}/codedna/ai_box.py +0 -0
- {codedna-0.9.2 → codedna-0.9.3}/codedna/ai_fingerprint.py +0 -0
- {codedna-0.9.2 → codedna-0.9.3}/codedna/analyzer.py +0 -0
- {codedna-0.9.2 → codedna-0.9.3}/codedna/api.py +0 -0
- {codedna-0.9.2 → codedna-0.9.3}/codedna/auth.py +0 -0
- {codedna-0.9.2 → codedna-0.9.3}/codedna/bus_factor.py +0 -0
- {codedna-0.9.2 → codedna-0.9.3}/codedna/db.py +0 -0
- {codedna-0.9.2 → codedna-0.9.3}/codedna/demo.py +0 -0
- {codedna-0.9.2 → codedna-0.9.3}/codedna/git_hook.py +0 -0
- {codedna-0.9.2 → codedna-0.9.3}/codedna/integrations/__init__.py +0 -0
- {codedna-0.9.2 → codedna-0.9.3}/codedna/integrations/github_bot.py +0 -0
- {codedna-0.9.2 → codedna-0.9.3}/codedna/integrations/jira.py +0 -0
- {codedna-0.9.2 → codedna-0.9.3}/codedna/integrations/lemonsqueezy.py +0 -0
- {codedna-0.9.2 → codedna-0.9.3}/codedna/interview.py +0 -0
- {codedna-0.9.2 → codedna-0.9.3}/codedna/nl.py +0 -0
- {codedna-0.9.2 → codedna-0.9.3}/codedna/nl_tools.json +0 -0
- {codedna-0.9.2 → codedna-0.9.3}/codedna/onboarding.py +0 -0
- {codedna-0.9.2 → codedna-0.9.3}/codedna/plan.py +0 -0
- {codedna-0.9.2 → codedna-0.9.3}/codedna/protection.py +0 -0
- {codedna-0.9.2 → codedna-0.9.3}/codedna/rate_limit.py +0 -0
- {codedna-0.9.2 → codedna-0.9.3}/codedna/sprint_health.py +0 -0
- {codedna-0.9.2 → codedna-0.9.3}/codedna/survey.py +0 -0
- {codedna-0.9.2 → codedna-0.9.3}/codedna/tech_debt.py +0 -0
- {codedna-0.9.2 → codedna-0.9.3}/codedna/webhooks.py +0 -0
- {codedna-0.9.2 → codedna-0.9.3}/dashboard/.env.local.example +0 -0
- {codedna-0.9.2 → codedna-0.9.3}/dashboard/.gitignore +0 -0
- {codedna-0.9.2 → codedna-0.9.3}/dashboard/Dockerfile +0 -0
- {codedna-0.9.2 → codedna-0.9.3}/dashboard/README.md +0 -0
- {codedna-0.9.2 → codedna-0.9.3}/dashboard/app/ai-compare/page.tsx +0 -0
- {codedna-0.9.2 → codedna-0.9.3}/dashboard/app/api/auth/login/route.ts +0 -0
- {codedna-0.9.2 → codedna-0.9.3}/dashboard/app/api/auth/logout/route.ts +0 -0
- {codedna-0.9.2 → codedna-0.9.3}/dashboard/app/api/auth/me/route.ts +0 -0
- {codedna-0.9.2 → codedna-0.9.3}/dashboard/app/api/auth/register/route.ts +0 -0
- {codedna-0.9.2 → codedna-0.9.3}/dashboard/app/api/billing/checkout/route.ts +0 -0
- {codedna-0.9.2 → codedna-0.9.3}/dashboard/app/billing/page.tsx +0 -0
- {codedna-0.9.2 → codedna-0.9.3}/dashboard/app/bus-factor/page.tsx +0 -0
- {codedna-0.9.2 → codedna-0.9.3}/dashboard/app/commits/page.tsx +0 -0
- {codedna-0.9.2 → codedna-0.9.3}/dashboard/app/debt/page.tsx +0 -0
- {codedna-0.9.2 → codedna-0.9.3}/dashboard/app/favicon.ico +0 -0
- {codedna-0.9.2 → codedna-0.9.3}/dashboard/app/feedback/page.tsx +0 -0
- {codedna-0.9.2 → codedna-0.9.3}/dashboard/app/files/page.tsx +0 -0
- {codedna-0.9.2 → codedna-0.9.3}/dashboard/app/fonts/GeistMonoVF.woff +0 -0
- {codedna-0.9.2 → codedna-0.9.3}/dashboard/app/fonts/GeistVF.woff +0 -0
- {codedna-0.9.2 → codedna-0.9.3}/dashboard/app/globals.css +0 -0
- {codedna-0.9.2 → codedna-0.9.3}/dashboard/app/interview/page.tsx +0 -0
- {codedna-0.9.2 → codedna-0.9.3}/dashboard/app/layout.tsx +0 -0
- {codedna-0.9.2 → codedna-0.9.3}/dashboard/app/login/page.tsx +0 -0
- {codedna-0.9.2 → codedna-0.9.3}/dashboard/app/onboarding/page.tsx +0 -0
- {codedna-0.9.2 → codedna-0.9.3}/dashboard/app/page.tsx +0 -0
- {codedna-0.9.2 → codedna-0.9.3}/dashboard/app/pricing/page.tsx +0 -0
- {codedna-0.9.2 → codedna-0.9.3}/dashboard/app/protected/page.tsx +0 -0
- {codedna-0.9.2 → codedna-0.9.3}/dashboard/app/register/page.tsx +0 -0
- {codedna-0.9.2 → codedna-0.9.3}/dashboard/app/repos/page.tsx +0 -0
- {codedna-0.9.2 → codedna-0.9.3}/dashboard/app/settings/integrations/page.tsx +0 -0
- {codedna-0.9.2 → codedna-0.9.3}/dashboard/app/sprints/page.tsx +0 -0
- {codedna-0.9.2 → codedna-0.9.3}/dashboard/app/trends/page.tsx +0 -0
- {codedna-0.9.2 → codedna-0.9.3}/dashboard/components/AuthProvider.tsx +0 -0
- {codedna-0.9.2 → codedna-0.9.3}/dashboard/components/ClientProviders.tsx +0 -0
- {codedna-0.9.2 → codedna-0.9.3}/dashboard/components/CommitTable.tsx +0 -0
- {codedna-0.9.2 → codedna-0.9.3}/dashboard/components/CostInfoTooltip.tsx +0 -0
- {codedna-0.9.2 → codedna-0.9.3}/dashboard/components/ErrorBanner.tsx +0 -0
- {codedna-0.9.2 → codedna-0.9.3}/dashboard/components/FeatureGate.tsx +0 -0
- {codedna-0.9.2 → codedna-0.9.3}/dashboard/components/FileHeatmap.tsx +0 -0
- {codedna-0.9.2 → codedna-0.9.3}/dashboard/components/LanguageSwitcher.tsx +0 -0
- {codedna-0.9.2 → codedna-0.9.3}/dashboard/components/NavClient.tsx +0 -0
- {codedna-0.9.2 → codedna-0.9.3}/dashboard/components/OverviewTitles.tsx +0 -0
- {codedna-0.9.2 → codedna-0.9.3}/dashboard/components/QuickInsights.tsx +0 -0
- {codedna-0.9.2 → codedna-0.9.3}/dashboard/components/RiskBadge.tsx +0 -0
- {codedna-0.9.2 → codedna-0.9.3}/dashboard/components/SummaryCards.tsx +0 -0
- {codedna-0.9.2 → codedna-0.9.3}/dashboard/components/UnderstandingChart.tsx +0 -0
- {codedna-0.9.2 → codedna-0.9.3}/dashboard/components/UpgradeBanner.tsx +0 -0
- {codedna-0.9.2 → codedna-0.9.3}/dashboard/lib/api.ts +0 -0
- {codedna-0.9.2 → codedna-0.9.3}/dashboard/lib/auth.ts +0 -0
- {codedna-0.9.2 → codedna-0.9.3}/dashboard/lib/i18n/en.ts +0 -0
- {codedna-0.9.2 → codedna-0.9.3}/dashboard/lib/i18n/index.tsx +0 -0
- {codedna-0.9.2 → codedna-0.9.3}/dashboard/lib/i18n/tr.ts +0 -0
- {codedna-0.9.2 → codedna-0.9.3}/dashboard/lib/plan.ts +0 -0
- {codedna-0.9.2 → codedna-0.9.3}/dashboard/next-env.d.ts +0 -0
- {codedna-0.9.2 → codedna-0.9.3}/dashboard/next.config.mjs +0 -0
- {codedna-0.9.2 → codedna-0.9.3}/dashboard/package-lock.json +0 -0
- {codedna-0.9.2 → codedna-0.9.3}/dashboard/package.json +0 -0
- {codedna-0.9.2 → codedna-0.9.3}/dashboard/postcss.config.mjs +0 -0
- {codedna-0.9.2 → codedna-0.9.3}/dashboard/tailwind.config.ts +0 -0
- {codedna-0.9.2 → codedna-0.9.3}/dashboard/tsconfig.json +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: codedna
|
|
3
|
-
Version: 0.9.
|
|
3
|
+
Version: 0.9.3
|
|
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
|
|
@@ -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
|
-
|
|
146
|
-
|
|
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
|
-
|
|
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="",
|
|
@@ -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
|
-
|
|
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.
|
|
3
|
+
version = "0.9.3"
|
|
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
|
|
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
|
|
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
|