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