repotoire 0.1.2__cp314-cp314-win_amd64.whl → 0.1.3__cp314-cp314-win_amd64.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.
@@ -19,6 +19,7 @@ from repotoire.api.v1.routes import (
19
19
  findings,
20
20
  fixes,
21
21
  github,
22
+ graph,
22
23
  historical,
23
24
  marketplace,
24
25
  notifications,
@@ -223,6 +224,7 @@ v1_app.include_router(customer_webhooks.router)
223
224
  v1_app.include_router(findings.router)
224
225
  v1_app.include_router(fixes.router)
225
226
  v1_app.include_router(github.router)
227
+ v1_app.include_router(graph.router)
226
228
  v1_app.include_router(historical.router)
227
229
  v1_app.include_router(marketplace.router)
228
230
  v1_app.include_router(notifications.router)
@@ -0,0 +1,476 @@
1
+ """Graph proxy API routes for CLI operations.
2
+
3
+ This module provides API endpoints that proxy graph database operations
4
+ to the internal FalkorDB instance. This allows the CLI to perform graph
5
+ operations without direct database access.
6
+
7
+ All operations are authenticated via API key and scoped to the user's
8
+ organization graph.
9
+ """
10
+
11
+ import asyncio
12
+ from dataclasses import dataclass
13
+ from typing import Any, Dict, List, Optional
14
+ from uuid import UUID
15
+
16
+ from fastapi import APIRouter, Depends, Header, HTTPException, status
17
+ from pydantic import BaseModel, Field
18
+ from sqlalchemy import select
19
+ from sqlalchemy.ext.asyncio import AsyncSession
20
+
21
+ from repotoire.api.shared.auth.clerk import get_clerk_client
22
+ from repotoire.db.models import Organization, OrganizationMembership, User
23
+ from repotoire.db.session import get_db
24
+ from repotoire.graph.tenant_factory import get_factory
25
+ from repotoire.logging_config import get_logger
26
+ from repotoire.models import (
27
+ Entity,
28
+ FileEntity,
29
+ ClassEntity,
30
+ FunctionEntity,
31
+ ModuleEntity,
32
+ NodeType,
33
+ Relationship,
34
+ RelationshipType,
35
+ )
36
+
37
+ logger = get_logger(__name__)
38
+
39
+ router = APIRouter(prefix="/graph", tags=["graph"])
40
+
41
+
42
+ # =============================================================================
43
+ # API Key Authentication
44
+ # =============================================================================
45
+
46
+
47
+ @dataclass
48
+ class APIKeyUser:
49
+ """Authenticated user from API key."""
50
+
51
+ org_id: str # Our internal org UUID
52
+ org_slug: str
53
+ user_id: Optional[str] = None
54
+
55
+
56
+ async def get_current_api_key_user(
57
+ db: AsyncSession = Depends(get_db),
58
+ authorization: Optional[str] = Header(None, alias="Authorization"),
59
+ ) -> APIKeyUser:
60
+ """Validate API key and return authenticated user with org info."""
61
+ if not authorization:
62
+ raise HTTPException(
63
+ status_code=status.HTTP_401_UNAUTHORIZED,
64
+ detail="Missing Authorization header",
65
+ )
66
+
67
+ parts = authorization.split(" ", 1)
68
+ if len(parts) != 2 or parts[0].lower() != "bearer":
69
+ raise HTTPException(
70
+ status_code=status.HTTP_401_UNAUTHORIZED,
71
+ detail="Invalid Authorization format. Use: Bearer <api_key>",
72
+ )
73
+
74
+ api_key = parts[1]
75
+
76
+ try:
77
+ clerk = get_clerk_client()
78
+ api_key_data = await asyncio.to_thread(
79
+ clerk.api_keys.verify_api_key, secret=api_key
80
+ )
81
+
82
+ subject = api_key_data.subject
83
+ clerk_org_id = None
84
+ org = None
85
+
86
+ if subject.startswith("org_"):
87
+ clerk_org_id = subject
88
+ elif hasattr(api_key_data, "org_id") and api_key_data.org_id:
89
+ clerk_org_id = api_key_data.org_id
90
+ elif subject.startswith("user_"):
91
+ # User-scoped key - look up user's organization
92
+ result = await db.execute(
93
+ select(User).where(User.clerk_user_id == subject)
94
+ )
95
+ db_user = result.scalar_one_or_none()
96
+ if not db_user:
97
+ raise HTTPException(
98
+ status_code=status.HTTP_401_UNAUTHORIZED,
99
+ detail="User not found",
100
+ )
101
+
102
+ result = await db.execute(
103
+ select(OrganizationMembership).where(
104
+ OrganizationMembership.user_id == db_user.id
105
+ )
106
+ )
107
+ membership = result.scalar_one_or_none()
108
+ if membership:
109
+ result = await db.execute(
110
+ select(Organization).where(
111
+ Organization.id == membership.organization_id
112
+ )
113
+ )
114
+ org = result.scalar_one_or_none()
115
+ if org:
116
+ clerk_org_id = org.clerk_org_id
117
+
118
+ # Look up org by Clerk ID
119
+ if not org and clerk_org_id:
120
+ result = await db.execute(
121
+ select(Organization).where(Organization.clerk_org_id == clerk_org_id)
122
+ )
123
+ org = result.scalar_one_or_none()
124
+
125
+ if not org:
126
+ raise HTTPException(
127
+ status_code=status.HTTP_401_UNAUTHORIZED,
128
+ detail="Organization not found",
129
+ )
130
+
131
+ return APIKeyUser(
132
+ org_id=str(org.id),
133
+ org_slug=org.slug,
134
+ user_id=subject if subject.startswith("user_") else None,
135
+ )
136
+
137
+ except HTTPException:
138
+ raise
139
+ except Exception as e:
140
+ logger.error(f"API key validation failed: {e}")
141
+ raise HTTPException(
142
+ status_code=status.HTTP_401_UNAUTHORIZED,
143
+ detail="Invalid or expired API key",
144
+ )
145
+
146
+
147
+ # =============================================================================
148
+ # Request/Response Models
149
+ # =============================================================================
150
+
151
+
152
+ class QueryRequest(BaseModel):
153
+ """Request to execute a Cypher query."""
154
+
155
+ query: str = Field(..., description="Cypher query to execute")
156
+ parameters: Optional[Dict[str, Any]] = Field(
157
+ default=None, description="Query parameters"
158
+ )
159
+ timeout: Optional[float] = Field(
160
+ default=None, description="Query timeout in seconds"
161
+ )
162
+
163
+
164
+ class QueryResponse(BaseModel):
165
+ """Response from a Cypher query."""
166
+
167
+ results: List[Dict[str, Any]] = Field(..., description="Query results")
168
+ count: int = Field(..., description="Number of results")
169
+
170
+
171
+ class StatsResponse(BaseModel):
172
+ """Graph statistics response."""
173
+
174
+ stats: Dict[str, int] = Field(..., description="Node/relationship counts")
175
+
176
+
177
+ class BatchNodesRequest(BaseModel):
178
+ """Request to batch create nodes."""
179
+
180
+ entities: List[Dict[str, Any]] = Field(..., description="Entities to create")
181
+
182
+
183
+ class BatchNodesResponse(BaseModel):
184
+ """Response from batch node creation."""
185
+
186
+ created: Dict[str, str] = Field(
187
+ ..., description="Map of qualified_name to node ID"
188
+ )
189
+ count: int = Field(..., description="Number of nodes created")
190
+
191
+
192
+ class BatchRelationshipsRequest(BaseModel):
193
+ """Request to batch create relationships."""
194
+
195
+ relationships: List[Dict[str, Any]] = Field(
196
+ ..., description="Relationships to create"
197
+ )
198
+
199
+
200
+ class BatchRelationshipsResponse(BaseModel):
201
+ """Response from batch relationship creation."""
202
+
203
+ count: int = Field(..., description="Number of relationships created")
204
+
205
+
206
+ class FileMetadataResponse(BaseModel):
207
+ """File metadata for incremental ingestion."""
208
+
209
+ metadata: Optional[Dict[str, Any]] = Field(
210
+ None, description="File metadata or None if not found"
211
+ )
212
+
213
+
214
+ class FilePathsResponse(BaseModel):
215
+ """List of file paths in the graph."""
216
+
217
+ paths: List[str] = Field(..., description="File paths")
218
+ count: int = Field(..., description="Number of files")
219
+
220
+
221
+ class DeleteResponse(BaseModel):
222
+ """Response from delete operation."""
223
+
224
+ deleted: int = Field(..., description="Number of items deleted")
225
+
226
+
227
+ # =============================================================================
228
+ # Helper Functions
229
+ # =============================================================================
230
+
231
+
232
+ def _get_client_for_user(user: APIKeyUser):
233
+ """Get graph client scoped to user's organization."""
234
+ factory = get_factory()
235
+ return factory.get_client(org_id=UUID(user.org_id), org_slug=user.org_slug)
236
+
237
+
238
+ # =============================================================================
239
+ # Endpoints
240
+ # =============================================================================
241
+
242
+
243
+ @router.post("/query", response_model=QueryResponse)
244
+ async def execute_query(
245
+ request: QueryRequest,
246
+ user: APIKeyUser = Depends(get_current_api_key_user),
247
+ ) -> QueryResponse:
248
+ """Execute a Cypher query on the organization's graph."""
249
+ client = _get_client_for_user(user)
250
+ try:
251
+ results = client.execute_query(
252
+ request.query,
253
+ parameters=request.parameters,
254
+ timeout=request.timeout,
255
+ )
256
+ return QueryResponse(results=results, count=len(results))
257
+ except Exception as e:
258
+ logger.error(f"Query failed: {e}", org_id=user.org_id)
259
+ raise HTTPException(
260
+ status_code=status.HTTP_400_BAD_REQUEST,
261
+ detail=f"Query failed: {str(e)}",
262
+ )
263
+ finally:
264
+ client.close()
265
+
266
+
267
+ @router.get("/stats", response_model=StatsResponse)
268
+ async def get_stats(
269
+ user: APIKeyUser = Depends(get_current_api_key_user),
270
+ ) -> StatsResponse:
271
+ """Get graph statistics."""
272
+ client = _get_client_for_user(user)
273
+ try:
274
+ stats = client.get_stats()
275
+ return StatsResponse(stats=stats)
276
+ finally:
277
+ client.close()
278
+
279
+
280
+ @router.delete("/clear", response_model=DeleteResponse)
281
+ async def clear_graph(
282
+ user: APIKeyUser = Depends(get_current_api_key_user),
283
+ ) -> DeleteResponse:
284
+ """Clear all nodes and relationships from the graph."""
285
+ client = _get_client_for_user(user)
286
+ try:
287
+ # Get count before clearing for response
288
+ stats = client.get_stats()
289
+ total = sum(stats.values())
290
+ client.clear_graph()
291
+ logger.info(f"Graph cleared", org_id=user.org_id, deleted=total)
292
+ return DeleteResponse(deleted=total)
293
+ finally:
294
+ client.close()
295
+
296
+
297
+ @router.post("/batch/nodes", response_model=BatchNodesResponse)
298
+ async def batch_create_nodes(
299
+ request: BatchNodesRequest,
300
+ user: APIKeyUser = Depends(get_current_api_key_user),
301
+ ) -> BatchNodesResponse:
302
+ """Batch create nodes in the graph."""
303
+ client = _get_client_for_user(user)
304
+ try:
305
+ # Convert dicts to appropriate Entity subclass
306
+ entities = []
307
+ for e in request.entities:
308
+ entity_type = e.get("entity_type", "Unknown")
309
+ node_type = NodeType(entity_type) if entity_type in [t.value for t in NodeType] else None
310
+
311
+ # Common fields for all entities
312
+ base_fields = {
313
+ "name": e["name"],
314
+ "qualified_name": e["qualified_name"],
315
+ "file_path": e.get("file_path", ""),
316
+ "line_start": e.get("line_start", 0),
317
+ "line_end": e.get("line_end", 0),
318
+ "docstring": e.get("docstring"),
319
+ }
320
+
321
+ # Create appropriate entity type
322
+ if entity_type == "File":
323
+ entity = FileEntity(
324
+ **base_fields,
325
+ node_type=NodeType.FILE,
326
+ language=e.get("language", "python"),
327
+ loc=e.get("loc", 0),
328
+ hash=e.get("hash"),
329
+ exports=e.get("exports", []),
330
+ )
331
+ elif entity_type == "Class":
332
+ entity = ClassEntity(
333
+ **base_fields,
334
+ node_type=NodeType.CLASS,
335
+ is_abstract=e.get("is_abstract", False),
336
+ complexity=e.get("complexity", 0),
337
+ decorators=e.get("decorators", []),
338
+ )
339
+ elif entity_type == "Function":
340
+ entity = FunctionEntity(
341
+ **base_fields,
342
+ node_type=NodeType.FUNCTION,
343
+ parameters=e.get("parameters", []),
344
+ return_type=e.get("return_type"),
345
+ is_async=e.get("is_async", False),
346
+ decorators=e.get("decorators", []),
347
+ complexity=e.get("complexity", 0),
348
+ is_method=e.get("is_method", False),
349
+ is_static=e.get("is_static", False),
350
+ is_classmethod=e.get("is_classmethod", False),
351
+ is_property=e.get("is_property", False),
352
+ )
353
+ elif entity_type == "Module":
354
+ entity = ModuleEntity(
355
+ **base_fields,
356
+ node_type=NodeType.MODULE,
357
+ is_external=e.get("is_external", False),
358
+ package=e.get("package"),
359
+ )
360
+ else:
361
+ # Fall back to base Entity for unknown types
362
+ entity = Entity(
363
+ **base_fields,
364
+ node_type=node_type,
365
+ )
366
+
367
+ # Set repo_id and repo_slug for multi-tenant isolation
368
+ if e.get("repo_id"):
369
+ entity.repo_id = e["repo_id"]
370
+ if e.get("repo_slug"):
371
+ entity.repo_slug = e["repo_slug"]
372
+
373
+ entities.append(entity)
374
+
375
+ created = client.batch_create_nodes(entities)
376
+ return BatchNodesResponse(created=created, count=len(created))
377
+ except Exception as e:
378
+ logger.error(f"Batch create nodes failed: {e}", org_id=user.org_id)
379
+ raise HTTPException(
380
+ status_code=status.HTTP_400_BAD_REQUEST,
381
+ detail=f"Batch create failed: {str(e)}",
382
+ )
383
+ finally:
384
+ client.close()
385
+
386
+
387
+ @router.post("/batch/relationships", response_model=BatchRelationshipsResponse)
388
+ async def batch_create_relationships(
389
+ request: BatchRelationshipsRequest,
390
+ user: APIKeyUser = Depends(get_current_api_key_user),
391
+ ) -> BatchRelationshipsResponse:
392
+ """Batch create relationships in the graph."""
393
+ client = _get_client_for_user(user)
394
+ try:
395
+ # Convert dicts to Relationship objects
396
+ relationships = []
397
+ for r in request.relationships:
398
+ # Parse rel_type from string to enum
399
+ rel_type_str = r.get("rel_type", "CALLS")
400
+ try:
401
+ rel_type = RelationshipType(rel_type_str)
402
+ except ValueError:
403
+ rel_type = RelationshipType.CALLS # Default fallback
404
+
405
+ rel = Relationship(
406
+ source_id=r["source_id"],
407
+ target_id=r["target_id"],
408
+ rel_type=rel_type,
409
+ properties=r.get("properties", {}),
410
+ )
411
+ relationships.append(rel)
412
+
413
+ count = client.batch_create_relationships(relationships)
414
+ return BatchRelationshipsResponse(count=count)
415
+ except Exception as e:
416
+ logger.error(f"Batch create relationships failed: {e}", org_id=user.org_id)
417
+ raise HTTPException(
418
+ status_code=status.HTTP_400_BAD_REQUEST,
419
+ detail=f"Batch create failed: {str(e)}",
420
+ )
421
+ finally:
422
+ client.close()
423
+
424
+
425
+ @router.get("/files", response_model=FilePathsResponse)
426
+ async def get_file_paths(
427
+ user: APIKeyUser = Depends(get_current_api_key_user),
428
+ ) -> FilePathsResponse:
429
+ """Get all file paths in the graph."""
430
+ client = _get_client_for_user(user)
431
+ try:
432
+ paths = client.get_all_file_paths()
433
+ return FilePathsResponse(paths=paths, count=len(paths))
434
+ finally:
435
+ client.close()
436
+
437
+
438
+ @router.get("/files/{file_path:path}/metadata", response_model=FileMetadataResponse)
439
+ async def get_file_metadata(
440
+ file_path: str,
441
+ user: APIKeyUser = Depends(get_current_api_key_user),
442
+ ) -> FileMetadataResponse:
443
+ """Get metadata for a specific file (for incremental ingestion)."""
444
+ client = _get_client_for_user(user)
445
+ try:
446
+ metadata = client.get_file_metadata(file_path)
447
+ return FileMetadataResponse(metadata=metadata)
448
+ finally:
449
+ client.close()
450
+
451
+
452
+ @router.delete("/files/{file_path:path}", response_model=DeleteResponse)
453
+ async def delete_file(
454
+ file_path: str,
455
+ user: APIKeyUser = Depends(get_current_api_key_user),
456
+ ) -> DeleteResponse:
457
+ """Delete a file and its related entities from the graph."""
458
+ client = _get_client_for_user(user)
459
+ try:
460
+ deleted = client.delete_file_entities(file_path)
461
+ return DeleteResponse(deleted=deleted)
462
+ finally:
463
+ client.close()
464
+
465
+
466
+ @router.post("/indexes")
467
+ async def create_indexes(
468
+ user: APIKeyUser = Depends(get_current_api_key_user),
469
+ ) -> Dict[str, str]:
470
+ """Create indexes for better query performance."""
471
+ client = _get_client_for_user(user)
472
+ try:
473
+ client.create_indexes()
474
+ return {"status": "ok", "message": "Indexes created"}
475
+ finally:
476
+ client.close()
repotoire/cli/__init__.py CHANGED
@@ -445,23 +445,6 @@ def whoami() -> None:
445
445
 
446
446
  @cli.command()
447
447
  @click.argument("repo_path", type=click.Path(exists=True))
448
- @click.option(
449
- "--db-type",
450
- type=click.Choice(["neo4j", "falkordb"], case_sensitive=False),
451
- default=None,
452
- envvar="REPOTOIRE_DB_TYPE",
453
- help="Database type: neo4j or falkordb (default: neo4j, or REPOTOIRE_DB_TYPE env)",
454
- )
455
- @click.option(
456
- "--neo4j-uri", default=None, help="Neo4j connection URI (overrides config)"
457
- )
458
- @click.option("--neo4j-user", default=None, help="Neo4j username (overrides config)")
459
- @click.option(
460
- "--neo4j-password",
461
- default=None,
462
- envvar="REPOTOIRE_NEO4J_PASSWORD",
463
- help="Neo4j password (overrides config, prompts if not provided for neo4j)",
464
- )
465
448
  @click.option(
466
449
  "--pattern",
467
450
  "-p",
@@ -556,10 +539,6 @@ def whoami() -> None:
556
539
  def ingest(
557
540
  ctx: click.Context,
558
541
  repo_path: str,
559
- db_type: str | None,
560
- neo4j_uri: str | None,
561
- neo4j_user: str | None,
562
- neo4j_password: str | None,
563
542
  pattern: tuple | None,
564
543
  follow_symlinks: bool | None,
565
544
  max_file_size: float | None,
@@ -579,11 +558,15 @@ def ingest(
579
558
  """Ingest a codebase into the knowledge graph.
580
559
 
581
560
  \b
582
- Parses source code and builds a Neo4j knowledge graph containing:
561
+ Parses source code and builds a knowledge graph containing:
583
562
  - Files, modules, classes, functions, and variables
584
563
  - Relationships: IMPORTS, CALLS, CONTAINS, INHERITS, USES
585
564
  - Optional: AI-powered semantic clues and vector embeddings
586
565
 
566
+ \b
567
+ PREREQUISITES:
568
+ Login first: repotoire login
569
+
587
570
  \b
588
571
  EXAMPLES:
589
572
  # Basic ingestion
@@ -595,9 +578,6 @@ def ingest(
595
578
  # Force full re-ingestion (ignore cache)
596
579
  $ repotoire ingest ./my-project --force-full
597
580
 
598
- # Use FalkorDB instead of Neo4j
599
- $ repotoire ingest ./my-project --db-type falkordb
600
-
601
581
  \b
602
582
  INCREMENTAL MODE (default):
603
583
  Only processes files changed since last ingestion. Uses MD5 hashes
@@ -611,11 +591,6 @@ def ingest(
611
591
  - File size limits (10MB default)
612
592
  - Secrets detection with configurable policy
613
593
 
614
- \b
615
- DATABASE BACKENDS:
616
- neo4j Full-featured graph database (recommended)
617
- falkordb Lightweight Redis-based alternative (faster startup)
618
-
619
594
  \b
620
595
  EMBEDDING BACKENDS:
621
596
  auto Auto-select best available (default)
@@ -626,9 +601,7 @@ def ingest(
626
601
 
627
602
  \b
628
603
  ENVIRONMENT VARIABLES:
629
- REPOTOIRE_NEO4J_URI Neo4j connection URI
630
- REPOTOIRE_NEO4J_PASSWORD Neo4j password
631
- REPOTOIRE_DB_TYPE Database type (neo4j/falkordb)
604
+ REPOTOIRE_API_KEY API key (or use 'repotoire login')
632
605
  OPENAI_API_KEY For OpenAI embeddings
633
606
  VOYAGE_API_KEY For Voyage embeddings
634
607
  DEEPINFRA_API_KEY For DeepInfra embeddings
@@ -636,24 +609,12 @@ def ingest(
636
609
  # Get config from context
637
610
  config: FalkorConfig = ctx.obj['config']
638
611
 
639
- # Determine database type
640
- if db_type:
641
- final_db_type = db_type
642
- elif hasattr(config.neo4j, 'db_type') and config.neo4j.db_type:
643
- final_db_type = config.neo4j.db_type
644
- else:
645
- final_db_type = "neo4j"
646
- use_falkordb = final_db_type.lower() == "falkordb"
647
-
648
612
  # Validate inputs before execution
649
613
  try:
650
614
  # Validate repository path
651
615
  validated_repo_path = validate_repository_path(repo_path)
652
616
 
653
617
  # Apply config defaults (CLI options override config)
654
- final_neo4j_uri = neo4j_uri or config.neo4j.uri
655
- final_neo4j_user = neo4j_user or config.neo4j.user
656
- final_neo4j_password = neo4j_password or config.neo4j.password
657
618
  final_patterns = list(pattern) if pattern else config.ingestion.patterns
658
619
  final_follow_symlinks = follow_symlinks if follow_symlinks is not None else config.ingestion.follow_symlinks
659
620
  final_max_file_size = max_file_size if max_file_size is not None else config.ingestion.max_file_size_mb
@@ -667,43 +628,12 @@ def ingest(
667
628
  # Convert secrets policy string to enum
668
629
  final_secrets_policy = SecretsPolicy(final_secrets_policy_str)
669
630
 
670
- if use_falkordb:
671
- # FalkorDB validation - simpler, no auth required
672
- console.print("[dim]Checking FalkorDB connectivity...[/dim]")
673
- # FalkorDB uses Redis protocol, not bolt
674
- # Password is optional for FalkorDB
675
- else:
676
- # Neo4j validation
677
- # Validate Neo4j URI
678
- final_neo4j_uri = validate_neo4j_uri(final_neo4j_uri)
679
-
680
- # Prompt for password if not provided
681
- if not final_neo4j_password:
682
- final_neo4j_password = click.prompt("Neo4j password", hide_input=True)
683
-
684
- # Validate credentials
685
- final_neo4j_user, final_neo4j_password = validate_neo4j_credentials(
686
- final_neo4j_user, final_neo4j_password
687
- )
688
-
689
- # Test Neo4j connection is reachable
690
- console.print("[dim]Checking Neo4j connectivity...[/dim]")
691
- validate_neo4j_connection(final_neo4j_uri, final_neo4j_user, final_neo4j_password)
692
- console.print("[green]✓[/green] Neo4j connection validated\n")
693
-
694
631
  # Validate file size limit
695
632
  final_max_file_size = validate_file_size_limit(final_max_file_size)
696
633
 
697
634
  # Validate batch size
698
635
  validated_batch_size = validate_batch_size(final_batch_size)
699
636
 
700
- # Validate retry configuration
701
- validated_retries = validate_retry_config(
702
- config.neo4j.max_retries,
703
- config.neo4j.retry_backoff_factor,
704
- config.neo4j.retry_base_delay
705
- )
706
-
707
637
  except ValidationError as e:
708
638
  console.print(f"\n[red]❌ Validation Error:[/red] {e.message}")
709
639
  if e.suggestion:
@@ -712,7 +642,6 @@ def ingest(
712
642
 
713
643
  console.print(f"\n[bold cyan]🎼 Repotoire Ingestion[/bold cyan]\n")
714
644
  console.print(f"Repository: {repo_path}")
715
- console.print(f"Database: {final_db_type}")
716
645
  console.print(f"Patterns: {', '.join(final_patterns)}")
717
646
  console.print(f"Follow symlinks: {final_follow_symlinks}")
718
647
  console.print(f"Max file size: {final_max_file_size}MB")
@@ -741,19 +670,8 @@ def ingest(
741
670
  with LogContext(operation="ingest", repo_path=repo_path):
742
671
  logger.info("Starting ingestion")
743
672
 
744
- # Create database client using factory
745
- if use_falkordb:
746
- db = create_client(db_type="falkordb")
747
- else:
748
- db = create_client(
749
- uri=final_neo4j_uri,
750
- db_type="neo4j",
751
- username=final_neo4j_user,
752
- password=final_neo4j_password,
753
- max_retries=validated_retries[0],
754
- retry_backoff_factor=validated_retries[1],
755
- retry_base_delay=validated_retries[2],
756
- )
673
+ # Create database client (connects to Repotoire Cloud)
674
+ db = _get_db_client(quiet=quiet)
757
675
 
758
676
  try:
759
677
  # Clear database if force-full is requested
repotoire/cli/auth.py CHANGED
@@ -39,6 +39,22 @@ class AuthenticationError(Exception):
39
39
  pass
40
40
 
41
41
 
42
+ @dataclass
43
+ class CLICredentials:
44
+ """Credentials for CLI authentication.
45
+
46
+ Contains the access token (API key) and optional metadata
47
+ for making authenticated API requests.
48
+ """
49
+
50
+ access_token: str
51
+ org_id: Optional[str] = None
52
+ org_slug: Optional[str] = None
53
+ plan: Optional[str] = None
54
+ user_email: Optional[str] = None
55
+ user_id: Optional[str] = None
56
+
57
+
42
58
  @dataclass
43
59
  class OAuthCallbackResult:
44
60
  """Result from OAuth callback."""
@@ -310,6 +326,23 @@ class CLIAuth:
310
326
  if source:
311
327
  console.print(f"[bold]Stored in:[/] {source}")
312
328
 
329
+ def get_current_user(self) -> Optional[CLICredentials]:
330
+ """Get current user credentials if authenticated.
331
+
332
+ Returns:
333
+ CLICredentials if logged in, None otherwise
334
+ """
335
+ api_key = self.get_api_key()
336
+ if not api_key:
337
+ return None
338
+
339
+ return CLICredentials(access_token=api_key)
340
+
341
+ @property
342
+ def api_url(self) -> str:
343
+ """Get API URL for tier limit checks."""
344
+ return os.environ.get("REPOTOIRE_API_URL", "https://repotoire-api.fly.dev")
345
+
313
346
 
314
347
  def is_offline_mode() -> bool:
315
348
  """Check if running in offline mode.
@@ -90,7 +90,7 @@ class AsyncAntipatternDetector(CodeSmellDetector):
90
90
  """
91
91
  super().__init__(neo4j_client)
92
92
  self.enricher = enricher
93
- self.is_falkordb = type(neo4j_client).__name__ == "FalkorDBClient"
93
+ self.is_falkordb = getattr(neo4j_client, "is_falkordb", False) or type(neo4j_client).__name__ == "FalkorDBClient"
94
94
 
95
95
  # Allow config to override thresholds
96
96
  config = detector_config or {}
@@ -42,7 +42,7 @@ class CircularDependencyDetector(CodeSmellDetector):
42
42
  super().__init__(neo4j_client)
43
43
  self.enricher = enricher
44
44
  # FalkorDB uses id() while Neo4j uses elementId()
45
- self.is_falkordb = type(neo4j_client).__name__ == "FalkorDBClient"
45
+ self.is_falkordb = getattr(neo4j_client, "is_falkordb", False) or type(neo4j_client).__name__ == "FalkorDBClient"
46
46
  self.id_func = "id" if self.is_falkordb else "elementId"
47
47
 
48
48
  def detect(self) -> List[Finding]:
@@ -75,7 +75,7 @@ class DataClumpsDetector(CodeSmellDetector):
75
75
  super().__init__(neo4j_client)
76
76
  self.enricher = enricher
77
77
  # FalkorDB uses id() while Neo4j uses elementId()
78
- self.is_falkordb = type(neo4j_client).__name__ == "FalkorDBClient"
78
+ self.is_falkordb = getattr(neo4j_client, "is_falkordb", False) or type(neo4j_client).__name__ == "FalkorDBClient"
79
79
 
80
80
  # Allow config to override thresholds
81
81
  config = detector_config or {}
@@ -36,7 +36,7 @@ class DeadCodeDetector(CodeSmellDetector):
36
36
  self.validated_confidence = 0.95 # When Vulture confirms
37
37
 
38
38
  # FalkorDB doesn't support EXISTS {} subqueries
39
- self.is_falkordb = type(neo4j_client).__name__ == "FalkorDBClient"
39
+ self.is_falkordb = getattr(neo4j_client, "is_falkordb", False) or type(neo4j_client).__name__ == "FalkorDBClient"
40
40
 
41
41
  @property
42
42
  def needs_previous_findings(self) -> bool:
@@ -132,7 +132,7 @@ class AnalysisEngine:
132
132
  self.parallel = parallel
133
133
  self.max_workers = max_workers
134
134
  # Check if using FalkorDB (no GDS support)
135
- self.is_falkordb = type(neo4j_client).__name__ == "FalkorDBClient"
135
+ self.is_falkordb = getattr(neo4j_client, "is_falkordb", False) or type(neo4j_client).__name__ == "FalkorDBClient"
136
136
  config = detector_config or {}
137
137
 
138
138
  # Initialize GraphEnricher for cross-detector collaboration (REPO-151 Phase 2)
@@ -65,7 +65,7 @@ class GeneratorMisuseDetector(CodeSmellDetector):
65
65
  """
66
66
  super().__init__(neo4j_client)
67
67
  self.enricher = enricher
68
- self.is_falkordb = type(neo4j_client).__name__ == "FalkorDBClient"
68
+ self.is_falkordb = getattr(neo4j_client, "is_falkordb", False) or type(neo4j_client).__name__ == "FalkorDBClient"
69
69
 
70
70
  # Allow config to override thresholds
71
71
  config = detector_config or {}
@@ -150,7 +150,7 @@ class GraphAlgorithms:
150
150
  True if GDS is available, False otherwise
151
151
  """
152
152
  # FalkorDB doesn't have GDS - skip immediately without retry
153
- if type(self.client).__name__ == "FalkorDBClient":
153
+ if getattr(self.client, "is_falkordb", False) or type(self.client).__name__ == "FalkorDBClient":
154
154
  logger.info("FalkorDB detected - GDS not available (using Rust algorithms)")
155
155
  return False
156
156
 
@@ -31,7 +31,7 @@ class InappropriateIntimacyDetector(CodeSmellDetector):
31
31
  self.min_mutual_access = config.get("min_mutual_access", 5)
32
32
  self.logger = get_logger(__name__)
33
33
  # FalkorDB uses id() while Neo4j uses elementId()
34
- self.is_falkordb = type(neo4j_client).__name__ == "FalkorDBClient"
34
+ self.is_falkordb = getattr(neo4j_client, "is_falkordb", False) or type(neo4j_client).__name__ == "FalkorDBClient"
35
35
  self.id_func = "id" if self.is_falkordb else "elementId"
36
36
 
37
37
  def detect(self) -> List[Finding]:
@@ -58,7 +58,7 @@ class LongParameterListDetector(CodeSmellDetector):
58
58
  """
59
59
  super().__init__(neo4j_client)
60
60
  self.enricher = enricher
61
- self.is_falkordb = type(neo4j_client).__name__ == "FalkorDBClient"
61
+ self.is_falkordb = getattr(neo4j_client, "is_falkordb", False) or type(neo4j_client).__name__ == "FalkorDBClient"
62
62
 
63
63
  # Allow config to override thresholds
64
64
  config = detector_config or {}
@@ -68,7 +68,7 @@ class MessageChainDetector(CodeSmellDetector):
68
68
  """
69
69
  super().__init__(neo4j_client)
70
70
  self.enricher = enricher
71
- self.is_falkordb = type(neo4j_client).__name__ == "FalkorDBClient"
71
+ self.is_falkordb = getattr(neo4j_client, "is_falkordb", False) or type(neo4j_client).__name__ == "FalkorDBClient"
72
72
 
73
73
  # Allow config to override thresholds
74
74
  config = detector_config or {}
@@ -125,7 +125,7 @@ class TestSmellDetector(CodeSmellDetector):
125
125
  """
126
126
  super().__init__(neo4j_client)
127
127
  self.enricher = enricher
128
- self.is_falkordb = type(neo4j_client).__name__ == "FalkorDBClient"
128
+ self.is_falkordb = getattr(neo4j_client, "is_falkordb", False) or type(neo4j_client).__name__ == "FalkorDBClient"
129
129
 
130
130
  # Allow config to override thresholds
131
131
  config = detector_config or {}
@@ -70,7 +70,7 @@ class TypeHintCoverageDetector(CodeSmellDetector):
70
70
  """
71
71
  super().__init__(neo4j_client)
72
72
  self.enricher = enricher
73
- self.is_falkordb = type(neo4j_client).__name__ == "FalkorDBClient"
73
+ self.is_falkordb = getattr(neo4j_client, "is_falkordb", False) or type(neo4j_client).__name__ == "FalkorDBClient"
74
74
 
75
75
  # Allow config to override thresholds
76
76
  config = detector_config or {}
@@ -0,0 +1,215 @@
1
+ """Cloud proxy client for graph database operations.
2
+
3
+ This client proxies all graph operations through the Repotoire API,
4
+ allowing the CLI to work without direct database access.
5
+ """
6
+
7
+ import os
8
+ from typing import Any, Dict, List, Optional
9
+
10
+ import httpx
11
+
12
+ from repotoire.graph.base import DatabaseClient
13
+ from repotoire.logging_config import get_logger
14
+ from repotoire.models import Entity, Relationship
15
+
16
+ logger = get_logger(__name__)
17
+
18
+ DEFAULT_API_URL = "https://repotoire-api.fly.dev"
19
+
20
+
21
+ class CloudProxyClient(DatabaseClient):
22
+ """Graph database client that proxies through the Repotoire API.
23
+
24
+ All operations are sent to the API which executes them on the
25
+ internal FalkorDB instance. This allows the CLI to work without
26
+ direct database connectivity.
27
+ """
28
+
29
+ def __init__(
30
+ self,
31
+ api_key: str,
32
+ api_url: Optional[str] = None,
33
+ timeout: float = 60.0,
34
+ ):
35
+ """Initialize the cloud proxy client.
36
+
37
+ Args:
38
+ api_key: Repotoire API key for authentication
39
+ api_url: API base URL (defaults to production)
40
+ timeout: Request timeout in seconds
41
+ """
42
+ self.api_key = api_key
43
+ self.api_url = api_url or os.environ.get("REPOTOIRE_API_URL", DEFAULT_API_URL)
44
+ self.timeout = timeout
45
+ self._client = httpx.Client(
46
+ base_url=f"{self.api_url}/api/v1/graph",
47
+ headers={"Authorization": f"Bearer {api_key}"},
48
+ timeout=timeout,
49
+ )
50
+
51
+ @property
52
+ def is_falkordb(self) -> bool:
53
+ """Cloud backend uses FalkorDB."""
54
+ return True
55
+
56
+ def close(self) -> None:
57
+ """Close the HTTP client."""
58
+ self._client.close()
59
+
60
+ def _request(
61
+ self,
62
+ method: str,
63
+ endpoint: str,
64
+ json: Optional[Dict] = None,
65
+ params: Optional[Dict] = None,
66
+ ) -> Dict:
67
+ """Make an API request.
68
+
69
+ Args:
70
+ method: HTTP method
71
+ endpoint: API endpoint (relative to /api/v1/graph)
72
+ json: JSON body
73
+ params: Query parameters
74
+
75
+ Returns:
76
+ Response JSON
77
+
78
+ Raises:
79
+ Exception: On API error
80
+ """
81
+ response = self._client.request(
82
+ method=method,
83
+ url=endpoint,
84
+ json=json,
85
+ params=params,
86
+ )
87
+
88
+ if response.status_code >= 400:
89
+ try:
90
+ error = response.json()
91
+ detail = error.get("detail", str(error))
92
+ except Exception:
93
+ detail = response.text
94
+ raise Exception(f"API error ({response.status_code}): {detail}")
95
+
96
+ return response.json()
97
+
98
+ def execute_query(
99
+ self,
100
+ query: str,
101
+ parameters: Optional[Dict] = None,
102
+ timeout: Optional[float] = None,
103
+ ) -> List[Dict]:
104
+ """Execute a Cypher query via the API."""
105
+ response = self._request(
106
+ "POST",
107
+ "/query",
108
+ json={
109
+ "query": query,
110
+ "parameters": parameters,
111
+ "timeout": timeout,
112
+ },
113
+ )
114
+ return response.get("results", [])
115
+
116
+ def create_node(self, entity: Entity) -> str:
117
+ """Create a single node."""
118
+ result = self.batch_create_nodes([entity])
119
+ return result.get(entity.qualified_name, "")
120
+
121
+ def create_relationship(self, rel: Relationship) -> None:
122
+ """Create a single relationship."""
123
+ self.batch_create_relationships([rel])
124
+
125
+ def batch_create_nodes(self, entities: List[Entity]) -> Dict[str, str]:
126
+ """Create multiple nodes via the API."""
127
+ entity_dicts = []
128
+ for e in entities:
129
+ entity_dict = {
130
+ "entity_type": e.node_type.value if e.node_type else "Unknown",
131
+ "name": e.name,
132
+ "qualified_name": e.qualified_name,
133
+ "file_path": e.file_path,
134
+ "line_start": e.line_start,
135
+ "line_end": e.line_end,
136
+ "docstring": e.docstring,
137
+ }
138
+
139
+ # Add repo_id and repo_slug for multi-tenant isolation
140
+ if e.repo_id:
141
+ entity_dict["repo_id"] = e.repo_id
142
+ if e.repo_slug:
143
+ entity_dict["repo_slug"] = e.repo_slug
144
+
145
+ # Add type-specific fields (matching FalkorDB client)
146
+ for attr in ["is_external", "package", "loc", "hash", "language",
147
+ "exports", "is_abstract", "complexity", "parameters",
148
+ "return_type", "is_async", "decorators", "is_method",
149
+ "is_static", "is_classmethod", "is_property"]:
150
+ if hasattr(e, attr):
151
+ val = getattr(e, attr)
152
+ if val is not None:
153
+ entity_dict[attr] = val
154
+
155
+ entity_dicts.append(entity_dict)
156
+
157
+ response = self._request(
158
+ "POST",
159
+ "/batch/nodes",
160
+ json={"entities": entity_dicts},
161
+ )
162
+ return response.get("created", {})
163
+
164
+ def batch_create_relationships(self, relationships: List[Relationship]) -> int:
165
+ """Create multiple relationships via the API."""
166
+ rel_dicts = []
167
+ for r in relationships:
168
+ rel_dict = {
169
+ "source_id": r.source_id,
170
+ "target_id": r.target_id,
171
+ "rel_type": r.rel_type.value if hasattr(r.rel_type, 'value') else str(r.rel_type),
172
+ "properties": r.properties or {},
173
+ }
174
+ rel_dicts.append(rel_dict)
175
+
176
+ response = self._request(
177
+ "POST",
178
+ "/batch/relationships",
179
+ json={"relationships": rel_dicts},
180
+ )
181
+ return response.get("count", 0)
182
+
183
+ def clear_graph(self) -> None:
184
+ """Clear all nodes and relationships."""
185
+ self._request("DELETE", "/clear")
186
+
187
+ def create_indexes(self) -> None:
188
+ """Create indexes for better performance."""
189
+ self._request("POST", "/indexes")
190
+
191
+ def get_stats(self) -> Dict[str, int]:
192
+ """Get graph statistics."""
193
+ response = self._request("GET", "/stats")
194
+ return response.get("stats", {})
195
+
196
+ def get_all_file_paths(self) -> List[str]:
197
+ """Get all file paths in the graph."""
198
+ response = self._request("GET", "/files")
199
+ return response.get("paths", [])
200
+
201
+ def get_file_metadata(self, file_path: str) -> Optional[Dict[str, Any]]:
202
+ """Get metadata for a specific file."""
203
+ try:
204
+ response = self._request(
205
+ "GET",
206
+ f"/files/{file_path}/metadata",
207
+ )
208
+ return response.get("metadata")
209
+ except Exception:
210
+ return None
211
+
212
+ def delete_file_entities(self, file_path: str) -> int:
213
+ """Delete a file and its related entities."""
214
+ response = self._request("DELETE", f"/files/{file_path}")
215
+ return response.get("deleted", 0)
@@ -354,10 +354,11 @@ def create_cloud_client(
354
354
  show_indicator: bool = True,
355
355
  command: Optional[str] = None,
356
356
  ) -> DatabaseClient:
357
- """Create a cloud-connected FalkorDB client.
357
+ """Create a cloud proxy client.
358
358
 
359
- Validates the API key against the Repotoire Cloud API and returns
360
- a FalkorDB client configured for the user's organization.
359
+ Returns a client that proxies all graph operations through the
360
+ Repotoire API. This allows the CLI to work without direct database
361
+ connectivity.
361
362
 
362
363
  Args:
363
364
  api_key: Repotoire API key (starts with 'ak_' or 'rp_')
@@ -365,13 +366,13 @@ def create_cloud_client(
365
366
  command: CLI command being executed (for audit logging)
366
367
 
367
368
  Returns:
368
- FalkorDBClient configured for cloud mode
369
+ CloudProxyClient that proxies operations through the API
369
370
 
370
371
  Raises:
371
372
  CloudAuthenticationError: If API key is invalid or expired
372
373
  CloudConnectionError: If cannot connect to Repotoire Cloud
373
374
  """
374
- from repotoire.graph.falkordb_client import FalkorDBClient
375
+ from repotoire.graph.cloud_client import CloudProxyClient
375
376
 
376
377
  # Check cache first
377
378
  auth_info = _get_cached_auth(api_key)
@@ -391,19 +392,8 @@ def create_cloud_client(
391
392
  # Log connection to Neon (fire-and-forget, non-blocking)
392
393
  _log_cloud_connection(api_key, auth_info, cached=used_cache, command=command)
393
394
 
394
- # Create FalkorDB client with cloud config
395
- db_config = auth_info.db_config
396
-
397
- return FalkorDBClient(
398
- host=db_config["host"],
399
- port=db_config.get("port", 6379),
400
- graph_name=db_config["graph"],
401
- # REPO-395: Use derived password from API response
402
- # The password is derived from the API key using HMAC-SHA256,
403
- # so users never see the master FalkorDB password
404
- password=db_config.get("password"),
405
- ssl=db_config.get("ssl", False),
406
- )
395
+ # Create cloud proxy client
396
+ return CloudProxyClient(api_key=api_key)
407
397
 
408
398
 
409
399
  def _validate_api_key(api_key: str) -> CloudAuthInfo:
repotoire/graph/schema.py CHANGED
@@ -201,8 +201,8 @@ class GraphSchema:
201
201
  client: Neo4j or FalkorDB client instance
202
202
  """
203
203
  self.client = client
204
- # Detect if we're using FalkorDB
205
- self.is_falkordb = type(client).__name__ == "FalkorDBClient"
204
+ # Detect if we're using FalkorDB (check property first, then class name)
205
+ self.is_falkordb = getattr(client, "is_falkordb", False) or type(client).__name__ == "FalkorDBClient"
206
206
 
207
207
  def create_constraints(self) -> None:
208
208
  """Create all uniqueness constraints."""
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: repotoire
3
- Version: 0.1.2
3
+ Version: 0.1.3
4
4
  Classifier: Development Status :: 3 - Alpha
5
5
  Classifier: Intended Audience :: Developers
6
6
  Classifier: Programming Language :: Python :: 3
@@ -9,7 +9,7 @@ Classifier: Programming Language :: Python :: 3.11
9
9
  Classifier: Programming Language :: Python :: 3.12
10
10
  Classifier: Programming Language :: Python :: 3.13
11
11
  Classifier: Topic :: Software Development :: Quality Assurance
12
- Requires-Dist: neo4j>=5.14.0
12
+ Requires-Dist: falkordb>=1.0.0
13
13
  Requires-Dist: openai>=1.0.0
14
14
  Requires-Dist: spacy>=3.7.0
15
15
  Requires-Dist: click>=8.1.0
@@ -49,6 +49,12 @@ Requires-Dist: uv-secure>=0.15.0
49
49
  Requires-Dist: datasets>=2.14.0
50
50
  Requires-Dist: sentence-transformers>=2.2.0
51
51
  Requires-Dist: accelerate>=0.26.0
52
+ Requires-Dist: mypy>=1.7.0
53
+ Requires-Dist: pylint>=3.0.0
54
+ Requires-Dist: bandit>=1.7.0
55
+ Requires-Dist: radon>=6.0.0
56
+ Requires-Dist: vulture>=2.0.0
57
+ Requires-Dist: semgrep>=1.0.0
52
58
  Requires-Dist: pytest>=7.4.0 ; extra == 'dev'
53
59
  Requires-Dist: pytest-cov>=4.1.0 ; extra == 'dev'
54
60
  Requires-Dist: pytest-xdist>=3.5.0 ; extra == 'dev'
@@ -81,12 +87,6 @@ Requires-Dist: opentelemetry-api>=1.20.0 ; extra == 'observability'
81
87
  Requires-Dist: opentelemetry-sdk>=1.20.0 ; extra == 'observability'
82
88
  Requires-Dist: opentelemetry-exporter-otlp>=1.20.0 ; extra == 'observability'
83
89
  Requires-Dist: sentry-sdk[fastapi,celery,sqlalchemy]>=2.0.0 ; extra == 'observability'
84
- Requires-Dist: mypy>=1.7.0 ; extra == 'detectors'
85
- Requires-Dist: pylint>=3.0.0 ; extra == 'detectors'
86
- Requires-Dist: bandit>=1.7.0 ; extra == 'detectors'
87
- Requires-Dist: radon>=6.0.0 ; extra == 'detectors'
88
- Requires-Dist: vulture>=2.0.0 ; extra == 'detectors'
89
- Requires-Dist: semgrep>=1.0.0 ; extra == 'detectors'
90
90
  Requires-Dist: questionary>=2.0.0 ; extra == 'ml'
91
91
  Requires-Dist: scikit-learn>=1.3.0 ; extra == 'ml'
92
92
  Requires-Dist: joblib>=1.3.0 ; extra == 'ml'
@@ -105,7 +105,6 @@ Provides-Extra: graphiti
105
105
  Provides-Extra: security
106
106
  Provides-Extra: local-embeddings
107
107
  Provides-Extra: observability
108
- Provides-Extra: detectors
109
108
  Provides-Extra: ml
110
109
  Provides-Extra: sandbox
111
110
  Provides-Extra: voyage
@@ -1,8 +1,8 @@
1
1
  LICENSE,sha256=kZQMuoI-6QQZH9KFUiVK5u_tDmT9SO-Z4QFp0hE-cfg,1105
2
- repotoire-0.1.2.dist-info/METADATA,sha256=d7QSJUtm9KN10MGBz2p0GSvjIxGOV2pOFFx5Gw2zrgg,23958
3
- repotoire-0.1.2.dist-info/WHEEL,sha256=ZrAiv7lKoKZh206bhSq4g-n3q3ySUOJQmQ4WXAA7TE8,97
4
- repotoire-0.1.2.dist-info/entry_points.txt,sha256=qDZ_wx7dF6GDyrQJ70OXdRPBMJF6hEZIOxEZqX8jfdw,299
5
- repotoire-0.1.2.dist-info/licenses/LICENSE,sha256=kZQMuoI-6QQZH9KFUiVK5u_tDmT9SO-Z4QFp0hE-cfg,1105
2
+ repotoire-0.1.3.dist-info/METADATA,sha256=gMjG0KQAcg2E9VfA6hM86xrg8eaFfM4e52-_bNl4r4E,23796
3
+ repotoire-0.1.3.dist-info/WHEEL,sha256=ZrAiv7lKoKZh206bhSq4g-n3q3ySUOJQmQ4WXAA7TE8,97
4
+ repotoire-0.1.3.dist-info/entry_points.txt,sha256=qDZ_wx7dF6GDyrQJ70OXdRPBMJF6hEZIOxEZqX8jfdw,299
5
+ repotoire-0.1.3.dist-info/licenses/LICENSE,sha256=kZQMuoI-6QQZH9KFUiVK5u_tDmT9SO-Z4QFp0hE-cfg,1105
6
6
  repotoire/__init__.py,sha256=hgmEsSDh0eG73leeSicWDGAb_BDdff9ItMpKw51veHg,515
7
7
  repotoire/ai/__init__.py,sha256=LYHXLUjXF9hcXangbWNhrUg8fbGyDrdxj7DvpwBtJIA,1955
8
8
  repotoire/ai/contextual.py,sha256=CBepfMFUwESqrQf1GgfDuxdfOKojPVj_JDKLot_9tP4,15667
@@ -80,7 +80,7 @@ repotoire/api/shared/services/encryption.py,sha256=o3DQmQv1ZfcSnN61NVP03Mec4DctW
80
80
  repotoire/api/shared/services/gdpr.py,sha256=mEGokakr5WjXyqgFiiSDu-UfBooDm5Bw99wspwur4KY,18282
81
81
  repotoire/api/shared/services/github.py,sha256=BKGaPMsgmHE0hnabTaPsK_wuGWSF0Q6Ph5Vfv-rFHEw,11094
82
82
  repotoire/api/shared/services/stripe_service.py,sha256=WnZmUee3K6YpBk3GfWrpXnX1UrZJ7ZoM73coFb9E_wE,24363
83
- repotoire/api/v1/__init__.py,sha256=hhl-ddaqBiEp4kQFOlNp39EnCu2gENw-RJ9TPwvsspE,8373
83
+ repotoire/api/v1/__init__.py,sha256=xO7jwYU3Nhdo4qtVb6m1g4GL6XkTAT1ov6nFbuE2ztM,8422
84
84
  repotoire/api/v1/routes/__init__.py,sha256=EJAQ63XgFtFap2RS3tIZxP14huFo1y7jndA8mKIcJb4,793
85
85
  repotoire/api/v1/routes/account.py,sha256=L2C3dkmym9IdrgSgsSKU8lwg3oGeukrLiAIQqvfzEck,19505
86
86
  repotoire/api/v1/routes/admin/__init__.py,sha256=YUEAmdX1vuhVVpTuXuTahxXHM7iUa0Kl_e3GuGyYhK8,179
@@ -100,6 +100,7 @@ repotoire/api/v1/routes/customer_webhooks.py,sha256=lgYLZQ1F6H1UI7WR1HMkdUTcLxXC
100
100
  repotoire/api/v1/routes/findings.py,sha256=mmWrWY0HMNOs8AWaCrKjNHm6NciC7JBveeiUBfGKDJk,24686
101
101
  repotoire/api/v1/routes/fixes.py,sha256=egdOH0_jPPCg3kL4tTZR5TdHOzsjnDdQPui7PkLkgK0,47019
102
102
  repotoire/api/v1/routes/github.py,sha256=0XPFcaXPDANUYGFfKxx3Pn0L4SP43t6609Xe7CI9k58,53089
103
+ repotoire/api/v1/routes/graph.py,sha256=bTnNVUhbL3kxTvsOxTmzyl9TaxKhGaub1d22o1Oyy5Q,16312
103
104
  repotoire/api/v1/routes/historical.py,sha256=NqAzG_KnBDVd5LYGM3uFRe3WXyK9skZFevM18yP_MW0,11470
104
105
  repotoire/api/v1/routes/marketplace.py,sha256=tPTWE463KFowU3JZZib-Qm3rObjltZvEfBgfuYnPm20,89371
105
106
  repotoire/api/v1/routes/notifications.py,sha256=e_geSoFAagN2Beo5o7e9VRPH462W1EPgMKdydY-Prtc,6473
@@ -145,9 +146,9 @@ repotoire/cache/base.py,sha256=8Yr1Y4onn2ns3F-GlBoRX6u6O4jzeDl-whJ9-4frlaQ,12040
145
146
  repotoire/cache/preview.py,sha256=hzGk0xlE6CwYWeABkF0SvP26W4xeAURLKUCQ5CwfIRo,5261
146
147
  repotoire/cache/scan.py,sha256=vwmMtC17ea8tO_GyqcWGf9FwoXWZIP53jvPVxwxhSHY,7581
147
148
  repotoire/cache/skill.py,sha256=TZ7RRRd2NAxsZjivcLu9iO7S_OC6L4WMm8ilqnS7y0s,10672
148
- repotoire/cli/__init__.py,sha256=PLnq2mqiqTyDfkMnLVIkJZ3msLdhpQpUde-Ed3o3zYg,229966
149
+ repotoire/cli/__init__.py,sha256=EmH_jDFYS2AmUEKeL57kAEejPZMHkB_VL8Mmb1tnCnQ,226612
149
150
  repotoire/cli/api_keys.py,sha256=H5wJZXJFDWacKEp9lvkFDLeOXCKRYDYShBPsXMCDOZQ,6777
150
- repotoire/cli/auth.py,sha256=vs4YyX3PbktXyWovn3R0up53Stneu9MV-yfO8RPM8QA,10576
151
+ repotoire/cli/auth.py,sha256=zU0EB9tfuGmiDkchuFa6qKnLYR7tEKO92LwLeQLv9AQ,11515
151
152
  repotoire/cli/auth_commands.py,sha256=tfKpovH_xqbdFkcmupN2ga_WvEEYEu4qPxd4O443dw4,2297
152
153
  repotoire/cli/credentials.py,sha256=ZTt-yp326xn5Mm-HQKXtcom4jPHkQM9T0E02IGqSXQ4,8991
153
154
  repotoire/cli/graph.py,sha256=FYtKZGzKwbOGcsU19Z7JfM57Z9SkRR7gs6Dh4ZbyyNw,13895
@@ -188,28 +189,28 @@ repotoire/db/repositories/quota_override.py,sha256=TYdYc07GXxfcF_acmUDiprPDHI4pL
188
189
  repotoire/db/session.py,sha256=SISDlr5JJQAAmh5kXXqHG9faUc7NsntpmHONvl1qY0o,6793
189
190
  repotoire/detectors/__init__.py,sha256=SfUgFV8BlIqkjye5VgYBr9Yr1hvQLRGaCn3atTFFj3Q,3099
190
191
  repotoire/detectors/architectural_bottleneck.py,sha256=7WFmV4QLsou6-_wuVdG-znkPmjKF8t7RZTlvTjqox9g,14555
191
- repotoire/detectors/async_antipattern.py,sha256=LRgzFdlILFIqCAMwJNI-y8nDuQVXN06iA8G5VHA7x6Y,19309
192
+ repotoire/detectors/async_antipattern.py,sha256=zZOVixe_4I6lJ_7eIcJD7PYOreo4SSbkaWsVTFWb7Kg,19356
192
193
  repotoire/detectors/bandit_detector.py,sha256=DVXw7v10RO4lO9baQTEhPjyE3pp4B6uYbj89UJh4xz0,17057
193
194
  repotoire/detectors/base.py,sha256=-9KWUvn0f3l9VALUfeuK8MLiG_oVhixE60b4ah4N2J4,1633
194
- repotoire/detectors/circular_dependency.py,sha256=abhWCxgyglFVDpGJKL3JrFfJotfsmK4pHhfrzhnVuys,13232
195
+ repotoire/detectors/circular_dependency.py,sha256=54LfQpjLzivjfxpSLqMPvGJaFt-zkyWrsGIbp_Ub5PE,13279
195
196
  repotoire/detectors/core_utility.py,sha256=ovp5QTt0Kv5qKYg_hLImmRK4W8zLZF4VZuBFLs4aW6o,12136
196
- repotoire/detectors/data_clumps.py,sha256=Zk1ta7hAJtrZjj9dseWh_AQLS-WCT_8V0BopKAAPyZE,16631
197
- repotoire/detectors/dead_code.py,sha256=GHycS1HXLkNcHo5m8yrlbNKpE4lfRAukBf1EztetGoU,30247
197
+ repotoire/detectors/data_clumps.py,sha256=5csvR7JTY45CqwZos4LyLQXK5BPIGJQcS715-P45x00,16678
198
+ repotoire/detectors/dead_code.py,sha256=So8EHoaKJgBBfeNx4miwYaWSgQ9zmg5Bh0-nhZ5VbsU,30294
198
199
  repotoire/detectors/deduplicator.py,sha256=_lqtJ8-Us0s-sHxmbg9999_edq5s5xV6IVcrcpJnbA0,14678
199
200
  repotoire/detectors/degree_centrality.py,sha256=Q7qQHLvdm-uIx6GWZWyYaWRrGvuun4_VSAAXdt5ysUg,16580
200
201
  repotoire/detectors/duplicate_rust.py,sha256=A7R60RKh9AsLL0nQNd6M_aPRqySbl7MpUxE9m6_jaaY,18280
201
- repotoire/detectors/engine.py,sha256=qcJba-9RO5NYGxXxRMXMZ4eg4mZ6tRb48wr59opno8Y,40254
202
+ repotoire/detectors/engine.py,sha256=1LNObA0dvDWWftn5mbaiCgG1vhZEUQEowpFcua5ioZo,40301
202
203
  repotoire/detectors/feature_envy.py,sha256=Ci1PP7zL4SH7tLrX4xyWhq1Y7pjMtQeZp5visWS8t_U,11672
203
- repotoire/detectors/generator_misuse.py,sha256=ia6OnAaXo9Wap56wK21ZFJpN6Z3Mvvo2GEO-YvfHqyc,28926
204
+ repotoire/detectors/generator_misuse.py,sha256=Z4eoYza4m6ClN2nr1v7qa74esKHVQ6X9NJau17GflSI,28973
204
205
  repotoire/detectors/god_class.py,sha256=qsyPyhhnPMjvjpz_grwq2oK8iwl7zj2nV9oF23AVFOY,38078
205
- repotoire/detectors/graph_algorithms.py,sha256=J_3tOfiYBjWtglo3eUD3TBhB_A5YVULHcoS93BqjLh8,58563
206
+ repotoire/detectors/graph_algorithms.py,sha256=UAySR9IvaRAWrXDn0IiB9B-N5Q6yQ1Dp54IXF1h6fQs,58609
206
207
  repotoire/detectors/graphsage_detector.py,sha256=FN-2Y1AUDQYxA93BckZ_sOUfPp6rJoZxDCaF_kV3F_c,10303
207
- repotoire/detectors/inappropriate_intimacy.py,sha256=bc_TxvSWbrM3ER8lug9ZV9E-JBVWCJ3CLrv1EPbOkSs,8519
208
+ repotoire/detectors/inappropriate_intimacy.py,sha256=OsPLNo1bEzQMVxpox0foYphjR84l98QuchDAzL1KI7I,8566
208
209
  repotoire/detectors/influential_code.py,sha256=3bunilaxP6K5ShwSeLln1-iToQoiYGGQn-oM7ZuVTEo,16064
209
210
  repotoire/detectors/jscpd_detector.py,sha256=bolku-5QKr2FftpIyGrOTdbSgFyqTF2y42NFWyEIl8U,16637
210
211
  repotoire/detectors/lazy_class.py,sha256=ciO4E2s9I4OCkhT8PD_wNMqc3AXMW19cwzIl68c3US8,8951
211
- repotoire/detectors/long_parameter_list.py,sha256=TyKfGxVaK6JCRG_VDcEU7NXnqkHNRTRe779Aoigmgro,15545
212
- repotoire/detectors/message_chain.py,sha256=7YYrcP_58DVZeXQMRsbpHBZ1VSy6oxN9Rw_qW_tH2zY,18832
212
+ repotoire/detectors/long_parameter_list.py,sha256=MOsJL8emMb4eHMslpdgS1IKij6SXBOgIynHACF9hlK8,15592
213
+ repotoire/detectors/message_chain.py,sha256=84o0gXg2t2K800kIge9WHAK8CbngbeYTi6Mg6umtWcQ,18879
213
214
  repotoire/detectors/middle_man.py,sha256=HEpZILxxk7YV3Mu4gh9oP0LxdTnM8ZwyvMXuT8xWTNQ,8181
214
215
  repotoire/detectors/ml_bug_detector.py,sha256=PbHSNwqRA-8PDaLsnVmjr617PpHJDwe9fJfB2bg4Hlw,9954
215
216
  repotoire/detectors/module_cohesion.py,sha256=lnOrwC20YilFKVnPLhInIw998QXzlX1jjc52j3GYEC4,15803
@@ -227,9 +228,9 @@ repotoire/detectors/semgrep_detector.py,sha256=taYuIjkda8N6WiyvbyLgPHVAIUVX-cV2P
227
228
  repotoire/detectors/shotgun_surgery.py,sha256=uKVYCWO-TnECx6wG2W-SgrlDtM6pbsp4o0SAXH2kA7Y,7552
228
229
  repotoire/detectors/taint_detector.py,sha256=Ol7jU1gyKY9AFbyHqssiJecLlwf_oy3M1f0FFD1q8a4,23273
229
230
  repotoire/detectors/temporal_metrics.py,sha256=YTU7-ud9YyFVXew1tToCYe7AX4Asjq0St30s74Bm3dg,16551
230
- repotoire/detectors/test_smell.py,sha256=re36RlIWbPiHb1toguFFvwEOeINSL2fkuK2X5ynIqns,27071
231
+ repotoire/detectors/test_smell.py,sha256=KR6ITF4CaPCL2UeX-hAfqZikfCq-8nT_vxLvb9hUW6o,27118
231
232
  repotoire/detectors/truly_unused_imports.py,sha256=M2JFFEBPOPIQs1aqwW44XBOpVaGji3Ftcbdm57kx9LY,8622
232
- repotoire/detectors/type_hint_coverage.py,sha256=fUGsUYjqhHOLFrkf-AAJCaoSAna9Pdl9A4tSYFPADO4,20362
233
+ repotoire/detectors/type_hint_coverage.py,sha256=sF_Nj6oZ3kT8Ssd9A2HkvMcmwqB7_fLUE831xku7Wck,20409
233
234
  repotoire/detectors/voting_engine.py,sha256=wjo3rpMh5gg8KcWDBGmXj4hrChDQUbDxApmyUBAHHP4,22204
234
235
  repotoire/detectors/vulture_detector.py,sha256=M8QEEIaSFIyrKAkpcsUoMNpAgN1aGKoCZZdWHRbQd4I,23507
235
236
  repotoire/github/__init__.py,sha256=MQ1cwg-UPYRXR8-nahSkFRfKSVGwdxr4_4_u2jqMrDQ,112
@@ -238,9 +239,10 @@ repotoire/github/pr_commenter.py,sha256=-MyUb3tlDzC_iW9Sm1sB2Z9aPD-o8KzpLPmMbLTI
238
239
  repotoire/graph/__init__.py,sha256=jgChDzW_7iE_ATIIH5qMQZELCK1T9jYfrM9MG1nOWVg,1362
239
240
  repotoire/graph/base.py,sha256=9jEcqhVnZLlFCTs1PM0RNu0CB3OvKIgxgAFH3UnjmkM,5714
240
241
  repotoire/graph/client.py,sha256=eryWMuuGgDoUGnui-dTJWtYD_PWUvpmz4HtmuPJbKhU,39988
242
+ repotoire/graph/cloud_client.py,sha256=MfHUa0MkTIIq1xnb_i__fTZGa2unfthkCmnS541Ifys,7147
241
243
  repotoire/graph/enricher.py,sha256=5IOITHRSVsZhKNJdl_YMFJjYH2Nog5hUHDcJWJYEAFU,17441
242
244
  repotoire/graph/external_labels.py,sha256=eMdDaG11wTP1_mmgr-cUCFZl8Trs3XUh8vath7E3IB4,5446
243
- repotoire/graph/factory.py,sha256=ODvUGJNqc7HHNW694QxkocFfmnC5bi84zo5nuQNaR8s,18758
245
+ repotoire/graph/factory.py,sha256=cFBE6TQHntmTk0vCEMWthcect9OYlQVqpAgW9NiMgDs,18349
244
246
  repotoire/graph/falkordb_client.py,sha256=4Yv6BPkzeZE8RB123MUvk6GRlOrkRABMRfO9Qs1VPhw,21190
245
247
  repotoire/graph/incremental_scc.py,sha256=MbYsGzPah5_0VzJhU5OFYcyO9DK3z9iCTeJfjUM3yeY,14387
246
248
  repotoire/graph/migration.py,sha256=Ya8O8yRT7CNJzqm9N6GkVUjycNiMKV6Kl4qjdVhyl_Q,14813
@@ -249,7 +251,7 @@ repotoire/graph/queries/__init__.py,sha256=iErJN8jB13njO73rnuJpnSzZarbcGjKs4pNp2
249
251
  repotoire/graph/queries/builders.py,sha256=ng1BLfE4s4MEkcKB47ZQcT2l_T4yzLWRkninQCmSmyI,11333
250
252
  repotoire/graph/queries/patterns.py,sha256=Wdg1DYHHOCalhiLcBsk1ODHAxVF04eIPiGaadymy83M,15662
251
253
  repotoire/graph/queries/traversal.py,sha256=MkeE-GSS6xkjgGf74ziaWNfdqwegkplwiu8Av-wMff0,12405
252
- repotoire/graph/schema.py,sha256=vXctqYYKJ04mmjySMdST11tWFAwPvSXHU6M7jBtD8JY,17254
254
+ repotoire/graph/schema.py,sha256=SeTJRT7oDvyFco0ObuNWjXah6Ymc5tCGnDUwC6gP4ac,17335
253
255
  repotoire/graph/tenant_factory.py,sha256=kREcK-AFdLo_GH-RoEkIK6iIEioomTobV0YGNA7uCYs,19457
254
256
  repotoire/historical/__init__.py,sha256=wpaf5LQfeIXcV6V-hdUwWDpPPjt7hB4N46c2uWQjnRU,709
255
257
  repotoire/historical/git_graphiti.py,sha256=9RuMMhZ9_o14sfTlkXJfrHNGYRtdT0qMje_ai-6RGoE,12066
@@ -372,5 +374,5 @@ repotoire/workers/tasks.py,sha256=uqyIWg6JSEQGRBQnpDVLauja0gG7Hw9hqJ5HuVxjGLY,32
372
374
  repotoire/workers/webhook_delivery.py,sha256=RrOGyGIQakGvYKiQgAD9-eOfG1QXGQpx2ouPCZnGMJQ,17932
373
375
  repotoire/workers/webhooks.py,sha256=gpG-2N2Ik_oD1YFiPiXCysOcZY7jem3wg-o_jSgQyeo,17717
374
376
  repotoire_fast/__init__.py,sha256=kauI9ONyuAO3Opp6qD63Xkqxin9RXvZRkYRPyoLnlQU,11709
375
- repotoire_fast/repotoire_fast.cp314-win_amd64.pyd,sha256=KHx35WHv1dtRqiUXAoD6Li2_H7rU3UqM0ZiUZ262Is0,9132032
376
- repotoire-0.1.2.dist-info/RECORD,,
377
+ repotoire_fast/repotoire_fast.cp314-win_amd64.pyd,sha256=jTI7Zj3_RZe8Yt7gkwItnCvM8RFdJBkLVlQyHyba6xU,9132032
378
+ repotoire-0.1.3.dist-info/RECORD,,