samcode-cli 1.0.3__py3-none-any.whl → 1.0.5__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,9 +2,8 @@
2
2
  """
3
3
  ╔══════════════════════════════════════════════════════════════════════════════╗
4
4
  ║ S A M C O D E C L I ║
5
- ║ Autonomous Coding Agent v5.4
6
- ║ Similar to Claude Code & GitHub Copilot Workspace ║
7
- ╚══════════════════════════════════════════════════════════════════════════════╝
5
+ ║ Autonomous Coding Agent v6.5 (Full Integration)
6
+ ══════════════════════════════════════════════════════════════════════════════╝
8
7
  """
9
8
 
10
9
  import os
@@ -15,13 +14,18 @@ import re
15
14
  import time
16
15
  import urllib.parse
17
16
  import webbrowser
17
+ import warnings
18
18
  from typing import List, Dict, Tuple, Optional, Any
19
19
  from pathlib import Path
20
20
  from dataclasses import dataclass
21
21
  from enum import Enum
22
22
 
23
- # Rich UI Components
24
- from rich.console import Console
23
+ # Suppress warnings
24
+ warnings.filterwarnings("ignore", category=DeprecationWarning)
25
+ warnings.filterwarnings("ignore", message=".*urllib3.*")
26
+ warnings.filterwarnings("ignore", message=".*NumPy.*")
27
+
28
+ from rich.console import Console, Group
25
29
  from rich.panel import Panel
26
30
  from rich.syntax import Syntax
27
31
  from rich.table import Table
@@ -29,7 +33,6 @@ from rich.prompt import Prompt, Confirm
29
33
  from rich.columns import Columns
30
34
  from rich.text import Text
31
35
 
32
- # Interactive Selection & Autocomplete
33
36
  try:
34
37
  import questionary
35
38
  from questionary import Choice
@@ -48,25 +51,31 @@ except ImportError:
48
51
  from prompt_toolkit.styles import Style
49
52
  from prompt_toolkit.enums import EditingMode
50
53
 
51
- # HTTP Client
52
54
  try:
53
55
  import requests
54
56
  except ImportError:
55
57
  subprocess.run([sys.executable, "-m", "pip", "install", "requests", "-q"], capture_output=True)
56
58
  import requests
57
59
 
58
- # Document Processing & Web Parsing Libraries Auto-Install
60
+ # FIX: Define console BEFORE the auto-installer loop
61
+ console = Console()
62
+
59
63
  LIBS_TO_INSTALL = {
60
64
  'pdfplumber': 'pdfplumber', 'docx': 'python-docx', 'openpyxl': 'openpyxl',
61
- 'pptx': 'python-pptx', 'PIL': 'Pillow', 'bs4': 'beautifulsoup4'
65
+ 'pptx': 'python-pptx', 'PIL': 'Pillow', 'bs4': 'beautifulsoup4',
66
+ 'chromadb': 'chromadb', 'sentence_transformers': 'sentence-transformers',
67
+ 'ruff': 'ruff', 'pandas': 'pandas',
68
+ 'matplotlib': 'matplotlib', 'seaborn': 'seaborn', 'nbformat': 'nbformat', 'sqlalchemy': 'sqlalchemy',
69
+ 'pyarrow': 'pyarrow', 'psycopg2_binary': 'psycopg2-binary', 'pymysql': 'pymysql',
70
+ 'nbconvert': 'nbconvert'
62
71
  }
72
+
63
73
  for module, package in LIBS_TO_INSTALL.items():
64
74
  try:
65
75
  __import__(module)
66
76
  except ImportError:
67
- subprocess.run([sys.executable, "-m", "pip", "install", package, "-q"], capture_output=True)
68
-
69
- console = Console()
77
+ console.print(f"[cyan]Installing {package}...[/cyan]")
78
+ subprocess.run([sys.executable, "-m", "pip", "install", "--no-cache-dir", package], capture_output=True)
70
79
 
71
80
  menu_style = Style.from_dict({
72
81
  'completion-menu': 'bg:#1e1e1e',
@@ -74,34 +83,134 @@ menu_style = Style.from_dict({
74
83
  'completion-menu.completion.current': 'fg:#ffffff bg:#00af00 bold',
75
84
  })
76
85
 
77
- # ═══════════════════════════════════════════════════════════════════════════════
78
- # KEYBOARD SHORTCUTS FOR PROMPT (Only custom ones)
79
- # ═══════════════════════════════════════════════════════════════════════════════
80
-
81
86
  from prompt_toolkit.key_binding import KeyBindings
82
-
83
87
  kb = KeyBindings()
84
88
 
85
89
  @kb.add('c-w')
86
90
  def _(event):
87
- "Delete word backward (Ctrl+W)"
88
91
  b = event.app.current_buffer
89
92
  pos = b.document.find_start_of_previous_word()
90
- if pos is not None:
91
- b.delete_before_cursor(-pos)
93
+ if pos is not None: b.delete_before_cursor(-pos)
92
94
 
93
95
  @kb.add('c-u')
94
96
  def _(event):
95
- "Clear line backward (Ctrl+U)"
96
97
  event.app.current_buffer.delete_before_cursor(event.app.current_buffer.cursor_position)
97
98
 
98
99
  @kb.add('c-k')
99
100
  def _(event):
100
- "Delete to end of line (Ctrl+K)"
101
101
  b = event.app.current_buffer
102
102
  b.delete(len(b.text) - b.cursor_position)
103
103
 
104
104
 
105
+ class CodebaseMemory:
106
+ def __init__(self, workspace_dir: str):
107
+ self.workspace_dir = workspace_dir
108
+ self.db_path = os.path.join(workspace_dir, ".samcode", "vector_db")
109
+ os.makedirs(self.db_path, exist_ok=True)
110
+ self._is_indexed = False
111
+ self.model = None
112
+ self.collection = None
113
+
114
+ try:
115
+ import chromadb
116
+ from sentence_transformers import SentenceTransformer
117
+ self.client = chromadb.PersistentClient(path=self.db_path)
118
+ self.collection = self.client.get_or_create_collection(name="codebase", metadata={"hnsw:space": "cosine"})
119
+ self.model = SentenceTransformer('all-MiniLM-L6-v2')
120
+ except Exception as e:
121
+ console.print(f"[yellow]⚠️ Vector memory unavailable. Run: pip install 'numpy<2' torch transformers sentence-transformers[/yellow]")
122
+
123
+ def index_workspace(self, file_manager):
124
+ if self._is_indexed or not self.model: return
125
+ console.print("[cyan]🧠 Building vector memory index...[/cyan]")
126
+ files = file_manager.scan_workspace(max_files=500)
127
+ docs, ids, metadatas = [], [], []
128
+ code_extensions = {'.py', '.js', '.ts', '.jsx', '.tsx', '.java', '.go', '.rs', '.html', '.css', '.scss', '.sql', '.sh'}
129
+
130
+ for f in files:
131
+ if Path(f).suffix.lower() in code_extensions:
132
+ content = file_manager.read_file(f)
133
+ if content and len(content) < 10000:
134
+ docs.append(f"File: {f}\nContent:\n{content}")
135
+ ids.append(f.replace(os.sep, "_"))
136
+ metadatas.append({"file": f, "ext": Path(f).suffix.lower()})
137
+
138
+ if docs:
139
+ embeddings = self.model.encode(docs, show_progress_bar=False).tolist()
140
+ self.collection.upsert(documents=docs, embeddings=embeddings, ids=ids, metadatas=metadatas)
141
+ console.print(f"[green]✓ Indexed {len(docs)} files into vector memory.[/green]")
142
+ self._is_indexed = True
143
+
144
+ def search(self, query: str, n_results: int = 3) -> List[Dict]:
145
+ if not self.model: return []
146
+ query_embedding = self.model.encode([query]).tolist()
147
+ results = self.collection.query(query_embeddings=query_embedding, n_results=n_results)
148
+ return [{"file": m["file"], "snippet": d} for m, d in zip(results["metadatas"][0], results["documents"][0])]
149
+
150
+
151
+ class CodeDoctor:
152
+ INTENT_KEYWORDS = ['fix', 'bug', 'error', 'issue', 'broken', 'not working', 'optimize', 'performance', 'slow', 'refactor', 'clean', 'unused', 'dead code', 'analyze', 'malfunction', 'improve', 'lint', 'format', 'style', 'best practice', 'debug']
153
+
154
+ def __init__(self, workspace_dir: str): self.workspace_dir = workspace_dir
155
+ def should_analyze(self, user_prompt: str) -> bool:
156
+ return any(kw in user_prompt.lower() for kw in self.INTENT_KEYWORDS)
157
+
158
+ def get_relevant_files(self, user_prompt: str, file_manager, memory=None) -> List[str]:
159
+ if memory and memory.model:
160
+ results = memory.search(user_prompt, n_results=5)
161
+ if results: return [r['file'] for r in results]
162
+ files = file_manager.scan_workspace(max_files=100)
163
+ mentioned = [f for f in files if Path(f).stem.lower() in user_prompt.lower() or f.lower() in user_prompt.lower()]
164
+ if not mentioned: return [f for f in files if any(ext in f for ext in ['.py', '.js', '.ts', '.jsx', '.tsx'])][:10]
165
+ return mentioned[:10]
166
+
167
+ def run_analysis(self, files: List[str]) -> Dict[str, str]:
168
+ findings = {}
169
+ for filepath in files:
170
+ ext = Path(filepath).suffix.lower()
171
+ full_path = os.path.join(self.workspace_dir, filepath)
172
+ if not os.path.exists(full_path): continue
173
+ try:
174
+ if ext == '.py':
175
+ result = subprocess.run(['ruff', 'check', '--output-format=json', full_path], cwd=self.workspace_dir, capture_output=True, text=True, timeout=30)
176
+ if result.stdout.strip():
177
+ issues = json.loads(result.stdout)
178
+ if issues:
179
+ summary = f"\n🐍 Python Issues in {filepath}:\n"
180
+ for issue in issues[:5]:
181
+ summary += f" • Line {issue.get('location', {}).get('row', '?')}: [{issue.get('code', '')}] {issue.get('message', '')}\n"
182
+ findings[filepath] = summary
183
+ except: pass
184
+ return findings
185
+
186
+
187
+ class UniversalDataReader:
188
+ @staticmethod
189
+ def read(source: str, **kwargs) -> Tuple[Any, str]:
190
+ import pandas as pd
191
+ if source.startswith(('postgresql://', 'mysql://', 'sqlite:///', 'mssql+pyodbc://')):
192
+ try:
193
+ from sqlalchemy import create_engine
194
+ engine = create_engine(source)
195
+ query = kwargs.get('query', 'SELECT * FROM information_schema.tables LIMIT 10')
196
+ df = pd.read_sql(query, engine)
197
+ return df, f"🗄️ Database Source: {source.split('@')[-1]}\nQuery executed successfully."
198
+ except Exception as e: return None, f"[DB_ERROR] {str(e)}"
199
+
200
+ ext = Path(source).suffix.lower()
201
+ try:
202
+ if ext == '.csv': df = pd.read_csv(source, **kwargs)
203
+ elif ext in ['.xlsx', '.xls']: df = pd.read_excel(source, sheet_name=kwargs.get('sheet_name', 0), **kwargs)
204
+ elif ext == '.parquet': df = pd.read_parquet(source, **kwargs)
205
+ elif ext == '.feather': df = pd.read_feather(source, **kwargs)
206
+ elif ext == '.json': df = pd.read_json(source, orient=kwargs.get('orient', 'columns'), **kwargs)
207
+ elif ext == '.hdf5': df = pd.read_hdf(source, key=kwargs.get('key'), **kwargs)
208
+ else: return None, f"[UNSUPPORTED] Extension '{ext}' is not supported."
209
+ meta = f"📊 Dataset: {Path(source).name}\nRows: {len(df):,} | Columns: {len(df.columns)}\nDtypes:\n{df.dtypes.to_dict()}\nMissing Values:\n{df.isnull().sum().to_dict()}\nFirst 3 Rows Preview:\n{df.head(3).to_markdown()}"
210
+ return df, meta
211
+ except Exception as e: return None, f"[READ_ERROR] {str(e)}"
212
+
213
+
105
214
  class DocumentReader:
106
215
  @staticmethod
107
216
  def extract_text(filepath: str) -> str:
@@ -109,88 +218,40 @@ class DocumentReader:
109
218
  try:
110
219
  if ext == '.pdf':
111
220
  import pdfplumber
112
- text = []
113
- with pdfplumber.open(filepath) as pdf:
114
- for page in pdf.pages:
115
- page_text = page.extract_text()
116
- if page_text: text.append(page_text)
117
- return "\n".join(text) if text else "[No extractable text found in PDF]"
221
+ return "\n".join([p.extract_text() or "" for p in pdfplumber.open(filepath).pages])
118
222
  elif ext == '.docx':
119
223
  import docx
120
- doc = docx.Document(filepath)
121
- paragraphs = [p.text for p in doc.paragraphs if p.text.strip()]
122
- for table in doc.tables:
123
- for row in table.rows:
124
- row_text = " | ".join([cell.text.strip() for cell in row.cells])
125
- paragraphs.append(row_text)
126
- return "\n".join(paragraphs)
224
+ return "\n".join([p.text for p in docx.Document(filepath).paragraphs if p.text.strip()])
127
225
  elif ext in ['.xlsx', '.xls']:
128
226
  import openpyxl
129
227
  wb = openpyxl.load_workbook(filepath, data_only=True)
130
- text = []
131
- for sheet in wb.sheetnames:
132
- text.append(f"\n--- Sheet: {sheet} ---")
133
- for row in wb[sheet].iter_rows(values_only=True):
134
- text.append(" | ".join([str(cell) if cell is not None else "" for cell in row]))
135
- return "\n".join(text)
228
+ return "\n".join([f"\n--- Sheet: {s} ---\n" + "\n".join([" | ".join([str(c) if c else "" for c in r]) for r in wb[s].iter_rows(values_only=True)]) for s in wb.sheetnames])
136
229
  elif ext == '.pptx':
137
230
  import pptx
138
- prs = pptx.Presentation(filepath)
139
- text = []
140
- for i, slide in enumerate(prs.slides):
141
- text.append(f"\n--- Slide {i+1} ---")
142
- for shape in slide.shapes:
143
- if hasattr(shape, "text") and shape.text.strip():
144
- text.append(shape.text)
145
- return "\n".join(text)
231
+ return "\n".join([f"\n--- Slide {i+1} ---\n" + "\n".join([sh.text for sh in sl.shapes if hasattr(sh, "text") and sh.text.strip()]) for i, sl in enumerate(pptx.Presentation(filepath).slides)])
146
232
  elif ext in ['.jpg', '.jpeg', '.png', '.gif', '.bmp', '.webp', '.tiff']:
147
233
  from PIL import Image
148
234
  img = Image.open(filepath)
149
- return f"[Image: {Path(filepath).name}]\nFormat: {img.format}\nDimensions: {img.size[0]}x{img.size[1]}\nMode: {img.mode}\n(Note: Visual content converted to metadata for text-only model compatibility)"
235
+ return f"[Image: {Path(filepath).name}]\nFormat: {img.format}\nDimensions: {img.size[0]}x{img.size[1]}\nMode: {img.mode}"
150
236
  else:
151
- with open(filepath, 'r', encoding='utf-8', errors='ignore') as f:
152
- return f.read()
153
- except Exception as e:
154
- return f"[Error extracting {filepath}: {str(e)}]"
237
+ with open(filepath, 'r', encoding='utf-8', errors='ignore') as f: return f.read()
238
+ except Exception as e: return f"[Error extracting {filepath}: {str(e)}]"
155
239
 
156
240
  def search_web(query: str, max_results: int = 5) -> str:
157
241
  try:
158
242
  from bs4 import BeautifulSoup
159
243
  session = requests.Session()
160
- session.headers.update({
161
- 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36',
162
- 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8',
163
- 'Accept-Language': 'en-US,en;q=0.9',
164
- 'Accept-Encoding': 'gzip, deflate, br',
165
- 'Connection': 'keep-alive',
166
- 'Upgrade-Insecure-Requests': '1',
167
- 'Sec-Fetch-Dest': 'document',
168
- 'Sec-Fetch-Mode': 'navigate',
169
- 'Sec-Fetch-Site': 'none',
170
- 'Sec-Fetch-User': '?1',
171
- })
172
- url = f"https://html.duckduckgo.com/html/?q={urllib.parse.quote(query)}"
173
- resp = session.get(url, timeout=10)
174
-
175
- if resp.status_code != 200:
176
- return f"[SCRAPE_FAILED: Status {resp.status_code}]"
177
-
244
+ session.headers.update({'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36'})
245
+ resp = session.get(f"https://html.duckduckgo.com/html/?q={urllib.parse.quote(query)}", timeout=10)
246
+ if resp.status_code != 200: return f"[SCRAPE_FAILED: Status {resp.status_code}]"
178
247
  soup = BeautifulSoup(resp.text, 'html.parser')
179
- results = []
180
- for a in soup.find_all('a', class_='result__snippet', limit=max_results):
181
- text = a.get_text(strip=True)
182
- if text: results.append(f"Result: {text}")
183
-
248
+ results = [a.get_text(strip=True) for a in soup.find_all('a', class_='result__snippet', limit=max_results) if a.get_text(strip=True)]
184
249
  if not results:
185
250
  for div in soup.find_all('div', class_='result__body', limit=max_results):
186
251
  snippet = div.find('a', class_='result__snippet')
187
- if snippet: results.append(f"Result: {snippet.get_text(strip=True)}")
188
-
189
- if not results:
190
- return "[SCRAPE_FAILED: No text found]"
191
- return "\n---\n".join(results)
192
- except Exception as e:
193
- return f"[SCRAPE_FAILED: {str(e)}]"
252
+ if snippet: results.append(snippet.get_text(strip=True))
253
+ return "\n---\n".join([f"Result: {r}" for r in results]) if results else "[SCRAPE_FAILED: No text found]"
254
+ except Exception as e: return f"[SCRAPE_FAILED: {str(e)}]"
194
255
 
195
256
  class CavemanMode(Enum):
196
257
  OFF = 0; BASIC = 1; ULTRA = 2
@@ -270,8 +331,7 @@ class FileSystemManager:
270
331
  class CommandRunner:
271
332
  def __init__(self, workspace_dir: str):
272
333
  self.workspace_dir = workspace_dir
273
- self.dangerous_commands = ["rm -rf /", "sudo rm -rf", "mkfs", "dd if="]
274
- def is_dangerous(self, cmd: str) -> bool: return any(dangerous in cmd for dangerous in self.dangerous_commands)
334
+ self.active_processes = {}
275
335
  def run(self, cmd: str, timeout: int = 120) -> Tuple[bool, str]:
276
336
  try:
277
337
  result = subprocess.run(cmd, shell=True, cwd=self.workspace_dir, capture_output=True, text=True, timeout=timeout)
@@ -279,20 +339,66 @@ class CommandRunner:
279
339
  return result.returncode == 0, output
280
340
  except subprocess.TimeoutExpired: return False, "Command timed out"
281
341
  except Exception as e: return False, f"Error: {str(e)}"
342
+
343
+ def execute_script(self, filepath: str) -> Tuple[bool, str]:
344
+ """Execute a script file, opening a side terminal on Windows."""
345
+ full_path = os.path.join(self.workspace_dir, filepath)
346
+ if not os.path.exists(full_path): return False, f"File not found: {filepath}"
347
+
348
+ ext = Path(filepath).suffix.lower()
349
+ commands = {
350
+ '.py': f'python "{full_path}"', '.js': f'node "{full_path}"', '.ts': f'ts-node "{full_path}"',
351
+ '.java': f'java -cp "{self.workspace_dir}" {Path(filepath).stem}', '.go': f'go run "{full_path}"',
352
+ '.rs': f'rustc "{full_path}" && {Path(filepath).stem}', '.sh': f'bash "{full_path}"',
353
+ '.bat': f'"{full_path}"', '.ps1': f'powershell -ExecutionPolicy Bypass -File "{full_path}"'
354
+ }
355
+
356
+ if ext not in commands: return False, f"Unsupported script type: {ext}"
357
+ cmd = commands[ext]
358
+
359
+ if sys.platform == 'win32':
360
+ process = subprocess.Popen(f'start cmd /k {cmd}', shell=True, cwd=self.workspace_dir)
361
+ else:
362
+ process = subprocess.Popen(cmd, shell=True, cwd=self.workspace_dir, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
363
+
364
+ self.active_processes[filepath] = process
365
+ return True, f"✅ Script opened in side terminal. Command: {cmd}"
366
+
367
+ def check_process(self, filepath: str) -> str:
368
+ if filepath not in self.active_processes: return "No active process for this file."
369
+ process = self.active_processes[filepath]
370
+ if process.poll() is None: return "🔄 Still running..."
371
+ return f"✅ Process finished with return code: {process.returncode}"
372
+
373
+ def execute_notebook(self, filepath: str) -> Tuple[bool, str]:
374
+ """Execute a Jupyter notebook and save with outputs."""
375
+ full_path = os.path.join(self.workspace_dir, filepath)
376
+ if not os.path.exists(full_path): return False, f"Notebook not found: {filepath}"
377
+
378
+ try:
379
+ import nbformat
380
+ from nbconvert.preprocessors import ExecutePreprocessor
381
+ with open(full_path, 'r', encoding='utf-8') as f: nb = nbformat.read(f, as_version=4)
382
+ ep = ExecutePreprocessor(timeout=600, kernel_name='python3')
383
+ ep.preprocess(nb, {'metadata': {'path': self.workspace_dir}})
384
+ with open(full_path, 'w', encoding='utf-8') as f: nbformat.write(nb, f)
385
+ return True, f"✅ Notebook executed successfully. Outputs saved to {filepath}"
386
+ except Exception as e: return False, f"Notebook execution failed: {str(e)}"
387
+
282
388
 
283
389
  class GitManager:
284
390
  def __init__(self, workspace_dir: str, console: Console):
285
391
  self.workspace_dir = workspace_dir; self.console = console; self.git_dir = os.path.join(workspace_dir, ".git")
286
392
  def is_git_installed(self) -> bool:
287
393
  try: subprocess.run(["git", "--version"], capture_output=True, check=True); return True
288
- except (subprocess.CalledProcessError, FileNotFoundError): return False
394
+ except: return False
289
395
  def is_repo(self) -> bool: return os.path.exists(self.git_dir)
290
396
  def get_current_branch(self) -> str:
291
397
  try: return subprocess.run(["git", "branch", "--show-current"], cwd=self.workspace_dir, capture_output=True, text=True, timeout=10).stdout.strip() or "main"
292
- except Exception: return "main"
398
+ except: return "main"
293
399
  def get_remote_url(self) -> str:
294
400
  try: return subprocess.run(["git", "remote", "get-url", "origin"], cwd=self.workspace_dir, capture_output=True, text=True, timeout=10).stdout.strip()
295
- except Exception: return ""
401
+ except: return ""
296
402
  def check_github_auth(self) -> Dict[str, Any]:
297
403
  result = {"logged_in": False, "user": "", "method": ""}
298
404
  try:
@@ -305,13 +411,13 @@ class GitManager:
305
411
  for i, part in enumerate(parts):
306
412
  if part == "as" and i + 1 < len(parts): result["user"] = parts[i + 1].strip("()"); break
307
413
  return result
308
- except FileNotFoundError: pass
414
+ except: pass
309
415
  try:
310
416
  user_result = subprocess.run(["git", "config", "user.name"], cwd=self.workspace_dir, capture_output=True, text=True, timeout=10)
311
417
  email_result = subprocess.run(["git", "config", "user.email"], cwd=self.workspace_dir, capture_output=True, text=True, timeout=10)
312
418
  if user_result.stdout.strip():
313
419
  result["logged_in"] = True; result["user"] = f"{user_result.stdout.strip()} <{email_result.stdout.strip()}>"; result["method"] = "Git credentials"
314
- except Exception: pass
420
+ except: pass
315
421
  return result
316
422
  def get_status(self) -> Dict[str, Any]:
317
423
  if not self.is_repo(): return {"error": "Not a git repository"}
@@ -339,7 +445,7 @@ class GitManager:
339
445
  def get_branches(self) -> List[str]:
340
446
  if not self.is_repo(): return []
341
447
  try: return [b.strip().lstrip("* ") for b in subprocess.run(["git", "branch"], cwd=self.workspace_dir, capture_output=True, text=True, timeout=10).stdout.strip().split("\n") if b.strip()]
342
- except Exception: return []
448
+ except: return []
343
449
  def init_repo(self) -> Tuple[bool, str]:
344
450
  try:
345
451
  subprocess.run(["git", "init"], cwd=self.workspace_dir, capture_output=True, text=True, timeout=10, check=True)
@@ -405,7 +511,7 @@ class AIModelClient:
405
511
  for detail in data["error"]["details"]:
406
512
  if "retryDelay" in detail: retry_delay = int(''.join(filter(str.isdigit, detail["retryDelay"]))); break
407
513
  elif "Retry-After" in resp.headers: retry_delay = int(resp.headers["Retry-After"])
408
- except Exception: pass
514
+ except: pass
409
515
  console.print(f"\n[yellow]⚠️ Rate limit (429). Waiting {retry_delay}s before retry ({attempt + 1}/{max_retries})...[/yellow]")
410
516
  time.sleep(retry_delay + 1); continue
411
517
  return resp
@@ -432,7 +538,7 @@ class AIModelClient:
432
538
  resp = self._request('GET', f"{base}/api/tags", timeout=15)
433
539
  if resp.ok:
434
540
  for m in resp.json().get("models", []): models.append(ModelInfo(id=m.get("name", ""), name=m.get("name", ""), provider=self.provider))
435
- except Exception: pass
541
+ except: pass
436
542
  if not models:
437
543
  provider_config = ProviderRegistry().get_provider(self.provider)
438
544
  models.append(ModelInfo(provider_config.default_model if provider_config else self.model, self.model, self.provider))
@@ -478,8 +584,11 @@ class AIModelClient:
478
584
  if not resp.ok: return f"Error: {resp.status_code} - {resp.text}"
479
585
  return self._handle_stream(resp) if stream else resp.json().get("message", {}).get("content", "")
480
586
  except Exception as e: return f"Error: {str(e)}"
587
+
481
588
  def _handle_stream(self, resp) -> str:
482
589
  full_response = ""
590
+ in_code_block = False
591
+ buffer = ""
483
592
  try:
484
593
  for line in resp.iter_lines():
485
594
  if not line: continue
@@ -503,9 +612,24 @@ class AIModelClient:
503
612
  if content is None:
504
613
  for key in ["content", "text", "response"]:
505
614
  if key in data and isinstance(data[key], str): content = data[key]; break
615
+
506
616
  if content:
507
- console.print(content, end="", markup=False, highlight=False)
508
617
  full_response += content
618
+ buffer += content
619
+
620
+ if "```" in buffer:
621
+ parts = buffer.split("```", 1)
622
+ if not in_code_block and parts[0].strip():
623
+ console.print(parts[0], end="", markup=False, highlight=False)
624
+ in_code_block = not in_code_block
625
+ if in_code_block:
626
+ console.print("\n[bold cyan]✨ Generating code...[/bold cyan]", end="", markup=False)
627
+ else:
628
+ console.print("\n", end="", markup=False)
629
+ buffer = parts[1]
630
+ elif not in_code_block:
631
+ console.print(buffer, end="", markup=False, highlight=False)
632
+ buffer = ""
509
633
  except Exception as e: console.print(f"\n[red]Stream error: {str(e)}[/red]")
510
634
  console.print()
511
635
  return full_response
@@ -515,18 +639,35 @@ class SamCodeCLI:
515
639
  self.workspace_dir = os.getcwd()
516
640
  self.config_dir = os.path.join(self.workspace_dir, ".samcode")
517
641
  self.config_path = os.path.join(self.config_dir, "config.json")
642
+ self.memory_path = os.path.join(self.config_dir, "session_memory.json")
518
643
  os.makedirs(self.config_dir, exist_ok=True)
644
+
519
645
  self.provider_registry = ProviderRegistry()
520
646
  self.active_provider = "openai"
521
647
  self.active_model = "gpt-4o"
522
648
  self.api_key = ""
523
649
  self.custom_base_url = ""
524
650
  self.caveman_mode = CavemanMode.OFF
651
+ self.frontend_mode = False
652
+ self.data_mode = False
653
+ self.data_session = {"datasets": {}, "analysis_steps": [], "agreed_notebook": None}
525
654
  self.session_context = []
655
+
656
+ self.session_history = []
657
+ if os.path.exists(self.memory_path):
658
+ try:
659
+ with open(self.memory_path, 'r') as f: self.session_history = json.load(f)
660
+ except: pass
661
+
526
662
  self.file_manager = FileSystemManager(self.workspace_dir)
527
663
  self.command_runner = CommandRunner(self.workspace_dir)
528
664
  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)
665
+
666
+ self.memory = CodebaseMemory(self.workspace_dir)
667
+ self.memory.index_workspace(self.file_manager)
668
+ self.code_doctor = CodeDoctor(self.workspace_dir)
669
+
670
+ self.command_completer = WordCompleter(['/connect', '/models', '/upload', '/clear-uploads', '/caveman', '/frontend', '/data', '/reindex', '/clear-memory', '/clear', '/exit', '/help', '/aboutme', '/searchweb', '/git'], sentence=True)
530
671
  self.prompt_text = FormattedText([('ansicyan bold', '❯ ')])
531
672
  self.load_configuration()
532
673
 
@@ -541,11 +682,16 @@ class SamCodeCLI:
541
682
  self.custom_base_url = data.get("custom_base_url", "")
542
683
  caveman_val = data.get("caveman_mode", 0)
543
684
  self.caveman_mode = CavemanMode(caveman_val) if caveman_val in [0, 1, 2] else CavemanMode.OFF
685
+ self.frontend_mode = data.get("frontend_mode", False)
544
686
  except: pass
545
687
 
546
688
  def save_configuration(self):
547
689
  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)
690
+ 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, "frontend_mode": self.frontend_mode}, f, indent=2)
691
+
692
+ def save_session_memory(self):
693
+ self.session_history = self.session_history[-20:]
694
+ with open(self.memory_path, "w") as f: json.dump(self.session_history, f, indent=2)
549
695
 
550
696
  def get_client(self) -> Optional[AIModelClient]:
551
697
  provider_config = self.provider_registry.get_provider(self.active_provider)
@@ -553,20 +699,44 @@ class SamCodeCLI:
553
699
  base_url = self.custom_base_url if self.custom_base_url else provider_config.base_url
554
700
  return AIModelClient(self.active_provider, self.api_key, base_url, self.active_model)
555
701
 
702
+ # ✅ NEW: Auto-ignore dotfiles
703
+ def auto_ignore_dotfiles(self):
704
+ gitignore_path = os.path.join(self.workspace_dir, ".gitignore")
705
+ dotfiles_to_ignore = []
706
+ for item in os.listdir(self.workspace_dir):
707
+ if item.startswith('.') and item not in ['.git', '.gitignore']:
708
+ full_path = os.path.join(self.workspace_dir, item)
709
+ entry = f"{item}/" if os.path.isdir(full_path) else item
710
+ dotfiles_to_ignore.append(entry)
711
+ if ".samcode/" not in dotfiles_to_ignore: dotfiles_to_ignore.append(".samcode/")
712
+
713
+ content = ""
714
+ if os.path.exists(gitignore_path):
715
+ with open(gitignore_path, "r") as f: content = f.read()
716
+
717
+ new_entries = [e for e in dotfiles_to_ignore if e not in content]
718
+ if new_entries:
719
+ with open(gitignore_path, "a") as f:
720
+ f.write("\n# Auto-ignored dotfiles by SamCode CLI\n")
721
+ for entry in new_entries: f.write(f"{entry}\n")
722
+ console.print(f"[green]✓ Automatically added {len(new_entries)} config file(s) to .gitignore[/green]")
723
+
556
724
  def show_main_header(self):
557
725
  print("\033[2J\033[H", end="")
558
726
  header_table = Table(show_header=False, box=None, padding=(0, 1))
559
727
  header_table.add_column(style="bold cyan", justify="left")
560
728
  header_table.add_column(style="dim", justify="center")
561
729
  header_table.add_column(style="bold green", justify="right")
730
+
562
731
  caveman_indicator = f" | [red]🦴 {self.caveman_mode.name}[/red]" if self.caveman_mode != CavemanMode.OFF else ""
563
732
  upload_indicator = f" | [magenta]📎 {len(self.session_context)} Docs[/magenta]" if self.session_context else ""
564
- git_indicator = ""
565
- if self.git_manager.is_repo():
566
- branch = self.git_manager.get_current_branch()
567
- 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)))
733
+ git_indicator = f" | [yellow]🌿 {self.git_manager.get_current_branch()}[/yellow]" if self.git_manager.is_repo() else ""
734
+ frontend_indicator = " | [bold magenta]🎨 FRONTEND[/bold magenta]" if self.frontend_mode else ""
735
+ data_indicator = " | [bold cyan]📊 DATA[/bold cyan]" if self.data_mode else ""
736
+
737
+ header_table.add_row("⚡ SamCode CLI", "Autonomous Coding Agent", f"{self.active_provider} | {self.active_model[:15]}{caveman_indicator}{upload_indicator}{git_indicator}{frontend_indicator}{data_indicator}")
738
+ path_text = Text(f" Workspace: {self.workspace_dir}", style="dim italic")
739
+ console.print(Panel(Group(header_table, path_text), border_style="bright_black", padding=(0, 2)))
570
740
  console.print("[dim]Type your request naturally, or use /help for commands.[/dim]\n")
571
741
 
572
742
  def cmd_connect(self):
@@ -578,14 +748,13 @@ class SamCodeCLI:
578
748
  provider_config = self.provider_registry.get_provider(selected)
579
749
  if provider_config.auth_type != "none":
580
750
  self.api_key = Prompt.ask(f"[cyan]Enter API Key for {provider_config.name}[/cyan]", password=True)
581
- if selected == "custom":
582
- self.custom_base_url = Prompt.ask("[cyan]Enter Custom Base URL[/cyan]")
751
+ if selected == "custom": self.custom_base_url = Prompt.ask("[cyan]Enter Custom Base URL[/cyan]")
583
752
  self.save_configuration()
584
753
  console.print(f"\n[green]✓ Connected to {provider_config.name}[/green]\n")
585
754
  if Confirm.ask("Select a model?", default=True): self.cmd_models()
586
755
 
587
756
  def cmd_models(self):
588
- console.print("\n[bold cyan]📋 Model Selector[/bold cyan]\n")
757
+ console.print("\n[bold cyan] Model Selector[/bold cyan]\n")
589
758
  client = self.get_client()
590
759
  if not client or not self.api_key: console.print("[red]Configure provider first with /connect[/red]"); return
591
760
  console.print("[cyan]Fetching models from API...[/cyan]")
@@ -617,7 +786,7 @@ class SamCodeCLI:
617
786
  else: console.print("[yellow]⚠️ Already on the last page.[/yellow]")
618
787
  elif user_input in ["less", "prev", "back"]:
619
788
  if current_page > 0: current_page -= 1; print("\033[2J\033[H", end=""); break
620
- else: console.print("[yellow] Already on the first page.[/yellow]")
789
+ else: console.print("[yellow]⚠️ Already on the first page.[/yellow]")
621
790
  else:
622
791
  try:
623
792
  idx = int(user_input) - 1
@@ -633,6 +802,54 @@ class SamCodeCLI:
633
802
  elif self.caveman_mode == CavemanMode.ULTRA: console.print("[red]🔇 Caveman Mode ULTRA. Maximum token saving. Grunt-like brevity.[/red]")
634
803
  self.show_main_header()
635
804
 
805
+ def cmd_frontend(self):
806
+ self.frontend_mode = not self.frontend_mode
807
+ self.save_configuration()
808
+ if self.frontend_mode:
809
+ msg = "[bold magenta]🎨 FRONTEND ARCHITECT MODE: ON[/bold magenta]\n"
810
+ msg += "[dim]AI will now act as a senior frontend developer with unique design systems, custom palettes, and performance-first architecture.[/dim]"
811
+ else:
812
+ msg = "[bold cyan]⚙️ FRONTEND ARCHITECT MODE: OFF[/bold cyan]\n"
813
+ msg += "[dim]Returning to standard coding assistant behavior.[/dim]"
814
+ console.print(f"\n{msg}\n")
815
+ self.show_main_header()
816
+
817
+ def cmd_data(self):
818
+ self.data_mode = not self.data_mode
819
+ if self.data_mode:
820
+ msg = "[bold cyan] DATA ANALYST MODE: ON[/bold cyan]\n"
821
+ 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]"
822
+ console.print(f"\n{msg}\n")
823
+ else:
824
+ msg = "[bold green]✅ DATA ANALYST MODE: OFF[/bold green]\n"
825
+ msg += "[dim]Returning to standard coding assistant behavior.[/dim]"
826
+ if self.data_session.get("agreed_notebook"): self._save_notebook(self.data_session["agreed_notebook"])
827
+ console.print(f"\n{msg}\n")
828
+ self.show_main_header()
829
+
830
+ def _save_notebook(self, notebook_content: dict):
831
+ import nbformat
832
+ nb = nbformat.v4.new_notebook()
833
+ nb.cells.append(nbformat.v4.new_markdown_cell("# 📊 Automated Data Analysis Report\n*Generated by SamCode CLI Data Analyst Mode*"))
834
+ for step in self.data_session["analysis_steps"]:
835
+ nb.cells.append(nbformat.v4.new_markdown_cell(f"## {step['title']}\n{step['description']}"))
836
+ nb.cells.append(nbformat.v4.new_code_cell(step['code']))
837
+ filename = f"data_analysis_{int(time.time())}.ipynb"
838
+ filepath = os.path.join(self.workspace_dir, filename)
839
+ with open(filepath, 'w') as f: nbformat.write(nb, f)
840
+ console.print(f"[green]✓ Saved professional notebook: {filename}[/green]")
841
+
842
+ def cmd_reindex(self):
843
+ console.print("[cyan]🔄 Re-indexing codebase memory...[/cyan]")
844
+ self.memory._is_indexed = False
845
+ self.memory.index_workspace(self.file_manager)
846
+ console.print("[green]✓ Re-indexing complete.[/green]\n")
847
+
848
+ def cmd_clear_memory(self):
849
+ self.session_history = []
850
+ self.save_session_memory()
851
+ console.print("[yellow]️ Session memory cleared. The agent will forget previous goals.[/yellow]\n")
852
+
636
853
  def cmd_upload(self, filepath: str = ""):
637
854
  if not filepath: filepath = Prompt.ask("[cyan]Enter file path to upload[/cyan]").strip()
638
855
  if not filepath: return
@@ -690,7 +907,6 @@ class SamCodeCLI:
690
907
  {"role": "system", "content": "You are an expert research assistant. Provide a clear, accurate, and helpful answer. Use markdown formatting."},
691
908
  {"role": "user", "content": ai_prompt}
692
909
  ]
693
-
694
910
  console.print(f"\n[bold cyan]🤖 AI Synthesizing Results...[/bold cyan]\n")
695
911
  client.chat(messages, stream=True)
696
912
  console.print()
@@ -710,7 +926,7 @@ class SamCodeCLI:
710
926
  branch = status.get("branch", "unknown"); remote = status.get("remote", "none")
711
927
  total_changes = len(status.get("staged", [])) + len(status.get("unstaged", [])) + len(status.get("untracked", []))
712
928
  console.print(f"[dim]Branch: [bold]{branch}[/bold] | Remote: [bold]{remote or 'none'}[/bold] | Changes: [bold]{total_changes}[/bold][/dim]\n")
713
- actions = [Choice("📊 View Status (see all changes)", "status"), Choice("📝 Commit Changes", "commit"), Choice("⬆️ Push to Remote", "push"), Choice("⬇️ Pull from Remote", "pull"), Choice("🌿 Branches (list/create/switch)", "branch"), Choice("📜 Recent Commits (log)", "log"), Choice("🔍 View Diff (detailed changes)", "diff"), Choice("📦 Stash Changes (save temporarily)", "stash"), Choice("🔗 Change Remote URL", "remote"), Choice("🔑 Check GitHub Login", "auth")]
929
+ actions = [Choice("📊 View Status (see all changes)", "status"), Choice(" Commit Changes", "commit"), Choice(" Push to Remote", "push"), Choice("⬇️ Pull from Remote", "pull"), Choice("🌿 Branches (list/create/switch)", "branch"), Choice("📜 Recent Commits (log)", "log"), Choice("🔍 View Diff (detailed changes)", "diff"), Choice(" Stash Changes (save temporarily)", "stash"), Choice("🔗 Change Remote URL", "remote"), Choice("🔑 Check GitHub Login", "auth")]
714
930
  action = questionary.select("Select Git Operation:", choices=actions).ask()
715
931
  if not action: return
716
932
  if action == "status": self._git_show_status(status)
@@ -726,28 +942,13 @@ class SamCodeCLI:
726
942
  self.show_main_header()
727
943
 
728
944
  def _initialize_git_repo(self):
729
- console.print("\n[bold cyan] Initialize Git Repository[/bold cyan]\n")
945
+ console.print("\n[bold cyan]🚀 Initialize Git Repository[/bold cyan]\n")
730
946
  success, msg = self.git_manager.init_repo()
731
947
  if not success: console.print(f"[red]✗ {msg}[/red]\n"); return
732
948
  console.print(f"[green]✓ {msg}[/green]\n")
733
949
 
734
- # --- AUTOMATIC GITIGNORE PROTECTION ---
735
- gitignore_path = os.path.join(self.workspace_dir, ".gitignore")
736
- ignore_entry = "\n.samcode/\n"
737
-
738
- if os.path.exists(gitignore_path):
739
- with open(gitignore_path, "r") as f:
740
- content = f.read()
741
- if ".samcode/" not in content:
742
- with open(gitignore_path, "a") as f:
743
- f.write(ignore_entry)
744
- console.print("[green]✓ Automatically added .samcode/ to existing .gitignore[/green]\n")
745
- else:
746
- with open(gitignore_path, "w") as f:
747
- f.write("# Automatically generated by SamCode CLI to protect API keys\n")
748
- f.write(ignore_entry)
749
- console.print("[green]✓ Automatically created .gitignore to protect .samcode/ folder[/green]\n")
750
- # ----------------------------------------
950
+ # NEW: Automatically ignore all dotfiles
951
+ self.auto_ignore_dotfiles()
751
952
 
752
953
  try:
753
954
  user_check = subprocess.run(["git", "config", "user.name"], cwd=self.workspace_dir, capture_output=True, text=True, timeout=5)
@@ -757,7 +958,7 @@ class SamCodeCLI:
757
958
  subprocess.run(["git", "config", "user.name", user_name], cwd=self.workspace_dir, timeout=5)
758
959
  subprocess.run(["git", "config", "user.email", user_email], cwd=self.workspace_dir, timeout=5)
759
960
  console.print(f"[green]✓ Git user set to: {user_name} <{user_email}>[/green]\n")
760
- except Exception: pass
961
+ except: pass
761
962
  console.print("[dim]You'll need a GitHub repository URL. Format: https://github.com/username/repo.git[/dim]\n")
762
963
  repo_url = Prompt.ask("[cyan]Enter GitHub repository URL[/cyan]").strip()
763
964
  if not repo_url: console.print("[yellow]Repository URL is required. Aborting.[/yellow]\n"); return
@@ -817,7 +1018,7 @@ class SamCodeCLI:
817
1018
  if not remote: console.print("[yellow]No remote configured. Use 'Change Remote URL' first.[/yellow]\n"); return
818
1019
  branch = self.git_manager.get_current_branch()
819
1020
  console.print(f"[dim]Branch: {branch} | Remote: {remote}[/dim]\n")
820
- force = Confirm.ask("Force push? (⚠️ Overwrites remote history)", default=False)
1021
+ force = Confirm.ask("Force push? ( Overwrites remote history)", default=False)
821
1022
  console.print("[cyan]Pushing...[/cyan]")
822
1023
  success, msg = self.git_manager.push(branch, force)
823
1024
  if success: console.print(f"\n[green]✓ Successfully pushed to {remote}[/green]\n")
@@ -866,12 +1067,12 @@ class SamCodeCLI:
866
1067
  else: console.print("[yellow]Cannot delete the current branch.[/yellow]\n")
867
1068
 
868
1069
  def _git_log(self):
869
- console.print("\n[bold cyan]📜 Recent Commits[/bold cyan]\n")
1070
+ console.print("\n[bold cyan] Recent Commits[/bold cyan]\n")
870
1071
  log = self.git_manager.get_log(15)
871
1072
  console.print(Panel(f"[dim]{log}[/dim]", title="Git Log", border_style="cyan")); console.print()
872
1073
 
873
1074
  def _git_diff(self):
874
- console.print("\n[bold cyan]🔍 View Diff[/bold cyan]\n")
1075
+ console.print("\n[bold cyan] View Diff[/bold cyan]\n")
875
1076
  diff_type = questionary.select("Show diff for:", choices=[Choice("Unstaged changes", "unstaged"), Choice("Staged changes", "staged")]).ask()
876
1077
  if not diff_type: return
877
1078
  staged = diff_type == "staged"; diff = self.git_manager.get_diff(staged)
@@ -897,7 +1098,7 @@ class SamCodeCLI:
897
1098
  else: console.print(f"\n[red]✗ {msg}[/red]\n")
898
1099
 
899
1100
  def _git_change_remote(self):
900
- console.print("\n[bold cyan]🔗 Change Remote URL[/bold cyan]\n")
1101
+ console.print("\n[bold cyan] Change Remote URL[/bold cyan]\n")
901
1102
  current = self.git_manager.get_remote_url()
902
1103
  if current: console.print(f"[dim]Current remote: {current}[/dim]\n")
903
1104
  new_url = Prompt.ask("[cyan]New remote URL[/cyan]").strip()
@@ -914,18 +1115,86 @@ class SamCodeCLI:
914
1115
  console.print("[red]✗ Not logged into GitHub[/red]\n")
915
1116
  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
1117
 
1118
+ def _detect_data_intent(self, prompt: str) -> bool:
1119
+ data_keywords = ['analyze', 'plot', 'chart', 'graph', 'visualize', 'trend', 'correlation', 'distribution', 'histogram', 'scatter', 'heatmap', 'dataset', 'csv', 'excel', 'database', 'sql', 'bi ', 'dashboard', 'kpi', 'metric', 'aggregate', 'groupby', 'pivot']
1120
+ return any(kw in prompt.lower() for kw in data_keywords)
1121
+
917
1122
  def cmd_agent_ask(self, question: str):
918
1123
  client = self.get_client()
919
- if not client or not self.api_key: console.print("[red]Configure AI first with /connect[/red]"); return
1124
+ if not client or not self.api_key:
1125
+ console.print("[red]Configure AI first with /connect[/red]")
1126
+ return
1127
+
1128
+ proactive_findings = ""
1129
+ if self.code_doctor.should_analyze(question):
1130
+ console.print("[cyan]🩺 Code Doctor: Analyzing project for issues...[/cyan]")
1131
+ relevant_files = self.code_doctor.get_relevant_files(question, self.file_manager, getattr(self, 'memory', None))
1132
+ if relevant_files:
1133
+ findings = self.code_doctor.run_analysis(relevant_files)
1134
+ if findings:
1135
+ proactive_findings = "\n\n🩺 PROACTIVE CODE ANALYSIS FINDINGS:\n"
1136
+ for filepath, report in findings.items(): proactive_findings += report
1137
+ console.print(f"[green]✓ Found issues in {len(findings)} file(s). Injecting into context.[/green]")
1138
+ else: console.print("[yellow]️ No linting issues found. Proceeding normally.[/yellow]")
1139
+ else: console.print("[yellow]⚠️ Could not determine relevant files to analyze.[/yellow]")
1140
+
920
1141
  files = self.file_manager.scan_workspace()
921
1142
  workspace_tree = "\n".join(files) if files else "(Empty workspace)"
922
1143
  context_str = ""
923
1144
  if self.session_context:
924
- context_str = "\n\nUPLOADED DOCUMENTS CONTEXT (The user has uploaded these files, you can reference them directly):\n"
1145
+ context_str = "\n\nUPLOADED DOCUMENTS CONTEXT:\n"
925
1146
  for doc in self.session_context: context_str += f"=== {doc['filename']} ===\n{doc['content']}\n\n"
1147
+ if proactive_findings: context_str += proactive_findings
1148
+
1149
+ token_economy_rule = """
1150
+ 9. CRITICAL SYSTEM RULE: NEVER output markdown code blocks (```python ... ```) in your chat response.
1151
+ - If you output a code block, the UI will crash.
1152
+ - Instead, you MUST say ONLY: "✨ Generating [filename]..." or "📖 Reading [filename]...".
1153
+ - Actual code MUST be placed inside [WRITE_FILE] tags.
1154
+ - Actual file content MUST come from [READ_FILE] tool results.
1155
+ - To execute a script, use [EXECUTE_SCRIPT: <path>].
1156
+ - To execute a notebook, use [EXECUTE_NOTEBOOK: <path>].
1157
+ """
1158
+
1159
+ frontend_rules = ""
1160
+ if self.frontend_mode:
1161
+ frontend_rules = """
1162
+ 10. 🎨 EXPERT FRONTEND DEVELOPER MODE IS ACTIVE: You are NOT an AI coding assistant. You are a Senior Frontend Architect & Design Systems Engineer.
1163
+ - NEVER use generic AI styling patterns (no default blue/gray palettes, no Inter/Roboto/Arial fonts, no basic Tailwind utility soup).
1164
+ - 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.
1165
+ - Create custom CSS variables or theme tokens for colors, typography, spacing, and shadows. Define these BEFORE writing any component code.
1166
+ - Prioritize modern, performant web standards (CSS Container Queries, :has(), native nesting, view transitions API, subgrid). Avoid outdated patterns.
1167
+ - When generating UI, explain your DESIGN RATIONALE first (why this palette? why this type scale? why this layout pattern?), THEN provide the implementation.
1168
+ - 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.
1169
+ """
1170
+
1171
+ data_analyst_rules = ""
1172
+ is_data_task = self.data_mode or self._detect_data_intent(question)
1173
+ if is_data_task:
1174
+ if self.caveman_mode != CavemanMode.OFF:
1175
+ data_analyst_rules = """
1176
+ 11. DATA ANALYST MODE (CAVEMAN ACTIVE):
1177
+ - NO explanations. NO business insights. NO markdown formatting for text.
1178
+ - Output ONLY raw code, chart generation scripts, or direct answers.
1179
+ - If asked to analyze, return ONLY the Python/pandas/seaborn code block. Nothing else.
1180
+ - Do NOT describe what you are doing. Just output the tool call or code.
1181
+ """
1182
+ else:
1183
+ data_analyst_rules = """
1184
+ 11. 📊 SENIOR DATA ANALYST & BI EXPERT MODE ACTIVE:
1185
+ - You are NOT just a coder. You are a Senior Business Intelligence Analyst.
1186
+ - ALWAYS start by reading the dataset using [RUN_TERMINAL: python -c "..."] or providing Python code to load it via UniversalDataReader.
1187
+ - Before writing ANY code, EXPLAIN your analytical approach: What KPIs matter? What business questions are we answering?
1188
+ - Generate publication-quality visualizations using seaborn/matplotlib. Always include titles, axis labels, and legends.
1189
+ - Provide actionable business insights, not just statistical observations. Connect data patterns to real-world business outcomes.
1190
+ - 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.
1191
+ - Support ALL data formats: CSV, Excel, Parquet, Feather, JSON, HDF5, PostgreSQL, MySQL, SQLite, MSSQL, and any SQLAlchemy-compatible data warehouse.
1192
+ """
1193
+
926
1194
  caveman_rules = ""
927
- 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."
928
- 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."
1195
+ 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."
1196
+ elif self.caveman_mode == CavemanMode.ULTRA: caveman_rules = "\n12. CAVEMAN MODE (ULTRA) IS ACTIVE: MAXIMUM TOKEN SAVING. Output ONLY code/tool calls. Zero explanations. Zero greetings. Grunt-like brevity."
1197
+
929
1198
  system_msg = f"""You are SamCode CLI, an expert autonomous AI coding agent.
930
1199
  You are currently working in the directory: {self.workspace_dir}
931
1200
 
@@ -939,28 +1208,55 @@ You have access to the following tools to help you complete tasks:
939
1208
  [END_WRITE_FILE]
940
1209
  [RUN_TERMINAL: <command>]
941
1210
  [SEARCH_CODE: <query>]
1211
+ [SEARCH_SEMANTIC: <natural_language_query>]
1212
+ [EXECUTE_SCRIPT: <path>]
1213
+ [EXECUTE_NOTEBOOK: <path>]
1214
+ [CHECK_PROCESS: <path>]
942
1215
 
943
1216
  CRITICAL RULES:
944
- 1. You have full access to the workspace. NEVER say you cannot access files.
1217
+ 1. You have full access to the workspace. NEVER say you cannot access files or execute commands.
945
1218
  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
1219
  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
1220
  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
1221
  5. If you need to see a file's content, use [READ_FILE: <path>].
949
1222
  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. Once you have completed the task, provide your final answer to the user WITHOUT using any tools.{caveman_rules}"""
952
- messages = [{"role": "system", "content": system_msg}, {"role": "user", "content": question}]
1223
+ 7. If you need to run a terminal command (like git push, npm install, python main.py), use [RUN_TERMINAL: <command>].
1224
+ 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.
1225
+ 9. Only use [READ_FILE] after [SEARCH_SEMANTIC] returns specific file paths.
1226
+ 10. To execute a script file (Python, Node.js, etc.), use [EXECUTE_SCRIPT: <path>]. This opens a side terminal showing execution.
1227
+ 11. To execute a Jupyter notebook, use [EXECUTE_NOTEBOOK: <path>]. This runs all cells and saves outputs.
1228
+ 12. To check if a script is still running, use [CHECK_PROCESS: <path>].{token_economy_rule}{frontend_rules}{data_analyst_rules}{caveman_rules}"""
1229
+
1230
+ recent_history = self.session_history[-10:]
1231
+ messages = [{"role": "system", "content": system_msg}]
1232
+ messages.extend(recent_history)
1233
+
1234
+ caveman_reminder = ""
1235
+ if self.caveman_mode == CavemanMode.BASIC:
1236
+ caveman_reminder = "\n\n[SYSTEM REMINDER: CAVEMAN BASIC ACTIVE. Be extremely concise. No fluff. Short sentences only.]"
1237
+ elif self.caveman_mode == CavemanMode.ULTRA:
1238
+ caveman_reminder = "\n\n[SYSTEM REMINDER: CAVEMAN ULTRA ACTIVE. MAXIMUM TOKEN SAVING. Output ONLY code/tool calls. Zero explanations. Zero greetings. Grunt-like brevity.]"
1239
+
1240
+ messages.append({"role": "user", "content": question + caveman_reminder})
1241
+
953
1242
  max_iterations = 20
954
1243
  console.print(f"\n[bold cyan]🤖 Agent Activated[/bold cyan]")
1244
+
955
1245
  for i in range(max_iterations):
956
1246
  console.print(f"\n[dim]--- Agent Step {i+1} ---[/dim]")
957
1247
  console.print("[dim]🤖 Thinking...[/dim]")
958
1248
  try:
959
1249
  response = client.chat(messages, stream=True)
960
1250
  if not response or response.startswith("Error"): console.print(f"\n[red]{response}[/red]"); break
1251
+
1252
+ self.session_history.append({"role": "user", "content": question})
1253
+ self.session_history.append({"role": "assistant", "content": response})
1254
+ self.save_session_memory()
1255
+
961
1256
  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)
1257
+ single_match = re.search(r'\[(READ_FILE|RUN_TERMINAL|SEARCH_CODE|SEARCH_SEMANTIC|EXECUTE_SCRIPT|EXECUTE_NOTEBOOK|CHECK_PROCESS):\s*(.*?)\]', response)
963
1258
  tool_executed = False
1259
+
964
1260
  if write_match:
965
1261
  path = write_match.group(1).strip(); content = write_match.group(2).strip()
966
1262
  full_path = os.path.join(self.workspace_dir, path); ext = Path(path).suffix.lstrip('.') or 'text'
@@ -970,28 +1266,49 @@ CRITICAL RULES:
970
1266
  right_panel = Panel(Syntax(content, ext, theme="monokai", line_numbers=True), title="[bold green]Proposed[/bold green]", border_style="green")
971
1267
  console.print("\n"); console.print(Columns([left_panel, right_panel], equal=True, expand=True))
972
1268
  if Confirm.ask(f"\n[bold]Apply changes to {path}?[/bold]", default=True):
973
- self.file_manager.write_file(path, content); tool_result = f"Successfully updated {path}."
1269
+ self.file_manager.write_file(path, content)
1270
+ # ✅ NEW: Auto-ignore dotfiles
1271
+ if path.startswith('.'): self.auto_ignore_dotfiles()
1272
+ tool_result = f"Successfully updated {path}."
974
1273
  console.print(f"[green]✓ File updated: {path}[/green]")
975
1274
  tool_result += f"\n\n[SYSTEM INSTRUCTION - SELF REVIEW]: You must now review the file you just modified. Use [READ_FILE: {path}] to check your work. If you find any syntax errors, logical bugs, or missing requirements, fix them immediately using [WRITE_FILE]. Only stop when the code is flawless."
976
1275
  else: tool_result = f"User rejected the changes to {path}."; console.print(f"[yellow]✗ Changes rejected.[/yellow]")
977
1276
  else:
978
1277
  new_file_panel = Panel(Syntax(content, ext, theme="monokai", line_numbers=True), title=f"[bold green]✨ Creating New File: {path}[/bold green]", border_style="green")
979
1278
  console.print("\n"); console.print(new_file_panel)
980
- self.file_manager.write_file(path, content); tool_result = f"Successfully created {path}."
1279
+ self.file_manager.write_file(path, content)
1280
+ # ✅ NEW: Auto-ignore dotfiles
1281
+ if path.startswith('.'): self.auto_ignore_dotfiles()
1282
+ tool_result = f"Successfully created {path}."
981
1283
  console.print(f"[green]✓ File created: {path}[/green]")
982
1284
  tool_result += f"\n\n[SYSTEM INSTRUCTION - SELF REVIEW]: You must now review the file you just created. Use [READ_FILE: {path}] to check your work. If you find any syntax errors, logical bugs, or missing requirements, fix them immediately using [WRITE_FILE]. Only stop when the code is flawless."
983
1285
  tool_executed = True
984
1286
  elif single_match:
985
1287
  tool_name = single_match.group(1); tool_arg = single_match.group(2).strip()
986
- if tool_name == "READ_FILE":
1288
+
1289
+ if tool_name == "SEARCH_SEMANTIC":
1290
+ results = self.memory.search(tool_arg)
1291
+ if not results: tool_result = "No semantically relevant code found in the codebase."
1292
+ else:
1293
+ formatted = []
1294
+ for r in results:
1295
+ snippet_preview = r['snippet'][:300].replace('\n', ' ') + "..." if len(r['snippet']) > 300 else r['snippet'].replace('\n', ' ')
1296
+ formatted.append(f"📄 {r['file']}:\n{snippet_preview}")
1297
+ tool_result = "\n\n---\n\n".join(formatted)
1298
+ console.print(f"\n[blue]🧠 Semantic Search: '{tool_arg}'[/blue]")
1299
+ tool_executed = True
1300
+
1301
+ elif tool_name == "READ_FILE":
987
1302
  content = self.file_manager.read_file(tool_arg)
988
1303
  tool_result = f"Content of {tool_arg}:\n{content}" if content else f"Error: File '{tool_arg}' not found."
989
- console.print(f"\n[blue]📖 Read file: {tool_arg}[/blue]"); tool_executed = True
1304
+ console.print(f"\n[blue]📖 Reading {tool_arg}...[/blue]"); tool_executed = True
1305
+
990
1306
  elif tool_name == "SEARCH_CODE":
991
1307
  results = self.file_manager.search_in_files(tool_arg)
992
1308
  if not results: tool_result = "No matches found."
993
1309
  else: tool_result = "\n".join([f"{r['file']}: {' | '.join([m[1] for m in r['matches']])}" for r in results])
994
1310
  console.print(f"\n[blue]🔍 Searched code for: {tool_arg}[/blue]"); tool_executed = True
1311
+
995
1312
  elif tool_name == "RUN_TERMINAL":
996
1313
  console.print(Panel(f"[yellow]{tool_arg}[/yellow]", title="[bold orange3]⚡ Terminal Command Request[/bold orange3]", border_style="orange3"))
997
1314
  if Confirm.ask("Execute this command?", default=True):
@@ -1001,6 +1318,35 @@ CRITICAL RULES:
1001
1318
  if not success or "[STDERR]" in output: tool_result += "\n\n[SYSTEM INSTRUCTION - AUTO-FIX]: This command FAILED. You MUST analyze the error output above and use [WRITE_FILE] or [RUN_TERMINAL] to fix the issue immediately. Do not ask the user for help, fix it yourself."
1002
1319
  else: tool_result = "User rejected the command."; console.print("[yellow]✗ Command rejected.[/yellow]")
1003
1320
  tool_executed = True
1321
+
1322
+ elif tool_name == "EXECUTE_SCRIPT":
1323
+ console.print(f"\n[bold cyan]🚀 Executing script: {tool_arg}[/bold cyan]")
1324
+ success, output = self.command_runner.execute_script(tool_arg)
1325
+ tool_result = output
1326
+ if success:
1327
+ console.print(f"[green]✓ {output}[/green]")
1328
+ console.print("[dim]The script is running in a side terminal. Use [CHECK_PROCESS: <path>] to verify completion.[/dim]")
1329
+ else: console.print(f"[red]✗ {output}[/red]")
1330
+ tool_executed = True
1331
+
1332
+ elif tool_name == "EXECUTE_NOTEBOOK":
1333
+ console.print(f"\n[bold cyan]📓 Executing notebook: {tool_arg}[/bold cyan]")
1334
+ console.print("[cyan]Running all cells and saving outputs...[/cyan]")
1335
+ success, output = self.command_runner.execute_notebook(tool_arg)
1336
+ tool_result = output
1337
+ if success:
1338
+ console.print(f"[green]✓ {output}[/green]")
1339
+ console.print("[dim]Open the notebook in Jupyter/VS Code to see execution results in cells.[/dim]")
1340
+ else: console.print(f"[red]✗ {output}[/red]")
1341
+ tool_executed = True
1342
+
1343
+ elif tool_name == "CHECK_PROCESS":
1344
+ console.print(f"\n[bold cyan] Checking process: {tool_arg}[/bold cyan]")
1345
+ output = self.command_runner.check_process(tool_arg)
1346
+ tool_result = output
1347
+ console.print(f"[cyan]{output}[/cyan]")
1348
+ tool_executed = True
1349
+
1004
1350
  if tool_executed:
1005
1351
  messages.append({"role": "assistant", "content": response})
1006
1352
  messages.append({"role": "user", "content": f"Tool result:\n{tool_result}\n\nContinue your task."})
@@ -1010,7 +1356,22 @@ CRITICAL RULES:
1010
1356
 
1011
1357
  def cmd_help(self):
1012
1358
  console.print("\n[bold cyan]📚 SamCode CLI Commands[/bold cyan]\n")
1013
- 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"}
1359
+ commands = {
1360
+ "/connect": "Configure AI provider and API key",
1361
+ "/models": "Select AI model dynamically",
1362
+ "/upload <path>": "Upload & extract documents (PDF, DOCX, XLSX, PPTX, Images)",
1363
+ "/clear-uploads": "Clear uploaded documents from session context",
1364
+ "/searchweb <query>": "Search the web (opens browser) & get AI-synthesized answer",
1365
+ "/git": "Native Git operations (commit, push, pull, branch, etc.)",
1366
+ "/caveman": "Cycle token-saving modes (OFF ➔ BASIC ➔ ULTRA)",
1367
+ "/frontend": "Toggle expert frontend architect mode (unique design systems)",
1368
+ "/data": "Toggle professional data analyst & BI mode (notebooks, charts, SQL)",
1369
+ "/reindex": "Re-build vector memory index after code changes",
1370
+ "/clear-memory": "Clear session conversation history",
1371
+ "/aboutme": "About the developer and SamCode",
1372
+ "/clear": "Clear the screen",
1373
+ "/exit": "Exit SamCode CLI"
1374
+ }
1014
1375
  for cmd, desc in commands.items(): console.print(f" [cyan]{cmd:<20}[/cyan] {desc}")
1015
1376
  console.print("\n[dim]Just type your request naturally to activate the autonomous agent![/dim]\n")
1016
1377
 
@@ -1022,7 +1383,6 @@ CRITICAL RULES:
1022
1383
  self.show_main_header()
1023
1384
  while True:
1024
1385
  try:
1025
- # FIXED: Using editing_mode=EditingMode.EMACS for standard shortcuts + custom kb
1026
1386
  user_input = pt_prompt(
1027
1387
  self.prompt_text,
1028
1388
  completer=self.command_completer,
@@ -1037,6 +1397,10 @@ CRITICAL RULES:
1037
1397
  elif user_input.lower() in ["/connect", "/config"]: self.cmd_connect(); self.show_main_header()
1038
1398
  elif user_input.lower() in ["/models", "/model"]: self.cmd_models()
1039
1399
  elif user_input.lower() in ["/caveman"]: self.cmd_caveman()
1400
+ elif user_input.lower() in ["/frontend"]: self.cmd_frontend()
1401
+ elif user_input.lower() in ["/data"]: self.cmd_data()
1402
+ elif user_input.lower() in ["/reindex"]: self.cmd_reindex()
1403
+ elif user_input.lower() in ["/clear-memory"]: self.cmd_clear_memory()
1040
1404
  elif user_input.lower() in ["/aboutme", "/about"]: self.cmd_aboutme()
1041
1405
  elif user_input.lower() in ["/git", "/g"]: self.cmd_git()
1042
1406
  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.3
3
+ Version: 1.0.5
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=pUXCqCAbuUBZ-oyvrag1XkMioqaCcYfGMFgYCiu9V9o,92199
2
+ samcode_cli-1.0.5.dist-info/METADATA,sha256=WApOF2ZDT7_wqWvySRToO0YfsHdRC9ELAcutaePSxuI,6269
3
+ samcode_cli-1.0.5.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
4
+ samcode_cli-1.0.5.dist-info/entry_points.txt,sha256=pTSNSMG9LYqLdbOR0TZGIQS0I17ZEUdXLEapLCYmyqo,41
5
+ samcode_cli-1.0.5.dist-info/top_level.txt,sha256=ie3RFdU_m6daHft-jFl_UKNkKAA25CItx4-gyRRHbJY,8
6
+ samcode_cli-1.0.5.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,,