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.
- {samcode_cli-1.0.4/samcode_cli.egg-info → samcode_cli-1.0.5}/PKG-INFO +1 -1
- {samcode_cli-1.0.4 → samcode_cli-1.0.5}/samcode.py +312 -371
- {samcode_cli-1.0.4 → samcode_cli-1.0.5/samcode_cli.egg-info}/PKG-INFO +1 -1
- {samcode_cli-1.0.4 → samcode_cli-1.0.5}/setup.py +1 -1
- {samcode_cli-1.0.4 → samcode_cli-1.0.5}/MANIFEST.in +0 -0
- {samcode_cli-1.0.4 → samcode_cli-1.0.5}/README.md +0 -0
- {samcode_cli-1.0.4 → samcode_cli-1.0.5}/samcode_cli.egg-info/SOURCES.txt +0 -0
- {samcode_cli-1.0.4 → samcode_cli-1.0.5}/samcode_cli.egg-info/dependency_links.txt +0 -0
- {samcode_cli-1.0.4 → samcode_cli-1.0.5}/samcode_cli.egg-info/entry_points.txt +0 -0
- {samcode_cli-1.0.4 → samcode_cli-1.0.5}/samcode_cli.egg-info/requires.txt +0 -0
- {samcode_cli-1.0.4 → samcode_cli-1.0.5}/samcode_cli.egg-info/top_level.txt +0 -0
- {samcode_cli-1.0.4 → samcode_cli-1.0.5}/setup.cfg +0 -0
|
@@ -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.
|
|
6
|
-
|
|
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
|
-
#
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
|
149
|
-
|
|
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
|
-
|
|
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:
|
|
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":
|
|
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
|
-
|
|
145
|
+
if not self.model: return []
|
|
186
146
|
query_embedding = self.model.encode([query]).tolist()
|
|
187
|
-
results = self.collection.query(
|
|
188
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
220
|
-
|
|
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
|
-
|
|
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]:
|
|
264
|
-
|
|
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
|
-
|
|
298
|
-
|
|
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
|
-
|
|
308
|
-
elif ext
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
elif ext == '.
|
|
312
|
-
|
|
313
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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}
|
|
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
|
-
|
|
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
|
-
|
|
396
|
-
|
|
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(
|
|
422
|
-
|
|
423
|
-
|
|
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.
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
761
|
-
self.data_session = {"datasets": {}, "analysis_steps": [], "agreed_notebook": None}
|
|
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
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
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.
|
|
821
|
-
|
|
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
|
-
|
|
828
|
-
|
|
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]
|
|
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]
|
|
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]
|
|
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
|
-
|
|
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("
|
|
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
|
-
|
|
1057
|
-
|
|
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
|
|
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? (
|
|
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]
|
|
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]
|
|
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]
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
1278
|
-
for doc in self.session_context:
|
|
1279
|
-
|
|
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 = "
|
|
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
|
-
|
|
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
|
|
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.
|
|
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)
|
|
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)
|
|
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]📖
|
|
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()
|
|
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"):
|
|
@@ -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.
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|