devobin 1.0.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.
- devobin/__init__.py +4 -0
- devobin/app.py +42 -0
- devobin/cli/__init__.py +1 -0
- devobin/cli/chat.py +831 -0
- devobin/cli/commands.py +362 -0
- devobin/cli/slash_menu.py +74 -0
- devobin/cli/terminal.py +155 -0
- devobin/cli/ui.py +129 -0
- devobin/config/__init__.py +1 -0
- devobin/config/settings.py +124 -0
- devobin/core/__init__.py +1 -0
- devobin/core/analyzer.py +166 -0
- devobin/core/executor.py +244 -0
- devobin/core/memory.py +78 -0
- devobin/core/optimizer.py +106 -0
- devobin/core/prompt_builder.py +513 -0
- devobin/core/researcher.py +283 -0
- devobin/core/rtl.py +93 -0
- devobin/core/scanner.py +206 -0
- devobin/core/tools.py +283 -0
- devobin/core/validator.py +235 -0
- devobin/main.py +33 -0
- devobin/output/__init__.py +1 -0
- devobin/providers/__init__.py +85 -0
- devobin/providers/anthropic_provider.py +72 -0
- devobin/providers/google_provider.py +90 -0
- devobin/providers/ollama_provider.py +81 -0
- devobin/providers/openai_provider.py +102 -0
- devobin/storage/__init__.py +1 -0
- devobin/storage/database.py +207 -0
- devobin/ui/__init__.py +1 -0
- devobin/ui/ask_modal.py +96 -0
- devobin/ui/chat_screen.py +1029 -0
- devobin/ui/command_palette.py +131 -0
- devobin/ui/connect_modal.py +166 -0
- devobin/ui/export_modal.py +159 -0
- devobin/ui/history_modal.py +137 -0
- devobin/ui/memory_modal.py +88 -0
- devobin/ui/model_modal.py +143 -0
- devobin/ui/permission_modal.py +92 -0
- devobin/ui/project_modal.py +133 -0
- devobin/ui/settings_modal.py +89 -0
- devobin-1.0.0.dist-info/METADATA +364 -0
- devobin-1.0.0.dist-info/RECORD +47 -0
- devobin-1.0.0.dist-info/WHEEL +4 -0
- devobin-1.0.0.dist-info/entry_points.txt +2 -0
- devobin-1.0.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
"""Research engine - dynamically researches technologies for context generation."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
|
+
|
|
7
|
+
from devobin.core.analyzer import AnalysisResult, TechSignal
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@dataclass
|
|
11
|
+
class ResearchTopic:
|
|
12
|
+
"""A research topic with its gathered context."""
|
|
13
|
+
|
|
14
|
+
technology: str
|
|
15
|
+
category: str
|
|
16
|
+
best_practices: list[str] = field(default_factory=list)
|
|
17
|
+
architecture_rules: list[str] = field(default_factory=list)
|
|
18
|
+
performance_tips: list[str] = field(default_factory=list)
|
|
19
|
+
security_rules: list[str] = field(default_factory=list)
|
|
20
|
+
common_mistakes: list[str] = field(default_factory=list)
|
|
21
|
+
production_advice: list[str] = field(default_factory=list)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
# ── Research knowledge base (dynamically structured) ──────────────
|
|
25
|
+
|
|
26
|
+
RESEARCH_DB: dict[str, dict[str, list[str]]] = {
|
|
27
|
+
"Django": {
|
|
28
|
+
"best_practices": [
|
|
29
|
+
"Use select_related() and prefetch_related() to avoid N+1 queries",
|
|
30
|
+
"Keep views thin; use service layers for business logic",
|
|
31
|
+
"Use Django signals sparingly; prefer explicit calls",
|
|
32
|
+
"Use class-based views when possible for consistency",
|
|
33
|
+
"Organize apps by domain, not by technical layer",
|
|
34
|
+
"Use Django's built-in auth system instead of rolling your own",
|
|
35
|
+
"Use django-environ or pydantic-settings for environment configuration",
|
|
36
|
+
],
|
|
37
|
+
"architecture_rules": [
|
|
38
|
+
"Follow the MVC/MVT pattern strictly",
|
|
39
|
+
"Use serializers for API data transformation (DRF)",
|
|
40
|
+
"Implement a service layer between views and models",
|
|
41
|
+
"Use Django managers for complex query logic",
|
|
42
|
+
"Separate concerns: models for data, views for HTTP, services for logic",
|
|
43
|
+
],
|
|
44
|
+
"performance": [
|
|
45
|
+
"Add database indexes on frequently queried fields",
|
|
46
|
+
"Use Django cache framework with Redis/Memcached",
|
|
47
|
+
"Implement pagination for all list endpoints",
|
|
48
|
+
"Use django-debug-toolbar in development",
|
|
49
|
+
"Optimize admin queries with list_select_related",
|
|
50
|
+
"Use Celery for background tasks, not synchronous views",
|
|
51
|
+
"Consider django-cachalot for automatic query caching",
|
|
52
|
+
],
|
|
53
|
+
"security": [
|
|
54
|
+
"Always use Django's CSRF protection",
|
|
55
|
+
"Validate and sanitize all user inputs",
|
|
56
|
+
"Use Django's built-in XSS protection",
|
|
57
|
+
"Implement rate limiting with django-ratelimit",
|
|
58
|
+
"Use HTTPS everywhere in production",
|
|
59
|
+
"Never store secrets in settings.py or version control",
|
|
60
|
+
"Use Django's password validation and hashing",
|
|
61
|
+
"Implement proper CORS configuration with django-cors-headers",
|
|
62
|
+
],
|
|
63
|
+
"common_mistakes": [
|
|
64
|
+
"N+1 queries in templates and serializers",
|
|
65
|
+
"Using Django ORM for bulk operations instead of bulk_create/bulk_update",
|
|
66
|
+
"Not using transactions for multi-step operations",
|
|
67
|
+
"Storing files locally instead of using S3/cloud storage",
|
|
68
|
+
"Ignoring database migration dependencies",
|
|
69
|
+
"Not using Django's built-in logging framework",
|
|
70
|
+
],
|
|
71
|
+
},
|
|
72
|
+
"FastAPI": {
|
|
73
|
+
"best_practices": [
|
|
74
|
+
"Use dependency injection for database sessions and services",
|
|
75
|
+
"Define Pydantic models for all request/response schemas",
|
|
76
|
+
"Use async/await for I/O-bound operations",
|
|
77
|
+
"Structure routers by domain, not by HTTP method",
|
|
78
|
+
"Use APIRouter for modular endpoint organization",
|
|
79
|
+
"Implement health check endpoints",
|
|
80
|
+
],
|
|
81
|
+
"architecture_rules": [
|
|
82
|
+
"Separate routers, services, and repositories",
|
|
83
|
+
"Use dependency injection chains for complex setups",
|
|
84
|
+
"Define OpenAPI schemas explicitly for documentation",
|
|
85
|
+
"Use background tasks for non-blocking operations",
|
|
86
|
+
],
|
|
87
|
+
"performance": [
|
|
88
|
+
"Use async SQLAlchemy or Tortoise ORM",
|
|
89
|
+
"Implement connection pooling",
|
|
90
|
+
"Use Redis for caching and session management",
|
|
91
|
+
"Add response compression middleware",
|
|
92
|
+
"Use Pydantic v2 for faster serialization",
|
|
93
|
+
],
|
|
94
|
+
"security": [
|
|
95
|
+
"Implement OAuth2 with JWT tokens",
|
|
96
|
+
"Use rate limiting middleware",
|
|
97
|
+
"Validate all inputs with Pydantic",
|
|
98
|
+
"Use HTTPS and secure CORS settings",
|
|
99
|
+
"Implement proper error handling without leaking internals",
|
|
100
|
+
],
|
|
101
|
+
"common_mistakes": [
|
|
102
|
+
"Blocking the event loop with synchronous I/O",
|
|
103
|
+
"Not using dependency injection properly",
|
|
104
|
+
"Over-fetching data without response models",
|
|
105
|
+
"Ignoring OpenAPI documentation",
|
|
106
|
+
],
|
|
107
|
+
},
|
|
108
|
+
"React": {
|
|
109
|
+
"best_practices": [
|
|
110
|
+
"Use functional components with hooks",
|
|
111
|
+
"Keep components small and focused (single responsibility)",
|
|
112
|
+
"Extract custom hooks for reusable logic",
|
|
113
|
+
"Use React.memo() for expensive render optimizations",
|
|
114
|
+
"Implement proper error boundaries",
|
|
115
|
+
"Use TypeScript for type safety",
|
|
116
|
+
"Follow the component composition pattern over inheritance",
|
|
117
|
+
],
|
|
118
|
+
"architecture_rules": [
|
|
119
|
+
"Co-locate related files (component, styles, tests, types)",
|
|
120
|
+
"Use a centralized state management solution (Context, Zustand, Redux Toolkit)",
|
|
121
|
+
"Separate presentational and container components",
|
|
122
|
+
"Use atomic design principles for component hierarchy",
|
|
123
|
+
],
|
|
124
|
+
"performance": [
|
|
125
|
+
"Use React.lazy() and Suspense for code splitting",
|
|
126
|
+
"Implement virtualized lists for large datasets",
|
|
127
|
+
"Avoid inline function definitions in JSX",
|
|
128
|
+
"Use useMemo and useCallback judiciously (not everywhere)",
|
|
129
|
+
"Optimize images with lazy loading and proper formats",
|
|
130
|
+
"Use the React DevTools Profiler to identify bottlenecks",
|
|
131
|
+
],
|
|
132
|
+
"security": [
|
|
133
|
+
"Never use dangerouslySetInnerHTML with user content",
|
|
134
|
+
"Sanitize all user inputs before rendering",
|
|
135
|
+
"Implement Content Security Policy (CSP)",
|
|
136
|
+
"Use environment variables for sensitive configuration",
|
|
137
|
+
"Prevent XSS by escaping dynamic content",
|
|
138
|
+
],
|
|
139
|
+
"common_mistakes": [
|
|
140
|
+
"Excessive re-renders from improper state management",
|
|
141
|
+
"Using index as key for dynamic lists",
|
|
142
|
+
"Mutating state directly instead of creating new references",
|
|
143
|
+
"Not cleaning up side effects in useEffect",
|
|
144
|
+
"Prop drilling deep component trees instead of using context",
|
|
145
|
+
],
|
|
146
|
+
},
|
|
147
|
+
"PostgreSQL": {
|
|
148
|
+
"best_practices": [
|
|
149
|
+
"Use EXPLAIN ANALYZE to optimize slow queries",
|
|
150
|
+
"Create indexes on columns used in WHERE, JOIN, and ORDER BY",
|
|
151
|
+
"Use appropriate data types (e.g., UUID, JSONB, TIMESTAMPTZ)",
|
|
152
|
+
"Implement database-level constraints (NOT NULL, UNIQUE, CHECK)",
|
|
153
|
+
"Use migrations for all schema changes",
|
|
154
|
+
"Partition large tables by range or hash",
|
|
155
|
+
],
|
|
156
|
+
"architecture_rules": [
|
|
157
|
+
"Normalize to 3NF, then denormalize strategically",
|
|
158
|
+
"Use foreign keys for data integrity",
|
|
159
|
+
"Implement soft deletes with deleted_at timestamp",
|
|
160
|
+
"Use database views for complex read queries",
|
|
161
|
+
"Separate read and write workloads when scaling",
|
|
162
|
+
],
|
|
163
|
+
"performance": [
|
|
164
|
+
"Use connection pooling (PgBouncer, pgbouncer)",
|
|
165
|
+
"Add partial indexes for filtered queries",
|
|
166
|
+
"Use BRIN indexes for time-series data",
|
|
167
|
+
"Implement table partitioning for large datasets",
|
|
168
|
+
"Use materialized views for complex aggregations",
|
|
169
|
+
"Monitor with pg_stat_statements",
|
|
170
|
+
"Tune shared_buffers, work_mem, and effective_cache_size",
|
|
171
|
+
],
|
|
172
|
+
"security": [
|
|
173
|
+
"Use row-level security (RLS) for multi-tenant apps",
|
|
174
|
+
"Encrypt sensitive columns with pgcrypto",
|
|
175
|
+
"Use SSL connections",
|
|
176
|
+
"Implement proper backup strategy (pg_dump, WAL archiving)",
|
|
177
|
+
"Restrict database user permissions (least privilege)",
|
|
178
|
+
"Never expose database directly to the internet",
|
|
179
|
+
],
|
|
180
|
+
"common_mistakes": [
|
|
181
|
+
"Missing indexes on foreign key columns",
|
|
182
|
+
"Using SELECT * instead of specific columns",
|
|
183
|
+
"Not using transactions for multi-step operations",
|
|
184
|
+
"Over-normalizing causing excessive JOINs",
|
|
185
|
+
"Ignoring connection pool limits",
|
|
186
|
+
],
|
|
187
|
+
},
|
|
188
|
+
"Docker": {
|
|
189
|
+
"best_practices": [
|
|
190
|
+
"Use multi-stage builds to reduce image size",
|
|
191
|
+
"Pin base image versions for reproducibility",
|
|
192
|
+
"Use .dockerignore to exclude unnecessary files",
|
|
193
|
+
"Run as non-root user in containers",
|
|
194
|
+
"Use health checks in Dockerfile",
|
|
195
|
+
"Layer dependencies before application code",
|
|
196
|
+
],
|
|
197
|
+
"architecture_rules": [
|
|
198
|
+
"One process per container",
|
|
199
|
+
"Use docker-compose for local development",
|
|
200
|
+
"Externalize configuration via environment variables",
|
|
201
|
+
"Use volumes for persistent data",
|
|
202
|
+
],
|
|
203
|
+
"performance": [
|
|
204
|
+
"Minimize layers by combining RUN commands",
|
|
205
|
+
"Use BuildKit for faster builds",
|
|
206
|
+
"Leverage Docker cache for repeated builds",
|
|
207
|
+
"Use .dockerignore to speed up build context",
|
|
208
|
+
],
|
|
209
|
+
"security": [
|
|
210
|
+
"Scan images for vulnerabilities (Trivy, Snyk)",
|
|
211
|
+
"Don't store secrets in images",
|
|
212
|
+
"Use read-only file systems where possible",
|
|
213
|
+
"Limit container resources (CPU, memory)",
|
|
214
|
+
"Use Docker secrets for sensitive data",
|
|
215
|
+
],
|
|
216
|
+
},
|
|
217
|
+
"Redis": {
|
|
218
|
+
"best_practices": [
|
|
219
|
+
"Use Redis for caching, sessions, and pub/sub",
|
|
220
|
+
"Set appropriate TTL for all cached values",
|
|
221
|
+
"Use Redis data structures wisely (hashes, sorted sets)",
|
|
222
|
+
"Implement cache invalidation strategies",
|
|
223
|
+
"Use pipelining for batch operations",
|
|
224
|
+
],
|
|
225
|
+
"performance": [
|
|
226
|
+
"Use connection pooling",
|
|
227
|
+
"Avoid large keys (>1MB)",
|
|
228
|
+
"Use SCAN instead of KEYS for production",
|
|
229
|
+
"Monitor memory usage and evictions",
|
|
230
|
+
"Use Redis Cluster for horizontal scaling",
|
|
231
|
+
],
|
|
232
|
+
"security": [
|
|
233
|
+
"Require authentication (requirepass or ACLs)",
|
|
234
|
+
"Bind to localhost or private network",
|
|
235
|
+
"Disable dangerous commands (FLUSHALL, CONFIG)",
|
|
236
|
+
"Use TLS for connections",
|
|
237
|
+
],
|
|
238
|
+
},
|
|
239
|
+
"Stripe": {
|
|
240
|
+
"best_practices": [
|
|
241
|
+
"Use Stripe Checkout for PCI compliance",
|
|
242
|
+
"Implement webhook endpoints for payment events",
|
|
243
|
+
"Use idempotency keys for payment operations",
|
|
244
|
+
"Test with Stripe test mode and test cards",
|
|
245
|
+
"Handle failed payments gracefully with retry logic",
|
|
246
|
+
],
|
|
247
|
+
"security": [
|
|
248
|
+
"Never log full card numbers",
|
|
249
|
+
"Verify webhook signatures",
|
|
250
|
+
"Use Stripe.js for client-side card handling",
|
|
251
|
+
"Store payment method IDs, not card details",
|
|
252
|
+
"Implement proper error handling for payment failures",
|
|
253
|
+
],
|
|
254
|
+
},
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
def research_technology(tech: TechSignal) -> ResearchTopic:
|
|
259
|
+
"""Research a detected technology and return structured findings."""
|
|
260
|
+
topic = ResearchTopic(technology=tech.name, category=tech.category)
|
|
261
|
+
|
|
262
|
+
research = RESEARCH_DB.get(tech.name, {})
|
|
263
|
+
topic.best_practices = research.get("best_practices", [])
|
|
264
|
+
topic.architecture_rules = research.get("architecture_rules", [])
|
|
265
|
+
topic.performance_tips = research.get("performance", [])
|
|
266
|
+
topic.security_rules = research.get("security", [])
|
|
267
|
+
topic.common_mistakes = research.get("common_mistakes", [])
|
|
268
|
+
topic.production_advice = research.get("production", [])
|
|
269
|
+
|
|
270
|
+
return topic
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
def research_analysis(analysis: AnalysisResult) -> dict[str, ResearchTopic]:
|
|
274
|
+
"""Research all detected technologies from an analysis result."""
|
|
275
|
+
results: dict[str, ResearchTopic] = {}
|
|
276
|
+
seen: set[str] = set()
|
|
277
|
+
|
|
278
|
+
for tech in analysis.detected_techs:
|
|
279
|
+
if tech.name not in seen:
|
|
280
|
+
seen.add(tech.name)
|
|
281
|
+
results[tech.name] = research_technology(tech)
|
|
282
|
+
|
|
283
|
+
return results
|
devobin/core/rtl.py
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
"""RTL text reshaping for Persian/Arabic display in terminals."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
|
|
7
|
+
try:
|
|
8
|
+
import arabic_reshaper
|
|
9
|
+
from bidi.algorithm import get_display
|
|
10
|
+
_HAS_RTL = True
|
|
11
|
+
except ImportError:
|
|
12
|
+
_HAS_RTL = False
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def reshape_rtl(text: str) -> str:
|
|
16
|
+
"""Reshape Persian/Arabic text for correct terminal display.
|
|
17
|
+
|
|
18
|
+
Handles:
|
|
19
|
+
- Connected letter forms (arabic_reshaper)
|
|
20
|
+
- Bidirectional text layout (python-bidi)
|
|
21
|
+
- Mixed RTL/LTR content
|
|
22
|
+
"""
|
|
23
|
+
if not _HAS_RTL or not text:
|
|
24
|
+
return text
|
|
25
|
+
|
|
26
|
+
# Check if text contains RTL characters
|
|
27
|
+
if not _has_rtl_chars(text):
|
|
28
|
+
return text
|
|
29
|
+
|
|
30
|
+
try:
|
|
31
|
+
# Reshape Arabic/Persian characters
|
|
32
|
+
reshaped = arabic_reshaper.reshape(text)
|
|
33
|
+
# Apply bidirectional algorithm
|
|
34
|
+
return get_display(reshaped)
|
|
35
|
+
except Exception:
|
|
36
|
+
return text
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _has_rtl_chars(text: str) -> bool:
|
|
40
|
+
"""Check if text contains RTL characters (Arabic, Persian, Hebrew)."""
|
|
41
|
+
for char in text:
|
|
42
|
+
cp = ord(char)
|
|
43
|
+
# Arabic: 0x0600-0x06FF
|
|
44
|
+
# Arabic Supplement: 0x0750-0x077F
|
|
45
|
+
# Arabic Extended: 0x08A0-0x08FF
|
|
46
|
+
# Persian extended: 0xFB50-0xFDFF, 0xFE70-0xFEFF
|
|
47
|
+
if (0x0600 <= cp <= 0x06FF or
|
|
48
|
+
0x0750 <= cp <= 0x077F or
|
|
49
|
+
0x08A0 <= cp <= 0x08FF or
|
|
50
|
+
0xFB50 <= cp <= 0xFDFF or
|
|
51
|
+
0xFE70 <= cp <= 0xFEFF):
|
|
52
|
+
return True
|
|
53
|
+
return False
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def smart_display(text: str) -> str:
|
|
57
|
+
"""Smart text display that handles mixed RTL/LTR content.
|
|
58
|
+
|
|
59
|
+
Splits text into lines and reshapes each line individually,
|
|
60
|
+
preserving markdown formatting.
|
|
61
|
+
"""
|
|
62
|
+
if not text:
|
|
63
|
+
return text
|
|
64
|
+
|
|
65
|
+
lines = text.split("\n")
|
|
66
|
+
result = []
|
|
67
|
+
|
|
68
|
+
for line in lines:
|
|
69
|
+
# Preserve markdown code blocks
|
|
70
|
+
if line.strip().startswith("```") or line.strip().startswith(" "):
|
|
71
|
+
result.append(line)
|
|
72
|
+
continue
|
|
73
|
+
|
|
74
|
+
# Preserve markdown headers
|
|
75
|
+
if line.startswith("#"):
|
|
76
|
+
# Reshape the text part, keep the #
|
|
77
|
+
hash_part = line[:len(line) - len(line.lstrip("#"))]
|
|
78
|
+
text_part = line[len(hash_part):]
|
|
79
|
+
result.append(hash_part + reshape_rtl(text_part))
|
|
80
|
+
continue
|
|
81
|
+
|
|
82
|
+
# Preserve markdown list markers
|
|
83
|
+
list_match = re.match(r"^(\s*[-*•·]\s*)", line)
|
|
84
|
+
if list_match:
|
|
85
|
+
marker = list_match.group(1)
|
|
86
|
+
text_part = line[len(marker):]
|
|
87
|
+
result.append(marker + reshape_rtl(text_part))
|
|
88
|
+
continue
|
|
89
|
+
|
|
90
|
+
# Regular line
|
|
91
|
+
result.append(reshape_rtl(line))
|
|
92
|
+
|
|
93
|
+
return "\n".join(result)
|
devobin/core/scanner.py
ADDED
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
"""Workspace scanner — autonomously reads the real project like an agent.
|
|
2
|
+
|
|
3
|
+
Unlike ``analyzer`` (which only keyword-matches the user's text), this module
|
|
4
|
+
walks the actual working directory, ignores noise, classifies files, detects the
|
|
5
|
+
real technology stack from manifests, and produces a compressed project map that
|
|
6
|
+
can be injected into an AI prompt. It reads the whole project — not one file —
|
|
7
|
+
and never modifies anything.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from dataclasses import dataclass, field
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
|
|
15
|
+
# ── What to skip ───────────────────────────────────────────────────
|
|
16
|
+
|
|
17
|
+
IGNORE_DIRS = {
|
|
18
|
+
".git", ".hg", ".svn", "__pycache__", ".pytest_cache", ".mypy_cache",
|
|
19
|
+
".ruff_cache", "node_modules", ".venv", "venv", "env", ".env", "dist",
|
|
20
|
+
"build", ".next", ".nuxt", ".svelte-kit", "target", "bin", "obj",
|
|
21
|
+
".idea", ".vscode", ".gradle", "vendor", ".cache", "coverage",
|
|
22
|
+
".mimocode", ".claude",
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
# Extensions we never try to read as text.
|
|
26
|
+
BINARY_EXTS = {
|
|
27
|
+
".png", ".jpg", ".jpeg", ".gif", ".webp", ".ico", ".bmp", ".svg", ".pdf",
|
|
28
|
+
".mp3", ".mp4", ".wav", ".mov", ".avi", ".zip", ".tar", ".gz", ".rar",
|
|
29
|
+
".7z", ".exe", ".dll", ".so", ".dylib", ".bin", ".pyc", ".pyo", ".class",
|
|
30
|
+
".jar", ".woff", ".woff2", ".ttf", ".eot", ".otf", ".lock", ".db",
|
|
31
|
+
".sqlite", ".sqlite3", ".o", ".a", ".wasm",
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
# Files that reveal the stack at a glance — read these first.
|
|
35
|
+
MANIFEST_FILES = {
|
|
36
|
+
"package.json", "pyproject.toml", "requirements.txt", "setup.py",
|
|
37
|
+
"setup.cfg", "Pipfile", "poetry.lock", "composer.json", "go.mod",
|
|
38
|
+
"cargo.toml", "Cargo.toml", "pom.xml", "build.gradle", "Gemfile",
|
|
39
|
+
"gemfile", "*.csproj", "*.sln", "Dockerfile", "docker-compose.yml",
|
|
40
|
+
"docker-compose.yaml", "tsconfig.json", "next.config.js", "vite.config.ts",
|
|
41
|
+
"manage.py", "artisan", "README.md", "readme.md", ".env.example",
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
# Extension → language label, derived from real content, not assumptions.
|
|
45
|
+
EXT_LANG = {
|
|
46
|
+
".py": "Python", ".js": "JavaScript", ".mjs": "JavaScript", ".cjs": "JavaScript",
|
|
47
|
+
".ts": "TypeScript", ".tsx": "TypeScript/React", ".jsx": "JavaScript/React",
|
|
48
|
+
".vue": "Vue", ".svelte": "Svelte", ".go": "Go", ".rs": "Rust",
|
|
49
|
+
".java": "Java", ".kt": "Kotlin", ".cs": "C#", ".php": "PHP", ".rb": "Ruby",
|
|
50
|
+
".c": "C", ".h": "C", ".cpp": "C++", ".hpp": "C++", ".swift": "Swift",
|
|
51
|
+
".html": "HTML", ".css": "CSS", ".scss": "SCSS", ".sql": "SQL",
|
|
52
|
+
".sh": "Shell", ".ps1": "PowerShell", ".md": "Markdown",
|
|
53
|
+
".json": "JSON", ".toml": "TOML", ".yaml": "YAML", ".yml": "YAML",
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
MAX_FILES = 4000 # hard cap on how many files we enumerate
|
|
57
|
+
MAX_MANIFEST_BYTES = 200_000 # per-manifest read cap (generous, not 500 lines)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
@dataclass
|
|
61
|
+
class FileNode:
|
|
62
|
+
"""A single discovered file."""
|
|
63
|
+
|
|
64
|
+
path: str # relative to root, posix-style
|
|
65
|
+
name: str
|
|
66
|
+
parent: str
|
|
67
|
+
ext: str
|
|
68
|
+
size: int
|
|
69
|
+
lang: str = ""
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
@dataclass
|
|
73
|
+
class ProjectMap:
|
|
74
|
+
"""Compressed knowledge of the scanned workspace."""
|
|
75
|
+
|
|
76
|
+
root: str
|
|
77
|
+
files: list[FileNode] = field(default_factory=list)
|
|
78
|
+
dirs: list[str] = field(default_factory=list)
|
|
79
|
+
languages: dict[str, int] = field(default_factory=dict) # lang -> file count
|
|
80
|
+
manifests: dict[str, str] = field(default_factory=dict) # rel path -> content
|
|
81
|
+
entry_points: list[str] = field(default_factory=list)
|
|
82
|
+
total_files: int = 0
|
|
83
|
+
truncated: bool = False
|
|
84
|
+
|
|
85
|
+
@property
|
|
86
|
+
def primary_language(self) -> str:
|
|
87
|
+
if not self.languages:
|
|
88
|
+
return "unknown"
|
|
89
|
+
return max(self.languages, key=self.languages.get)
|
|
90
|
+
|
|
91
|
+
def to_context(self, max_chars: int = 8000) -> str:
|
|
92
|
+
"""Render a compact, token-friendly summary for the AI prompt."""
|
|
93
|
+
lines: list[str] = []
|
|
94
|
+
lines.append(f"# Project Map — {Path(self.root).name}")
|
|
95
|
+
lines.append(f"Root: {self.root}")
|
|
96
|
+
lines.append(f"Files scanned: {self.total_files}" + (" (truncated)" if self.truncated else ""))
|
|
97
|
+
lines.append(f"Primary language: {self.primary_language}")
|
|
98
|
+
|
|
99
|
+
if self.languages:
|
|
100
|
+
top = sorted(self.languages.items(), key=lambda kv: kv[1], reverse=True)
|
|
101
|
+
lang_str = ", ".join(f"{k} ({v})" for k, v in top[:10])
|
|
102
|
+
lines.append(f"Languages: {lang_str}")
|
|
103
|
+
|
|
104
|
+
if self.entry_points:
|
|
105
|
+
lines.append(f"Entry points: {', '.join(self.entry_points[:10])}")
|
|
106
|
+
|
|
107
|
+
if self.dirs:
|
|
108
|
+
lines.append("\n## Structure")
|
|
109
|
+
for d in self.dirs[:60]:
|
|
110
|
+
lines.append(f"- {d}/")
|
|
111
|
+
|
|
112
|
+
if self.manifests:
|
|
113
|
+
lines.append("\n## Manifests")
|
|
114
|
+
for rel, content in self.manifests.items():
|
|
115
|
+
snippet = content.strip()
|
|
116
|
+
if len(snippet) > 1200:
|
|
117
|
+
snippet = snippet[:1200] + "\n… (truncated)"
|
|
118
|
+
lines.append(f"\n### {rel}\n{snippet}")
|
|
119
|
+
|
|
120
|
+
out = "\n".join(lines)
|
|
121
|
+
if len(out) > max_chars:
|
|
122
|
+
out = out[:max_chars] + "\n… (map truncated)"
|
|
123
|
+
return out
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def _is_ignored(rel_parts: tuple[str, ...]) -> bool:
|
|
127
|
+
return any(part in IGNORE_DIRS for part in rel_parts)
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def _looks_binary(path: Path) -> bool:
|
|
131
|
+
if path.suffix.lower() in BINARY_EXTS:
|
|
132
|
+
return True
|
|
133
|
+
try:
|
|
134
|
+
with path.open("rb") as fh:
|
|
135
|
+
chunk = fh.read(1024)
|
|
136
|
+
return b"\x00" in chunk
|
|
137
|
+
except Exception:
|
|
138
|
+
return True
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def scan_workspace(root: str | Path | None = None) -> ProjectMap:
|
|
142
|
+
"""Walk the workspace and build a compressed :class:`ProjectMap`."""
|
|
143
|
+
root_path = Path(root).resolve() if root else Path.cwd()
|
|
144
|
+
pmap = ProjectMap(root=str(root_path))
|
|
145
|
+
|
|
146
|
+
dirs_seen: set[str] = set()
|
|
147
|
+
count = 0
|
|
148
|
+
|
|
149
|
+
for path in root_path.rglob("*"):
|
|
150
|
+
rel = path.relative_to(root_path)
|
|
151
|
+
rel_parts = rel.parts
|
|
152
|
+
if _is_ignored(rel_parts):
|
|
153
|
+
continue
|
|
154
|
+
|
|
155
|
+
if path.is_dir():
|
|
156
|
+
dirs_seen.add(rel.as_posix())
|
|
157
|
+
continue
|
|
158
|
+
|
|
159
|
+
if count >= MAX_FILES:
|
|
160
|
+
pmap.truncated = True
|
|
161
|
+
break
|
|
162
|
+
count += 1
|
|
163
|
+
|
|
164
|
+
ext = path.suffix.lower()
|
|
165
|
+
try:
|
|
166
|
+
size = path.stat().st_size
|
|
167
|
+
except Exception:
|
|
168
|
+
size = 0
|
|
169
|
+
|
|
170
|
+
node = FileNode(
|
|
171
|
+
path=rel.as_posix(),
|
|
172
|
+
name=path.name,
|
|
173
|
+
parent=rel.parent.as_posix() if rel.parent.as_posix() != "." else "",
|
|
174
|
+
ext=ext,
|
|
175
|
+
size=size,
|
|
176
|
+
lang=EXT_LANG.get(ext, ""),
|
|
177
|
+
)
|
|
178
|
+
pmap.files.append(node)
|
|
179
|
+
|
|
180
|
+
if node.lang:
|
|
181
|
+
pmap.languages[node.lang] = pmap.languages.get(node.lang, 0) + 1
|
|
182
|
+
|
|
183
|
+
# Read manifests (stack-revealing files) up front.
|
|
184
|
+
if _is_manifest(path.name) and not _looks_binary(path):
|
|
185
|
+
try:
|
|
186
|
+
content = path.read_text(encoding="utf-8", errors="replace")
|
|
187
|
+
pmap.manifests[rel.as_posix()] = content[:MAX_MANIFEST_BYTES]
|
|
188
|
+
except Exception:
|
|
189
|
+
pass
|
|
190
|
+
|
|
191
|
+
if path.name in ("main.py", "app.py", "index.js", "index.ts", "manage.py",
|
|
192
|
+
"server.js", "main.go", "Program.cs", "app.py", "__main__.py"):
|
|
193
|
+
pmap.entry_points.append(rel.as_posix())
|
|
194
|
+
|
|
195
|
+
pmap.total_files = count
|
|
196
|
+
pmap.dirs = sorted(dirs_seen)
|
|
197
|
+
return pmap
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def _is_manifest(name: str) -> bool:
|
|
201
|
+
if name in MANIFEST_FILES:
|
|
202
|
+
return True
|
|
203
|
+
for pat in MANIFEST_FILES:
|
|
204
|
+
if pat.startswith("*.") and name.endswith(pat[1:]):
|
|
205
|
+
return True
|
|
206
|
+
return False
|