janexai 0.1.0__py3-none-any.whl → 0.2.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.
- janex/__init__.py +1 -1
- janex/main.py +216 -30
- {janexai-0.1.0.dist-info → janexai-0.2.0.dist-info}/METADATA +1 -1
- janexai-0.2.0.dist-info/RECORD +7 -0
- janexai-0.1.0.dist-info/RECORD +0 -7
- {janexai-0.1.0.dist-info → janexai-0.2.0.dist-info}/WHEEL +0 -0
- {janexai-0.1.0.dist-info → janexai-0.2.0.dist-info}/entry_points.txt +0 -0
- {janexai-0.1.0.dist-info → janexai-0.2.0.dist-info}/top_level.txt +0 -0
janex/__init__.py
CHANGED
janex/main.py
CHANGED
|
@@ -25,7 +25,7 @@ from rich import print as rprint
|
|
|
25
25
|
from . import __version__
|
|
26
26
|
|
|
27
27
|
# API Configuration
|
|
28
|
-
DEFAULT_API_URL = "https://
|
|
28
|
+
DEFAULT_API_URL = "https://api.janexai.dev"
|
|
29
29
|
CONFIG_DIR = Path.home() / ".janex"
|
|
30
30
|
CONFIG_FILE = CONFIG_DIR / "config.json"
|
|
31
31
|
|
|
@@ -165,13 +165,23 @@ def login(
|
|
|
165
165
|
config["api_token"] = token
|
|
166
166
|
save_config(config)
|
|
167
167
|
|
|
168
|
-
# Test the token
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
168
|
+
# Test the token by calling authenticated endpoint
|
|
169
|
+
try:
|
|
170
|
+
response = api_request("GET", "/me/")
|
|
171
|
+
if response.status_code == 200:
|
|
172
|
+
console.print("[green]✓[/green] Successfully logged in!")
|
|
173
|
+
console.print(f"Config saved to: {CONFIG_FILE}")
|
|
174
|
+
elif response.status_code == 401:
|
|
175
|
+
console.print("[red]Error:[/red] Invalid API token")
|
|
176
|
+
# Remove invalid token
|
|
177
|
+
config.pop("api_token", None)
|
|
178
|
+
save_config(config)
|
|
179
|
+
raise typer.Exit(1)
|
|
180
|
+
else:
|
|
181
|
+
console.print("[yellow]Token saved, but could not verify.[/yellow]")
|
|
182
|
+
console.print(f"Server returned: {response.status_code}")
|
|
183
|
+
except Exception as e:
|
|
184
|
+
console.print(f"[yellow]Token saved, but could not verify: {e}[/yellow]")
|
|
175
185
|
|
|
176
186
|
|
|
177
187
|
@app.command()
|
|
@@ -213,6 +223,7 @@ def scan(
|
|
|
213
223
|
no_claude: bool = typer.Option(False, "--no-claude", help="Skip Claude analysis"),
|
|
214
224
|
watch: bool = typer.Option(False, "--watch", "-w", help="Watch and wait for results"),
|
|
215
225
|
verbose: bool = typer.Option(False, "--verbose", "-v", help="Verbose output"),
|
|
226
|
+
force: bool = typer.Option(False, "--force", "-f", help="Force re-ingest (delete cached data)"),
|
|
216
227
|
):
|
|
217
228
|
"""
|
|
218
229
|
Scan a GitHub repository for security and quality issues.
|
|
@@ -243,12 +254,31 @@ def scan(
|
|
|
243
254
|
console=console,
|
|
244
255
|
) as progress:
|
|
245
256
|
|
|
257
|
+
# Step 0: If force, delete existing repo first
|
|
258
|
+
if force:
|
|
259
|
+
task = progress.add_task("Deleting cached data...", total=None)
|
|
260
|
+
# Try to find and delete existing repo
|
|
261
|
+
try:
|
|
262
|
+
check_response = api_request("GET", f"/repositories/{owner}/{repo_name}/", timeout=30)
|
|
263
|
+
if check_response.status_code == 200:
|
|
264
|
+
existing = check_response.json()
|
|
265
|
+
existing_id = existing.get("id")
|
|
266
|
+
if existing_id:
|
|
267
|
+
del_response = api_request("POST", f"/repositories/{existing_id}/delete/", timeout=30)
|
|
268
|
+
if del_response.status_code == 200:
|
|
269
|
+
progress.update(task, description="[green]✓[/green] Cached data deleted")
|
|
270
|
+
else:
|
|
271
|
+
progress.update(task, description="[yellow]![/yellow] Could not delete (continuing anyway)")
|
|
272
|
+
except Exception:
|
|
273
|
+
pass # Ignore errors, just proceed with ingestion
|
|
274
|
+
|
|
246
275
|
# Step 1: Ingest repository
|
|
247
276
|
task = progress.add_task("Ingesting repository...", total=None)
|
|
248
277
|
|
|
249
278
|
response = api_request("POST", "/repositories/ingest/", {
|
|
250
279
|
"owner": owner,
|
|
251
280
|
"repo": repo_name,
|
|
281
|
+
"force": force, # Tell backend to force re-ingest
|
|
252
282
|
}, timeout=120)
|
|
253
283
|
|
|
254
284
|
if response.status_code == 401:
|
|
@@ -262,10 +292,13 @@ def scan(
|
|
|
262
292
|
raise typer.Exit(1)
|
|
263
293
|
|
|
264
294
|
ingest_data = response.json()
|
|
265
|
-
repo_id = ingest_data.get("repository_id")
|
|
295
|
+
repo_id = ingest_data.get("id") or ingest_data.get("repository_id")
|
|
296
|
+
already_exists = ingest_data.get("already_exists", False)
|
|
266
297
|
|
|
267
298
|
if not repo_id:
|
|
268
299
|
console.print("[red]Error:[/red] No repository ID returned")
|
|
300
|
+
if verbose:
|
|
301
|
+
console.print(f"Response: {ingest_data}")
|
|
269
302
|
raise typer.Exit(1)
|
|
270
303
|
|
|
271
304
|
progress.update(task, description=f"[green]✓[/green] Repository ingested ({ingest_data.get('files_count', 0)} files)")
|
|
@@ -273,19 +306,55 @@ def scan(
|
|
|
273
306
|
if verbose:
|
|
274
307
|
console.print(f" Commit: {ingest_data.get('commit_sha', 'unknown')[:8]}")
|
|
275
308
|
|
|
276
|
-
|
|
277
|
-
progress.update(task, description="Running security & quality analysis...")
|
|
309
|
+
report = None
|
|
278
310
|
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
311
|
+
# If repo already exists (not forced), check for existing reports first
|
|
312
|
+
if already_exists and not force:
|
|
313
|
+
progress.update(task, description="Checking for existing reports...")
|
|
314
|
+
reports_response = api_request("GET", f"/repositories/{repo_id}/reports/", timeout=30)
|
|
315
|
+
|
|
316
|
+
if reports_response.status_code == 200:
|
|
317
|
+
reports_data = reports_response.json()
|
|
318
|
+
reports_list = reports_data.get("reports", [])
|
|
319
|
+
|
|
320
|
+
if reports_list:
|
|
321
|
+
# Get the latest report
|
|
322
|
+
latest_report_id = reports_list[0].get("id")
|
|
323
|
+
if latest_report_id:
|
|
324
|
+
progress.update(task, description="Fetching existing report...")
|
|
325
|
+
report_response = api_request("GET", f"/reports/{latest_report_id}/", timeout=30)
|
|
326
|
+
|
|
327
|
+
if report_response.status_code == 200:
|
|
328
|
+
report = report_response.json()
|
|
329
|
+
# Normalize: get_report returns 'analysis_result', analyze returns 'analysis'
|
|
330
|
+
if 'analysis_result' in report and 'analysis' not in report:
|
|
331
|
+
report['analysis'] = report['analysis_result']
|
|
332
|
+
progress.update(task, description="[green]✓[/green] Using existing report")
|
|
333
|
+
if verbose:
|
|
334
|
+
console.print(" [dim]Using cached report. Use -f to force re-analysis.[/dim]")
|
|
282
335
|
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
336
|
+
# If no existing report, run analysis
|
|
337
|
+
if report is None:
|
|
338
|
+
progress.update(task, description="Running security & quality analysis...")
|
|
339
|
+
|
|
340
|
+
analyze_response = api_request("POST", f"/repositories/{repo_id}/analyze/", {
|
|
341
|
+
"use_claude": not no_claude,
|
|
342
|
+
}, timeout=600) # 10 min timeout for analysis
|
|
343
|
+
|
|
344
|
+
if analyze_response.status_code != 200:
|
|
345
|
+
try:
|
|
346
|
+
error = analyze_response.json().get("error", analyze_response.text)
|
|
347
|
+
except Exception:
|
|
348
|
+
error = analyze_response.text or f"Server returned {analyze_response.status_code}"
|
|
349
|
+
console.print(f"\n[red]Analysis Error:[/red] {error}")
|
|
350
|
+
raise typer.Exit(1)
|
|
351
|
+
|
|
352
|
+
try:
|
|
353
|
+
report = analyze_response.json()
|
|
354
|
+
except Exception:
|
|
355
|
+
console.print("\n[red]Error:[/red] Server returned invalid response (connection may have dropped)")
|
|
356
|
+
console.print("The server might have restarted during analysis. Try again.")
|
|
357
|
+
raise typer.Exit(1)
|
|
289
358
|
|
|
290
359
|
progress.update(task, description="[green]✓[/green] Analysis complete!")
|
|
291
360
|
|
|
@@ -302,21 +371,134 @@ def scan(
|
|
|
302
371
|
json.dump(report, f, indent=2, default=str)
|
|
303
372
|
console.print(f"\n[green]Report saved to:[/green] {output_path}")
|
|
304
373
|
|
|
305
|
-
# Display summary
|
|
306
|
-
|
|
374
|
+
# Display summary or full report
|
|
375
|
+
if verbose:
|
|
376
|
+
display_full_report(report)
|
|
377
|
+
else:
|
|
378
|
+
display_summary(report)
|
|
379
|
+
console.print("[dim]Use -v for detailed report, or -o report.json to save full results[/dim]")
|
|
307
380
|
|
|
308
381
|
except requests.exceptions.RequestException as e:
|
|
309
382
|
console.print(f"[red]Request Error:[/red] {e}")
|
|
310
383
|
raise typer.Exit(1)
|
|
311
384
|
|
|
312
385
|
|
|
386
|
+
def display_full_report(report: dict):
|
|
387
|
+
"""Display detailed scan report."""
|
|
388
|
+
console.print()
|
|
389
|
+
|
|
390
|
+
# The actual report data is nested under 'analysis'
|
|
391
|
+
analysis = report.get("analysis", {})
|
|
392
|
+
if not analysis:
|
|
393
|
+
console.print("[yellow]No detailed analysis data found[/yellow]")
|
|
394
|
+
display_summary(report)
|
|
395
|
+
return
|
|
396
|
+
|
|
397
|
+
# Architecture Summary
|
|
398
|
+
arch_summary = (
|
|
399
|
+
analysis.get("arch_summary") or
|
|
400
|
+
analysis.get("architecture_summary") or
|
|
401
|
+
analysis.get("summary") or
|
|
402
|
+
analysis.get("executive_summary")
|
|
403
|
+
)
|
|
404
|
+
if arch_summary:
|
|
405
|
+
console.print(Panel(
|
|
406
|
+
arch_summary,
|
|
407
|
+
title="[bold blue]Architecture Summary[/bold blue]",
|
|
408
|
+
border_style="blue"
|
|
409
|
+
))
|
|
410
|
+
console.print()
|
|
411
|
+
|
|
412
|
+
# Trust Score from analysis
|
|
413
|
+
trust_score = analysis.get("trust_score")
|
|
414
|
+
scores = analysis.get("scores", {})
|
|
415
|
+
if trust_score or scores:
|
|
416
|
+
console.print(f"[bold]Trust Score:[/bold] {trust_score or 'N/A'}/100")
|
|
417
|
+
if scores:
|
|
418
|
+
console.print(f" Security: {scores.get('security', 'N/A')}/100")
|
|
419
|
+
console.print(f" Quality: {scores.get('quality', 'N/A')}/100")
|
|
420
|
+
console.print(f" Architecture: {scores.get('architecture', 'N/A')}/100")
|
|
421
|
+
console.print()
|
|
422
|
+
|
|
423
|
+
# Structural Findings (from arch agent)
|
|
424
|
+
structural_findings = analysis.get("structural_findings") or analysis.get("findings", [])
|
|
425
|
+
if structural_findings:
|
|
426
|
+
console.print("[bold red]Findings[/bold red]")
|
|
427
|
+
console.print("=" * 60)
|
|
428
|
+
for i, finding in enumerate(structural_findings[:20], 1):
|
|
429
|
+
if isinstance(finding, dict):
|
|
430
|
+
severity = str(finding.get("severity", "medium")).upper()
|
|
431
|
+
title = finding.get("title") or finding.get("finding_type") or finding.get("type", "Issue")
|
|
432
|
+
desc = finding.get("description") or finding.get("summary", "")
|
|
433
|
+
location = finding.get("location") or finding.get("file", "")
|
|
434
|
+
|
|
435
|
+
color = "red" if severity == "HIGH" else "yellow" if severity == "MEDIUM" else "green"
|
|
436
|
+
console.print(f"\n[{color}][{severity}][/{color}] {title}")
|
|
437
|
+
if location:
|
|
438
|
+
console.print(f" [dim]Location:[/dim] {location}")
|
|
439
|
+
if desc:
|
|
440
|
+
console.print(f" {desc[:400]}")
|
|
441
|
+
else:
|
|
442
|
+
console.print(f"\n• {finding}")
|
|
443
|
+
console.print()
|
|
444
|
+
|
|
445
|
+
# Fix Prompts
|
|
446
|
+
fix_prompts = analysis.get("fix_prompts") or analysis.get("fixes", [])
|
|
447
|
+
if fix_prompts:
|
|
448
|
+
console.print("[bold green]Suggested Fixes (Cursor-ready)[/bold green]")
|
|
449
|
+
console.print("=" * 60)
|
|
450
|
+
for i, fix in enumerate(fix_prompts[:10], 1):
|
|
451
|
+
if isinstance(fix, dict):
|
|
452
|
+
title = fix.get("title") or fix.get("finding_type") or f"Fix {i}"
|
|
453
|
+
prompt = fix.get("prompt") or fix.get("fix_prompt") or fix.get("description", "")
|
|
454
|
+
console.print(f"\n[green]{i}. {title}[/green]")
|
|
455
|
+
if prompt:
|
|
456
|
+
console.print(f" {prompt[:600]}")
|
|
457
|
+
else:
|
|
458
|
+
console.print(f"\n{i}. {fix}")
|
|
459
|
+
console.print()
|
|
460
|
+
|
|
461
|
+
# Architecture Map
|
|
462
|
+
arch_map = analysis.get("architecture_map", {})
|
|
463
|
+
if arch_map:
|
|
464
|
+
console.print("[bold blue]Architecture Map[/bold blue]")
|
|
465
|
+
console.print("=" * 60)
|
|
466
|
+
|
|
467
|
+
entry_points = arch_map.get("entry_points", [])
|
|
468
|
+
if entry_points:
|
|
469
|
+
console.print("\n[bold]Entry Points:[/bold]")
|
|
470
|
+
for ep in entry_points[:10]:
|
|
471
|
+
if isinstance(ep, dict):
|
|
472
|
+
console.print(f" • {ep.get('file', '')} - {ep.get('type', '')}")
|
|
473
|
+
else:
|
|
474
|
+
console.print(f" • {ep}")
|
|
475
|
+
|
|
476
|
+
key_modules = arch_map.get("key_modules", [])
|
|
477
|
+
if key_modules:
|
|
478
|
+
console.print("\n[bold]Key Modules:[/bold]")
|
|
479
|
+
for mod in key_modules[:10]:
|
|
480
|
+
if isinstance(mod, dict):
|
|
481
|
+
console.print(f" • {mod.get('name', mod.get('file', ''))}")
|
|
482
|
+
else:
|
|
483
|
+
console.print(f" • {mod}")
|
|
484
|
+
|
|
485
|
+
console.print()
|
|
486
|
+
|
|
487
|
+
# Raw response structure hint
|
|
488
|
+
console.print("[dim]Use -o report.json to save the full detailed report[/dim]")
|
|
489
|
+
console.print()
|
|
490
|
+
|
|
491
|
+
|
|
313
492
|
def display_summary(report: dict):
|
|
314
493
|
"""Display a summary of the scan results."""
|
|
315
494
|
console.print()
|
|
316
495
|
|
|
317
|
-
#
|
|
318
|
-
|
|
319
|
-
|
|
496
|
+
# Get scores from analysis.scores or top-level
|
|
497
|
+
analysis = report.get("analysis", {})
|
|
498
|
+
scores = analysis.get("scores", {})
|
|
499
|
+
|
|
500
|
+
security_score = scores.get("security") or report.get("security_score") or 0
|
|
501
|
+
quality_score = scores.get("quality") or report.get("quality_score") or 0
|
|
320
502
|
|
|
321
503
|
# Score colors
|
|
322
504
|
def score_color(score):
|
|
@@ -344,11 +526,13 @@ def display_summary(report: dict):
|
|
|
344
526
|
|
|
345
527
|
# Security findings
|
|
346
528
|
security = report.get("security_summary", {})
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
529
|
+
by_severity = security.get("by_severity", {})
|
|
530
|
+
high_count = by_severity.get("high", 0) or security.get("high_severity_count", 0)
|
|
531
|
+
medium_count = by_severity.get("medium", 0) or security.get("medium_severity_count", 0)
|
|
532
|
+
low_count = by_severity.get("low", 0) or security.get("low_severity_count", 0)
|
|
533
|
+
total_security = security.get("total_findings", 0)
|
|
350
534
|
|
|
351
|
-
if high_count or medium_count or low_count:
|
|
535
|
+
if high_count or medium_count or low_count or total_security:
|
|
352
536
|
security_table = Table(title="Security Findings", show_header=True)
|
|
353
537
|
security_table.add_column("Severity", style="bold")
|
|
354
538
|
security_table.add_column("Count", justify="right")
|
|
@@ -359,6 +543,8 @@ def display_summary(report: dict):
|
|
|
359
543
|
security_table.add_row("[yellow]Medium[/yellow]", str(medium_count))
|
|
360
544
|
if low_count > 0:
|
|
361
545
|
security_table.add_row("[green]Low[/green]", str(low_count))
|
|
546
|
+
if total_security and not (high_count or medium_count or low_count):
|
|
547
|
+
security_table.add_row("Total", str(total_security))
|
|
362
548
|
|
|
363
549
|
console.print(security_table)
|
|
364
550
|
else:
|
|
@@ -368,7 +554,7 @@ def display_summary(report: dict):
|
|
|
368
554
|
|
|
369
555
|
# Quality findings
|
|
370
556
|
quality = report.get("quality_summary", {})
|
|
371
|
-
quality_count = quality.get("total_findings", 0)
|
|
557
|
+
quality_count = quality.get("total_issues", 0) or quality.get("total_findings", 0)
|
|
372
558
|
|
|
373
559
|
if quality_count:
|
|
374
560
|
console.print(f"[yellow]Quality issues:[/yellow] {quality_count}")
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
janex/__init__.py,sha256=PJHtHR6gj22P2ZhNUZbnTYKFNGYprX96yddnHz50qKs,90
|
|
2
|
+
janex/main.py,sha256=tWRIk6DduWJ3awzv04C7dmSa139czWODOsaCB3Cbb00,22720
|
|
3
|
+
janexai-0.2.0.dist-info/METADATA,sha256=8NiT3TJ9wGCjpqGetA33fenQ9w295cLp1hNL_Uzn0D8,3920
|
|
4
|
+
janexai-0.2.0.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
|
|
5
|
+
janexai-0.2.0.dist-info/entry_points.txt,sha256=h9RnrJ-9Tj8f3Zp0ps50sjkn6TudN-WMIye03yMman8,42
|
|
6
|
+
janexai-0.2.0.dist-info/top_level.txt,sha256=9y1F6wpYoa5Jt5dSCmuQA5KnUYfXZVLNhV2iaxRpiJo,6
|
|
7
|
+
janexai-0.2.0.dist-info/RECORD,,
|
janexai-0.1.0.dist-info/RECORD
DELETED
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
janex/__init__.py,sha256=iNKnW7A7P6XgFDi_oreFwirFv-_ujrP89euX_0RGehc,90
|
|
2
|
-
janex/main.py,sha256=tT_5cAVXm2P4V7-xyQ6SryMc0n-gzY1faUbIP85BfSw,13369
|
|
3
|
-
janexai-0.1.0.dist-info/METADATA,sha256=ME_NDylQjM43GDG0PnzFV2i-n4Cv9Awkdl7zqJ1wCYE,3920
|
|
4
|
-
janexai-0.1.0.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
|
|
5
|
-
janexai-0.1.0.dist-info/entry_points.txt,sha256=h9RnrJ-9Tj8f3Zp0ps50sjkn6TudN-WMIye03yMman8,42
|
|
6
|
-
janexai-0.1.0.dist-info/top_level.txt,sha256=9y1F6wpYoa5Jt5dSCmuQA5KnUYfXZVLNhV2iaxRpiJo,6
|
|
7
|
-
janexai-0.1.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|