samcode-cli 1.0.2__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 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 v5.4
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
- self.command_completer = WordCompleter(['/connect', '/models', '/upload', '/clear-uploads', '/caveman', '/clear', '/exit', '/help', '/aboutme', '/searchweb', '/git'], sentence=True)
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({"provider": self.active_provider, "model": self.active_model, "api_key": self.api_key, "custom_base_url": self.custom_base_url, "caveman_mode": self.caveman_mode.value}, f, indent=2)
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
- header_table.add_row("⚡ SamCode CLI", "Autonomous Coding Agent", f"{self.active_provider} | {self.active_model[:20]}{caveman_indicator}{upload_indicator}{git_indicator}")
569
- console.print(Panel(header_table, border_style="bright_black", padding=(0, 1)))
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
@@ -726,10 +1048,27 @@ class SamCodeCLI:
726
1048
  self.show_main_header()
727
1049
 
728
1050
  def _initialize_git_repo(self):
729
- console.print("\n[bold cyan]🚀 Initialize Git Repository[/bold cyan]\n")
1051
+ console.print("\n[bold cyan] Initialize Git Repository[/bold cyan]\n")
730
1052
  success, msg = self.git_manager.init_repo()
731
1053
  if not success: console.print(f"[red]✗ {msg}[/red]\n"); return
732
1054
  console.print(f"[green]✓ {msg}[/green]\n")
1055
+
1056
+ gitignore_path = os.path.join(self.workspace_dir, ".gitignore")
1057
+ ignore_entry = "\n.samcode/\n"
1058
+
1059
+ if os.path.exists(gitignore_path):
1060
+ with open(gitignore_path, "r") as f:
1061
+ content = f.read()
1062
+ if ".samcode/" not in content:
1063
+ with open(gitignore_path, "a") as f:
1064
+ f.write(ignore_entry)
1065
+ console.print("[green]✓ Automatically added .samcode/ to existing .gitignore[/green]\n")
1066
+ else:
1067
+ with open(gitignore_path, "w") as f:
1068
+ f.write("# Automatically generated by SamCode CLI to protect API keys\n")
1069
+ f.write(ignore_entry)
1070
+ console.print("[green]✓ Automatically created .gitignore to protect .samcode/ folder[/green]\n")
1071
+
733
1072
  try:
734
1073
  user_check = subprocess.run(["git", "config", "user.name"], cwd=self.workspace_dir, capture_output=True, text=True, timeout=5)
735
1074
  if not user_check.stdout.strip():
@@ -895,18 +1234,88 @@ class SamCodeCLI:
895
1234
  console.print("[red]✗ Not logged into GitHub[/red]\n")
896
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")
897
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
+
898
1247
  def cmd_agent_ask(self, question: str):
899
1248
  client = self.get_client()
900
- if not client or not self.api_key: console.print("[red]Configure AI first with /connect[/red]"); return
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
+
901
1273
  files = self.file_manager.scan_workspace()
902
1274
  workspace_tree = "\n".join(files) if files else "(Empty workspace)"
903
1275
  context_str = ""
904
1276
  if self.session_context:
905
1277
  context_str = "\n\nUPLOADED DOCUMENTS CONTEXT (The user has uploaded these files, you can reference them directly):\n"
906
- for doc in self.session_context: context_str += f"=== {doc['filename']} ===\n{doc['content']}\n\n"
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
+
907
1315
  caveman_rules = ""
908
- if self.caveman_mode == CavemanMode.BASIC: caveman_rules = "\n9. CAVEMAN MODE (BASIC) IS ACTIVE: Be extremely concise. No pleasantries, no fluff. Use short sentences. Get straight to the point."
909
- elif self.caveman_mode == CavemanMode.ULTRA: caveman_rules = "\n9. CAVEMAN MODE (ULTRA) IS ACTIVE: MAXIMUM TOKEN SAVING. Output ONLY code or absolute minimum words. No explanations. No greetings. Grunt-like brevity."
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
+
910
1319
  system_msg = f"""You are SamCode CLI, an expert autonomous AI coding agent.
911
1320
  You are currently working in the directory: {self.workspace_dir}
912
1321
 
@@ -920,16 +1329,19 @@ You have access to the following tools to help you complete tasks:
920
1329
  [END_WRITE_FILE]
921
1330
  [RUN_TERMINAL: <command>]
922
1331
  [SEARCH_CODE: <query>]
1332
+ [SEARCH_SEMANTIC: <natural_language_query>]
923
1333
 
924
1334
  CRITICAL RULES:
925
- 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.
926
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.
927
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.
928
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.
929
1339
  5. If you need to see a file's content, use [READ_FILE: <path>].
930
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].
931
- 7. If you need to run a terminal command, use [RUN_TERMINAL: <command>].
932
- 8. Once you have completed the task, provide your final answer to the user WITHOUT using any tools.{caveman_rules}"""
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
+
933
1345
  messages = [{"role": "system", "content": system_msg}, {"role": "user", "content": question}]
934
1346
  max_iterations = 20
935
1347
  console.print(f"\n[bold cyan]🤖 Agent Activated[/bold cyan]")
@@ -940,7 +1352,7 @@ CRITICAL RULES:
940
1352
  response = client.chat(messages, stream=True)
941
1353
  if not response or response.startswith("Error"): console.print(f"\n[red]{response}[/red]"); break
942
1354
  write_match = re.search(r'\[WRITE_FILE:\s*(.*?)\](.*?)\[END_WRITE_FILE\]', response, re.DOTALL)
943
- 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)
944
1356
  tool_executed = False
945
1357
  if write_match:
946
1358
  path = write_match.group(1).strip(); content = write_match.group(2).strip()
@@ -964,7 +1376,21 @@ CRITICAL RULES:
964
1376
  tool_executed = True
965
1377
  elif single_match:
966
1378
  tool_name = single_match.group(1); tool_arg = single_match.group(2).strip()
967
- if tool_name == "READ_FILE":
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":
968
1394
  content = self.file_manager.read_file(tool_arg)
969
1395
  tool_result = f"Content of {tool_arg}:\n{content}" if content else f"Error: File '{tool_arg}' not found."
970
1396
  console.print(f"\n[blue]📖 Read file: {tool_arg}[/blue]"); tool_executed = True
@@ -991,7 +1417,21 @@ CRITICAL RULES:
991
1417
 
992
1418
  def cmd_help(self):
993
1419
  console.print("\n[bold cyan]📚 SamCode CLI Commands[/bold cyan]\n")
994
- commands = {"/connect": "Configure AI provider and API key", "/models": "Select AI model dynamically", "/upload <path>": "Upload & extract documents (PDF, DOCX, XLSX, PPTX, Images)", "/clear-uploads": "Clear uploaded documents from session context", "/searchweb <query>": "Search the web (opens browser) & get AI-synthesized answer", "/git": "Native Git operations (commit, push, pull, branch, etc.)", "/caveman": "Cycle token-saving modes (OFF ➔ BASIC ➔ ULTRA)", "/aboutme": "About the developer and SamCode", "/clear": "Clear the screen", "/exit": "Exit SamCode CLI"}
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
+ }
995
1435
  for cmd, desc in commands.items(): console.print(f" [cyan]{cmd:<20}[/cyan] {desc}")
996
1436
  console.print("\n[dim]Just type your request naturally to activate the autonomous agent![/dim]\n")
997
1437
 
@@ -1003,7 +1443,6 @@ CRITICAL RULES:
1003
1443
  self.show_main_header()
1004
1444
  while True:
1005
1445
  try:
1006
- # FIXED: Using editing_mode=EditingMode.EMACS for standard shortcuts + custom kb
1007
1446
  user_input = pt_prompt(
1008
1447
  self.prompt_text,
1009
1448
  completer=self.command_completer,
@@ -1018,6 +1457,9 @@ CRITICAL RULES:
1018
1457
  elif user_input.lower() in ["/connect", "/config"]: self.cmd_connect(); self.show_main_header()
1019
1458
  elif user_input.lower() in ["/models", "/model"]: self.cmd_models()
1020
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()
1021
1463
  elif user_input.lower() in ["/aboutme", "/about"]: self.cmd_aboutme()
1022
1464
  elif user_input.lower() in ["/git", "/g"]: self.cmd_git()
1023
1465
  elif user_input.lower().startswith("/searchweb"):
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: samcode-cli
3
- Version: 1.0.2
3
+ Version: 1.0.4
4
4
  Summary: An autonomous AI coding agent that runs in your terminal.
5
5
  Author: Magra Houssem Eddine
6
6
  Description-Content-Type: text/markdown
@@ -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=gaWxvW_FSYVkB3uOcIRdi09iWYxZnH1cG6N7v5DagQg,69136
2
- samcode_cli-1.0.2.dist-info/METADATA,sha256=R9B67J-JjuG_LsoqzA8ssWkGzQToyjgPaBEeclQpMhw,6269
3
- samcode_cli-1.0.2.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
4
- samcode_cli-1.0.2.dist-info/entry_points.txt,sha256=pTSNSMG9LYqLdbOR0TZGIQS0I17ZEUdXLEapLCYmyqo,41
5
- samcode_cli-1.0.2.dist-info/top_level.txt,sha256=ie3RFdU_m6daHft-jFl_UKNkKAA25CItx4-gyRRHbJY,8
6
- samcode_cli-1.0.2.dist-info/RECORD,,