samcode-cli 1.0.3__py3-none-any.whl → 1.0.4__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.
- samcode.py +452 -29
- {samcode_cli-1.0.3.dist-info → samcode_cli-1.0.4.dist-info}/METADATA +1 -1
- samcode_cli-1.0.4.dist-info/RECORD +6 -0
- samcode_cli-1.0.3.dist-info/RECORD +0 -6
- {samcode_cli-1.0.3.dist-info → samcode_cli-1.0.4.dist-info}/WHEEL +0 -0
- {samcode_cli-1.0.3.dist-info → samcode_cli-1.0.4.dist-info}/entry_points.txt +0 -0
- {samcode_cli-1.0.3.dist-info → samcode_cli-1.0.4.dist-info}/top_level.txt +0 -0
samcode.py
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"""
|
|
3
3
|
╔══════════════════════════════════════════════════════════════════════════════╗
|
|
4
4
|
║ S A M C O D E C L I ║
|
|
5
|
-
║ Autonomous Coding Agent
|
|
5
|
+
║ Autonomous Coding Agent v6.2 (RAG + Doctor + Data BI) ║
|
|
6
6
|
║ Similar to Claude Code & GitHub Copilot Workspace ║
|
|
7
7
|
╚══════════════════════════════════════════════════════════════════════════════╝
|
|
8
8
|
"""
|
|
@@ -21,7 +21,7 @@ from dataclasses import dataclass
|
|
|
21
21
|
from enum import Enum
|
|
22
22
|
|
|
23
23
|
# Rich UI Components
|
|
24
|
-
from rich.console import Console
|
|
24
|
+
from rich.console import Console, Group
|
|
25
25
|
from rich.panel import Panel
|
|
26
26
|
from rich.syntax import Syntax
|
|
27
27
|
from rich.table import Table
|
|
@@ -47,6 +47,9 @@ except ImportError:
|
|
|
47
47
|
from prompt_toolkit.formatted_text import FormattedText
|
|
48
48
|
from prompt_toolkit.styles import Style
|
|
49
49
|
from prompt_toolkit.enums import EditingMode
|
|
50
|
+
import warnings
|
|
51
|
+
warnings.filterwarnings("ignore", category=DeprecationWarning)
|
|
52
|
+
warnings.filterwarnings("ignore", message=".*urllib3.*")
|
|
50
53
|
|
|
51
54
|
# HTTP Client
|
|
52
55
|
try:
|
|
@@ -55,16 +58,6 @@ except ImportError:
|
|
|
55
58
|
subprocess.run([sys.executable, "-m", "pip", "install", "requests", "-q"], capture_output=True)
|
|
56
59
|
import requests
|
|
57
60
|
|
|
58
|
-
# Document Processing & Web Parsing Libraries Auto-Install
|
|
59
|
-
LIBS_TO_INSTALL = {
|
|
60
|
-
'pdfplumber': 'pdfplumber', 'docx': 'python-docx', 'openpyxl': 'openpyxl',
|
|
61
|
-
'pptx': 'python-pptx', 'PIL': 'Pillow', 'bs4': 'beautifulsoup4'
|
|
62
|
-
}
|
|
63
|
-
for module, package in LIBS_TO_INSTALL.items():
|
|
64
|
-
try:
|
|
65
|
-
__import__(module)
|
|
66
|
-
except ImportError:
|
|
67
|
-
subprocess.run([sys.executable, "-m", "pip", "install", package, "-q"], capture_output=True)
|
|
68
61
|
|
|
69
62
|
console = Console()
|
|
70
63
|
|
|
@@ -74,6 +67,31 @@ menu_style = Style.from_dict({
|
|
|
74
67
|
'completion-menu.completion.current': 'fg:#ffffff bg:#00af00 bold',
|
|
75
68
|
})
|
|
76
69
|
|
|
70
|
+
# Document Processing & Web Parsing Libraries Auto-Install
|
|
71
|
+
# Document Processing & Web Parsing Libraries Auto-Install
|
|
72
|
+
LIBS_TO_INSTALL = {
|
|
73
|
+
'pdfplumber': 'pdfplumber', 'docx': 'python-docx', 'openpyxl': 'openpyxl',
|
|
74
|
+
'pptx': 'python-pptx', 'PIL': 'Pillow', 'bs4': 'beautifulsoup4',
|
|
75
|
+
'chromadb': 'chromadb', 'sentence_transformers': 'sentence-transformers',
|
|
76
|
+
'ruff': 'ruff', 'pandas': 'pandas',
|
|
77
|
+
'matplotlib': 'matplotlib', 'seaborn': 'seaborn', 'nbformat': 'nbformat', 'sqlalchemy': 'sqlalchemy',
|
|
78
|
+
'pyarrow': 'pyarrow', 'psycopg2_binary': 'psycopg2-binary', 'pymysql': 'pymysql'
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
for module, package in LIBS_TO_INSTALL.items():
|
|
82
|
+
try:
|
|
83
|
+
__import__(module)
|
|
84
|
+
except ImportError:
|
|
85
|
+
console.print(f"[cyan]Installing {package}...[/cyan]")
|
|
86
|
+
# ✅ FIX: Use subprocess with explicit error checking
|
|
87
|
+
result = subprocess.run(
|
|
88
|
+
[sys.executable, "-m", "pip", "install", "--no-cache-dir", package],
|
|
89
|
+
capture_output=True, text=True
|
|
90
|
+
)
|
|
91
|
+
if result.returncode != 0:
|
|
92
|
+
console.print(f"[red]⚠️ Failed to install {package}. Run manually: pip install {package}[/red]")
|
|
93
|
+
console.print(f"[dim]{result.stderr[:200]}[/dim]")
|
|
94
|
+
|
|
77
95
|
# ═══════════════════════════════════════════════════════════════════════════════
|
|
78
96
|
# KEYBOARD SHORTCUTS FOR PROMPT (Only custom ones)
|
|
79
97
|
# ═══════════════════════════════════════════════════════════════════════════════
|
|
@@ -102,6 +120,222 @@ def _(event):
|
|
|
102
120
|
b.delete(len(b.text) - b.cursor_position)
|
|
103
121
|
|
|
104
122
|
|
|
123
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
124
|
+
# VECTOR-BASED CODEBASE MEMORY (RAG)
|
|
125
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
126
|
+
|
|
127
|
+
class CodebaseMemory:
|
|
128
|
+
def __init__(self, workspace_dir: str):
|
|
129
|
+
self.workspace_dir = workspace_dir
|
|
130
|
+
self.db_path = os.path.join(workspace_dir, ".samcode", "vector_db")
|
|
131
|
+
os.makedirs(self.db_path, exist_ok=True)
|
|
132
|
+
|
|
133
|
+
# ✅ LAZY LOAD: Only import when actually needed
|
|
134
|
+
try:
|
|
135
|
+
import chromadb
|
|
136
|
+
from sentence_transformers import SentenceTransformer
|
|
137
|
+
self.client = chromadb.PersistentClient(path=self.db_path)
|
|
138
|
+
self.collection = self.client.get_or_create_collection(
|
|
139
|
+
name="codebase", metadata={"hnsw:space": "cosine"}
|
|
140
|
+
)
|
|
141
|
+
self.model = SentenceTransformer('all-MiniLM-L6-v2')
|
|
142
|
+
self._is_indexed = False
|
|
143
|
+
except Exception as e:
|
|
144
|
+
console.print(f"[yellow]⚠️ Vector memory unavailable: {str(e)[:80]}...[/yellow]")
|
|
145
|
+
console.print("[dim]Run: pip install sentence-transformers transformers 'numpy<2'[/dim]")
|
|
146
|
+
self._is_indexed = True # Prevents repeated crash attempts
|
|
147
|
+
|
|
148
|
+
def index_workspace(self, file_manager: 'FileSystemManager'):
|
|
149
|
+
"""Scan and embed all code files in the workspace."""
|
|
150
|
+
if self._is_indexed:
|
|
151
|
+
return
|
|
152
|
+
|
|
153
|
+
console.print("[cyan]🧠 Building vector memory index...[/cyan]")
|
|
154
|
+
files = file_manager.scan_workspace(max_files=500)
|
|
155
|
+
docs = []
|
|
156
|
+
ids = []
|
|
157
|
+
metadatas = []
|
|
158
|
+
|
|
159
|
+
code_extensions = {'.py', '.js', '.ts', '.jsx', '.tsx', '.java', '.go', '.rs', '.html', '.css', '.scss', '.sql', '.sh'}
|
|
160
|
+
|
|
161
|
+
for f in files:
|
|
162
|
+
ext = Path(f).suffix.lower()
|
|
163
|
+
if ext in code_extensions:
|
|
164
|
+
content = file_manager.read_file(f)
|
|
165
|
+
if content and len(content) < 10000: # Skip huge files
|
|
166
|
+
docs.append(f"File: {f}\nContent:\n{content}")
|
|
167
|
+
ids.append(f.replace(os.sep, "_"))
|
|
168
|
+
metadatas.append({"file": f, "ext": ext})
|
|
169
|
+
|
|
170
|
+
if docs:
|
|
171
|
+
embeddings = self.model.encode(docs, show_progress_bar=False).tolist()
|
|
172
|
+
self.collection.upsert(
|
|
173
|
+
documents=docs,
|
|
174
|
+
embeddings=embeddings,
|
|
175
|
+
ids=ids,
|
|
176
|
+
metadatas=metadatas
|
|
177
|
+
)
|
|
178
|
+
console.print(f"[green]✓ Indexed {len(docs)} files into vector memory.[/green]")
|
|
179
|
+
else:
|
|
180
|
+
console.print("[yellow]⚠️ No code files found to index.[/yellow]")
|
|
181
|
+
|
|
182
|
+
self._is_indexed = True
|
|
183
|
+
|
|
184
|
+
def search(self, query: str, n_results: int = 3) -> List[Dict]:
|
|
185
|
+
"""Find semantically relevant code snippets."""
|
|
186
|
+
query_embedding = self.model.encode([query]).tolist()
|
|
187
|
+
results = self.collection.query(
|
|
188
|
+
query_embeddings=query_embedding,
|
|
189
|
+
n_results=n_results
|
|
190
|
+
)
|
|
191
|
+
return [
|
|
192
|
+
{"file": m["file"], "snippet": d}
|
|
193
|
+
for m, d in zip(results["metadatas"][0], results["documents"][0])
|
|
194
|
+
]
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
198
|
+
# PROACTIVE CODE DOCTOR
|
|
199
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
200
|
+
|
|
201
|
+
class CodeDoctor:
|
|
202
|
+
"""Automatically analyzes code when user mentions bugs, optimization, or cleanup."""
|
|
203
|
+
|
|
204
|
+
INTENT_KEYWORDS = [
|
|
205
|
+
'fix', 'bug', 'error', 'issue', 'broken', 'not working',
|
|
206
|
+
'optimize', 'performance', 'slow', 'refactor', 'clean',
|
|
207
|
+
'unused', 'dead code', 'analyze', 'malfunction', 'improve',
|
|
208
|
+
'lint', 'format', 'style', 'best practice', 'debug'
|
|
209
|
+
]
|
|
210
|
+
|
|
211
|
+
def __init__(self, workspace_dir: str):
|
|
212
|
+
self.workspace_dir = workspace_dir
|
|
213
|
+
|
|
214
|
+
def should_analyze(self, user_prompt: str) -> bool:
|
|
215
|
+
"""Check if user's natural language implies code analysis is needed."""
|
|
216
|
+
prompt_lower = user_prompt.lower()
|
|
217
|
+
return any(keyword in prompt_lower for keyword in self.INTENT_KEYWORDS)
|
|
218
|
+
|
|
219
|
+
def get_relevant_files(self, user_prompt: str, file_manager: 'FileSystemManager', memory: Optional['CodebaseMemory'] = None) -> List[str]:
|
|
220
|
+
"""Determine which files to analyze based on user intent."""
|
|
221
|
+
# Try semantic search first if available
|
|
222
|
+
if memory:
|
|
223
|
+
results = memory.search(user_prompt, n_results=5)
|
|
224
|
+
if results:
|
|
225
|
+
return [r['file'] for r in results]
|
|
226
|
+
|
|
227
|
+
# Fallback: extract filenames mentioned in prompt
|
|
228
|
+
files = file_manager.scan_workspace(max_files=100)
|
|
229
|
+
mentioned = []
|
|
230
|
+
for f in files:
|
|
231
|
+
basename = Path(f).stem
|
|
232
|
+
if basename.lower() in user_prompt.lower() or f.lower() in user_prompt.lower():
|
|
233
|
+
mentioned.append(f)
|
|
234
|
+
|
|
235
|
+
# If no specific files mentioned, analyze key files
|
|
236
|
+
if not mentioned:
|
|
237
|
+
key_files = [f for f in files if any(ext in f for ext in ['.py', '.js', '.ts', '.jsx', '.tsx'])][:10]
|
|
238
|
+
return key_files
|
|
239
|
+
|
|
240
|
+
return mentioned[:10] # Limit to avoid overwhelming context
|
|
241
|
+
|
|
242
|
+
def run_analysis(self, files: List[str]) -> Dict[str, str]:
|
|
243
|
+
"""Run static analysis on specified files and return findings."""
|
|
244
|
+
findings = {}
|
|
245
|
+
|
|
246
|
+
for filepath in files:
|
|
247
|
+
ext = Path(filepath).suffix.lower()
|
|
248
|
+
full_path = os.path.join(self.workspace_dir, filepath)
|
|
249
|
+
|
|
250
|
+
if not os.path.exists(full_path):
|
|
251
|
+
continue
|
|
252
|
+
|
|
253
|
+
try:
|
|
254
|
+
if ext == '.py':
|
|
255
|
+
result = subprocess.run(
|
|
256
|
+
['ruff', 'check', '--output-format=json', full_path],
|
|
257
|
+
cwd=self.workspace_dir, capture_output=True, text=True, timeout=30
|
|
258
|
+
)
|
|
259
|
+
if result.stdout.strip():
|
|
260
|
+
issues = json.loads(result.stdout)
|
|
261
|
+
if issues:
|
|
262
|
+
summary = f"\n🐍 Python Issues in {filepath}:\n"
|
|
263
|
+
for issue in issues[:5]: # Limit per file
|
|
264
|
+
row = issue.get('location', {}).get('row', '?')
|
|
265
|
+
code = issue.get('code', '')
|
|
266
|
+
msg = issue.get('message', '')
|
|
267
|
+
summary += f" • Line {row}: [{code}] {msg}\n"
|
|
268
|
+
findings[filepath] = summary
|
|
269
|
+
|
|
270
|
+
except (subprocess.TimeoutExpired, FileNotFoundError, Exception):
|
|
271
|
+
# Silently skip if tool isn't installed or fails
|
|
272
|
+
pass
|
|
273
|
+
|
|
274
|
+
return findings
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
278
|
+
# UNIVERSAL DATA READER
|
|
279
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
280
|
+
|
|
281
|
+
class UniversalDataReader:
|
|
282
|
+
"""Reads datasets from any source: CSV, Excel, Parquet, SQL, NoSQL, or Cloud Warehouses."""
|
|
283
|
+
|
|
284
|
+
SUPPORTED_EXTENSIONS = {'.csv', '.xlsx', '.xls', '.parquet', '.feather', '.json', '.hdf5'}
|
|
285
|
+
|
|
286
|
+
@staticmethod
|
|
287
|
+
def read(source: str, **kwargs) -> Tuple[Any, str]:
|
|
288
|
+
import pandas as pd
|
|
289
|
+
|
|
290
|
+
# Handle Database / Data Warehouse Connections
|
|
291
|
+
if source.startswith(('postgresql://', 'mysql://', 'sqlite:///', 'mssql+pyodbc://')):
|
|
292
|
+
try:
|
|
293
|
+
from sqlalchemy import create_engine
|
|
294
|
+
engine = create_engine(source)
|
|
295
|
+
query = kwargs.get('query', 'SELECT * FROM information_schema.tables LIMIT 10')
|
|
296
|
+
df = pd.read_sql(query, engine)
|
|
297
|
+
meta = f"🗄️ Database Source: {source.split('@')[-1]}\nQuery executed successfully."
|
|
298
|
+
return df, meta
|
|
299
|
+
except Exception as e:
|
|
300
|
+
return None, f"[DB_ERROR] Failed to connect/query: {str(e)}"
|
|
301
|
+
|
|
302
|
+
# Handle Local Files
|
|
303
|
+
ext = Path(source).suffix.lower()
|
|
304
|
+
|
|
305
|
+
try:
|
|
306
|
+
if ext == '.csv':
|
|
307
|
+
df = pd.read_csv(source, **kwargs)
|
|
308
|
+
elif ext in ['.xlsx', '.xls']:
|
|
309
|
+
sheet = kwargs.get('sheet_name', 0)
|
|
310
|
+
df = pd.read_excel(source, sheet_name=sheet, **kwargs)
|
|
311
|
+
elif ext == '.parquet':
|
|
312
|
+
df = pd.read_parquet(source, **kwargs)
|
|
313
|
+
elif ext == '.feather':
|
|
314
|
+
df = pd.read_feather(source, **kwargs)
|
|
315
|
+
elif ext == '.json':
|
|
316
|
+
orient = kwargs.get('orient', 'columns')
|
|
317
|
+
df = pd.read_json(source, orient=orient, **kwargs)
|
|
318
|
+
elif ext == '.hdf5':
|
|
319
|
+
key = kwargs.get('key')
|
|
320
|
+
if not key: raise ValueError("HDF5 requires a 'key' parameter")
|
|
321
|
+
df = pd.read_hdf(source, key=key, **kwargs)
|
|
322
|
+
else:
|
|
323
|
+
return None, f"[UNSUPPORTED] Extension '{ext}' is not supported."
|
|
324
|
+
|
|
325
|
+
# Generate rich metadata for the LLM
|
|
326
|
+
meta = (
|
|
327
|
+
f"📊 Dataset: {Path(source).name}\n"
|
|
328
|
+
f"Rows: {len(df):,} | Columns: {len(df.columns)}\n"
|
|
329
|
+
f"Dtypes:\n{df.dtypes.to_dict()}\n"
|
|
330
|
+
f"Missing Values:\n{df.isnull().sum().to_dict()}\n"
|
|
331
|
+
f"First 3 Rows Preview:\n{df.head(3).to_markdown()}"
|
|
332
|
+
)
|
|
333
|
+
return df, meta
|
|
334
|
+
|
|
335
|
+
except Exception as e:
|
|
336
|
+
return None, f"[READ_ERROR] {str(e)}"
|
|
337
|
+
|
|
338
|
+
|
|
105
339
|
class DocumentReader:
|
|
106
340
|
@staticmethod
|
|
107
341
|
def extract_text(filepath: str) -> str:
|
|
@@ -522,11 +756,22 @@ class SamCodeCLI:
|
|
|
522
756
|
self.api_key = ""
|
|
523
757
|
self.custom_base_url = ""
|
|
524
758
|
self.caveman_mode = CavemanMode.OFF
|
|
759
|
+
self.frontend_mode = False
|
|
760
|
+
self.data_mode = False # NEW: Professional Data Analyst Mode
|
|
761
|
+
self.data_session = {"datasets": {}, "analysis_steps": [], "agreed_notebook": None} # NEW: Data Session State
|
|
525
762
|
self.session_context = []
|
|
526
763
|
self.file_manager = FileSystemManager(self.workspace_dir)
|
|
527
764
|
self.command_runner = CommandRunner(self.workspace_dir)
|
|
528
765
|
self.git_manager = GitManager(self.workspace_dir, console)
|
|
529
|
-
|
|
766
|
+
|
|
767
|
+
# Initialize Vector Memory
|
|
768
|
+
self.memory = CodebaseMemory(self.workspace_dir)
|
|
769
|
+
self.memory.index_workspace(self.file_manager)
|
|
770
|
+
|
|
771
|
+
# Initialize Proactive Code Doctor
|
|
772
|
+
self.code_doctor = CodeDoctor(self.workspace_dir)
|
|
773
|
+
|
|
774
|
+
self.command_completer = WordCompleter(['/connect', '/models', '/upload', '/clear-uploads', '/caveman', '/frontend', '/data', '/reindex', '/clear', '/exit', '/help', '/aboutme', '/searchweb', '/git'], sentence=True)
|
|
530
775
|
self.prompt_text = FormattedText([('ansicyan bold', '❯ ')])
|
|
531
776
|
self.load_configuration()
|
|
532
777
|
|
|
@@ -541,11 +786,19 @@ class SamCodeCLI:
|
|
|
541
786
|
self.custom_base_url = data.get("custom_base_url", "")
|
|
542
787
|
caveman_val = data.get("caveman_mode", 0)
|
|
543
788
|
self.caveman_mode = CavemanMode(caveman_val) if caveman_val in [0, 1, 2] else CavemanMode.OFF
|
|
789
|
+
self.frontend_mode = data.get("frontend_mode", False)
|
|
544
790
|
except: pass
|
|
545
791
|
|
|
546
792
|
def save_configuration(self):
|
|
547
793
|
with open(self.config_path, "w") as f:
|
|
548
|
-
json.dump({
|
|
794
|
+
json.dump({
|
|
795
|
+
"provider": self.active_provider,
|
|
796
|
+
"model": self.active_model,
|
|
797
|
+
"api_key": self.api_key,
|
|
798
|
+
"custom_base_url": self.custom_base_url,
|
|
799
|
+
"caveman_mode": self.caveman_mode.value,
|
|
800
|
+
"frontend_mode": self.frontend_mode
|
|
801
|
+
}, f, indent=2)
|
|
549
802
|
|
|
550
803
|
def get_client(self) -> Optional[AIModelClient]:
|
|
551
804
|
provider_config = self.provider_registry.get_provider(self.active_provider)
|
|
@@ -555,18 +808,31 @@ class SamCodeCLI:
|
|
|
555
808
|
|
|
556
809
|
def show_main_header(self):
|
|
557
810
|
print("\033[2J\033[H", end="")
|
|
811
|
+
|
|
558
812
|
header_table = Table(show_header=False, box=None, padding=(0, 1))
|
|
559
813
|
header_table.add_column(style="bold cyan", justify="left")
|
|
560
814
|
header_table.add_column(style="dim", justify="center")
|
|
561
815
|
header_table.add_column(style="bold green", justify="right")
|
|
816
|
+
|
|
562
817
|
caveman_indicator = f" | [red]🦴 {self.caveman_mode.name}[/red]" if self.caveman_mode != CavemanMode.OFF else ""
|
|
563
818
|
upload_indicator = f" | [magenta]📎 {len(self.session_context)} Docs[/magenta]" if self.session_context else ""
|
|
564
819
|
git_indicator = ""
|
|
565
820
|
if self.git_manager.is_repo():
|
|
566
821
|
branch = self.git_manager.get_current_branch()
|
|
567
822
|
git_indicator = f" | [yellow]🌿 {branch}[/yellow]"
|
|
568
|
-
|
|
569
|
-
|
|
823
|
+
frontend_indicator = " | [bold magenta]🎨 FRONTEND ARCHITECT[/bold magenta]" if self.frontend_mode else ""
|
|
824
|
+
data_indicator = " | [bold cyan]📊 DATA ANALYST[/bold cyan]" if self.data_mode else ""
|
|
825
|
+
|
|
826
|
+
header_table.add_row(
|
|
827
|
+
"⚡ SamCode CLI",
|
|
828
|
+
"Autonomous Coding Agent",
|
|
829
|
+
f"{self.active_provider} | {self.active_model[:15]}{caveman_indicator}{upload_indicator}{git_indicator}{frontend_indicator}{data_indicator}"
|
|
830
|
+
)
|
|
831
|
+
|
|
832
|
+
path_text = Text(f"📂 Workspace: {self.workspace_dir}", style="dim italic")
|
|
833
|
+
combined_content = Group(header_table, path_text)
|
|
834
|
+
|
|
835
|
+
console.print(Panel(combined_content, border_style="bright_black", padding=(0, 2)))
|
|
570
836
|
console.print("[dim]Type your request naturally, or use /help for commands.[/dim]\n")
|
|
571
837
|
|
|
572
838
|
def cmd_connect(self):
|
|
@@ -633,6 +899,62 @@ class SamCodeCLI:
|
|
|
633
899
|
elif self.caveman_mode == CavemanMode.ULTRA: console.print("[red]🔇 Caveman Mode ULTRA. Maximum token saving. Grunt-like brevity.[/red]")
|
|
634
900
|
self.show_main_header()
|
|
635
901
|
|
|
902
|
+
def cmd_frontend(self):
|
|
903
|
+
self.frontend_mode = not self.frontend_mode
|
|
904
|
+
self.save_configuration()
|
|
905
|
+
if self.frontend_mode:
|
|
906
|
+
msg = "[bold magenta]🎨 FRONTEND ARCHITECT MODE: ON[/bold magenta]\n"
|
|
907
|
+
msg += "[dim]AI will now act as a senior frontend developer with unique design systems, custom palettes, and performance-first architecture.[/dim]"
|
|
908
|
+
else:
|
|
909
|
+
msg = "[bold cyan]⚙️ FRONTEND ARCHITECT MODE: OFF[/bold cyan]\n"
|
|
910
|
+
msg += "[dim]Returning to standard coding assistant behavior.[/dim]"
|
|
911
|
+
console.print(f"\n{msg}\n")
|
|
912
|
+
self.show_main_header()
|
|
913
|
+
|
|
914
|
+
# NEW: Professional Data Analyst Mode Toggle
|
|
915
|
+
def cmd_data(self):
|
|
916
|
+
"""Toggle Professional Data Analyst & BI Mode."""
|
|
917
|
+
self.data_mode = not self.data_mode
|
|
918
|
+
|
|
919
|
+
if self.data_mode:
|
|
920
|
+
msg = "[bold cyan]📊 DATA ANALYST MODE: ON[/bold cyan]\n"
|
|
921
|
+
msg += "[dim]Agent is now a Senior BI Analyst. Ready to read datasets, generate insights, create charts, and build production-ready Jupyter Notebooks with markdown documentation.[/dim]"
|
|
922
|
+
console.print(f"\n{msg}\n")
|
|
923
|
+
else:
|
|
924
|
+
msg = "[bold green]✅ DATA ANALYST MODE: OFF[/bold green]\n"
|
|
925
|
+
msg += "[dim]Returning to standard coding assistant behavior.[/dim]"
|
|
926
|
+
# Save agreed notebook if exists when exiting mode
|
|
927
|
+
if self.data_session.get("agreed_notebook"):
|
|
928
|
+
self._save_notebook(self.data_session["agreed_notebook"])
|
|
929
|
+
console.print(f"\n{msg}\n")
|
|
930
|
+
|
|
931
|
+
self.show_main_header()
|
|
932
|
+
|
|
933
|
+
def _save_notebook(self, notebook_content: dict):
|
|
934
|
+
"""Saves the agreed-upon analysis as a .ipynb file with rich markdown cells."""
|
|
935
|
+
import nbformat
|
|
936
|
+
nb = nbformat.v4.new_notebook()
|
|
937
|
+
|
|
938
|
+
# Add title cell
|
|
939
|
+
nb.cells.append(nbformat.v4.new_markdown_cell("# 📊 Automated Data Analysis Report\n*Generated by SamCode CLI Data Analyst Mode*"))
|
|
940
|
+
|
|
941
|
+
# Add agreed steps as markdown + code cells
|
|
942
|
+
for step in self.data_session["analysis_steps"]:
|
|
943
|
+
nb.cells.append(nbformat.v4.new_markdown_cell(f"## {step['title']}\n{step['description']}"))
|
|
944
|
+
nb.cells.append(nbformat.v4.new_code_cell(step['code']))
|
|
945
|
+
|
|
946
|
+
filename = f"data_analysis_{int(time.time())}.ipynb"
|
|
947
|
+
filepath = os.path.join(self.workspace_dir, filename)
|
|
948
|
+
with open(filepath, 'w') as f:
|
|
949
|
+
nbformat.write(nb, f)
|
|
950
|
+
console.print(f"[green]✓ Saved professional notebook: {filename}[/green]")
|
|
951
|
+
|
|
952
|
+
def cmd_reindex(self):
|
|
953
|
+
console.print("[cyan]🔄 Re-indexing codebase memory...[/cyan]")
|
|
954
|
+
self.memory._is_indexed = False
|
|
955
|
+
self.memory.index_workspace(self.file_manager)
|
|
956
|
+
console.print("[green]✓ Re-indexing complete.[/green]\n")
|
|
957
|
+
|
|
636
958
|
def cmd_upload(self, filepath: str = ""):
|
|
637
959
|
if not filepath: filepath = Prompt.ask("[cyan]Enter file path to upload[/cyan]").strip()
|
|
638
960
|
if not filepath: return
|
|
@@ -731,7 +1053,6 @@ class SamCodeCLI:
|
|
|
731
1053
|
if not success: console.print(f"[red]✗ {msg}[/red]\n"); return
|
|
732
1054
|
console.print(f"[green]✓ {msg}[/green]\n")
|
|
733
1055
|
|
|
734
|
-
# --- AUTOMATIC GITIGNORE PROTECTION ---
|
|
735
1056
|
gitignore_path = os.path.join(self.workspace_dir, ".gitignore")
|
|
736
1057
|
ignore_entry = "\n.samcode/\n"
|
|
737
1058
|
|
|
@@ -747,7 +1068,6 @@ class SamCodeCLI:
|
|
|
747
1068
|
f.write("# Automatically generated by SamCode CLI to protect API keys\n")
|
|
748
1069
|
f.write(ignore_entry)
|
|
749
1070
|
console.print("[green]✓ Automatically created .gitignore to protect .samcode/ folder[/green]\n")
|
|
750
|
-
# ----------------------------------------
|
|
751
1071
|
|
|
752
1072
|
try:
|
|
753
1073
|
user_check = subprocess.run(["git", "config", "user.name"], cwd=self.workspace_dir, capture_output=True, text=True, timeout=5)
|
|
@@ -914,18 +1234,88 @@ class SamCodeCLI:
|
|
|
914
1234
|
console.print("[red]✗ Not logged into GitHub[/red]\n")
|
|
915
1235
|
console.print("[bold]To log in:[/bold]\n 1. Install GitHub CLI: https://cli.github.com/\n 2. Run: gh auth login\n 3. Follow the prompts\n")
|
|
916
1236
|
|
|
1237
|
+
def _detect_data_intent(self, prompt: str) -> bool:
|
|
1238
|
+
"""Returns True if the user's natural language implies data analysis."""
|
|
1239
|
+
data_keywords = [
|
|
1240
|
+
'analyze', 'plot', 'chart', 'graph', 'visualize', 'trend',
|
|
1241
|
+
'correlation', 'distribution', 'histogram', 'scatter', 'heatmap',
|
|
1242
|
+
'dataset', 'csv', 'excel', 'database', 'sql', 'bi ', 'dashboard',
|
|
1243
|
+
'kpi', 'metric', 'aggregate', 'groupby', 'pivot'
|
|
1244
|
+
]
|
|
1245
|
+
return any(kw in prompt.lower() for kw in data_keywords)
|
|
1246
|
+
|
|
917
1247
|
def cmd_agent_ask(self, question: str):
|
|
918
1248
|
client = self.get_client()
|
|
919
|
-
if not client or not self.api_key:
|
|
1249
|
+
if not client or not self.api_key:
|
|
1250
|
+
console.print("[red]Configure AI first with /connect[/red]")
|
|
1251
|
+
return
|
|
1252
|
+
|
|
1253
|
+
# NEW: Proactive Code Doctor - Auto-analyze when intent detected
|
|
1254
|
+
proactive_findings = ""
|
|
1255
|
+
if self.code_doctor.should_analyze(question):
|
|
1256
|
+
console.print("[cyan]🩺 Code Doctor: Analyzing project for issues...[/cyan]")
|
|
1257
|
+
relevant_files = self.code_doctor.get_relevant_files(
|
|
1258
|
+
question, self.file_manager,
|
|
1259
|
+
getattr(self, 'memory', None)
|
|
1260
|
+
)
|
|
1261
|
+
if relevant_files:
|
|
1262
|
+
findings = self.code_doctor.run_analysis(relevant_files)
|
|
1263
|
+
if findings:
|
|
1264
|
+
proactive_findings = "\n\n🩺 PROACTIVE CODE ANALYSIS FINDINGS:\n"
|
|
1265
|
+
for filepath, report in findings.items():
|
|
1266
|
+
proactive_findings += report
|
|
1267
|
+
console.print(f"[green]✓ Found issues in {len(findings)} file(s). Injecting into context.[/green]")
|
|
1268
|
+
else:
|
|
1269
|
+
console.print("[yellow]⚠️ No linting issues found. Proceeding normally.[/yellow]")
|
|
1270
|
+
else:
|
|
1271
|
+
console.print("[yellow]⚠️ Could not determine relevant files to analyze.[/yellow]")
|
|
1272
|
+
|
|
920
1273
|
files = self.file_manager.scan_workspace()
|
|
921
1274
|
workspace_tree = "\n".join(files) if files else "(Empty workspace)"
|
|
922
1275
|
context_str = ""
|
|
923
1276
|
if self.session_context:
|
|
924
1277
|
context_str = "\n\nUPLOADED DOCUMENTS CONTEXT (The user has uploaded these files, you can reference them directly):\n"
|
|
925
|
-
for doc in self.session_context:
|
|
1278
|
+
for doc in self.session_context:
|
|
1279
|
+
context_str += f"=== {doc['filename']} ===\n{doc['content']}\n\n"
|
|
1280
|
+
|
|
1281
|
+
# Append proactive findings to context
|
|
1282
|
+
if proactive_findings:
|
|
1283
|
+
context_str += proactive_findings
|
|
1284
|
+
|
|
1285
|
+
token_economy_rule = "\n9. TOKEN ECONOMY RULE: NEVER output actual source code, scripts, or configuration files in your conversational text. Only describe what you are doing (e.g., \"Reading main.py...\", \"Generating the API endpoint...\"). Actual code must ONLY be placed inside the [WRITE_FILE] tool block. This saves tokens and keeps the chat clean."
|
|
1286
|
+
|
|
1287
|
+
frontend_rules = ""
|
|
1288
|
+
if self.frontend_mode:
|
|
1289
|
+
frontend_rules = """
|
|
1290
|
+
10. 🎨 EXPERT FRONTEND DEVELOPER MODE IS ACTIVE: You are NOT an AI coding assistant. You are a Senior Frontend Architect & Design Systems Engineer.
|
|
1291
|
+
- NEVER use generic AI styling patterns (no default blue/gray palettes, no Inter/Roboto/Arial fonts, no basic Tailwind utility soup).
|
|
1292
|
+
- ALWAYS analyze the project's subject matter FIRST. Derive a completely unique, bespoke design system from scratch based on the brand identity, target audience, and emotional tone of the project.
|
|
1293
|
+
- Create custom CSS variables or theme tokens for colors, typography, spacing, and shadows. Define these BEFORE writing any component code.
|
|
1294
|
+
- Prioritize modern, performant web standards (CSS Container Queries, :has(), native nesting, view transitions API, subgrid). Avoid outdated patterns.
|
|
1295
|
+
- When generating UI, explain your DESIGN RATIONALE first (why this palette? why this type scale? why this layout pattern?), THEN provide the implementation.
|
|
1296
|
+
- Treat every project as a unique product requiring a tailored visual language. Your output should look like it was crafted by a human design engineer, not generated by an LLM.
|
|
1297
|
+
"""
|
|
1298
|
+
|
|
1299
|
+
# NEW: Hybrid Data Analyst Rules (Auto-detect + Explicit Mode)
|
|
1300
|
+
data_analyst_rules = ""
|
|
1301
|
+
is_data_task = self.data_mode or self._detect_data_intent(question)
|
|
1302
|
+
|
|
1303
|
+
if is_data_task:
|
|
1304
|
+
data_analyst_rules = """
|
|
1305
|
+
11. 📊 SENIOR DATA ANALYST & BI EXPERT MODE ACTIVE:
|
|
1306
|
+
- You are NOT just a coder. You are a Senior Business Intelligence Analyst.
|
|
1307
|
+
- ALWAYS start by reading the dataset using [RUN_TERMINAL: python -c "..."] or providing Python code to load it via UniversalDataReader.
|
|
1308
|
+
- Before writing ANY code, EXPLAIN your analytical approach: What KPIs matter? What business questions are we answering?
|
|
1309
|
+
- Generate publication-quality visualizations using seaborn/matplotlib. Always include titles, axis labels, and legends.
|
|
1310
|
+
- Provide actionable business insights, not just statistical observations. Connect data patterns to real-world business outcomes.
|
|
1311
|
+
- When the user agrees on an analysis workflow, PROACTIVELY offer to save it as a production-ready Jupyter Notebook (.ipynb) with detailed markdown cells explaining each step, methodology, and business context.
|
|
1312
|
+
- Support ALL data formats: CSV, Excel, Parquet, Feather, JSON, HDF5, PostgreSQL, MySQL, SQLite, MSSQL, and any SQLAlchemy-compatible data warehouse.
|
|
1313
|
+
"""
|
|
1314
|
+
|
|
926
1315
|
caveman_rules = ""
|
|
927
|
-
if self.caveman_mode == CavemanMode.BASIC: caveman_rules = "\
|
|
928
|
-
elif self.caveman_mode == CavemanMode.ULTRA: caveman_rules = "\
|
|
1316
|
+
if self.caveman_mode == CavemanMode.BASIC: caveman_rules = "\n12. CAVEMAN MODE (BASIC) IS ACTIVE: Be extremely concise. No pleasantries, no fluff. Use short sentences. Get straight to the point."
|
|
1317
|
+
elif self.caveman_mode == CavemanMode.ULTRA: caveman_rules = "\n12. CAVEMAN MODE (ULTRA) IS ACTIVE: MAXIMUM TOKEN SAVING. Output ONLY code or absolute minimum words. No explanations. No greetings. Grunt-like brevity."
|
|
1318
|
+
|
|
929
1319
|
system_msg = f"""You are SamCode CLI, an expert autonomous AI coding agent.
|
|
930
1320
|
You are currently working in the directory: {self.workspace_dir}
|
|
931
1321
|
|
|
@@ -939,16 +1329,19 @@ You have access to the following tools to help you complete tasks:
|
|
|
939
1329
|
[END_WRITE_FILE]
|
|
940
1330
|
[RUN_TERMINAL: <command>]
|
|
941
1331
|
[SEARCH_CODE: <query>]
|
|
1332
|
+
[SEARCH_SEMANTIC: <natural_language_query>]
|
|
942
1333
|
|
|
943
1334
|
CRITICAL RULES:
|
|
944
|
-
1. You have full access to the workspace. NEVER say you cannot access files.
|
|
1335
|
+
1. You have full access to the workspace. NEVER say you cannot access files or execute commands.
|
|
945
1336
|
2. DO NOT use tools unless the user explicitly asks you to modify, create, or analyze specific files/code, or if you absolutely need to read a file to answer a technical question.
|
|
946
1337
|
3. If the user greets you, asks a general question, or gives a simple instruction that doesn't require file access, respond directly with text ONLY.
|
|
947
1338
|
4. If you MUST use a tool, output the tool call clearly. You may include brief reasoning before the tool call, but ensure the tool syntax is exact.
|
|
948
1339
|
5. If you need to see a file's content, use [READ_FILE: <path>].
|
|
949
1340
|
6. If you need to create or modify a file, use [WRITE_FILE: <path>] followed by the COMPLETE file content and [END_WRITE_FILE].
|
|
950
|
-
7. If you need to run a terminal command, use [RUN_TERMINAL: <command>].
|
|
951
|
-
8.
|
|
1341
|
+
7. If you need to run a terminal command (like git push, npm install, python main.py), use [RUN_TERMINAL: <command>].
|
|
1342
|
+
8. If the user asks about functionality, architecture, concepts, or where something is implemented (e.g., "how does auth work?", "find the payment logic", "where is the database configured"), ALWAYS use [SEARCH_SEMANTIC: <descriptive_natural_query>] FIRST before reading any files. This finds relevant code even if variable names differ.
|
|
1343
|
+
9. Only use [READ_FILE] after [SEARCH_SEMANTIC] returns specific file paths.{token_economy_rule}{frontend_rules}{data_analyst_rules}{caveman_rules}"""
|
|
1344
|
+
|
|
952
1345
|
messages = [{"role": "system", "content": system_msg}, {"role": "user", "content": question}]
|
|
953
1346
|
max_iterations = 20
|
|
954
1347
|
console.print(f"\n[bold cyan]🤖 Agent Activated[/bold cyan]")
|
|
@@ -959,7 +1352,7 @@ CRITICAL RULES:
|
|
|
959
1352
|
response = client.chat(messages, stream=True)
|
|
960
1353
|
if not response or response.startswith("Error"): console.print(f"\n[red]{response}[/red]"); break
|
|
961
1354
|
write_match = re.search(r'\[WRITE_FILE:\s*(.*?)\](.*?)\[END_WRITE_FILE\]', response, re.DOTALL)
|
|
962
|
-
single_match = re.search(r'\[(READ_FILE|RUN_TERMINAL|SEARCH_CODE):\s*(.*?)\]', response)
|
|
1355
|
+
single_match = re.search(r'\[(READ_FILE|RUN_TERMINAL|SEARCH_CODE|SEARCH_SEMANTIC):\s*(.*?)\]', response)
|
|
963
1356
|
tool_executed = False
|
|
964
1357
|
if write_match:
|
|
965
1358
|
path = write_match.group(1).strip(); content = write_match.group(2).strip()
|
|
@@ -983,7 +1376,21 @@ CRITICAL RULES:
|
|
|
983
1376
|
tool_executed = True
|
|
984
1377
|
elif single_match:
|
|
985
1378
|
tool_name = single_match.group(1); tool_arg = single_match.group(2).strip()
|
|
986
|
-
|
|
1379
|
+
|
|
1380
|
+
if tool_name == "SEARCH_SEMANTIC":
|
|
1381
|
+
results = self.memory.search(tool_arg)
|
|
1382
|
+
if not results:
|
|
1383
|
+
tool_result = "No semantically relevant code found in the codebase."
|
|
1384
|
+
else:
|
|
1385
|
+
formatted = []
|
|
1386
|
+
for r in results:
|
|
1387
|
+
snippet_preview = r['snippet'][:300].replace('\n', ' ') + "..." if len(r['snippet']) > 300 else r['snippet'].replace('\n', ' ')
|
|
1388
|
+
formatted.append(f"📄 {r['file']}:\n{snippet_preview}")
|
|
1389
|
+
tool_result = "\n\n---\n\n".join(formatted)
|
|
1390
|
+
console.print(f"\n[blue]🧠 Semantic Search: '{tool_arg}'[/blue]")
|
|
1391
|
+
tool_executed = True
|
|
1392
|
+
|
|
1393
|
+
elif tool_name == "READ_FILE":
|
|
987
1394
|
content = self.file_manager.read_file(tool_arg)
|
|
988
1395
|
tool_result = f"Content of {tool_arg}:\n{content}" if content else f"Error: File '{tool_arg}' not found."
|
|
989
1396
|
console.print(f"\n[blue]📖 Read file: {tool_arg}[/blue]"); tool_executed = True
|
|
@@ -1010,7 +1417,21 @@ CRITICAL RULES:
|
|
|
1010
1417
|
|
|
1011
1418
|
def cmd_help(self):
|
|
1012
1419
|
console.print("\n[bold cyan]📚 SamCode CLI Commands[/bold cyan]\n")
|
|
1013
|
-
commands = {
|
|
1420
|
+
commands = {
|
|
1421
|
+
"/connect": "Configure AI provider and API key",
|
|
1422
|
+
"/models": "Select AI model dynamically",
|
|
1423
|
+
"/upload <path>": "Upload & extract documents (PDF, DOCX, XLSX, PPTX, Images)",
|
|
1424
|
+
"/clear-uploads": "Clear uploaded documents from session context",
|
|
1425
|
+
"/searchweb <query>": "Search the web (opens browser) & get AI-synthesized answer",
|
|
1426
|
+
"/git": "Native Git operations (commit, push, pull, branch, etc.)",
|
|
1427
|
+
"/caveman": "Cycle token-saving modes (OFF ➔ BASIC ➔ ULTRA)",
|
|
1428
|
+
"/frontend": "Toggle expert frontend architect mode (unique design systems)",
|
|
1429
|
+
"/data": "Toggle professional data analyst & BI mode (notebooks, charts, SQL)",
|
|
1430
|
+
"/reindex": "Re-build vector memory index after code changes",
|
|
1431
|
+
"/aboutme": "About the developer and SamCode",
|
|
1432
|
+
"/clear": "Clear the screen",
|
|
1433
|
+
"/exit": "Exit SamCode CLI"
|
|
1434
|
+
}
|
|
1014
1435
|
for cmd, desc in commands.items(): console.print(f" [cyan]{cmd:<20}[/cyan] {desc}")
|
|
1015
1436
|
console.print("\n[dim]Just type your request naturally to activate the autonomous agent![/dim]\n")
|
|
1016
1437
|
|
|
@@ -1022,7 +1443,6 @@ CRITICAL RULES:
|
|
|
1022
1443
|
self.show_main_header()
|
|
1023
1444
|
while True:
|
|
1024
1445
|
try:
|
|
1025
|
-
# FIXED: Using editing_mode=EditingMode.EMACS for standard shortcuts + custom kb
|
|
1026
1446
|
user_input = pt_prompt(
|
|
1027
1447
|
self.prompt_text,
|
|
1028
1448
|
completer=self.command_completer,
|
|
@@ -1037,6 +1457,9 @@ CRITICAL RULES:
|
|
|
1037
1457
|
elif user_input.lower() in ["/connect", "/config"]: self.cmd_connect(); self.show_main_header()
|
|
1038
1458
|
elif user_input.lower() in ["/models", "/model"]: self.cmd_models()
|
|
1039
1459
|
elif user_input.lower() in ["/caveman"]: self.cmd_caveman()
|
|
1460
|
+
elif user_input.lower() in ["/frontend"]: self.cmd_frontend()
|
|
1461
|
+
elif user_input.lower() in ["/data"]: self.cmd_data() # NEW: Handle data mode toggle
|
|
1462
|
+
elif user_input.lower() in ["/reindex"]: self.cmd_reindex()
|
|
1040
1463
|
elif user_input.lower() in ["/aboutme", "/about"]: self.cmd_aboutme()
|
|
1041
1464
|
elif user_input.lower() in ["/git", "/g"]: self.cmd_git()
|
|
1042
1465
|
elif user_input.lower().startswith("/searchweb"):
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
samcode.py,sha256=y0-sa4PWldRhAwW8Wdtqd_WSmKXWuXVVl7hntZhb8Go,92067
|
|
2
|
+
samcode_cli-1.0.4.dist-info/METADATA,sha256=5rW2bzQ1dmZ7-vNWiG1ZJRIRle3BhKcbJjSLIgD-a0I,6269
|
|
3
|
+
samcode_cli-1.0.4.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
|
|
4
|
+
samcode_cli-1.0.4.dist-info/entry_points.txt,sha256=pTSNSMG9LYqLdbOR0TZGIQS0I17ZEUdXLEapLCYmyqo,41
|
|
5
|
+
samcode_cli-1.0.4.dist-info/top_level.txt,sha256=ie3RFdU_m6daHft-jFl_UKNkKAA25CItx4-gyRRHbJY,8
|
|
6
|
+
samcode_cli-1.0.4.dist-info/RECORD,,
|
|
@@ -1,6 +0,0 @@
|
|
|
1
|
-
samcode.py,sha256=1CjVOavyAwvIHXwlDBfKsbJr4bWI2c_BYdI4TMU702k,70036
|
|
2
|
-
samcode_cli-1.0.3.dist-info/METADATA,sha256=nNxnlmmVbKo-_JMukTvomKxidoBQKll6iQmTlsApVeg,6269
|
|
3
|
-
samcode_cli-1.0.3.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
|
|
4
|
-
samcode_cli-1.0.3.dist-info/entry_points.txt,sha256=pTSNSMG9LYqLdbOR0TZGIQS0I17ZEUdXLEapLCYmyqo,41
|
|
5
|
-
samcode_cli-1.0.3.dist-info/top_level.txt,sha256=ie3RFdU_m6daHft-jFl_UKNkKAA25CItx4-gyRRHbJY,8
|
|
6
|
-
samcode_cli-1.0.3.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|