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 CHANGED
@@ -1,3 +1,3 @@
1
1
  """JanexAI CLI - AI-powered code security and quality analysis."""
2
2
 
3
- __version__ = "0.1.0"
3
+ __version__ = "0.2.0"
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://trust-layer-ai.onrender.com"
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
- response = api_request("GET", "/health/")
170
- if response.status_code == 200:
171
- console.print("[green]✓[/green] Successfully logged in!")
172
- console.print(f"Config saved to: {CONFIG_FILE}")
173
- else:
174
- console.print("[yellow]Token saved, but could not verify.[/yellow]")
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
- # Step 2: Run analysis
277
- progress.update(task, description="Running security & quality analysis...")
309
+ report = None
278
310
 
279
- analyze_response = api_request("POST", f"/repositories/{repo_id}/analyze/", {
280
- "use_claude": not no_claude,
281
- }, timeout=600) # 10 min timeout for analysis
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
- if analyze_response.status_code != 200:
284
- error = analyze_response.json().get("error", analyze_response.text)
285
- console.print(f"\n[red]Analysis Error:[/red] {error}")
286
- raise typer.Exit(1)
287
-
288
- report = analyze_response.json()
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
- display_summary(report)
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
- # Overall scores
318
- security_score = report.get("security_score", 0)
319
- quality_score = report.get("quality_score", 0)
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
- high_count = security.get("high_severity_count", 0)
348
- medium_count = security.get("medium_severity_count", 0)
349
- low_count = security.get("low_severity_count", 0)
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}")
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: janexai
3
- Version: 0.1.0
3
+ Version: 0.2.0
4
4
  Summary: AI-powered code security and quality analysis CLI
5
5
  Author-email: JanexAI Team <hello@janex.ai>
6
6
  License: MIT
@@ -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,,
@@ -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,,