samcode-cli 1.0.4__tar.gz → 1.0.5__tar.gz

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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: samcode-cli
3
- Version: 1.0.4
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
@@ -2,9 +2,8 @@
2
2
  """
3
3
  ╔══════════════════════════════════════════════════════════════════════════════╗
4
4
  ║ S A M C O D E C L I ║
5
- ║ Autonomous Coding Agent v6.2 (RAG + Doctor + Data BI)
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,12 +14,17 @@ 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
23
+ # Suppress warnings
24
+ warnings.filterwarnings("ignore", category=DeprecationWarning)
25
+ warnings.filterwarnings("ignore", message=".*urllib3.*")
26
+ warnings.filterwarnings("ignore", message=".*NumPy.*")
27
+
24
28
  from rich.console import Console, Group
25
29
  from rich.panel import Panel
26
30
  from rich.syntax import Syntax
@@ -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
@@ -47,35 +50,24 @@ except ImportError:
47
50
  from prompt_toolkit.formatted_text import FormattedText
48
51
  from prompt_toolkit.styles import Style
49
52
  from prompt_toolkit.enums import EditingMode
50
- import warnings
51
- warnings.filterwarnings("ignore", category=DeprecationWarning)
52
- warnings.filterwarnings("ignore", message=".*urllib3.*")
53
53
 
54
- # HTTP Client
55
54
  try:
56
55
  import requests
57
56
  except ImportError:
58
57
  subprocess.run([sys.executable, "-m", "pip", "install", "requests", "-q"], capture_output=True)
59
58
  import requests
60
59
 
61
-
60
+ # ✅ FIX: Define console BEFORE the auto-installer loop
62
61
  console = Console()
63
62
 
64
- menu_style = Style.from_dict({
65
- 'completion-menu': 'bg:#1e1e1e',
66
- 'completion-menu.completion': 'fg:#00d7d7 bg:#1e1e1e',
67
- 'completion-menu.completion.current': 'fg:#ffffff bg:#00af00 bold',
68
- })
69
-
70
- # Document Processing & Web Parsing Libraries Auto-Install
71
- # Document Processing & Web Parsing Libraries Auto-Install
72
63
  LIBS_TO_INSTALL = {
73
64
  'pdfplumber': 'pdfplumber', 'docx': 'python-docx', 'openpyxl': 'openpyxl',
74
65
  'pptx': 'python-pptx', 'PIL': 'Pillow', 'bs4': 'beautifulsoup4',
75
66
  'chromadb': 'chromadb', 'sentence_transformers': 'sentence-transformers',
76
67
  'ruff': 'ruff', 'pandas': 'pandas',
77
68
  'matplotlib': 'matplotlib', 'seaborn': 'seaborn', 'nbformat': 'nbformat', 'sqlalchemy': 'sqlalchemy',
78
- 'pyarrow': 'pyarrow', 'psycopg2_binary': 'psycopg2-binary', 'pymysql': 'pymysql'
69
+ 'pyarrow': 'pyarrow', 'psycopg2_binary': 'psycopg2-binary', 'pymysql': 'pymysql',
70
+ 'nbconvert': 'nbconvert'
79
71
  }
80
72
 
81
73
  for module, package in LIBS_TO_INSTALL.items():
@@ -83,257 +75,140 @@ for module, package in LIBS_TO_INSTALL.items():
83
75
  __import__(module)
84
76
  except ImportError:
85
77
  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
-
95
- # ═══════════════════════════════════════════════════════════════════════════════
96
- # KEYBOARD SHORTCUTS FOR PROMPT (Only custom ones)
97
- # ═══════════════════════════════════════════════════════════════════════════════
78
+ subprocess.run([sys.executable, "-m", "pip", "install", "--no-cache-dir", package], capture_output=True)
98
79
 
99
- from prompt_toolkit.key_binding import KeyBindings
80
+ menu_style = Style.from_dict({
81
+ 'completion-menu': 'bg:#1e1e1e',
82
+ 'completion-menu.completion': 'fg:#00d7d7 bg:#1e1e1e',
83
+ 'completion-menu.completion.current': 'fg:#ffffff bg:#00af00 bold',
84
+ })
100
85
 
86
+ from prompt_toolkit.key_binding import KeyBindings
101
87
  kb = KeyBindings()
102
88
 
103
89
  @kb.add('c-w')
104
90
  def _(event):
105
- "Delete word backward (Ctrl+W)"
106
91
  b = event.app.current_buffer
107
92
  pos = b.document.find_start_of_previous_word()
108
- if pos is not None:
109
- b.delete_before_cursor(-pos)
93
+ if pos is not None: b.delete_before_cursor(-pos)
110
94
 
111
95
  @kb.add('c-u')
112
96
  def _(event):
113
- "Clear line backward (Ctrl+U)"
114
97
  event.app.current_buffer.delete_before_cursor(event.app.current_buffer.cursor_position)
115
98
 
116
99
  @kb.add('c-k')
117
100
  def _(event):
118
- "Delete to end of line (Ctrl+K)"
119
101
  b = event.app.current_buffer
120
102
  b.delete(len(b.text) - b.cursor_position)
121
103
 
122
104
 
123
- # ═══════════════════════════════════════════════════════════════════════════════
124
- # VECTOR-BASED CODEBASE MEMORY (RAG)
125
- # ═══════════════════════════════════════════════════════════════════════════════
126
-
127
105
  class CodebaseMemory:
128
106
  def __init__(self, workspace_dir: str):
129
107
  self.workspace_dir = workspace_dir
130
108
  self.db_path = os.path.join(workspace_dir, ".samcode", "vector_db")
131
109
  os.makedirs(self.db_path, exist_ok=True)
132
-
133
- # LAZY LOAD: Only import when actually needed
110
+ self._is_indexed = False
111
+ self.model = None
112
+ self.collection = None
113
+
134
114
  try:
135
115
  import chromadb
136
116
  from sentence_transformers import SentenceTransformer
137
117
  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
- )
118
+ self.collection = self.client.get_or_create_collection(name="codebase", metadata={"hnsw:space": "cosine"})
141
119
  self.model = SentenceTransformer('all-MiniLM-L6-v2')
142
- self._is_indexed = False
143
120
  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
121
+ console.print(f"[yellow]⚠️ Vector memory unavailable. Run: pip install 'numpy<2' torch transformers sentence-transformers[/yellow]")
147
122
 
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
-
123
+ def index_workspace(self, file_manager):
124
+ if self._is_indexed or not self.model: return
153
125
  console.print("[cyan]🧠 Building vector memory index...[/cyan]")
154
126
  files = file_manager.scan_workspace(max_files=500)
155
- docs = []
156
- ids = []
157
- metadatas = []
158
-
127
+ docs, ids, metadatas = [], [], []
159
128
  code_extensions = {'.py', '.js', '.ts', '.jsx', '.tsx', '.java', '.go', '.rs', '.html', '.css', '.scss', '.sql', '.sh'}
160
129
 
161
130
  for f in files:
162
- ext = Path(f).suffix.lower()
163
- if ext in code_extensions:
131
+ if Path(f).suffix.lower() in code_extensions:
164
132
  content = file_manager.read_file(f)
165
- if content and len(content) < 10000: # Skip huge files
133
+ if content and len(content) < 10000:
166
134
  docs.append(f"File: {f}\nContent:\n{content}")
167
135
  ids.append(f.replace(os.sep, "_"))
168
- metadatas.append({"file": f, "ext": ext})
136
+ metadatas.append({"file": f, "ext": Path(f).suffix.lower()})
169
137
 
170
138
  if docs:
171
139
  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
- )
140
+ self.collection.upsert(documents=docs, embeddings=embeddings, ids=ids, metadatas=metadatas)
178
141
  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
142
  self._is_indexed = True
183
143
 
184
144
  def search(self, query: str, n_results: int = 3) -> List[Dict]:
185
- """Find semantically relevant code snippets."""
145
+ if not self.model: return []
186
146
  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
-
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])]
196
149
 
197
- # ═══════════════════════════════════════════════════════════════════════════════
198
- # PROACTIVE CODE DOCTOR
199
- # ═══════════════════════════════════════════════════════════════════════════════
200
150
 
201
151
  class CodeDoctor:
202
- """Automatically analyzes code when user mentions bugs, optimization, or cleanup."""
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']
203
153
 
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
-
154
+ def __init__(self, workspace_dir: str): self.workspace_dir = workspace_dir
214
155
  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)
156
+ return any(kw in user_prompt.lower() for kw in self.INTENT_KEYWORDS)
218
157
 
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:
158
+ def get_relevant_files(self, user_prompt: str, file_manager, memory=None) -> List[str]:
159
+ if memory and memory.model:
223
160
  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
161
+ if results: return [r['file'] for r in results]
228
162
  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
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]
241
166
 
242
167
  def run_analysis(self, files: List[str]) -> Dict[str, str]:
243
- """Run static analysis on specified files and return findings."""
244
168
  findings = {}
245
-
246
169
  for filepath in files:
247
170
  ext = Path(filepath).suffix.lower()
248
171
  full_path = os.path.join(self.workspace_dir, filepath)
249
-
250
- if not os.path.exists(full_path):
251
- continue
252
-
172
+ if not os.path.exists(full_path): continue
253
173
  try:
254
174
  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
- )
175
+ result = subprocess.run(['ruff', 'check', '--output-format=json', full_path], cwd=self.workspace_dir, capture_output=True, text=True, timeout=30)
259
176
  if result.stdout.strip():
260
177
  issues = json.loads(result.stdout)
261
178
  if issues:
262
179
  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"
180
+ for issue in issues[:5]:
181
+ summary += f" • Line {issue.get('location', {}).get('row', '?')}: [{issue.get('code', '')}] {issue.get('message', '')}\n"
268
182
  findings[filepath] = summary
269
-
270
- except (subprocess.TimeoutExpired, FileNotFoundError, Exception):
271
- # Silently skip if tool isn't installed or fails
272
- pass
273
-
183
+ except: pass
274
184
  return findings
275
185
 
276
186
 
277
- # ═══════════════════════════════════════════════════════════════════════════════
278
- # UNIVERSAL DATA READER
279
- # ═══════════════════════════════════════════════════════════════════════════════
280
-
281
187
  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
188
  @staticmethod
287
189
  def read(source: str, **kwargs) -> Tuple[Any, str]:
288
190
  import pandas as pd
289
-
290
- # Handle Database / Data Warehouse Connections
291
191
  if source.startswith(('postgresql://', 'mysql://', 'sqlite:///', 'mssql+pyodbc://')):
292
192
  try:
293
193
  from sqlalchemy import create_engine
294
194
  engine = create_engine(source)
295
195
  query = kwargs.get('query', 'SELECT * FROM information_schema.tables LIMIT 10')
296
196
  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)}"
197
+ return df, f"🗄️ Database Source: {source.split('@')[-1]}\nQuery executed successfully."
198
+ except Exception as e: return None, f"[DB_ERROR] {str(e)}"
301
199
 
302
- # Handle Local Files
303
200
  ext = Path(source).suffix.lower()
304
-
305
201
  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
- )
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()}"
333
210
  return df, meta
334
-
335
- except Exception as e:
336
- return None, f"[READ_ERROR] {str(e)}"
211
+ except Exception as e: return None, f"[READ_ERROR] {str(e)}"
337
212
 
338
213
 
339
214
  class DocumentReader:
@@ -343,88 +218,40 @@ class DocumentReader:
343
218
  try:
344
219
  if ext == '.pdf':
345
220
  import pdfplumber
346
- text = []
347
- with pdfplumber.open(filepath) as pdf:
348
- for page in pdf.pages:
349
- page_text = page.extract_text()
350
- if page_text: text.append(page_text)
351
- 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])
352
222
  elif ext == '.docx':
353
223
  import docx
354
- doc = docx.Document(filepath)
355
- paragraphs = [p.text for p in doc.paragraphs if p.text.strip()]
356
- for table in doc.tables:
357
- for row in table.rows:
358
- row_text = " | ".join([cell.text.strip() for cell in row.cells])
359
- paragraphs.append(row_text)
360
- return "\n".join(paragraphs)
224
+ return "\n".join([p.text for p in docx.Document(filepath).paragraphs if p.text.strip()])
361
225
  elif ext in ['.xlsx', '.xls']:
362
226
  import openpyxl
363
227
  wb = openpyxl.load_workbook(filepath, data_only=True)
364
- text = []
365
- for sheet in wb.sheetnames:
366
- text.append(f"\n--- Sheet: {sheet} ---")
367
- for row in wb[sheet].iter_rows(values_only=True):
368
- text.append(" | ".join([str(cell) if cell is not None else "" for cell in row]))
369
- 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])
370
229
  elif ext == '.pptx':
371
230
  import pptx
372
- prs = pptx.Presentation(filepath)
373
- text = []
374
- for i, slide in enumerate(prs.slides):
375
- text.append(f"\n--- Slide {i+1} ---")
376
- for shape in slide.shapes:
377
- if hasattr(shape, "text") and shape.text.strip():
378
- text.append(shape.text)
379
- 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)])
380
232
  elif ext in ['.jpg', '.jpeg', '.png', '.gif', '.bmp', '.webp', '.tiff']:
381
233
  from PIL import Image
382
234
  img = Image.open(filepath)
383
- 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}"
384
236
  else:
385
- with open(filepath, 'r', encoding='utf-8', errors='ignore') as f:
386
- return f.read()
387
- except Exception as e:
388
- 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)}]"
389
239
 
390
240
  def search_web(query: str, max_results: int = 5) -> str:
391
241
  try:
392
242
  from bs4 import BeautifulSoup
393
243
  session = requests.Session()
394
- session.headers.update({
395
- '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',
396
- 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8',
397
- 'Accept-Language': 'en-US,en;q=0.9',
398
- 'Accept-Encoding': 'gzip, deflate, br',
399
- 'Connection': 'keep-alive',
400
- 'Upgrade-Insecure-Requests': '1',
401
- 'Sec-Fetch-Dest': 'document',
402
- 'Sec-Fetch-Mode': 'navigate',
403
- 'Sec-Fetch-Site': 'none',
404
- 'Sec-Fetch-User': '?1',
405
- })
406
- url = f"https://html.duckduckgo.com/html/?q={urllib.parse.quote(query)}"
407
- resp = session.get(url, timeout=10)
408
-
409
- if resp.status_code != 200:
410
- return f"[SCRAPE_FAILED: Status {resp.status_code}]"
411
-
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}]"
412
247
  soup = BeautifulSoup(resp.text, 'html.parser')
413
- results = []
414
- for a in soup.find_all('a', class_='result__snippet', limit=max_results):
415
- text = a.get_text(strip=True)
416
- if text: results.append(f"Result: {text}")
417
-
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)]
418
249
  if not results:
419
250
  for div in soup.find_all('div', class_='result__body', limit=max_results):
420
251
  snippet = div.find('a', class_='result__snippet')
421
- if snippet: results.append(f"Result: {snippet.get_text(strip=True)}")
422
-
423
- if not results:
424
- return "[SCRAPE_FAILED: No text found]"
425
- return "\n---\n".join(results)
426
- except Exception as e:
427
- 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)}]"
428
255
 
429
256
  class CavemanMode(Enum):
430
257
  OFF = 0; BASIC = 1; ULTRA = 2
@@ -504,8 +331,7 @@ class FileSystemManager:
504
331
  class CommandRunner:
505
332
  def __init__(self, workspace_dir: str):
506
333
  self.workspace_dir = workspace_dir
507
- self.dangerous_commands = ["rm -rf /", "sudo rm -rf", "mkfs", "dd if="]
508
- def is_dangerous(self, cmd: str) -> bool: return any(dangerous in cmd for dangerous in self.dangerous_commands)
334
+ self.active_processes = {}
509
335
  def run(self, cmd: str, timeout: int = 120) -> Tuple[bool, str]:
510
336
  try:
511
337
  result = subprocess.run(cmd, shell=True, cwd=self.workspace_dir, capture_output=True, text=True, timeout=timeout)
@@ -513,20 +339,66 @@ class CommandRunner:
513
339
  return result.returncode == 0, output
514
340
  except subprocess.TimeoutExpired: return False, "Command timed out"
515
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
+
516
388
 
517
389
  class GitManager:
518
390
  def __init__(self, workspace_dir: str, console: Console):
519
391
  self.workspace_dir = workspace_dir; self.console = console; self.git_dir = os.path.join(workspace_dir, ".git")
520
392
  def is_git_installed(self) -> bool:
521
393
  try: subprocess.run(["git", "--version"], capture_output=True, check=True); return True
522
- except (subprocess.CalledProcessError, FileNotFoundError): return False
394
+ except: return False
523
395
  def is_repo(self) -> bool: return os.path.exists(self.git_dir)
524
396
  def get_current_branch(self) -> str:
525
397
  try: return subprocess.run(["git", "branch", "--show-current"], cwd=self.workspace_dir, capture_output=True, text=True, timeout=10).stdout.strip() or "main"
526
- except Exception: return "main"
398
+ except: return "main"
527
399
  def get_remote_url(self) -> str:
528
400
  try: return subprocess.run(["git", "remote", "get-url", "origin"], cwd=self.workspace_dir, capture_output=True, text=True, timeout=10).stdout.strip()
529
- except Exception: return ""
401
+ except: return ""
530
402
  def check_github_auth(self) -> Dict[str, Any]:
531
403
  result = {"logged_in": False, "user": "", "method": ""}
532
404
  try:
@@ -539,13 +411,13 @@ class GitManager:
539
411
  for i, part in enumerate(parts):
540
412
  if part == "as" and i + 1 < len(parts): result["user"] = parts[i + 1].strip("()"); break
541
413
  return result
542
- except FileNotFoundError: pass
414
+ except: pass
543
415
  try:
544
416
  user_result = subprocess.run(["git", "config", "user.name"], cwd=self.workspace_dir, capture_output=True, text=True, timeout=10)
545
417
  email_result = subprocess.run(["git", "config", "user.email"], cwd=self.workspace_dir, capture_output=True, text=True, timeout=10)
546
418
  if user_result.stdout.strip():
547
419
  result["logged_in"] = True; result["user"] = f"{user_result.stdout.strip()} <{email_result.stdout.strip()}>"; result["method"] = "Git credentials"
548
- except Exception: pass
420
+ except: pass
549
421
  return result
550
422
  def get_status(self) -> Dict[str, Any]:
551
423
  if not self.is_repo(): return {"error": "Not a git repository"}
@@ -573,7 +445,7 @@ class GitManager:
573
445
  def get_branches(self) -> List[str]:
574
446
  if not self.is_repo(): return []
575
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()]
576
- except Exception: return []
448
+ except: return []
577
449
  def init_repo(self) -> Tuple[bool, str]:
578
450
  try:
579
451
  subprocess.run(["git", "init"], cwd=self.workspace_dir, capture_output=True, text=True, timeout=10, check=True)
@@ -639,7 +511,7 @@ class AIModelClient:
639
511
  for detail in data["error"]["details"]:
640
512
  if "retryDelay" in detail: retry_delay = int(''.join(filter(str.isdigit, detail["retryDelay"]))); break
641
513
  elif "Retry-After" in resp.headers: retry_delay = int(resp.headers["Retry-After"])
642
- except Exception: pass
514
+ except: pass
643
515
  console.print(f"\n[yellow]⚠️ Rate limit (429). Waiting {retry_delay}s before retry ({attempt + 1}/{max_retries})...[/yellow]")
644
516
  time.sleep(retry_delay + 1); continue
645
517
  return resp
@@ -666,7 +538,7 @@ class AIModelClient:
666
538
  resp = self._request('GET', f"{base}/api/tags", timeout=15)
667
539
  if resp.ok:
668
540
  for m in resp.json().get("models", []): models.append(ModelInfo(id=m.get("name", ""), name=m.get("name", ""), provider=self.provider))
669
- except Exception: pass
541
+ except: pass
670
542
  if not models:
671
543
  provider_config = ProviderRegistry().get_provider(self.provider)
672
544
  models.append(ModelInfo(provider_config.default_model if provider_config else self.model, self.model, self.provider))
@@ -712,8 +584,11 @@ class AIModelClient:
712
584
  if not resp.ok: return f"Error: {resp.status_code} - {resp.text}"
713
585
  return self._handle_stream(resp) if stream else resp.json().get("message", {}).get("content", "")
714
586
  except Exception as e: return f"Error: {str(e)}"
587
+
715
588
  def _handle_stream(self, resp) -> str:
716
589
  full_response = ""
590
+ in_code_block = False
591
+ buffer = ""
717
592
  try:
718
593
  for line in resp.iter_lines():
719
594
  if not line: continue
@@ -737,9 +612,24 @@ class AIModelClient:
737
612
  if content is None:
738
613
  for key in ["content", "text", "response"]:
739
614
  if key in data and isinstance(data[key], str): content = data[key]; break
615
+
740
616
  if content:
741
- console.print(content, end="", markup=False, highlight=False)
742
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 = ""
743
633
  except Exception as e: console.print(f"\n[red]Stream error: {str(e)}[/red]")
744
634
  console.print()
745
635
  return full_response
@@ -749,7 +639,9 @@ class SamCodeCLI:
749
639
  self.workspace_dir = os.getcwd()
750
640
  self.config_dir = os.path.join(self.workspace_dir, ".samcode")
751
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")
752
643
  os.makedirs(self.config_dir, exist_ok=True)
644
+
753
645
  self.provider_registry = ProviderRegistry()
754
646
  self.active_provider = "openai"
755
647
  self.active_model = "gpt-4o"
@@ -757,21 +649,25 @@ class SamCodeCLI:
757
649
  self.custom_base_url = ""
758
650
  self.caveman_mode = CavemanMode.OFF
759
651
  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
652
+ self.data_mode = False
653
+ self.data_session = {"datasets": {}, "analysis_steps": [], "agreed_notebook": None}
762
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
+
763
662
  self.file_manager = FileSystemManager(self.workspace_dir)
764
663
  self.command_runner = CommandRunner(self.workspace_dir)
765
664
  self.git_manager = GitManager(self.workspace_dir, console)
766
665
 
767
- # Initialize Vector Memory
768
666
  self.memory = CodebaseMemory(self.workspace_dir)
769
667
  self.memory.index_workspace(self.file_manager)
770
-
771
- # Initialize Proactive Code Doctor
772
668
  self.code_doctor = CodeDoctor(self.workspace_dir)
773
669
 
774
- self.command_completer = WordCompleter(['/connect', '/models', '/upload', '/clear-uploads', '/caveman', '/frontend', '/data', '/reindex', '/clear', '/exit', '/help', '/aboutme', '/searchweb', '/git'], sentence=True)
670
+ self.command_completer = WordCompleter(['/connect', '/models', '/upload', '/clear-uploads', '/caveman', '/frontend', '/data', '/reindex', '/clear-memory', '/clear', '/exit', '/help', '/aboutme', '/searchweb', '/git'], sentence=True)
775
671
  self.prompt_text = FormattedText([('ansicyan bold', '❯ ')])
776
672
  self.load_configuration()
777
673
 
@@ -791,14 +687,11 @@ class SamCodeCLI:
791
687
 
792
688
  def save_configuration(self):
793
689
  with open(self.config_path, "w") as f:
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)
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)
802
695
 
803
696
  def get_client(self) -> Optional[AIModelClient]:
804
697
  provider_config = self.provider_registry.get_provider(self.active_provider)
@@ -806,9 +699,30 @@ class SamCodeCLI:
806
699
  base_url = self.custom_base_url if self.custom_base_url else provider_config.base_url
807
700
  return AIModelClient(self.active_provider, self.api_key, base_url, self.active_model)
808
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
+
809
724
  def show_main_header(self):
810
725
  print("\033[2J\033[H", end="")
811
-
812
726
  header_table = Table(show_header=False, box=None, padding=(0, 1))
813
727
  header_table.add_column(style="bold cyan", justify="left")
814
728
  header_table.add_column(style="dim", justify="center")
@@ -816,23 +730,13 @@ class SamCodeCLI:
816
730
 
817
731
  caveman_indicator = f" | [red]🦴 {self.caveman_mode.name}[/red]" if self.caveman_mode != CavemanMode.OFF else ""
818
732
  upload_indicator = f" | [magenta]📎 {len(self.session_context)} Docs[/magenta]" if self.session_context else ""
819
- git_indicator = ""
820
- if self.git_manager.is_repo():
821
- branch = self.git_manager.get_current_branch()
822
- git_indicator = f" | [yellow]🌿 {branch}[/yellow]"
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 ""
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 ""
825
736
 
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)))
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)))
836
740
  console.print("[dim]Type your request naturally, or use /help for commands.[/dim]\n")
837
741
 
838
742
  def cmd_connect(self):
@@ -844,14 +748,13 @@ class SamCodeCLI:
844
748
  provider_config = self.provider_registry.get_provider(selected)
845
749
  if provider_config.auth_type != "none":
846
750
  self.api_key = Prompt.ask(f"[cyan]Enter API Key for {provider_config.name}[/cyan]", password=True)
847
- if selected == "custom":
848
- 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]")
849
752
  self.save_configuration()
850
753
  console.print(f"\n[green]✓ Connected to {provider_config.name}[/green]\n")
851
754
  if Confirm.ask("Select a model?", default=True): self.cmd_models()
852
755
 
853
756
  def cmd_models(self):
854
- console.print("\n[bold cyan]📋 Model Selector[/bold cyan]\n")
757
+ console.print("\n[bold cyan] Model Selector[/bold cyan]\n")
855
758
  client = self.get_client()
856
759
  if not client or not self.api_key: console.print("[red]Configure provider first with /connect[/red]"); return
857
760
  console.print("[cyan]Fetching models from API...[/cyan]")
@@ -883,7 +786,7 @@ class SamCodeCLI:
883
786
  else: console.print("[yellow]⚠️ Already on the last page.[/yellow]")
884
787
  elif user_input in ["less", "prev", "back"]:
885
788
  if current_page > 0: current_page -= 1; print("\033[2J\033[H", end=""); break
886
- else: console.print("[yellow] Already on the first page.[/yellow]")
789
+ else: console.print("[yellow]⚠️ Already on the first page.[/yellow]")
887
790
  else:
888
791
  try:
889
792
  idx = int(user_input) - 1
@@ -911,42 +814,29 @@ class SamCodeCLI:
911
814
  console.print(f"\n{msg}\n")
912
815
  self.show_main_header()
913
816
 
914
- # NEW: Professional Data Analyst Mode Toggle
915
817
  def cmd_data(self):
916
- """Toggle Professional Data Analyst & BI Mode."""
917
818
  self.data_mode = not self.data_mode
918
-
919
819
  if self.data_mode:
920
- msg = "[bold cyan]📊 DATA ANALYST MODE: ON[/bold cyan]\n"
820
+ msg = "[bold cyan] DATA ANALYST MODE: ON[/bold cyan]\n"
921
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]"
922
822
  console.print(f"\n{msg}\n")
923
823
  else:
924
824
  msg = "[bold green]✅ DATA ANALYST MODE: OFF[/bold green]\n"
925
825
  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"])
826
+ if self.data_session.get("agreed_notebook"): self._save_notebook(self.data_session["agreed_notebook"])
929
827
  console.print(f"\n{msg}\n")
930
-
931
828
  self.show_main_header()
932
829
 
933
830
  def _save_notebook(self, notebook_content: dict):
934
- """Saves the agreed-upon analysis as a .ipynb file with rich markdown cells."""
935
831
  import nbformat
936
832
  nb = nbformat.v4.new_notebook()
937
-
938
- # Add title cell
939
833
  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
834
  for step in self.data_session["analysis_steps"]:
943
835
  nb.cells.append(nbformat.v4.new_markdown_cell(f"## {step['title']}\n{step['description']}"))
944
836
  nb.cells.append(nbformat.v4.new_code_cell(step['code']))
945
-
946
837
  filename = f"data_analysis_{int(time.time())}.ipynb"
947
838
  filepath = os.path.join(self.workspace_dir, filename)
948
- with open(filepath, 'w') as f:
949
- nbformat.write(nb, f)
839
+ with open(filepath, 'w') as f: nbformat.write(nb, f)
950
840
  console.print(f"[green]✓ Saved professional notebook: {filename}[/green]")
951
841
 
952
842
  def cmd_reindex(self):
@@ -955,6 +845,11 @@ class SamCodeCLI:
955
845
  self.memory.index_workspace(self.file_manager)
956
846
  console.print("[green]✓ Re-indexing complete.[/green]\n")
957
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
+
958
853
  def cmd_upload(self, filepath: str = ""):
959
854
  if not filepath: filepath = Prompt.ask("[cyan]Enter file path to upload[/cyan]").strip()
960
855
  if not filepath: return
@@ -1012,7 +907,6 @@ class SamCodeCLI:
1012
907
  {"role": "system", "content": "You are an expert research assistant. Provide a clear, accurate, and helpful answer. Use markdown formatting."},
1013
908
  {"role": "user", "content": ai_prompt}
1014
909
  ]
1015
-
1016
910
  console.print(f"\n[bold cyan]🤖 AI Synthesizing Results...[/bold cyan]\n")
1017
911
  client.chat(messages, stream=True)
1018
912
  console.print()
@@ -1032,7 +926,7 @@ class SamCodeCLI:
1032
926
  branch = status.get("branch", "unknown"); remote = status.get("remote", "none")
1033
927
  total_changes = len(status.get("staged", [])) + len(status.get("unstaged", [])) + len(status.get("untracked", []))
1034
928
  console.print(f"[dim]Branch: [bold]{branch}[/bold] | Remote: [bold]{remote or 'none'}[/bold] | Changes: [bold]{total_changes}[/bold][/dim]\n")
1035
- 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")]
1036
930
  action = questionary.select("Select Git Operation:", choices=actions).ask()
1037
931
  if not action: return
1038
932
  if action == "status": self._git_show_status(status)
@@ -1048,26 +942,13 @@ class SamCodeCLI:
1048
942
  self.show_main_header()
1049
943
 
1050
944
  def _initialize_git_repo(self):
1051
- console.print("\n[bold cyan] Initialize Git Repository[/bold cyan]\n")
945
+ console.print("\n[bold cyan]🚀 Initialize Git Repository[/bold cyan]\n")
1052
946
  success, msg = self.git_manager.init_repo()
1053
947
  if not success: console.print(f"[red]✗ {msg}[/red]\n"); return
1054
948
  console.print(f"[green]✓ {msg}[/green]\n")
1055
949
 
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")
950
+ # NEW: Automatically ignore all dotfiles
951
+ self.auto_ignore_dotfiles()
1071
952
 
1072
953
  try:
1073
954
  user_check = subprocess.run(["git", "config", "user.name"], cwd=self.workspace_dir, capture_output=True, text=True, timeout=5)
@@ -1077,7 +958,7 @@ class SamCodeCLI:
1077
958
  subprocess.run(["git", "config", "user.name", user_name], cwd=self.workspace_dir, timeout=5)
1078
959
  subprocess.run(["git", "config", "user.email", user_email], cwd=self.workspace_dir, timeout=5)
1079
960
  console.print(f"[green]✓ Git user set to: {user_name} <{user_email}>[/green]\n")
1080
- except Exception: pass
961
+ except: pass
1081
962
  console.print("[dim]You'll need a GitHub repository URL. Format: https://github.com/username/repo.git[/dim]\n")
1082
963
  repo_url = Prompt.ask("[cyan]Enter GitHub repository URL[/cyan]").strip()
1083
964
  if not repo_url: console.print("[yellow]Repository URL is required. Aborting.[/yellow]\n"); return
@@ -1137,7 +1018,7 @@ class SamCodeCLI:
1137
1018
  if not remote: console.print("[yellow]No remote configured. Use 'Change Remote URL' first.[/yellow]\n"); return
1138
1019
  branch = self.git_manager.get_current_branch()
1139
1020
  console.print(f"[dim]Branch: {branch} | Remote: {remote}[/dim]\n")
1140
- force = Confirm.ask("Force push? (⚠️ Overwrites remote history)", default=False)
1021
+ force = Confirm.ask("Force push? ( Overwrites remote history)", default=False)
1141
1022
  console.print("[cyan]Pushing...[/cyan]")
1142
1023
  success, msg = self.git_manager.push(branch, force)
1143
1024
  if success: console.print(f"\n[green]✓ Successfully pushed to {remote}[/green]\n")
@@ -1186,12 +1067,12 @@ class SamCodeCLI:
1186
1067
  else: console.print("[yellow]Cannot delete the current branch.[/yellow]\n")
1187
1068
 
1188
1069
  def _git_log(self):
1189
- console.print("\n[bold cyan]📜 Recent Commits[/bold cyan]\n")
1070
+ console.print("\n[bold cyan] Recent Commits[/bold cyan]\n")
1190
1071
  log = self.git_manager.get_log(15)
1191
1072
  console.print(Panel(f"[dim]{log}[/dim]", title="Git Log", border_style="cyan")); console.print()
1192
1073
 
1193
1074
  def _git_diff(self):
1194
- console.print("\n[bold cyan]🔍 View Diff[/bold cyan]\n")
1075
+ console.print("\n[bold cyan] View Diff[/bold cyan]\n")
1195
1076
  diff_type = questionary.select("Show diff for:", choices=[Choice("Unstaged changes", "unstaged"), Choice("Staged changes", "staged")]).ask()
1196
1077
  if not diff_type: return
1197
1078
  staged = diff_type == "staged"; diff = self.git_manager.get_diff(staged)
@@ -1217,7 +1098,7 @@ class SamCodeCLI:
1217
1098
  else: console.print(f"\n[red]✗ {msg}[/red]\n")
1218
1099
 
1219
1100
  def _git_change_remote(self):
1220
- console.print("\n[bold cyan]🔗 Change Remote URL[/bold cyan]\n")
1101
+ console.print("\n[bold cyan] Change Remote URL[/bold cyan]\n")
1221
1102
  current = self.git_manager.get_remote_url()
1222
1103
  if current: console.print(f"[dim]Current remote: {current}[/dim]\n")
1223
1104
  new_url = Prompt.ask("[cyan]New remote URL[/cyan]").strip()
@@ -1235,13 +1116,7 @@ class SamCodeCLI:
1235
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")
1236
1117
 
1237
1118
  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
- ]
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']
1245
1120
  return any(kw in prompt.lower() for kw in data_keywords)
1246
1121
 
1247
1122
  def cmd_agent_ask(self, question: str):
@@ -1250,39 +1125,36 @@ class SamCodeCLI:
1250
1125
  console.print("[red]Configure AI first with /connect[/red]")
1251
1126
  return
1252
1127
 
1253
- # NEW: Proactive Code Doctor - Auto-analyze when intent detected
1254
1128
  proactive_findings = ""
1255
1129
  if self.code_doctor.should_analyze(question):
1256
1130
  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
- )
1131
+ relevant_files = self.code_doctor.get_relevant_files(question, self.file_manager, getattr(self, 'memory', None))
1261
1132
  if relevant_files:
1262
1133
  findings = self.code_doctor.run_analysis(relevant_files)
1263
1134
  if findings:
1264
1135
  proactive_findings = "\n\n🩺 PROACTIVE CODE ANALYSIS FINDINGS:\n"
1265
- for filepath, report in findings.items():
1266
- proactive_findings += report
1136
+ for filepath, report in findings.items(): proactive_findings += report
1267
1137
  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]")
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]")
1272
1140
 
1273
1141
  files = self.file_manager.scan_workspace()
1274
1142
  workspace_tree = "\n".join(files) if files else "(Empty workspace)"
1275
1143
  context_str = ""
1276
1144
  if self.session_context:
1277
- context_str = "\n\nUPLOADED DOCUMENTS CONTEXT (The user has uploaded these files, you can reference them directly):\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
1145
+ context_str = "\n\nUPLOADED DOCUMENTS CONTEXT:\n"
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
1284
1148
 
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."
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
+ """
1286
1158
 
1287
1159
  frontend_rules = ""
1288
1160
  if self.frontend_mode:
@@ -1296,12 +1168,19 @@ class SamCodeCLI:
1296
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.
1297
1169
  """
1298
1170
 
1299
- # NEW: Hybrid Data Analyst Rules (Auto-detect + Explicit Mode)
1300
1171
  data_analyst_rules = ""
1301
1172
  is_data_task = self.data_mode or self._detect_data_intent(question)
1302
-
1303
1173
  if is_data_task:
1304
- data_analyst_rules = """
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 = """
1305
1184
  11. 📊 SENIOR DATA ANALYST & BI EXPERT MODE ACTIVE:
1306
1185
  - You are NOT just a coder. You are a Senior Business Intelligence Analyst.
1307
1186
  - ALWAYS start by reading the dataset using [RUN_TERMINAL: python -c "..."] or providing Python code to load it via UniversalDataReader.
@@ -1314,7 +1193,7 @@ class SamCodeCLI:
1314
1193
 
1315
1194
  caveman_rules = ""
1316
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."
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."
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."
1318
1197
 
1319
1198
  system_msg = f"""You are SamCode CLI, an expert autonomous AI coding agent.
1320
1199
  You are currently working in the directory: {self.workspace_dir}
@@ -1330,6 +1209,9 @@ You have access to the following tools to help you complete tasks:
1330
1209
  [RUN_TERMINAL: <command>]
1331
1210
  [SEARCH_CODE: <query>]
1332
1211
  [SEARCH_SEMANTIC: <natural_language_query>]
1212
+ [EXECUTE_SCRIPT: <path>]
1213
+ [EXECUTE_NOTEBOOK: <path>]
1214
+ [CHECK_PROCESS: <path>]
1333
1215
 
1334
1216
  CRITICAL RULES:
1335
1217
  1. You have full access to the workspace. NEVER say you cannot access files or execute commands.
@@ -1340,20 +1222,41 @@ CRITICAL RULES:
1340
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].
1341
1223
  7. If you need to run a terminal command (like git push, npm install, python main.py), use [RUN_TERMINAL: <command>].
1342
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.
1343
- 9. Only use [READ_FILE] after [SEARCH_SEMANTIC] returns specific file paths.{token_economy_rule}{frontend_rules}{data_analyst_rules}{caveman_rules}"""
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})
1344
1241
 
1345
- messages = [{"role": "system", "content": system_msg}, {"role": "user", "content": question}]
1346
1242
  max_iterations = 20
1347
1243
  console.print(f"\n[bold cyan]🤖 Agent Activated[/bold cyan]")
1244
+
1348
1245
  for i in range(max_iterations):
1349
1246
  console.print(f"\n[dim]--- Agent Step {i+1} ---[/dim]")
1350
1247
  console.print("[dim]🤖 Thinking...[/dim]")
1351
1248
  try:
1352
1249
  response = client.chat(messages, stream=True)
1353
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
+
1354
1256
  write_match = re.search(r'\[WRITE_FILE:\s*(.*?)\](.*?)\[END_WRITE_FILE\]', response, re.DOTALL)
1355
- single_match = re.search(r'\[(READ_FILE|RUN_TERMINAL|SEARCH_CODE|SEARCH_SEMANTIC):\s*(.*?)\]', response)
1257
+ single_match = re.search(r'\[(READ_FILE|RUN_TERMINAL|SEARCH_CODE|SEARCH_SEMANTIC|EXECUTE_SCRIPT|EXECUTE_NOTEBOOK|CHECK_PROCESS):\s*(.*?)\]', response)
1356
1258
  tool_executed = False
1259
+
1357
1260
  if write_match:
1358
1261
  path = write_match.group(1).strip(); content = write_match.group(2).strip()
1359
1262
  full_path = os.path.join(self.workspace_dir, path); ext = Path(path).suffix.lstrip('.') or 'text'
@@ -1363,14 +1266,20 @@ CRITICAL RULES:
1363
1266
  right_panel = Panel(Syntax(content, ext, theme="monokai", line_numbers=True), title="[bold green]Proposed[/bold green]", border_style="green")
1364
1267
  console.print("\n"); console.print(Columns([left_panel, right_panel], equal=True, expand=True))
1365
1268
  if Confirm.ask(f"\n[bold]Apply changes to {path}?[/bold]", default=True):
1366
- 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}."
1367
1273
  console.print(f"[green]✓ File updated: {path}[/green]")
1368
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."
1369
1275
  else: tool_result = f"User rejected the changes to {path}."; console.print(f"[yellow]✗ Changes rejected.[/yellow]")
1370
1276
  else:
1371
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")
1372
1278
  console.print("\n"); console.print(new_file_panel)
1373
- 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}."
1374
1283
  console.print(f"[green]✓ File created: {path}[/green]")
1375
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."
1376
1285
  tool_executed = True
@@ -1379,8 +1288,7 @@ CRITICAL RULES:
1379
1288
 
1380
1289
  if tool_name == "SEARCH_SEMANTIC":
1381
1290
  results = self.memory.search(tool_arg)
1382
- if not results:
1383
- tool_result = "No semantically relevant code found in the codebase."
1291
+ if not results: tool_result = "No semantically relevant code found in the codebase."
1384
1292
  else:
1385
1293
  formatted = []
1386
1294
  for r in results:
@@ -1393,12 +1301,14 @@ CRITICAL RULES:
1393
1301
  elif tool_name == "READ_FILE":
1394
1302
  content = self.file_manager.read_file(tool_arg)
1395
1303
  tool_result = f"Content of {tool_arg}:\n{content}" if content else f"Error: File '{tool_arg}' not found."
1396
- 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
+
1397
1306
  elif tool_name == "SEARCH_CODE":
1398
1307
  results = self.file_manager.search_in_files(tool_arg)
1399
1308
  if not results: tool_result = "No matches found."
1400
1309
  else: tool_result = "\n".join([f"{r['file']}: {' | '.join([m[1] for m in r['matches']])}" for r in results])
1401
1310
  console.print(f"\n[blue]🔍 Searched code for: {tool_arg}[/blue]"); tool_executed = True
1311
+
1402
1312
  elif tool_name == "RUN_TERMINAL":
1403
1313
  console.print(Panel(f"[yellow]{tool_arg}[/yellow]", title="[bold orange3]⚡ Terminal Command Request[/bold orange3]", border_style="orange3"))
1404
1314
  if Confirm.ask("Execute this command?", default=True):
@@ -1408,6 +1318,35 @@ CRITICAL RULES:
1408
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."
1409
1319
  else: tool_result = "User rejected the command."; console.print("[yellow]✗ Command rejected.[/yellow]")
1410
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
+
1411
1350
  if tool_executed:
1412
1351
  messages.append({"role": "assistant", "content": response})
1413
1352
  messages.append({"role": "user", "content": f"Tool result:\n{tool_result}\n\nContinue your task."})
@@ -1428,6 +1367,7 @@ CRITICAL RULES:
1428
1367
  "/frontend": "Toggle expert frontend architect mode (unique design systems)",
1429
1368
  "/data": "Toggle professional data analyst & BI mode (notebooks, charts, SQL)",
1430
1369
  "/reindex": "Re-build vector memory index after code changes",
1370
+ "/clear-memory": "Clear session conversation history",
1431
1371
  "/aboutme": "About the developer and SamCode",
1432
1372
  "/clear": "Clear the screen",
1433
1373
  "/exit": "Exit SamCode CLI"
@@ -1458,8 +1398,9 @@ CRITICAL RULES:
1458
1398
  elif user_input.lower() in ["/models", "/model"]: self.cmd_models()
1459
1399
  elif user_input.lower() in ["/caveman"]: self.cmd_caveman()
1460
1400
  elif user_input.lower() in ["/frontend"]: self.cmd_frontend()
1461
- elif user_input.lower() in ["/data"]: self.cmd_data() # NEW: Handle data mode toggle
1401
+ elif user_input.lower() in ["/data"]: self.cmd_data()
1462
1402
  elif user_input.lower() in ["/reindex"]: self.cmd_reindex()
1403
+ elif user_input.lower() in ["/clear-memory"]: self.cmd_clear_memory()
1463
1404
  elif user_input.lower() in ["/aboutme", "/about"]: self.cmd_aboutme()
1464
1405
  elif user_input.lower() in ["/git", "/g"]: self.cmd_git()
1465
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.4
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
@@ -6,7 +6,7 @@ long_description = (this_directory / "README.md").read_text(encoding="utf-8")
6
6
 
7
7
  setup(
8
8
  name='samcode-cli',
9
- version='1.0.4', # IMPORTANT: Bumped version!
9
+ version='1.0.5', # IMPORTANT: Bumped version!
10
10
  description='An autonomous AI coding agent that runs in your terminal.',
11
11
  long_description=long_description,
12
12
  long_description_content_type="text/markdown",
File without changes
File without changes
File without changes