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