diff --git a/.github/workflows/build-release.yml b/.github/workflows/build-release.yml new file mode 100644 index 0000000..2aa7e99 --- /dev/null +++ b/.github/workflows/build-release.yml @@ -0,0 +1,97 @@ +name: Build & Release Desktop App + +on: + workflow_dispatch: + inputs: + version: + description: "NPM-AutoCode-AI big Update." + required: true + default: "v3.0.0" + +permissions: + contents: write + +jobs: + build: + name: Build (${{ matrix.os }}) + strategy: + fail-fast: false + matrix: + include: + - os: ubuntu-latest + artifact_name: NPM-AutoCode-AI-linux + data_sep: ":" + - os: windows-latest + artifact_name: NPM-AutoCode-AI-windows + data_sep: ";" + - os: macos-latest + artifact_name: NPM-AutoCode-AI-macos + data_sep: ":" + runs-on: ${{ matrix.os }} + + steps: + - name: Checkout NPM-AutoCode-AI + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.11" + + # npmai-agents / npmai are already pinned in requirements.txt and published + # on PyPI, so they install straight from there — no separate source build needed. + - name: Install app dependencies + working-directory: Desktop_App + run: | + pip install -r requirements.txt + pip install pyinstaller + pip install Pillow + pip install --force-reinstall --no-binary charset_normalizer charset_normalizer + + - name: Build with PyInstaller + shell: bash + working-directory: Desktop_App + run: | + pyinstaller --noconfirm --onedir --windowed \ + --collect-all charset_normalizer \ + --name "NPM-AutoCode-AI" \ + --icon "../npmai.png" \ + --add-data "../npmai.png${{ matrix.data_sep }}." \ + --add-data "app_config.json${{ matrix.data_sep }}." \ + app.py + + - name: Zip build output (Linux/macOS) + if: matrix.os != 'windows-latest' + working-directory: Desktop_App/dist + run: zip -r "../../${{ matrix.artifact_name }}.zip" . + + - name: Zip build output (Windows) + if: matrix.os == 'windows-latest' + working-directory: Desktop_App/dist + run: Compress-Archive -Path * -DestinationPath "../../${{ matrix.artifact_name }}.zip" + + - name: Upload build artifact + uses: actions/upload-artifact@v4 + with: + name: ${{ matrix.artifact_name }} + path: ${{ matrix.artifact_name }}.zip + + release: + name: Publish Release + needs: build + runs-on: ubuntu-latest + steps: + - name: Download all built zips + uses: actions/download-artifact@v4 + with: + path: artifacts + + - name: Create release with all three zips attached + uses: softprops/action-gh-release@v2 + with: + tag_name: ${{ github.event.inputs.version }} + name: "NPM-AutoCode-AI ${{ github.event.inputs.version }}" + files: | + artifacts/NPM-AutoCode-AI-linux/NPM-AutoCode-AI-linux.zip + artifacts/NPM-AutoCode-AI-windows/NPM-AutoCode-AI-windows.zip + artifacts/NPM-AutoCode-AI-macos/NPM-AutoCode-AI-macos.zip diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml new file mode 100644 index 0000000..e71b99c --- /dev/null +++ b/.github/workflows/codeql.yml @@ -0,0 +1,51 @@ +name: "CodeQL Advanced" + +on: + push: + branches: [ "main" ] + pull_request: + branches: [ "main" ] + schedule: + - cron: '41 21 * * 6' + +jobs: + analyze: + name: Analyze (${{ matrix.language }}) + runs-on: ${{ (matrix.language == 'swift' && 'macos-latest') || 'ubuntu-latest' }} + permissions: + security-events: write + + packages: read + + actions: read + contents: read + + strategy: + fail-fast: false + matrix: + include: + - language: python + build-mode: none + - language: javascript + build-mode: none + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Initialize CodeQL + uses: github/codeql-action/init@v4 + with: + languages: ${{ matrix.language }} + build-mode: ${{ matrix.build-mode }} + + - name: Run manual build steps + if: matrix.build-mode == 'manual' + shell: bash + run: | + echo "Reviewing code" + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v4 + with: + category: "/language:${{matrix.language}}" diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..beb22ef --- /dev/null +++ b/.gitignore @@ -0,0 +1,40 @@ +# Python +__pycache__/ +*.py[cod] +*.pyo +*.egg-info/ +dist/ +build/ +*.egg +*.whl + +# Virtual environments +venv/ +.venv/ +env/ +.env/ + +# IDE +.vscode/ +.idea/ +*.swp +*.swo +*~ + +# OS +.DS_Store +Thumbs.db + +# User config & credentials +.npmai_agent/ +Desktop_App/app_config.json +LICENSE +README.md + +# Build artifacts +*.exe +*.zip +*.dmg + +# Logs +*.log diff --git a/AutoCode.py b/AutoCode.py deleted file mode 100644 index 2793f53..0000000 --- a/AutoCode.py +++ /dev/null @@ -1,29 +0,0 @@ -from langchain_core.prompts import PromptTemplate -from npmai import Ollama -from langchain_core.output_parsers import StrOutputParser - -prompt = PromptTemplate( - input_variables=["input"], - template="Hey you are helpfull code assistant that write code just write code nothing else of it and maintain proper indentation and no hectics just imagine you will give the code and we will execute it, you will be asked to generate code aboout a query generate code,this is the query:{input}" -) - -user_actual_query = input("Describe the task you want to automate with NPM AutoCode AI:") - -llm = Ollama( - model="codeLLaMA-Instruct 7b", - temperature=1 -) - -parser=StrOutputParser() - -chain=prompt | llm | parser -response=chain.invoke(prompt.format(input=user_actual_query)) -print(response) - -cleaned_response = response.strip() -if cleaned_response.startswith('```python'): - cleaned_response = cleaned_response[len('```python'):] -if cleaned_response.endswith('```'): - cleaned_response = cleaned_response[:-len('```')] - -exec(cleaned_response.strip()) diff --git a/Desktop_App/app.py b/Desktop_App/app.py new file mode 100644 index 0000000..c7c4f90 --- /dev/null +++ b/Desktop_App/app.py @@ -0,0 +1,1515 @@ +""" +app.py +NPM-AutoCode-AI Desktop — v4 (npmai_agents edition) + +THIS IS NOT YET DEPLOYED. +Here it is about Desktop APP. +""" +import sys, os, math, random, json +from pathlib import Path +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) + + +def resource_path(filename: str) -> str: + """Resolve a bundled data file (icon, config) whether running from source + or from a PyInstaller-frozen build. PyInstaller unpacks --add-data files + next to sys.executable (onedir) or into sys._MEIPASS (onefile); neither + is the current working directory, so a bare relative path silently fails.""" + base = getattr(sys, "_MEIPASS", None) or os.path.dirname(os.path.abspath(__file__)) + return os.path.join(base, filename) + + +APP_ICON_PATH = resource_path("npmai.png") + +from npmai_agents import AgentBrain, Workspace, CredStore + +from PySide6.QtWidgets import ( + QApplication, QWidget, QVBoxLayout, QHBoxLayout, QLabel, + QTextEdit, QLineEdit, QPushButton, QFrame, QScrollArea, + QStackedWidget, QSizePolicy, QGraphicsOpacityEffect, + QTabWidget, QDialog, QDialogButtonBox, QMessageBox, + QGroupBox, QComboBox +) +from PySide6.QtCore import ( + QThread, Signal, Qt, QTimer, QPointF, QRectF, QRect, + QPropertyAnimation, QEasingCurve, +) +from PySide6.QtGui import ( + QFont, QColor, QPalette, QLinearGradient, QPainter, QBrush, QPen, + QPainterPath, QRadialGradient, QIcon +) + +P = { + "void": "#04030C", "deep": "#080618", "space": "#0D0B20", + "panel": "#111028", "lift": "#171530", "ridge": "#1E1C38", + "rim": "#2A2748", "glow": "#3D3960", + "mint": "#2AFFA0", "cyan": "#00E5FF", "violet":"#A78BFA", + "rose": "#FF6B9D", "amber": "#FFB347", "sky": "#38BDF8", + "bright": "#F0EEFF", "mid": "#8E8AAE", "dim": "#4E4B6A", + "ghost": "#2A2744", +} + +# ── LLM provider registry — mirrors npmai_agents.core backends + cli.build_backend ── +# Each entry: key -> (display label, [(field_key, field_label, is_secret), ...]) +# "model" is handled separately (every provider except a couple takes a model/model_id name). +LLM_PROVIDERS = { + "npmai": {"label": "🌐 NPMAI (cloud Ollama — default)", "needs_key": False, "model_label": "Model", "model_default": "llama3.2:3b", "extra_fields": []}, + "local": {"label": "💻 Local Ollama", "needs_key": False, "model_label": "Model", "model_default": "llama3.2:3b", "extra_fields": []}, + "openai": {"label": "🟢 OpenAI", "needs_key": True, "model_label": "Model", "model_default": "gpt-4o", "extra_fields": []}, + "anthropic": {"label": "🟣 Anthropic", "needs_key": True, "model_label": "Model", "model_default": "claude-sonnet-4-6", "extra_fields": []}, + "gemini": {"label": "🔵 Google Gemini", "needs_key": True, "model_label": "Model", "model_default": "gemini-2.0-flash", "extra_fields": []}, + "groq": {"label": "⚡ Groq", "needs_key": True, "model_label": "Model", "model_default": "llama-3.3-70b-versatile", "extra_fields": []}, + "mistral": {"label": "🌬 Mistral", "needs_key": True, "model_label": "Model", "model_default": "mistral-large-latest", "extra_fields": []}, + "cohere": {"label": "🔶 Cohere", "needs_key": True, "model_label": "Model", "model_default": "command-r-plus", "extra_fields": []}, + "azure": {"label": "🔷 Azure OpenAI", "needs_key": True, "model_label": "Deployment", "model_default": "", "extra_fields": [("endpoint","Endpoint URL"),("api_version","API version (default 2024-08-01-preview)")]}, + "bedrock": {"label": "🟠 AWS Bedrock", "needs_key": False, "model_label": "Model ID", "model_default": "anthropic.claude-3-sonnet-20240229-v1:0", "extra_fields": [("region","AWS region (default us-east-1)")]}, + "hf": {"label": "🤗 HuggingFace", "needs_key": True, "model_label": "Model", "model_default": "meta-llama/Llama-3.1-8B-Instruct", "extra_fields": []}, + "llamacpp": {"label": "🦙 llama.cpp (local server)", "needs_key": False, "model_label": "Base URL", "model_default": "http://localhost:8080", "extra_fields": []}, +} + +# The 6 AgentBrain roles/stages that can each use a different LLM +AGENT_STAGES = [ + ("planner", "🧭 Planner", "Breaks the task into atomic steps"), + ("tool_manager", "🧰 Tool Manager", "Selects which of the 1371 tools to use"), + ("coder", "👨‍💻 Coder", "Writes the Python for each step"), + ("auditor", "🛡 Auditor", "Reviews code for safety before execution"), + ("verifier", "✅ Verifier", "Confirms a step actually completed"), + ("chatter", "💬 Chatter", "Handles plain conversation (non-task replies)"), +] + +_LLM_CONFIG_PATH = Path.home() / ".npmai_agent" / "llm_roles.json" + + +def _load_llm_stage_config() -> dict: + cfg = {} + if _LLM_CONFIG_PATH.exists(): + try: + cfg = json.loads(_LLM_CONFIG_PATH.read_text()) + except Exception: + cfg = {} + + for stage_key, _, _ in AGENT_STAGES: + if stage_key not in cfg: + cfg[stage_key] = { + "provider": "npmai", + "model": LLM_PROVIDERS["npmai"]["model_default"] + } + return cfg + + +def _save_llm_stage_config(cfg: dict): + _LLM_CONFIG_PATH.parent.mkdir(exist_ok=True) + _LLM_CONFIG_PATH.write_text(json.dumps(cfg, indent=2)) + + +def _credstore_delete(name: str): + """CredStore has no delete() — this mirrors its own encryption logic to remove one group.""" + from cryptography.fernet import Fernet + if not CredStore._PATH.exists(): + return + try: + f = Fernet(CredStore._key()) + store = json.loads(f.decrypt(CredStore._PATH.read_bytes())) + if name in store: + del store[name] + CredStore._PATH.write_bytes(f.encrypt(json.dumps(store).encode())) + except Exception: + pass + + +def _build_llm_backend(provider: str, model: str): + """Mirrors npmai_agents.cli.build_backend — constructs a real LLMBackend from CredStore creds.""" + from npmai_agents import (Ollama_Local, OpenAIBackend, AnthropicBackend, GeminiBackend, + GroqBackend, MistralBackend, CohereBackend, AzureOpenAIBackend, + BedrockBackend, HuggingFaceBackend, LlamaCppBackend) + from npmai import Ollama + p = provider.lower() + if p == "npmai": return Ollama(model=model) + if p == "local": return Ollama_Local(model=model) + if p == "openai": return OpenAIBackend(model=model, api_key=CredStore.load("openai").get("api_key","")) + if p == "anthropic": return AnthropicBackend(model=model, api_key=CredStore.load("anthropic").get("api_key","")) + if p == "gemini": return GeminiBackend(model=model, api_key=CredStore.load("gemini").get("api_key","")) + if p == "groq": return GroqBackend(model=model, api_key=CredStore.load("groq").get("api_key","")) + if p == "mistral": return MistralBackend(model=model, api_key=CredStore.load("mistral").get("api_key","")) + if p == "cohere": return CohereBackend(model=model, api_key=CredStore.load("cohere").get("api_key","")) + if p == "azure": + c = CredStore.load("azure") + return AzureOpenAIBackend(api_key=c.get("api_key",""), endpoint=c.get("endpoint",""), + deployment=model, api_version=c.get("api_version","2024-08-01-preview")) + if p == "bedrock": + c = CredStore.load("bedrock") + return BedrockBackend(model_id=model, region=c.get("region","us-east-1")) + if p == "hf": return HuggingFaceBackend(model=model, api_key=CredStore.load("hf").get("api_key","")) + if p == "llamacpp": return LlamaCppBackend(base_url=model or "http://localhost:8080") + return Ollama(model=model) # unknown provider -> safe fallback + + +_TASK_KEYWORDS = [ + "file", "folder", "git", "github", "gitlab", "docker", "kubernetes", "k8s", + "terraform", "aws", "s3", "lambda", "cloudflare", "vercel", "netlify", "railway", + "stripe", "razorpay", "shopify", "invoice", "crm", "inventory", "contract", + "email", "smtp", "teams", "zoom", "twilio", "sendgrid", "webhook", "calendar", + "notion", "linear", "asana", "trello", "clickup", "todoist", "obsidian", + "figma", "blender", "svg", "canva", "diagram", "3d", + "scrape", "download", "screenshot", "database", "report", "chart", + "ffmpeg", "youtube", "audio", "video", "image", "resize", "convert", "podcast", + "ssh", "terminal", "run command", "backup", "zip", "unzip", "schedule", + "encrypt", "scan", "deploy", "network", "printer", "clipboard", +] + + +def _looks_like_task(text: str) -> bool: + words = text.strip().split() + if len(words) > 6: + return True + lower = text.lower() + return any(kw in lower for kw in _TASK_KEYWORDS) + + +class AgentWorker(QThread): + log_sig = Signal(str) + progress_sig = Signal(int) + status_sig = Signal(str) + done_sig = Signal(bool, str) + bubble_sig = Signal(str, bool) + + def __init__(self, task: str): + super().__init__() + self.task = task + self._killed = [False] + self._brain = None + + def kill(self): + self._killed[0] = True + if self._brain and self._brain.executor: + self._brain.executor.kill() + + def run(self): + stage_cfg = _load_llm_stage_config() + backends = {} + for stage_key, _, _ in AGENT_STAGES: + s = stage_cfg.get(stage_key, {"provider": "npmai", "model": LLM_PROVIDERS["npmai"]["model_default"]}) + try: + backends[stage_key] = _build_llm_backend(s.get("provider","npmai"), s.get("model","")) + except Exception as e: + self.log_sig.emit(f'LLM config error for {stage_key} ({s.get("provider")}): {e} — falling back to default.') + backends[stage_key] = None + self._brain = AgentBrain( + log_cb = self.log_sig.emit, + progress_cb = self.progress_sig.emit, + status_cb = self.status_sig.emit, + planner = backends.get("planner"), + tool_manager = backends.get("tool_manager"), + coder = backends.get("coder"), + auditor = backends.get("auditor"), + verifier = backends.get("verifier"), + chatter = backends.get("chatter"), + ) + if _looks_like_task(self.task): + ok = self._brain.run_task(self.task, killed_flag=self._killed) + msg = "✓ Task completed!" if ok else "✗ Task failed." + self.bubble_sig.emit(msg, True) + self.done_sig.emit(ok, msg) + else: + resp = self._brain.chat(self.task) + self.bubble_sig.emit(resp, True) + self.progress_sig.emit(100) + self.done_sig.emit(True, resp) + + +class CosmicBG(QWidget): + def __init__(self, parent=None): + super().__init__(parent) + self.setAttribute(Qt.WA_TransparentForMouseEvents) + self.setAttribute(Qt.WA_TranslucentBackground) + self._phase = 0.0 + self._particles = self._init_particles(140) + self._orbs = [ + (0.12, 0.22, 460, QColor(42,255,160,20), 0.007, 0.0), + (0.82, 0.38, 380, QColor(167,139,250,16), 0.005, 2.1), + (0.50, 0.78, 310, QColor(0,229,255,14), 0.009, 4.2), + (0.28, 0.68, 260, QColor(255,107,157,12), 0.006, 1.0), + (0.88, 0.82, 210, QColor(255,179,71,11), 0.011, 3.3), + ] + QTimer(self, timeout=self._tick, interval=16).start() + + def _init_particles(self, n): + cols = ["#2AFFA0","#00E5FF","#A78BFA","#FF6B9D","#FFB347","#38BDF8"] + return [{"x":random.random(),"y":random.random(), + "vx":(random.random()-0.5)*0.35,"vy":(random.random()-0.5)*0.35, + "r":random.random()*1.6+0.3, + "col":QColor(random.choice(cols)), + "ba":random.randint(50,150), + "life":random.random()} for _ in range(n)] + + def _tick(self): + self._phase = (self._phase + 0.011) % (math.pi * 2) + w,h = max(self.width(),1), max(self.height(),1) + for p in self._particles: + p["x"] += p["vx"]/w*100; p["y"] += p["vy"]/h*100 + p["life"] += 0.003 + if p["x"]<0 or p["x"]>1 or p["y"]<0 or p["y"]>1 or p["life"]>1: + p["x"]=random.random(); p["y"]=random.random(); p["life"]=0 + self.update() + + def paintEvent(self, _): + p = QPainter(self); p.setRenderHint(QPainter.Antialiasing) + w,h = self.width(), self.height() + p.fillRect(0,0,w,h, QColor(P["void"])) + for cx_r,cy_r,r,col,spd,off in self._orbs: + cx = cx_r*w + math.sin(self._phase*spd*80+off)*55 + cy = cy_r*h + math.cos(self._phase*spd*60+off)*40 + g = QRadialGradient(cx,cy,r); g.setColorAt(0,col) + outer=QColor(col); outer.setAlpha(0); g.setColorAt(1,outer) + p.fillRect(0,0,w,h,QBrush(g)) + p.setPen(QPen(QColor(255,255,255,4),1)) + for gx in range(0,w+70,70): p.drawLine(gx,0,gx,h) + for gy in range(0,h+70,70): p.drawLine(0,gy,w,gy) + p.setPen(Qt.NoPen) + for pt in self._particles: + a = int(pt["ba"]*abs(math.sin(self._phase*1.4+pt["life"]*8))) + c=QColor(pt["col"]); c.setAlpha(max(15,min(200,a))) + p.setBrush(c); p.drawEllipse(QPointF(pt["x"]*w,pt["y"]*h),pt["r"],pt["r"]) + vg=QRadialGradient(w/2,h/2,max(w,h)*0.72) + vg.setColorAt(0,QColor(0,0,0,0)); vg.setColorAt(1,QColor(0,0,0,165)) + p.fillRect(0,0,w,h,QBrush(vg)); p.end() + +class GlowCard(QWidget): + def __init__(self, parent=None, accent="#2AFFA0", radius=18, alpha=205): + super().__init__(parent) + self._accent=QColor(accent); self._radius=radius + self._alpha=alpha; self._glow=0.0; self._hover=False + self.setAttribute(Qt.WA_TranslucentBackground) + QTimer(self,timeout=self._anim,interval=16).start() + + def _anim(self): + t=1.0 if self._hover else 0.0 + self._glow += (t-self._glow)*0.09 + if abs(self._glow-t)>0.01: self.update() + + def enterEvent(self,e): self._hover=True + def leaveEvent(self,e): self._hover=False + + def paintEvent(self,_): + p=QPainter(self); p.setRenderHint(QPainter.Antialiasing) + rect=self.rect().adjusted(2,2,-2,-2) + path=QPainterPath(); path.addRoundedRect(QRectF(rect),self._radius,self._radius) + p.setClipPath(path) + p.fillPath(path,QColor(17,16,40,self._alpha)) + sh=QLinearGradient(0,rect.top(),0,rect.top()+55) + sh.setColorAt(0,QColor(255,255,255,13)); sh.setColorAt(1,QColor(255,255,255,0)) + p.fillPath(path,QBrush(sh)); p.setClipping(False) + a=QColor(self._accent); border_a=int(50+(180-50)*self._glow) + a.setAlpha(border_a); p.setPen(QPen(a,1.5)); p.drawPath(path) + if self._glow>0.05: + for ring in range(3): + exp=(ring+1)*3 + hp=QPainterPath() + hp.addRoundedRect(QRectF(rect.adjusted(-exp,-exp,exp,exp)), + self._radius+exp,self._radius+exp) + ha=int(28*self._glow/(ring+1)) + hc=QColor(self._accent.red(),self._accent.green(),self._accent.blue(),ha) + p.setPen(QPen(hc,1)); p.setBrush(Qt.NoBrush); p.drawPath(hp) + p.end() + +class PulseBtn(QPushButton): + def __init__(self, text, accent="#2AFFA0", dark_text=True, parent=None): + super().__init__(text,parent) + self._ac=QColor(accent); self._dt=dark_text + self._h=0.0; self._pulse=0.0; self._pd=1; self._press=False + self.setCursor(Qt.PointingHandCursor); self.setFixedHeight(48) + self.setFont(QFont("Segoe UI",12,QFont.Bold)) + QTimer(self,timeout=self._tick,interval=16).start() + + def _tick(self): + self._pulse+=0.035*self._pd + if self._pulse>=1: self._pd=-1 + if self._pulse<=0: self._pd=1 + self.update() + + def enterEvent(self,e): self._h=1.0; super().enterEvent(e) + def leaveEvent(self,e): self._h=0.0; super().leaveEvent(e) + def mousePressEvent(self,e): self._press=True; super().mousePressEvent(e) + def mouseReleaseEvent(self,e): self._press=False; super().mouseReleaseEvent(e) + + def paintEvent(self,_): + p=QPainter(self); p.setRenderHint(QPainter.Antialiasing) + r=self.rect(); path=QPainterPath() + path.addRoundedRect(QRectF(r.adjusted(1,1,-1,-1)),13,13) + g=QLinearGradient(r.left(),r.top(),r.right(),r.bottom()) + base=QColor(self._ac) + light=base.darker(110) if self._press else base.lighter(int(108+self._h*14+self._pulse*8)) + g.setColorAt(0,base); g.setColorAt(1,light) + p.fillPath(path,QBrush(g)) + ga=int((0.3+self._pulse*0.2+self._h*0.25)*255*0.4) + for ring in range(3): + exp=(ring+1)*3; hp=QPainterPath() + hp.addRoundedRect(QRectF(r.adjusted(-exp+1,-exp+1,exp-1,exp-1)),13+exp,13+exp) + hc=QColor(self._ac.red(),self._ac.green(),self._ac.blue(),max(0,ga//(ring+1))) + p.setPen(QPen(hc,1)); p.setBrush(Qt.NoBrush); p.drawPath(hp) + p.setPen(QColor("#050310") if self._dt else QColor(P["bright"])) + p.setFont(self.font()); p.drawText(r,Qt.AlignCenter,self.text()); p.end() + +class GhostBtn(QPushButton): + def __init__(self,text,accent="#2AFFA0",parent=None): + super().__init__(text,parent); self._ac=QColor(accent); self._h=0.0 + self.setCursor(Qt.PointingHandCursor); self.setFixedHeight(48) + self.setFont(QFont("Segoe UI",11,QFont.DemiBold)) + QTimer(self,timeout=self.update,interval=16).start() + + def enterEvent(self,e): self._h=1.0; super().enterEvent(e) + def leaveEvent(self,e): self._h=0.0; super().leaveEvent(e) + + def paintEvent(self,_): + p=QPainter(self); p.setRenderHint(QPainter.Antialiasing) + r=self.rect(); path=QPainterPath() + path.addRoundedRect(QRectF(r.adjusted(1,1,-1,-1)),13,13) + p.fillPath(path,QColor(self._ac.red(),self._ac.green(),self._ac.blue(),int(self._h*30))) + ba=int(70+self._h*150) + p.setPen(QPen(QColor(self._ac.red(),self._ac.green(),self._ac.blue(),ba),1.5)) + p.drawPath(path) + tc=QColor(self._ac) if self._h>0.3 else QColor(P["mid"]) + p.setPen(tc); p.setFont(self.font()); p.drawText(r,Qt.AlignCenter,self.text()); p.end() + +class GlowInput(QLineEdit): + def __init__(self,placeholder="",parent=None): + super().__init__(parent); self.setPlaceholderText(placeholder) + self.setFixedHeight(52); self.setFont(QFont("Segoe UI",12)) + self._focused=False; self._g=0.0 + QTimer(self,timeout=self._tick,interval=16).start() + + def _tick(self): + t=1.0 if self._focused else 0.0 + self._g+=(t-self._g)*0.1 + self.setStyleSheet(f""" + QLineEdit{{background:rgba(14,12,32,{int(185+self._g*40)}); + border:1.5px solid rgba({int(42+self._g*160)},{int(142+self._g*100)},{int(self._g*60+180)},{int(80+self._g*175)}); + border-radius:13px;padding:0 18px;color:{P['bright']};font-size:12px; + selection-background-color:rgba(42,255,160,80);}} + QLineEdit::placeholder{{color:{P['dim']};}}""") + + def focusInEvent(self,e): self._focused=True; super().focusInEvent(e) + def focusOutEvent(self,e): self._focused=False; super().focusOutEvent(e) + +class GlowProgress(QWidget): + def __init__(self,parent=None): + super().__init__(parent); self.setFixedHeight(7) + self._val=0.0; self._target=0.0; self._phase=0.0 + QTimer(self,timeout=self._tick,interval=16).start() + + def set_value(self,v): self._target=v + + def _tick(self): + self._val+=(self._target-self._val)*0.07 + self._phase+=0.05; self.update() + + def paintEvent(self,_): + p=QPainter(self); p.setRenderHint(QPainter.Antialiasing) + w,h=self.width(),self.height() + tp=QPainterPath(); tp.addRoundedRect(QRectF(0,0,w,h),h/2,h/2) + p.fillPath(tp,QColor(P["ridge"])) + fw=w*self._val/100 + if fw>2: + fp=QPainterPath(); fp.addRoundedRect(QRectF(0,0,fw,h),h/2,h/2) + g=QLinearGradient(0,0,fw,0) + g.setColorAt(0,QColor("#2AFFA0")); g.setColorAt(0.5,QColor("#00E5FF")) + g.setColorAt(1,QColor("#A78BFA")); p.fillPath(fp,QBrush(g)) + sx=fw*(0.5+math.sin(self._phase)*0.5) + sg=QRadialGradient(sx,h/2,fw*0.3) + sg.setColorAt(0,QColor(255,255,255,55)); sg.setColorAt(1,QColor(255,255,255,0)) + p.setClipPath(fp); p.fillRect(QRectF(0,0,fw,h),QBrush(sg)); p.setClipping(False) + p.end() + +class ChatBubble(QWidget): + def __init__(self, text:str, is_agent:bool, parent=None): + super().__init__(parent) + self.setAttribute(Qt.WA_TranslucentBackground) + lay = QHBoxLayout(self); lay.setContentsMargins(8,4,8,4) + bubble = QLabel(text) + bubble.setWordWrap(True) + bubble.setFont(QFont("Segoe UI",12)) + bubble.setTextInteractionFlags(Qt.TextSelectableByMouse) + bubble.setMaximumWidth(680) + bubble.setSizePolicy(QSizePolicy.Preferred,QSizePolicy.Minimum) + if is_agent: + bubble.setStyleSheet(f"""QLabel{{background:rgba(17,16,40,220); + border:1px solid rgba(42,255,160,80); + border-radius:16px;border-top-left-radius:4px; + padding:12px 16px;color:{P['bright']};line-height:1.6;}}""") + lay.addWidget(bubble); lay.addStretch() + else: + bubble.setStyleSheet(f"""QLabel{{background:qlineargradient(x1:0,y1:0,x2:1,y2:1, + stop:0 rgba(42,255,160,40),stop:1 rgba(0,229,255,30)); + border:1px solid rgba(42,255,160,120); + border-radius:16px;border-top-right-radius:4px; + padding:12px 16px;color:{P['bright']};}}""") + lay.addStretch(); lay.addWidget(bubble) + eff = QGraphicsOpacityEffect(bubble); bubble.setGraphicsEffect(eff) + anim = QPropertyAnimation(eff,b"opacity",self) + anim.setDuration(350); anim.setStartValue(0); anim.setEndValue(1) + anim.setEasingCurve(QEasingCurve.OutCubic); anim.start() + self._anim = anim + +class NavBtn(QWidget): + clicked = Signal(int) + ICONS = ["💬","📋","🛠","⚙","👤","📖"] + LABELS = ["Agent","History","Tools","Settings","Founder","Docs"] + COLORS = [P["mint"],P["cyan"],P["amber"],P["violet"],P["rose"],P["sky"]] + + def __init__(self,idx,parent=None): + super().__init__(parent); self._idx=idx + self._active=False; self._h=0.0; self._hf=False + self.setCursor(Qt.PointingHandCursor); self.setFixedHeight(58) + self.setAttribute(Qt.WA_TranslucentBackground) + QTimer(self,timeout=self._tick,interval=16).start() + + def _tick(self): + t=1.0 if self._hf else 0.0 + self._h+=(t-self._h)*0.1; self.update() + + def set_active(self,v): self._active=v; self.update() + def enterEvent(self,e): self._hf=True; super().enterEvent(e) + def leaveEvent(self,e): self._hf=False; super().leaveEvent(e) + def mousePressEvent(self,e): self.clicked.emit(self._idx) + + def paintEvent(self,_): + p=QPainter(self); p.setRenderHint(QPainter.Antialiasing) + w,h=self.width(),self.height() + col=QColor(self.COLORS[self._idx]) + blend=1.0 if self._active else self._h + if blend>0.01: + path=QPainterPath() + path.addRoundedRect(QRectF(8,5,w-16,h-10),11,11) + p.fillPath(path,QColor(col.red(),col.green(),col.blue(),int(blend*(60 if self._active else 35)))) + if self._active: + p.setPen(QPen(QColor(col.red(),col.green(),col.blue(),110),1.5)) + p.drawPath(path) + if self._active: + bar=QPainterPath(); bar.addRoundedRect(QRectF(0,h*0.2,3,h*0.6),2,2) + p.fillPath(bar,col) + bg=QRadialGradient(1,h/2,22) + bg.setColorAt(0,QColor(col.red(),col.green(),col.blue(),75)) + bg.setColorAt(1,QColor(col.red(),col.green(),col.blue(),0)) + p.fillRect(0,0,30,h,QBrush(bg)) + p.setFont(QFont("Segoe UI",16)) + p.setPen(col if (self._active or blend>0.3) else QColor(P["mid"])) + p.drawText(QRect(14,0,34,h),Qt.AlignVCenter|Qt.AlignLeft,self.ICONS[self._idx]) + p.setPen(QColor(P["bright"]) if (self._active or blend>0.3) else QColor(P["mid"])) + p.setFont(QFont("Segoe UI",11,QFont.Bold if self._active else QFont.Normal)) + p.drawText(QRect(52,0,w-60,h),Qt.AlignVCenter|Qt.AlignLeft,self.LABELS[self._idx]) + p.end() + +class LoginDialog(QDialog): + """Email/password only, per spec. Appears only when the user clicks + 'Get MCP Link' — never blocks any other part of the app.""" + + def __init__(self, link_mgr, parent=None): + super().__init__(parent) + self._mgr = link_mgr + self.setWindowTitle("Log in — MCP Link") + self.setWindowIcon(QIcon(APP_ICON_PATH)) + self.setFixedSize(360, 260) + self.setStyleSheet(f"QDialog{{background:{P['void']};}}") + lay = QVBoxLayout(self); lay.setContentsMargins(28,24,28,24); lay.setSpacing(12) + + title = QLabel("🔗 Get your MCP Link") + title.setFont(QFont("Segoe UI",15,QFont.Bold)) + title.setStyleSheet(f"color:{P['mint']};background:transparent;") + sub = QLabel("Login is only needed here — chat and tasks never require it.") + sub.setFont(QFont("Segoe UI",9)); sub.setWordWrap(True) + sub.setStyleSheet(f"color:{P['mid']};background:transparent;") + lay.addWidget(title); lay.addWidget(sub) + + self._email = GlowInput("Email"); lay.addWidget(self._email) + self._pw = GlowInput("Password"); self._pw.setEchoMode(QLineEdit.Password) + lay.addWidget(self._pw) + + row = QHBoxLayout(); row.setSpacing(10) + login_btn = PulseBtn("Log in", P["mint"], True) + signup_btn = GhostBtn("Sign up", P["cyan"]) + login_btn.clicked.connect(self._do_login) + signup_btn.clicked.connect(self._do_signup) + row.addWidget(login_btn); row.addWidget(signup_btn) + lay.addLayout(row) + + self._status = QLabel(""); self._status.setWordWrap(True) + self._status.setFont(QFont("Segoe UI",9)) + self._status.setStyleSheet(f"color:{P['rose']};background:transparent;") + lay.addWidget(self._status) + + def _do_login(self): + try: + self._mgr.log_in(self._email.text().strip(), self._pw.text()) + self.accept() + except Exception as e: + self._status.setText(str(e)) + + def _do_signup(self): + try: + self._mgr.sign_up(self._email.text().strip(), self._pw.text()) + self.accept() + except Exception as e: + self._status.setText(str(e)) + +class Sidebar(QWidget): + page_changed = Signal(int) + + def __init__(self,parent=None): + super().__init__(parent); self.setFixedWidth(215) + self.setAttribute(Qt.WA_TranslucentBackground) + self._btns=[] + self._link_mgr = None + self._build() + + def _build(self): + lay=QVBoxLayout(self); lay.setContentsMargins(0,0,0,0); lay.setSpacing(0) + + lw=QWidget(); lw.setAttribute(Qt.WA_TranslucentBackground); lw.setFixedHeight(86) + ll=QVBoxLayout(lw); ll.setContentsMargins(18,20,18,10); ll.setSpacing(3) + lg=QLabel("⚙ NPM-AutoCode-AI"); lg.setFont(QFont("Segoe UI",14,QFont.Bold)) + lg.setStyleSheet(f"color:{P['mint']};background:transparent;") + lv=QLabel("v4.0 · npmai_agents"); lv.setFont(QFont("Segoe UI",9)) + lv.setStyleSheet(f"color:{P['dim']};background:transparent;") + ll.addWidget(lg); ll.addWidget(lv); lay.addWidget(lw) + self._sep(lay) + + self._mcp_lbl=QLabel(" ● MCP Link: not connected") + self._mcp_lbl.setFont(QFont("Segoe UI",9)) + self._mcp_lbl.setStyleSheet(f"color:{P['dim']};background:transparent;padding:6px 0;") + lay.addWidget(self._mcp_lbl) + self._sep(lay); lay.addSpacing(6) + + nl=QLabel(" NAVIGATION"); nl.setFont(QFont("Segoe UI",8,QFont.Bold)) + nl.setStyleSheet(f"color:{P['dim']};background:transparent;letter-spacing:3px;") + lay.addWidget(nl); lay.addSpacing(2) + + for i in range(6): + b=NavBtn(i); b.set_active(i==0) + b.clicked.connect(self._on_nav) + self._btns.append(b); lay.addWidget(b) + + lay.addStretch(); self._sep(lay) + + fw=QWidget(); fw.setAttribute(Qt.WA_TranslucentBackground) + fl=QVBoxLayout(fw); fl.setContentsMargins(12,10,12,16); fl.setSpacing(8) + + self._mcp_btn=QPushButton("🔗 Get MCP Link") + self._mcp_btn.setCursor(Qt.PointingHandCursor); self._mcp_btn.setFixedHeight(34) + self._mcp_btn.setStyleSheet(self._btn_style()) + self._mcp_btn.clicked.connect(self._get_mcp_link) + fl.addWidget(self._mcp_btn) + + for lbl,url in [("🐍 PyPI","https://pypi.org/project/npmai"), + ("⭐ GitHub","https://github.com/npmaiecosystem")]: + b2=QPushButton(lbl); b2.setCursor(Qt.PointingHandCursor); b2.setFixedHeight(32) + b2.setStyleSheet(self._btn_style()) + import webbrowser + b2.clicked.connect(lambda _,u=url: webbrowser.open(u)) + fl.addWidget(b2) + + eco=QLabel("Powered by NPMAI Ecosystem"); eco.setFont(QFont("Segoe UI",9)) + eco.setStyleSheet(f"color:{P['dim']};background:transparent;"); eco.setAlignment(Qt.AlignCenter) + fl.addWidget(eco); lay.addWidget(fw) + + def _btn_style(self): + return f"""QPushButton{{background:rgba(42,255,160,10);border:1px solid rgba(42,255,160,35); +border-radius:9px;color:{P['mid']};font-size:11px;font-family:'Segoe UI';}} +QPushButton:hover{{background:rgba(42,255,160,22);border-color:rgba(42,255,160,90);color:{P['mint']};}}""" + + def _sep(self,lay): + s=QFrame(); s.setFixedHeight(1) + s.setStyleSheet(f"background:{P['ghost']};border:none;"); lay.addWidget(s) + + def _on_nav(self,idx): + for i,b in enumerate(self._btns): b.set_active(i==idx) + self.page_changed.emit(idx) + + def _get_mcp_link(self): + """ import here, not at module load time — mcp_link.py needs supabase """ + try: + from mcp_link import MCPLinkManager, SupabaseAuthError + except ImportError as e: + QMessageBox.warning(self, "Missing dependency", + f"MCP link needs the 'supabase' package.\nRun: pip install supabase\n\n{e}") + return + + if self._link_mgr is None: + self._link_mgr = MCPLinkManager(log_cb=lambda s: self._mcp_lbl.setText(f" ● {s[:28]}")) + + if not self._link_mgr.is_logged_in: + dlg = LoginDialog(self._link_mgr, self) + if dlg.exec() != QDialog.Accepted: + return + + try: + link = self._link_mgr.get_or_create_link() + self._link_mgr.start_listener() + self._mcp_lbl.setText(" ● MCP Link: connected") + self._mcp_lbl.setStyleSheet(f"color:{P['mint']};background:transparent;padding:6px 0;") + QMessageBox.information(self, "Your MCP Link", + f"Paste this into Claude/Grok's custom connector settings:\n\n{link}") + except Exception as e: + QMessageBox.warning(self, "Couldn't get MCP link", str(e)) + + def paintEvent(self,_): + p=QPainter(self); p.setRenderHint(QPainter.Antialiasing) + g=QLinearGradient(0,0,self.width(),0) + g.setColorAt(0,QColor(6,4,18,245)); g.setColorAt(1,QColor(9,7,22,215)) + p.fillRect(self.rect(),QBrush(g)) + p.setPen(QPen(QColor(P["ghost"]),1)) + p.drawLine(self.width()-1,0,self.width()-1,self.height()); p.end() + +class LLMConfigDialog(QDialog): + """'Configure LLMs' — Part ① sets up credentials/args per provider (only the + fields that provider's backend class actually needs). Part ② assigns a + provider+model to each of the 6 AgentBrain stages. Saved locally so the + Agent tab uses it automatically on every run without repeating setup.""" + + def __init__(self, parent=None): + super().__init__(parent) + self.setWindowTitle("Configure LLMs") + self.setWindowIcon(QIcon(APP_ICON_PATH)) + self.resize(640, 720) + self.setStyleSheet(f"QDialog{{background:{P['void']};}}") + + outer=QVBoxLayout(self); outer.setContentsMargins(0,0,0,0) + scroll=QScrollArea(); scroll.setWidgetResizable(True) + scroll.setStyleSheet("QScrollArea{border:none;background:transparent;}") + page=QWidget(); page.setAttribute(Qt.WA_TranslucentBackground) + lay=QVBoxLayout(page); lay.setContentsMargins(26,22,26,22); lay.setSpacing(16) + + t=QLabel("🤖 Configure LLMs"); t.setFont(QFont("Segoe UI",17,QFont.Bold)) + t.setStyleSheet(f"color:{P['mint']};background:transparent;"); lay.addWidget(t) + sub=QLabel("① Add credentials for any provider you plan to use — only the fields that provider " + "actually needs are shown. ② Then assign which provider+model handles each of the 6 " + "pipeline stages. Everything is saved locally and reused automatically on every run.") + sub.setFont(QFont("Segoe UI",9)); sub.setWordWrap(True) + sub.setStyleSheet(f"color:{P['mid']};background:transparent;"); lay.addWidget(sub) + + h1=QLabel("① Provider Credentials"); h1.setFont(QFont("Segoe UI",13,QFont.Bold)) + h1.setStyleSheet(f"color:{P['bright']};background:transparent;"); lay.addWidget(h1) + + self._provider_fields={} + for pkey,meta in LLM_PROVIDERS.items(): + card=GlowCard(accent=P["cyan"],radius=16,alpha=200) + cl=QVBoxLayout(card); cl.setContentsMargins(22,16,22,16); cl.setSpacing(8) + hl=QLabel(meta["label"]); hl.setFont(QFont("Segoe UI",12,QFont.Bold)) + hl.setStyleSheet(f"color:{P['bright']};background:transparent;"); cl.addWidget(hl) + existing=CredStore.load(pkey) + fields={} + if meta["needs_key"]: + lbl=QLabel("API key"); lbl.setFont(QFont("Segoe UI",9)) + lbl.setStyleSheet(f"color:{P['mid']};background:transparent;"); cl.addWidget(lbl) + api_in=GlowInput(f"{pkey} API key"); api_in.setEchoMode(QLineEdit.Password) + if existing.get("api_key"): api_in.setText("●"*8) + cl.addWidget(api_in); fields["api_key"]=api_in + for fkey,flabel in meta["extra_fields"]: + lbl=QLabel(flabel); lbl.setFont(QFont("Segoe UI",9)) + lbl.setStyleSheet(f"color:{P['mid']};background:transparent;"); cl.addWidget(lbl) + inp=GlowInput(flabel) + if existing.get(fkey): inp.setText(existing.get(fkey)) + cl.addWidget(inp); fields[fkey]=inp + if not fields: + nolbl=QLabel("No credentials needed — runs without an API key.") + nolbl.setFont(QFont("Segoe UI",9)); nolbl.setStyleSheet(f"color:{P['dim']};background:transparent;") + cl.addWidget(nolbl) + else: + save_btn = PulseBtn(f"💾 Save {pkey}", P["cyan"], True) + save_btn.setFixedHeight(36) + + def make_save_handler(provider_key, field_widgets): + def _save(): + data = {} + for k, w in field_widgets.items(): + v = w.text().strip() + if v and v != "●" * 8: + data[k] = v + if data: + ex = CredStore.load(provider_key) + ex.update(data) + CredStore.save(provider_key, ex) + QMessageBox.information(self, "Saved", f"'{provider_key}' configuration saved.") + else: + QMessageBox.warning(self, "Nothing to save", "Please enter a real API key (don't leave the masked ●●●●●●●●).") + return _save + + save_btn.clicked.connect(make_save_handler(pkey, fields)) + cl.addWidget(save_btn) + lay.addWidget(card) + self._provider_fields[pkey]=fields + + sep=QFrame(); sep.setFixedHeight(1); sep.setStyleSheet(f"background:{P['ghost']};border:none;") + lay.addWidget(sep) + + h2=QLabel("② Assign Provider + Model per Stage"); h2.setFont(QFont("Segoe UI",13,QFont.Bold)) + h2.setStyleSheet(f"color:{P['bright']};background:transparent;"); lay.addWidget(h2) + + stage_cfg=_load_llm_stage_config() + self._stage_combo={}; self._stage_model={} + for skey,slabel,sdesc in AGENT_STAGES: + card=GlowCard(accent=P["violet"],radius=16,alpha=200) + cl=QVBoxLayout(card); cl.setContentsMargins(22,16,22,16); cl.setSpacing(8) + hl=QLabel(slabel); hl.setFont(QFont("Segoe UI",12,QFont.Bold)) + hl.setStyleSheet(f"color:{P['bright']};background:transparent;"); cl.addWidget(hl) + dl=QLabel(sdesc); dl.setFont(QFont("Segoe UI",9)) + dl.setStyleSheet(f"color:{P['mid']};background:transparent;"); cl.addWidget(dl) + combo=QComboBox() + for pkey,meta in LLM_PROVIDERS.items(): + combo.addItem(meta["label"], pkey) + cur=stage_cfg.get(skey,{}).get("provider","npmai") + idx=combo.findData(cur) + if idx>=0: combo.setCurrentIndex(idx) + combo.setStyleSheet(f"QComboBox{{background:rgba(255,255,255,8);border:1px solid {P['ghost']};" + f"border-radius:8px;color:{P['bright']};padding:6px 10px;}}") + model_in=GlowInput("model name") + saved_model=stage_cfg.get(skey,{}).get("model","") + model_in.setText(saved_model or LLM_PROVIDERS[cur]["model_default"]) + def _provider_changed(i, combo=combo, model_in=model_in): + pkey=combo.itemData(i) + if not model_in.text().strip(): + model_in.setText(LLM_PROVIDERS[pkey]["model_default"]) + combo.currentIndexChanged.connect(_provider_changed) + cl.addWidget(combo); cl.addWidget(model_in) + lay.addWidget(card) + self._stage_combo[skey]=combo; self._stage_model[skey]=model_in + + save_all_btn=PulseBtn("💾 Save Stage Assignments",P["mint"],True); save_all_btn.setFixedHeight(44) + save_all_btn.clicked.connect(self._save_all_stages) + lay.addWidget(save_all_btn) + + close_btn=GhostBtn("Close",P["rose"]); close_btn.clicked.connect(self.accept) + lay.addWidget(close_btn) + + lay.addStretch(); scroll.setWidget(page); outer.addWidget(scroll) + + def _save_all_stages(self): + cfg=_load_llm_stage_config() + missing=[] + for skey,slabel,_ in AGENT_STAGES: + combo=self._stage_combo[skey]; pkey=combo.currentData() + model_val=self._stage_model[skey].text().strip() or LLM_PROVIDERS[pkey]["model_default"] + meta=LLM_PROVIDERS[pkey] + if meta["needs_key"] and not CredStore.load(pkey).get("api_key"): + missing.append(f"{slabel} → {meta['label']}") + cfg[skey]={"provider":pkey,"model":model_val} + _save_llm_stage_config(cfg) + if missing: + QMessageBox.warning(self,"Missing API keys", + "Stage assignments saved, but these still need an API key added in section ① above " + "before they'll actually run:\n\n" + "\n".join(f"- {m}" for m in missing)) + else: + QMessageBox.information(self,"Saved","LLM configuration saved — used automatically on every run.") + + +class AgentPage(QWidget): + def __init__(self,parent=None): + super().__init__(parent) + self.setAttribute(Qt.WA_TranslucentBackground) + self._worker=None; self._killed=[False] + self._build() + + def _build(self): + outer=QVBoxLayout(self); outer.setContentsMargins(0,0,0,0) + topbar=QWidget(); topbar.setAttribute(Qt.WA_TranslucentBackground); topbar.setFixedHeight(64) + tbl=QHBoxLayout(topbar); tbl.setContentsMargins(36,12,36,12); tbl.setSpacing(12) + title=QLabel("NPM-AutoCode-AI"); title.setFont(QFont("Segoe UI",18,QFont.Bold)) + title.setStyleSheet(f"color:{P['mint']};background:transparent;") + sub=QLabel("Runs fully locally · no login required"); sub.setFont(QFont("Segoe UI",10)) + sub.setStyleSheet(f"color:{P['dim']};background:transparent;") + tc=QVBoxLayout(); tc.setSpacing(0); tc.addWidget(title); tc.addWidget(sub) + tbl.addLayout(tc); tbl.addStretch() + + for badge,col in [("⚡ 1371 Tools",P["mint"]),("🔒 Local-first",P["violet"]),("🤖 Agentic",P["cyan"])]: + b=QLabel(f" {badge} "); b.setFont(QFont("Segoe UI",9,QFont.Bold)) + b.setStyleSheet(f"color:{col};background:rgba(42,255,160,12);border:1px solid rgba(42,255,160,35);border-radius:9px;padding:3px 8px;") + tbl.addWidget(b) + + llm_cfg_btn=GhostBtn("⚙ Configure LLMs",P["violet"]); llm_cfg_btn.setFixedHeight(30) + llm_cfg_btn.clicked.connect(self._open_llm_config) + tbl.addWidget(llm_cfg_btn) + + sep=QFrame(); sep.setFixedHeight(1); sep.setStyleSheet(f"background:{P['ghost']};border:none;") + outer.addWidget(topbar); outer.addWidget(sep) + chat_scroll=QScrollArea(); chat_scroll.setWidgetResizable(True) + chat_scroll.setStyleSheet("QScrollArea{border:none;background:transparent;}") + chat_scroll.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff) + self._chat_container=QWidget(); self._chat_container.setAttribute(Qt.WA_TranslucentBackground) + self._chat_layout=QVBoxLayout(self._chat_container) + self._chat_layout.setContentsMargins(28,20,28,20); self._chat_layout.setSpacing(12) + self._chat_layout.addStretch() + chat_scroll.setWidget(self._chat_container) + self._chat_scroll=chat_scroll + outer.addWidget(chat_scroll,1) + bottom=QWidget(); bottom.setAttribute(Qt.WA_TranslucentBackground) + bl=QVBoxLayout(bottom); bl.setContentsMargins(28,14,28,20); bl.setSpacing(10) + prow=QHBoxLayout(); prow.setSpacing(10) + self._status_lbl=QLabel("Ready"); self._status_lbl.setFont(QFont("Segoe UI",10)) + self._status_lbl.setStyleSheet(f"color:{P['mid']};background:transparent;") + self._pct_lbl=QLabel(""); self._pct_lbl.setFont(QFont("Segoe UI",10,QFont.Bold)) + self._pct_lbl.setStyleSheet(f"color:{P['mint']};background:transparent;") + prow.addWidget(self._status_lbl); prow.addStretch(); prow.addWidget(self._pct_lbl) + bl.addLayout(prow) + self._prog=GlowProgress(); bl.addWidget(self._prog) + self._log_box=QTextEdit(); self._log_box.setReadOnly(True) + self._log_box.setFont(QFont("Cascadia Code",10)) + self._log_box.setFixedHeight(140) + self._log_box.setStyleSheet(f"""QTextEdit{{background:rgba(4,3,12,190); +border:1px solid {P['ghost']};border-radius:11px;padding:10px;color:{P['bright']};}}""") + self._log_box.setHtml(f'Execution logs appear here…') + bl.addWidget(self._log_box) + irow=QHBoxLayout(); irow.setSpacing(10) + self._input=GlowInput("Ask anything or describe a task to automate…") + self._input.returnPressed.connect(self._send) + self._run_btn=PulseBtn("▶ Run",P["mint"]); self._run_btn.setFixedWidth(100) + self._run_btn.clicked.connect(self._send) + self._kill_btn=GhostBtn("■ Kill",P["rose"]); self._kill_btn.setFixedWidth(90) + self._kill_btn.clicked.connect(self._kill); self._kill_btn.setEnabled(False) + self._voice_btn=GhostBtn("🎤",P["sky"]); self._voice_btn.setFixedWidth(60) + self._voice_btn.clicked.connect(self._voice_input) + irow.addWidget(self._input); irow.addWidget(self._run_btn) + irow.addWidget(self._kill_btn); irow.addWidget(self._voice_btn) + bl.addLayout(irow) + chips=QHBoxLayout(); chips.setSpacing(8) + self._chip_tasks=[ + ("📁 Rename files","Rename all files in Downloads adding today's date prefix"), + ("🌐 Scrape web","Scrape top 20 headlines from https://news.ycombinator.com save to news.xlsx on Desktop"), + ("⭐ GitHub issue","Create a GitHub issue titled 'test' in my repo"), + ("☁ AWS S3","List all objects in my S3 bucket"), + ("🔒 Zip files","Find all PDFs in Documents sort by date zip into archive.zip on Desktop"), + ("🖼 Resize imgs","Resize all images in Desktop/Photos folder to 800x600"), + ] + for label,task in self._chip_tasks: + c=QPushButton(label); c.setCursor(Qt.PointingHandCursor); c.setFixedHeight(30) + c.setStyleSheet(f"""QPushButton{{background:rgba(42,255,160,10); +border:1px solid rgba(42,255,160,38);border-radius:9px; +color:rgba(42,255,160,180);font-size:10px;padding:0 12px;}} +QPushButton:hover{{background:rgba(42,255,160,22);border-color:rgba(42,255,160,100);color:{P['mint']};}}""") + c.clicked.connect(lambda _,t=task: self._input.setText(t)) + chips.addWidget(c) + chips.addStretch(); bl.addLayout(chips) + sep2=QFrame(); sep2.setFixedHeight(1); sep2.setStyleSheet(f"background:{P['ghost']};border:none;") + outer.addWidget(sep2); outer.addWidget(bottom) + + def _open_llm_config(self): + dlg=LLMConfigDialog(self) + dlg.exec() + + def _add_bubble(self, text:str, is_agent:bool): + b=ChatBubble(text, is_agent) + self._chat_layout.insertWidget(self._chat_layout.count()-1, b) + QTimer.singleShot(50, lambda: self._chat_scroll.verticalScrollBar().setValue( + self._chat_scroll.verticalScrollBar().maximum())) + + def _log(self, html:str): + self._log_box.append(html) + self._log_box.verticalScrollBar().setValue(self._log_box.verticalScrollBar().maximum()) + + def _voice_input(self): + """If you are working with voice input kindly read the documentation of npmai_agents and this code uses + npmai_agents version 1.0.2, npmai_agents change frequently although not syntax but it will be better to read + before making any change.""" + """ old VoiceTool.listen(): SpeechAITool.transcribe_realtime """ + from npmai_agents.Tools_security_ai import SpeechAITool + r = SpeechAITool.transcribe_realtime(duration=5) + if r.success and r.data: + self._input.setText(r.data.get("text","")) + + def _kill(self): + if self._worker: self._worker.kill() + self._kill_btn.setEnabled(False) + self._run_btn.setEnabled(True); self._run_btn.setText("▶ Run") + + def _send(self): + task=self._input.text().strip() + if not task: return + self._input.clear() + self._add_bubble(task, False) + self._run_btn.setEnabled(False); self._run_btn.setText("⏳") + self._kill_btn.setEnabled(True) + self._prog.set_value(3) + self._log_box.clear() + self._worker=AgentWorker(task) + self._worker.log_sig.connect(self._log) + self._worker.progress_sig.connect(self._prog.set_value) + self._worker.progress_sig.connect(lambda v: self._pct_lbl.setText(f"{v}%")) + self._worker.status_sig.connect(lambda s: self._status_lbl.setText(s)) + self._worker.bubble_sig.connect(self._add_bubble) + self._worker.done_sig.connect(self._done) + self._worker.start() + + def _done(self, ok:bool, msg:str): + self._run_btn.setEnabled(True); self._run_btn.setText("▶ Run") + self._kill_btn.setEnabled(False) + self._status_lbl.setText("Done ✓" if ok else "Failed") + + +class HistoryPage(QWidget): + def __init__(self,parent=None): + super().__init__(parent); self.setAttribute(Qt.WA_TranslucentBackground); self._build() + + def _build(self): + outer=QVBoxLayout(self); outer.setContentsMargins(0,0,0,0) + scroll=QScrollArea(); scroll.setWidgetResizable(True) + scroll.setStyleSheet("QScrollArea{border:none;background:transparent;}") + page=QWidget(); page.setAttribute(Qt.WA_TranslucentBackground) + self._lay=QVBoxLayout(page); self._lay.setContentsMargins(36,36,36,36); self._lay.setSpacing(10) + header=QHBoxLayout() + t=QLabel("📋 Task History"); t.setFont(QFont("Segoe UI",20,QFont.Bold)) + t.setStyleSheet(f"color:{P['cyan']};background:transparent;") + refresh=GhostBtn("↺ Refresh",P["cyan"]); refresh.setFixedSize(110,36) + refresh.clicked.connect(self.load) + header.addWidget(t); header.addStretch(); header.addWidget(refresh) + self._lay.addLayout(header) + sep=QFrame(); sep.setFixedHeight(1); sep.setStyleSheet(f"background:{P['ghost']};border:none;") + self._lay.addWidget(sep); self._lay.addSpacing(10) + self._items_layout=QVBoxLayout(); self._lay.addLayout(self._items_layout) + self._lay.addStretch() + scroll.setWidget(page); outer.addWidget(scroll) + self.load() + + def load(self): + while self._items_layout.count(): + item=self._items_layout.takeAt(0) + if item.widget(): item.widget().deleteLater() + history=AgentBrain.load_task_history() + if not history: + lbl=QLabel("No tasks run yet. Go to Agent tab and run your first task!") + lbl.setFont(QFont("Segoe UI",12)); lbl.setStyleSheet(f"color:{P['mid']};background:transparent;") + self._items_layout.addWidget(lbl); return + for entry in history: + row=GlowCard(accent=P["mint"] if entry["success"] else P["rose"],radius=14,alpha=190) + row.setFixedHeight(70) + rl=QHBoxLayout(row); rl.setContentsMargins(20,12,20,12); rl.setSpacing(14) + dot=QLabel("✓" if entry["success"] else "✗") + dot.setFont(QFont("Segoe UI",14,QFont.Bold)) + dot.setStyleSheet(f"color:{P['mint'] if entry['success'] else P['rose']};background:transparent;") + dot.setFixedWidth(22) + tc=QVBoxLayout(); tc.setSpacing(2) + task_lbl=QLabel(entry["task"][:80]+("…" if len(entry["task"])>80 else "")) + task_lbl.setFont(QFont("Segoe UI",11)) + task_lbl.setStyleSheet(f"color:{P['bright']};background:transparent;") + time_lbl=QLabel(entry["time"][:19].replace("T"," ")) + time_lbl.setFont(QFont("Segoe UI",9)) + time_lbl.setStyleSheet(f"color:{P['dim']};background:transparent;") + tc.addWidget(task_lbl); tc.addWidget(time_lbl) + rl.addWidget(dot); rl.addLayout(tc); rl.addStretch() + self._items_layout.addWidget(row) + + +def _discover_real_tools(): + import npmai_agents as pkg + tools = [] + for attr_name in dir(pkg): + obj = getattr(pkg, attr_name) + if isinstance(obj, type) and hasattr(obj, "name") and hasattr(obj, "description"): + tools.append((getattr(obj, "name"), attr_name, getattr(obj, "description"))) + return sorted(tools, key=lambda t: t[1]) + + +class ToolsPage(QWidget): + def __init__(self,parent=None): + super().__init__(parent); self.setAttribute(Qt.WA_TranslucentBackground); self._build() + + def _build(self): + outer=QVBoxLayout(self); outer.setContentsMargins(0,0,0,0) + scroll=QScrollArea(); scroll.setWidgetResizable(True) + scroll.setStyleSheet("QScrollArea{border:none;background:transparent;}") + page=QWidget(); page.setAttribute(Qt.WA_TranslucentBackground) + lay=QVBoxLayout(page); lay.setContentsMargins(36,36,36,36); lay.setSpacing(20) + + tools = _discover_real_tools() + t=QLabel(f"🛠 Tool Registry — {len(tools)} Integrated Tools") + t.setFont(QFont("Segoe UI",20,QFont.Bold)) + t.setStyleSheet(f"color:{P['amber']};background:transparent;") + lay.addWidget(t) + desc=QLabel("Read directly from npmai_agents — always matches what's actually installed. " + "All tools are available to the agent automatically; just describe your task.") + desc.setFont(QFont("Segoe UI",11)); desc.setWordWrap(True) + desc.setStyleSheet(f"color:{P['mid']};background:transparent;") + lay.addWidget(desc) + sep=QFrame(); sep.setFixedHeight(1); sep.setStyleSheet(f"background:{P['ghost']};border:none;") + lay.addWidget(sep); lay.addSpacing(6) + + cols_palette = [P["mint"],P["cyan"],P["violet"],P["amber"],P["sky"],P["rose"]] + grid_cols = 2 + grid_row = None + for i, (tool_name, cls_name, description) in enumerate(tools): + if i % grid_cols == 0: + grid_row = QHBoxLayout(); grid_row.setSpacing(16); lay.addLayout(grid_row) + col = cols_palette[i % len(cols_palette)] + card=GlowCard(accent=col,radius=16,alpha=195); card.setFixedHeight(108) + cl=QHBoxLayout(card); cl.setContentsMargins(20,16,20,16); cl.setSpacing(14) + cv=QVBoxLayout(); cv.setSpacing(4) + n=QLabel(cls_name); n.setFont(QFont("Segoe UI",13,QFont.Bold)) + n.setStyleSheet(f"color:{P['bright']};background:transparent;") + c2=QLabel(tool_name); c2.setFont(QFont("Cascadia Code",9)) + c2.setStyleSheet(f"color:{col};background:transparent;") + d=QLabel(description[:140]+("…" if len(description)>140 else "")) + d.setFont(QFont("Segoe UI",10)); d.setWordWrap(True) + d.setStyleSheet(f"color:{P['mid']};background:transparent;") + cv.addWidget(n); cv.addWidget(c2); cv.addWidget(d) + cl.addLayout(cv) + grid_row.addWidget(card) + if len(tools) % grid_cols != 0 and grid_row is not None: + grid_row.addStretch() + + lay.addStretch() + scroll.setWidget(page); outer.addWidget(scroll) + + +class CredKeyValueDialog(QDialog): + """Generic credential-group editor — user names the group (cred_key) and adds + any number of key/value pairs. Used by '+ Add Credential Group' in Settings + and by the per-provider forms inside Configure LLMs on the Agent page.""" + + def __init__(self, parent=None, group_name="", existing_data=None, lock_name=False, + title="🔑 Credential Group", subtitle=None): + super().__init__(parent) + self.setWindowTitle(title.replace("🔑","").strip() or "Credential Group") + self.setMinimumWidth(440) + self.setStyleSheet(f"QDialog{{background:{P['void']};}}") + self._row_widgets = [] + + if os.path.exists(APP_ICON_PATH): + self.setWindowIcon(QIcon(APP_ICON_PATH)) + else: + print(f"Warning: npmai.png not found at {APP_ICON_PATH}") + + lay = QVBoxLayout(self); lay.setContentsMargins(26,22,26,22); lay.setSpacing(12) + + t = QLabel(title); t.setFont(QFont("Segoe UI",15,QFont.Bold)) + t.setStyleSheet(f"color:{P['mint']};background:transparent;"); lay.addWidget(t) + + sub = QLabel(subtitle or ("Give this group a name (e.g. 'twilio', 'mailchimp') — check Docs for " + "the exact keys a tool expects — then add each key/value pair below.")) + sub.setFont(QFont("Segoe UI",9)); sub.setWordWrap(True) + sub.setStyleSheet(f"color:{P['mid']};background:transparent;"); lay.addWidget(sub) + + self._name_input = GlowInput("Group name, e.g. twilio") + if group_name: self._name_input.setText(group_name) + self._name_input.setEnabled(not lock_name) + lay.addWidget(self._name_input) + + sep = QFrame(); sep.setFixedHeight(1); sep.setStyleSheet(f"background:{P['ghost']};border:none;") + lay.addWidget(sep) + + self._rows_container = QWidget(); self._rows_container.setAttribute(Qt.WA_TranslucentBackground) + self._rows_layout = QVBoxLayout(self._rows_container) + self._rows_layout.setContentsMargins(0,0,0,0); self._rows_layout.setSpacing(8) + lay.addWidget(self._rows_container) + + add_row_btn = GhostBtn("+ Add Key", P["cyan"]) + add_row_btn.clicked.connect(self._add_row) # Removed unnecessary lambda + lay.addWidget(add_row_btn) + + existing_data = existing_data or {} + if existing_data: + for k, v in existing_data.items(): self._add_row(k, v) + else: + self._add_row() + + btns = QDialogButtonBox(QDialogButtonBox.Save | QDialogButtonBox.Cancel) + btns.accepted.connect(self.accept); btns.rejected.connect(self.reject) + lay.addWidget(btns) + + def _add_row(self, key="", value=""): + row = QWidget(); row.setAttribute(Qt.WA_TranslucentBackground) + rl = QHBoxLayout(row); rl.setContentsMargins(0,0,0,0); rl.setSpacing(8) + key_in = GlowInput("key name, e.g. token"); key_in.setText(key) + val_in = GlowInput("value"); val_in.setText(value) + rm_btn = GhostBtn("✕", P["rose"]); rm_btn.setFixedWidth(36) + + def _remove(): + self._rows_layout.removeWidget(row); row.deleteLater() + self._row_widgets[:] = [r for r in self._row_widgets if r[2] is not row] + + rm_btn.clicked.connect(_remove) + rl.addWidget(key_in,1); rl.addWidget(val_in,2); rl.addWidget(rm_btn) + self._rows_layout.addWidget(row) + self._row_widgets.append((key_in, val_in, row)) + + def get_data(self): + """Returns (group_name:str, data:dict) — rows with an empty key are skipped.""" + name = self._name_input.text().strip() + data = {} + for key_in, val_in, _ in self._row_widgets: + k = key_in.text().strip(); v = val_in.text().strip() + if k: data[k] = v + return name, data + + + +class SettingsPage(QWidget): + def __init__(self,parent=None): + super().__init__(parent); self.setAttribute(Qt.WA_TranslucentBackground); self._build() + + def _section(self, lay, title, fields, cred_key, accent): + card=GlowCard(accent=accent,radius=18,alpha=200) + cl=QVBoxLayout(card); cl.setContentsMargins(28,22,28,22); cl.setSpacing(14) + h=QLabel(title); h.setFont(QFont("Segoe UI",13,QFont.Bold)) + h.setStyleSheet(f"color:{P['bright']};background:transparent;"); cl.addWidget(h) + sep=QFrame(); sep.setFixedHeight(1); sep.setStyleSheet(f"background:{P['ghost']};border:none;") + cl.addWidget(sep) + inputs={} + for field_key,label,placeholder,is_pw in fields: + lbl=QLabel(label); lbl.setFont(QFont("Segoe UI",10)) + lbl.setStyleSheet(f"color:{P['mid']};background:transparent;"); cl.addWidget(lbl) + inp=GlowInput(placeholder) + if is_pw: inp.setEchoMode(QLineEdit.Password) + existing=CredStore.load(cred_key).get(field_key,"") + if existing: inp.setText("●"*8 if is_pw else existing) + inputs[field_key]=inp; cl.addWidget(inp) + save_btn=PulseBtn(f"💾 Save {title.split()[0]} Credentials",accent,True) + save_btn.setFixedHeight(42) + def _save(ck=cred_key,inp_ref=inputs): + data={} + for k,w in inp_ref.items(): + v=w.text().strip() + if v and v!="●"*8: data[k]=v + existing=CredStore.load(ck) + existing.update(data); CredStore.save(ck,existing) + QMessageBox.information(self,"Saved",f"{ck} credentials saved securely.") + save_btn.clicked.connect(_save); cl.addWidget(save_btn) + lay.addWidget(card) + + def _build(self): + outer=QVBoxLayout(self); outer.setContentsMargins(0,0,0,0) + scroll=QScrollArea(); scroll.setWidgetResizable(True) + scroll.setStyleSheet("QScrollArea{border:none;background:transparent;}") + page=QWidget(); page.setAttribute(Qt.WA_TranslucentBackground) + lay=QVBoxLayout(page); lay.setContentsMargins(36,36,36,36); lay.setSpacing(20) + t=QLabel("⚙ Settings & Credentials"); t.setFont(QFont("Segoe UI",20,QFont.Bold)) + t.setStyleSheet(f"color:{P['violet']};background:transparent;"); lay.addWidget(t) + sub=QLabel("All credentials are encrypted with a machine-specific key (CredStore) and stored " + "locally. Never sent anywhere. No login needed for anything on this page.") + sub.setFont(QFont("Segoe UI",10)); sub.setWordWrap(True) + sub.setStyleSheet(f"color:{P['mid']};background:transparent;"); lay.addWidget(sub) + sep=QFrame(); sep.setFixedHeight(1); sep.setStyleSheet(f"background:{P['ghost']};border:none;") + lay.addWidget(sep); lay.addSpacing(6) + + ws_card=GlowCard(accent=P["cyan"],radius=16,alpha=195) + wl=QHBoxLayout(ws_card); wl.setContentsMargins(24,16,24,16); wl.setSpacing(16) + wl_v=QVBoxLayout(); wl_v.setSpacing(4) + wl_t=QLabel("🖥 Workspace Scanner"); wl_t.setFont(QFont("Segoe UI",13,QFont.Bold)) + wl_t.setStyleSheet(f"color:{P['bright']};background:transparent;") + wl_d=QLabel("Scans your Desktop, Downloads, Documents, Pictures, Videos, Music — agent uses this to understand your file system") + wl_d.setFont(QFont("Segoe UI",10)); wl_d.setWordWrap(True) + wl_d.setStyleSheet(f"color:{P['mid']};background:transparent;") + wl_v.addWidget(wl_t); wl_v.addWidget(wl_d) + scan_btn=PulseBtn("🔍 Scan Now",P["cyan"],True); scan_btn.setFixedSize(130,38) + def _scan(): + ws=Workspace(); ws.scan() + QMessageBox.information(self,"Scanned","Workspace scanned successfully!") + scan_btn.clicked.connect(_scan); wl.addLayout(wl_v); wl.addStretch(); wl.addWidget(scan_btn) + lay.addWidget(ws_card) + + # ── Credential sections — cred_key + fields verified against source ── + self._section(lay,"⭐ GitHub",[ + ("token","Personal access token (repo + issues scope)","ghp_xxxxxxxxxxxx",True), + ],"github",P["cyan"]) + + self._section(lay,"📧 Email (SMTP)",[ + ("email","Email address","your@gmail.com",False), + ("password","App password","Gmail app password",True), + ("smtp_host","SMTP host","smtp.gmail.com",False), + ("smtp_port","SMTP port","587",False), + ],"smtp",P["mint"]) + + self._section(lay,"📓 Notion",[ + ("token","Integration token","secret_xxxxxxxxxxxx",True), + ],"notion",P["amber"]) + + self._section(lay,"💳 Stripe",[ + ("secret_key","Secret key","sk_live_xxxxxxxxxxxx",True), + ],"stripe",P["violet"]) + + self._section(lay,"☁ AWS",[ + ("access_key_id","Access key ID","AKIA...",False), + ("secret_access_key","Secret access key","",True), + ("region","Region","us-east-1",False), + ],"aws",P["sky"]) + + note=QLabel("Need a tool that isn't listed above (Twilio, GitLab, Stripe alternatives, Mailchimp, " + "Notion, Zoom, Cloudflare, and 30+ more)? Use '+ Add Credential Group' below — see the " + "Docs tab for the exact cred_key and field names each tool expects.") + note.setFont(QFont("Segoe UI",9)); note.setWordWrap(True) + note.setStyleSheet(f"color:{P['dim']};background:transparent;"); lay.addWidget(note) + + # ── Generic custom credential groups — for any tool not hardcoded above ── + custom_hdr=QHBoxLayout(); custom_hdr.setSpacing(12) + custom_t=QLabel("🗂 Custom Credential Groups"); custom_t.setFont(QFont("Segoe UI",13,QFont.Bold)) + custom_t.setStyleSheet(f"color:{P['bright']};background:transparent;") + custom_hdr.addWidget(custom_t); custom_hdr.addStretch() + add_group_btn=PulseBtn("➕ Add Credential Group",P["violet"],True); add_group_btn.setFixedHeight(38) + add_group_btn.clicked.connect(self._open_add_dialog) + custom_hdr.addWidget(add_group_btn) + lay.addLayout(custom_hdr) + + self._custom_creds_container=QWidget(); self._custom_creds_container.setAttribute(Qt.WA_TranslucentBackground) + self._custom_creds_layout=QVBoxLayout(self._custom_creds_container) + self._custom_creds_layout.setContentsMargins(0,0,0,0); self._custom_creds_layout.setSpacing(10) + lay.addWidget(self._custom_creds_container) + self._refresh_custom_creds() + + mcp_card=GlowCard(accent=P["mint"],radius=16,alpha=195) + ml=QVBoxLayout(mcp_card); ml.setContentsMargins(24,18,24,18); ml.setSpacing(8) + mt=QLabel("🔗 MCP Link"); mt.setFont(QFont("Segoe UI",13,QFont.Bold)) + mt.setStyleSheet(f"color:{P['bright']};background:transparent;"); ml.addWidget(mt) + md=QLabel( + "Click 'Get MCP Link' in the sidebar to log in and receive your personal link. " + "Paste it into Claude/Grok's custom connector settings — this app then executes " + "already-audited code sent by the hosted MCP server and reports results back.\n" + "This is the ONLY feature in the app that requires login." + ) + md.setFont(QFont("Segoe UI",10)); md.setWordWrap(True) + md.setStyleSheet(f"color:{P['mid']};background:transparent;"); ml.addWidget(md) + lay.addWidget(mcp_card) + lay.addStretch(); scroll.setWidget(page); outer.addWidget(scroll) + + _HARDCODED_KEYS = {"github","smtp","notion","stripe","aws"} + + def _refresh_custom_creds(self): + while self._custom_creds_layout.count(): + item=self._custom_creds_layout.takeAt(0) + w=item.widget() + if w: w.deleteLater() + hidden=self._HARDCODED_KEYS | set(LLM_PROVIDERS.keys()) + try: names=[n for n in CredStore.all_keys() if n not in hidden] + except Exception: names=[] + if not names: + empty=QLabel("No custom credential groups yet — click '➕ Add Credential Group' above.") + empty.setFont(QFont("Segoe UI",9)); empty.setStyleSheet(f"color:{P['dim']};background:transparent;") + self._custom_creds_layout.addWidget(empty); return + for name in names: + data=CredStore.load(name) + card=GlowCard(accent=P["sky"],radius=14,alpha=195) + cl=QHBoxLayout(card); cl.setContentsMargins(20,14,20,14); cl.setSpacing(12) + info=QVBoxLayout(); info.setSpacing(2) + nl=QLabel(f"🔑 {name}"); nl.setFont(QFont("Segoe UI",11,QFont.Bold)) + nl.setStyleSheet(f"color:{P['bright']};background:transparent;") + kl=QLabel(", ".join(data.keys()) if data else "no keys saved") + kl.setFont(QFont("Segoe UI",9)); kl.setStyleSheet(f"color:{P['mid']};background:transparent;") + info.addWidget(nl); info.addWidget(kl) + cl.addLayout(info); cl.addStretch() + edit_btn=GhostBtn("Edit",P["cyan"]); edit_btn.setFixedWidth(70) + edit_btn.clicked.connect(lambda _,n=name: self._open_edit_dialog(n)) + del_btn=GhostBtn("Delete",P["rose"]); del_btn.setFixedWidth(70) + del_btn.clicked.connect(lambda _,n=name: self._delete_group(n)) + cl.addWidget(edit_btn); cl.addWidget(del_btn) + self._custom_creds_layout.addWidget(card) + + def _open_add_dialog(self): + dlg=CredKeyValueDialog(self) + if dlg.exec(): + name,data=dlg.get_data() + if not name: + QMessageBox.warning(self,"Missing name","Please enter a group name."); return + if not data: + QMessageBox.warning(self,"No keys","Add at least one key/value pair."); return + existing=CredStore.load(name); existing.update(data); CredStore.save(name,existing) + QMessageBox.information(self,"Saved",f"'{name}' credentials saved securely.") + self._refresh_custom_creds() + + def _open_edit_dialog(self, name): + data=CredStore.load(name) + dlg=CredKeyValueDialog(self, group_name=name, existing_data=data, lock_name=True, + title=f"🔑 Edit '{name}'") + if dlg.exec(): + _,new_data=dlg.get_data(); CredStore.save(name,new_data) + QMessageBox.information(self,"Updated",f"'{name}' credentials updated.") + self._refresh_custom_creds() + + def _delete_group(self, name): + reply=QMessageBox.question(self,"Delete",f"Delete all credentials saved under '{name}'?", + QMessageBox.Yes | QMessageBox.No) + if reply==QMessageBox.Yes: + _credstore_delete(name); self._refresh_custom_creds() + + +class FounderPage(QWidget): + def __init__(self,parent=None): + super().__init__(parent); self.setAttribute(Qt.WA_TranslucentBackground); self._build() + + def _build(self): + outer=QVBoxLayout(self); outer.setContentsMargins(0,0,0,0) + scroll=QScrollArea(); scroll.setWidgetResizable(True) + scroll.setStyleSheet("QScrollArea{border:none;background:transparent;}") + page=QWidget(); page.setAttribute(Qt.WA_TranslucentBackground) + lay=QVBoxLayout(page); lay.setContentsMargins(36,36,36,36); lay.setSpacing(20) + + import webbrowser + t=QLabel("👤 The Founder"); t.setFont(QFont("Segoe UI",20,QFont.Bold)) + t.setStyleSheet(f"color:{P['violet']};background:transparent;"); lay.addWidget(t) + hero=GlowCard(accent=P["violet"],radius=22,alpha=210) + hl=QVBoxLayout(hero); hl.setContentsMargins(32,28,32,28); hl.setSpacing(14) + name=QLabel("Sonu Kumar 🇮🇳"); name.setFont(QFont("Segoe UI",22,QFont.Bold)) + name.setStyleSheet(f"color:{P['bright']};background:transparent;"); hl.addWidget(name) + tag=QLabel("Founder · NPMAI ECOSYSTEM · Bihar Viral Boy") + tag.setFont(QFont("Segoe UI",12)); tag.setStyleSheet(f"color:{P['violet']};background:transparent;") + hl.addWidget(tag) + links=QHBoxLayout(); links.setSpacing(10) + for lbl2,url in [("⭐ GitHub","https://github.com/sonuramashishnpm"), + ("🐍 PyPI","https://pypi.org/project/npmai")]: + b2=QPushButton(lbl2); b2.setCursor(Qt.PointingHandCursor); b2.setFixedHeight(32) + b2.setStyleSheet(f"QPushButton{{background:rgba(167,139,250,10);border:1px solid rgba(167,139,250,40);border-radius:9px;color:{P['mid']};font-size:11px;padding:0 12px;}}QPushButton:hover{{background:rgba(167,139,250,25);border-color:rgba(167,139,250,110);color:{P['bright']};}}") + b2.clicked.connect(lambda _,u=url: webbrowser.open(u)); links.addWidget(b2) + links.addStretch(); hl.addLayout(links); lay.addWidget(hero) + lay.addStretch(); scroll.setWidget(page); outer.addWidget(scroll) + + +class DocsPage(QWidget): + def __init__(self,parent=None): + super().__init__(parent); self.setAttribute(Qt.WA_TranslucentBackground); self._build() + + def _sec(self,lay,icon,title,col,items): + card=GlowCard(accent=col,radius=18,alpha=195) + cl=QVBoxLayout(card); cl.setContentsMargins(28,22,28,22); cl.setSpacing(14) + h=QHBoxLayout() + ic=QLabel(icon); ic.setFont(QFont("Segoe UI",16)); ic.setStyleSheet("background:transparent;"); ic.setFixedWidth(28) + tt=QLabel(title); tt.setFont(QFont("Segoe UI",13,QFont.Bold)) + tt.setStyleSheet(f"color:{P['bright']};background:transparent;") + h.addWidget(ic); h.addWidget(tt); h.addStretch(); cl.addLayout(h) + sep=QFrame(); sep.setFixedHeight(1); sep.setStyleSheet(f"background:{P['ghost']};border:none;") + cl.addWidget(sep) + for it,ib in items: + row=QHBoxLayout(); row.setSpacing(12) + dot=QLabel("▸"); dot.setFont(QFont("Segoe UI",11,QFont.Bold)) + dot.setStyleSheet(f"color:{col};background:transparent;"); dot.setFixedWidth(16); dot.setAlignment(Qt.AlignTop) + cv=QVBoxLayout(); cv.setSpacing(1) + it_l=QLabel(it); it_l.setFont(QFont("Segoe UI",11,QFont.Bold)) + it_l.setStyleSheet(f"color:{P['bright']};background:transparent;") + ib_l=QLabel(ib); ib_l.setFont(QFont("Segoe UI",11)); ib_l.setWordWrap(True) + ib_l.setStyleSheet(f"color:{P['mid']};background:transparent;") + cv.addWidget(it_l); cv.addWidget(ib_l) + row.addWidget(dot); row.addLayout(cv); cl.addLayout(row) + lay.addWidget(card) + + def _build(self): + outer=QVBoxLayout(self); outer.setContentsMargins(0,0,0,0) + scroll=QScrollArea(); scroll.setWidgetResizable(True) + scroll.setStyleSheet("QScrollArea{border:none;background:transparent;}") + page=QWidget(); page.setAttribute(Qt.WA_TranslucentBackground) + lay=QVBoxLayout(page); lay.setContentsMargins(36,36,36,36); lay.setSpacing(20) + + t=QLabel("📖 Documentation"); t.setFont(QFont("Segoe UI",20,QFont.Bold)) + t.setStyleSheet(f"color:{P['sky']};background:transparent;"); lay.addWidget(t) + sep=QFrame(); sep.setFixedHeight(1); sep.setStyleSheet(f"background:{P['ghost']};border:none;") + lay.addWidget(sep); lay.addSpacing(4) + + self._sec(lay,"🚀","Getting Started",P["mint"],[ + ("Install","pip install npmai_agents, then run this app."), + ("First scan","Go to Settings and click 'Scan Now' — agent learns your folder structure."), + ("Add credentials","Add GitHub/Notion/Stripe/AWS/SMTP creds in Settings. Encrypted locally."), + ("Type a task","In the Agent tab, describe any task in plain English and press Run. No login needed."), + ]) + self._sec(lay,"🤖","How the Agent Works (Standalone)",P["cyan"],[ + ("Detect intent","Local heuristic decides: conversation, or a task to execute?"), + ("Plan","Planner LLM breaks the task into atomic steps."), + ("Select tools","Tool Manager picks from the real 100-tool registry."), + ("Generate","Coder LLM writes Python for each step."), + ("Audit","Auditor LLM reviews the code before anything runs."), + ("Execute","Executor runs it as a real child process — streams to the log panel."), + ("Verify","Verifier LLM confirms the step actually completed."), + ("Retry","On failure, agent auto-debugs up to 12 times with prior error context."), + ]) + self._sec(lay,"🔗","MCP Link (opt-in, login required)",P["violet"],[ + ("Get a link","Sidebar → 'Get MCP Link' → log in (or sign up) → link issued for your account."), + ("Connect","Paste the link into Claude/Grok's custom connector settings."), + ("What runs here","Only the execution step — the LLM you connected does the planning/coding, " + "already audited before it reaches this app."), + ("Everything else stays local","Chat, tasks, and credentials never touch this — login is scoped " + "to this one feature only."), + ]) + self._sec(lay,"🗂","Credential Key Reference (for '+ Add Credential Group')",P["sky"],[ + ("github","{ token }"), + ("gitlab","{ token, url }"), + ("stripe","{ secret_key }"), + ("razorpay","{ key_id, key_secret }"), + ("shopify","{ store, access_token }"), + ("mailchimp","{ api_key, server_prefix }"), + ("aws","{ access_key_id, secret_access_key, region }"), + ("cloudflare","{ token } or { api_key, email }"), + ("vercel","{ token }"), + ("netlify","{ token }"), + ("railway","{ token }"), + ("twilio","{ account_sid, auth_token, from_number, whatsapp_from, verify_service_sid }"), + ("sendgrid","{ api_key }"), + ("zoom","{ account_id, client_id, client_secret }"), + ("smtp / gmail","{ email, password, smtp_host, smtp_port }"), + ("notion","{ token }"), + ("linear / asana / clickup / todoist / trello","{ token } (trello also needs { key } )"), + ("figma / canva / elevenlabs / stability","{ token } or { api_key }"), + ("googlemaps / openweather / virustotal / shodan / hibp","{ api_key }"), + ("google_analytics / google_calendar / google","{ ...service account or OAuth JSON... }"), + ("postgres / mysql / mongodb / redis","{ host, port, user, password, database } (varies)"), + ("ssh","{ host, user, key_path or password }"), + ("docker","usually no creds needed for local Docker"), + ]) + self._sec(lay,"🔒","Security",P["rose"],[ + ("Dual-role audit","Every script is reviewed by a separate Auditor role before execution."), + ("Subprocess isolation","Code runs as a child process — a broken script cannot crash the app."), + ("Kill button","Click ■ Kill at any time to terminate the running script immediately."), + ("Credential encryption","Fernet symmetric encryption, machine-specific key, local only. Never uploaded."), + ]) + lay.addStretch(); scroll.setWidget(page); outer.addWidget(scroll) + + +class AppWindow(QWidget): + def __init__(self): + super().__init__() + self.setWindowTitle("NPM-AutoCode-AI — NPMAI Ecosystem v3.0") + self.setWindowIcon(QIcon(APP_ICON_PATH)) + self.resize(1220,780); self.setMinimumSize(920,640) + self._build() + + def _build(self): + self.bg=CosmicBG(self); self.bg.lower() + root=QHBoxLayout(self); root.setContentsMargins(0,0,0,0); root.setSpacing(0) + self.sidebar=Sidebar(); self.sidebar.page_changed.connect(self._switch) + root.addWidget(self.sidebar) + content=QWidget(); content.setAttribute(Qt.WA_TranslucentBackground) + cl=QVBoxLayout(content); cl.setContentsMargins(0,0,0,0) + self.stack=QStackedWidget(); self.stack.setAttribute(Qt.WA_TranslucentBackground) + self.stack.setStyleSheet("background:transparent;") + self.stack.addWidget(AgentPage()) + self.stack.addWidget(HistoryPage()) + self.stack.addWidget(ToolsPage()) + self.stack.addWidget(SettingsPage()) + self.stack.addWidget(FounderPage()) + self.stack.addWidget(DocsPage()) + cl.addWidget(self.stack); root.addWidget(content) + + def resizeEvent(self,e): + super().resizeEvent(e); self.bg.setGeometry(0,0,self.width(),self.height()) + + def _switch(self,idx): + if idx==1: + self.stack.widget(1).load() + widget=self.stack.widget(idx) + eff=QGraphicsOpacityEffect(widget); widget.setGraphicsEffect(eff) + anim=QPropertyAnimation(eff,b"opacity",self) + anim.setDuration(280); anim.setStartValue(0); anim.setEndValue(1) + anim.setEasingCurve(QEasingCurve.OutCubic) + self.stack.setCurrentIndex(idx); anim.start() + self._anim=anim + + def closeEvent(self,e): + if hasattr(self.sidebar, "_link_mgr") and self.sidebar._link_mgr: + self.sidebar._link_mgr.stop_listener() + super().closeEvent(e) + + +if __name__=="__main__": + app=QApplication(sys.argv); app.setStyle("Fusion") + pal=QPalette() + pal.setColor(QPalette.Window, QColor(P["void"])) + pal.setColor(QPalette.WindowText, QColor(P["bright"])) + pal.setColor(QPalette.Base, QColor(P["deep"])) + pal.setColor(QPalette.AlternateBase, QColor(P["panel"])) + pal.setColor(QPalette.Text, QColor(P["bright"])) + pal.setColor(QPalette.Button, QColor(P["lift"])) + pal.setColor(QPalette.ButtonText, QColor(P["bright"])) + pal.setColor(QPalette.Highlight, QColor(P["mint"])) + pal.setColor(QPalette.HighlightedText, QColor("#050310")) + app.setPalette(pal) + win=AppWindow(); win.show(); sys.exit(app.exec()) diff --git a/Desktop_App/app_config.json b/Desktop_App/app_config.json new file mode 100644 index 0000000..efb9c7e --- /dev/null +++ b/Desktop_App/app_config.json @@ -0,0 +1,5 @@ +{ + "url": "https://qyxuvuhhaspkuhbognyk.supabase.co", + "anon_key": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6InF5eHV2dWhoYXNwa3VoYm9nbnlrIiwicm9sZSI6ImFub24iLCJpYXQiOjE3ODQ3MDYwNzYsImV4cCI6MjEwMDI4MjA3Nn0.h3hyVr_OV38SfY2_2qQ6Hgtm6_FbvNQ31AG5YO7mVko", + "mcp_base_url": "https://your-mcp-server.hf.space/mcp" +} diff --git a/Desktop_App/mcp_link.py b/Desktop_App/mcp_link.py new file mode 100644 index 0000000..b89541f --- /dev/null +++ b/Desktop_App/mcp_link.py @@ -0,0 +1,186 @@ +import json +import uuid +import threading +import time +import requests +from pathlib import Path +from typing import Callable, Optional + +CONFIG_PATH = Path.home() / ".npmai_agent" / "supabase_config.json" + + +CONFIG_URL = "https://raw.githubusercontent.com/npmaiecosystem/NPM-AutoCode-AI/refs/heads/main/Desktop_App/app_config.json" + +def load_config() -> dict: + """Fetch latest config from GitHub (primary), fall back to bundled file.""" + try: + response = requests.get(CONFIG_URL, timeout=8) + if response.status_code == 200: + return response.json() + except Exception as e: + print(f"Failed to fetch remote config: {e}") + bundled = Path(__file__).resolve().parent / "app_config.json" + if bundled.exists(): + try: + return json.loads(bundled.read_text()) + except Exception: + pass + return {} + + +def save_config(url: str, anon_key: str, mcp_base_url: str = None): + CONFIG_PATH.parent.mkdir(exist_ok=True) + cfg = load_config() + cfg["url"] = url + cfg["anon_key"] = anon_key + if mcp_base_url: + cfg["mcp_base_url"] = mcp_base_url + CONFIG_PATH.write_text(json.dumps(cfg)) + + +class SupabaseAuthError(Exception): + pass + + +class MCPLinkManager: + + JOBS_TABLE = "mcp_jobs" + RESULTS_TABLE = "mcp_job_results" + LINKS_TABLE = "mcp_links" + + def __init__(self, log_cb: Callable[[str], None] = None): + self._log = log_cb or print + self._client = None + self._session = None + self._listener_thread = None + self._stop = threading.Event() + from npmai_agents import Executor + self._executor = Executor(log_cb=self._log) + + def _get_client(self): + if self._client is not None: + return self._client + try: + from supabase import create_client + except ImportError: + raise SupabaseAuthError( + "The 'supabase' package isn't installed. Run: pip install supabase" + ) + cfg = load_config() + if not cfg.get("url") or not cfg.get("anon_key"): + raise SupabaseAuthError( + "Supabase isn't configured yet. Set SUPABASE_URL and SUPABASE_ANON_KEY " + "env vars, or call mcp_link.save_config(url, anon_key) once from Settings." + ) + self._client = create_client(cfg["url"], cfg["anon_key"]) + return self._client + + def sign_up(self, email: str, password: str): + client = self._get_client() + res = client.auth.sign_up({"email": email, "password": password}) + if res.user is None: + raise SupabaseAuthError("Sign-up failed — check the email/password and try again.") + self._session = res.session + return res.user + + def log_in(self, email: str, password: str): + client = self._get_client() + res = client.auth.sign_in_with_password({"email": email, "password": password}) + if res.user is None: + raise SupabaseAuthError("Login failed — check your credentials.") + self._session = res.session + return res.user + + def log_out(self): + self.stop_listener() + if self._client: + try: + self._client.auth.sign_out() + except Exception: + pass + self._session = None + + @property + def is_logged_in(self) -> bool: + return self._session is not None + + @property + def user_id(self) -> Optional[str]: + if self._session and getattr(self._session, "user", None): + return self._session.user.id + return None + + def get_or_create_link(self) -> str: + if not self.user_id: + raise SupabaseAuthError("Log in first — an MCP link can only be issued to a logged-in account.") + client = self._get_client() + existing = ( + client.table(self.LINKS_TABLE) + .select("*") + .eq("user_id", self.user_id) + .eq("platform", "desktop") + .execute() + ) + if existing.data: + token = existing.data[0]["token"] + else: + token = uuid.uuid4().hex + client.table(self.LINKS_TABLE).insert({ + "user_id": self.user_id, + "platform": "desktop", + "token": token, + }).execute() + base = load_config().get("MCP_BASE_URL", "https://YOUR-HF-SPACE.hf.space/mcp") + return f"{base}/{token}" + + def start_listener(self): + if self._listener_thread and self._listener_thread.is_alive(): + return + if not self.user_id: + raise SupabaseAuthError("Log in first — the job bridge is scoped to a logged-in account.") + self._stop.clear() + self._listener_thread = threading.Thread(target=self._listen_loop, daemon=True) + self._listener_thread.start() + + def stop_listener(self): + self._stop.set() + + def _listen_loop(self): + client = self._get_client() + channel = client.channel(f"jobs-{self.user_id}") + + def _on_insert(payload): + row = payload.get("record") or payload.get("new") or {} + if row.get("user_id") != self.user_id: + return + self._handle_job(row) + + channel.on_postgres_changes( + event="INSERT", schema="public", table=self.JOBS_TABLE, + callback=_on_insert, + ) + channel.subscribe() + self._log("MCP link listener connected — waiting for jobs from your connected LLM.") + try: + while not self._stop.is_set(): + time.sleep(0.5) + finally: + try: + channel.unsubscribe() + except Exception: + pass + self._log("MCP link listener stopped.") + + def _handle_job(self, row: dict): + job_id = row.get("id") + code = row.get("code", "") + self._log(f"Job {job_id} received — executing locally.") + success, output = self._executor.run(code) + client = self._get_client() + client.table(self.RESULTS_TABLE).insert({ + "job_id": job_id, + "user_id": self.user_id, + "success": success, + "output": output, + }).execute() + self._log(f"Job {job_id} {'completed' if success else 'failed'} — result sent back.") diff --git a/Desktop_App/requirements.txt b/Desktop_App/requirements.txt new file mode 100644 index 0000000..25bd6c8 --- /dev/null +++ b/Desktop_App/requirements.txt @@ -0,0 +1,9 @@ +PySide6 +langchain +langchain-core +langchain-community +npmai==0.1.9 +npmai-agents==1.0.2 +supabase +cryptography +requests diff --git a/Docs/Docs.md b/Docs/Docs.md new file mode 100644 index 0000000..3c07c81 --- /dev/null +++ b/Docs/Docs.md @@ -0,0 +1,59 @@ +# NPM AutoCode AI + +## 🚀 Quick Start +Install the app: +[Download Latest Release](https://github.com/sonuramashishnpm/NPM-AutoCode-AI/releases/download/v2.0/NPM_AutoCode_AI.zip) + +1. Unzip the file. +2. Run `NPM_AutoCode_AI.exe` (Windows) or `python Desktop_App/app.py` (Python 3.12+). +3. Enter your task in the input box (e.g., "Plot a sine wave"). +4. Click **Generate & Execute**. +5. Watch logs and progress bar – AI generates, checks safety, executes, and fixes errors if needed. + +**Note:** Requires Ollama with models `codellama:7b-instruct` and `qwen2.5-coder:7b`. Run `ollama pull` for them. + +## Workflow:- + +Example Screenshot + + +## 📖 Project Overview +NPM AutoCode AI is a Python desktop app for automatic code generation and execution using AI. Describe tasks in plain English, and it uses NPMAI (custom LLM tools) to create, validate, and run Python scripts safely. + +Built for non-technical users – turns ideas into working code without manual editing. + +## ✨ Features +- **Natural Language to Code**: AI generates Python scripts from your description. +- **Safety Check**: Scans code for risks (e.g., file deletions, remote access) before running. +- **Auto-Debug**: If errors happen, AI fixes and retries using error logs. +- **Live Logs & Progress**: See real-time updates in GUI. +- **Dependency Handling**: Installs libraries via `subprocess` in code. +- **Isolated Execution**: Runs in safe namespace to protect your system. +- **Memory Chains**: Remembers task history for better fixes. + +## 🔄 How It Works +1. Enter task → AI (`codellama:7b-instruct`) generates code via NPMAI Ollama. +2. Safety AI (`qwen2.5-coder:7b`) reviews: If risky, stops with warning. +3. Execute code in thread → If error, feed back to AI for fix → Retry loop. +4. Success: Logs "Task Completed Successfully". + +All in background (QThread) so UI stays responsive. Uses LangChain for prompts. + +## 🛠️ Tech Details +- **Language**: Python 3.12+ +- **GUI**: PySide6 (QWidget, QThread, etc.) +- **AI**: npmai.Ollama + Memory; LangChain Core for prompts/parsers. +- **Execution**: `exec()` in custom globals dict with error capture. + +## 👨‍💻 Developer +**Sonu Kumar Ramashish** (a.k.a. Bihar Viral Boy) +- Age: 14 | Student | TEDx Speaker | AI & Software Developer | DevOps Enthusiast +- Reach: 410K+ Facebook followers +- Location: Kota, Rajasthan + +Part of NPMAI ecosystem for AI automation tools. + +## 🤝 Contributing +Fork, add features (e.g., more models), and PR. License: MIT. + +Star if useful! 🚀 diff --git a/Docs/app.py b/Docs/app.py new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/Docs/app.py @@ -0,0 +1 @@ + diff --git a/Docs/requirements.txt b/Docs/requirements.txt new file mode 100644 index 0000000..cef5a16 --- /dev/null +++ b/Docs/requirements.txt @@ -0,0 +1,2 @@ +Flask +gunicorn diff --git a/Docs/templates/NPM_AutoCode_AI.html b/Docs/templates/NPM_AutoCode_AI.html new file mode 100644 index 0000000..0633079 --- /dev/null +++ b/Docs/templates/NPM_AutoCode_AI.html @@ -0,0 +1,1135 @@ +\ + + + + + +NPM-AutoCode-AI — v3 · NPMAI Ecosystem + + + + + + + +
+
+ +
+ + + + +
+
+
v3NPMAI Ecosystem · Free & Open Source
+

NPM-AutoCode-AI
turns plain English
into things done.

+

Describe a task. It plans it, writes the code, checks the code, runs it on your machine, and confirms it actually worked — a five-role agent loop, not a chatbot pretending to be one.

+ +
+
57,000+
Downloads
+
1371
Tools
+
Free
Forever
+
45+
LLMs
+
+
+
+
Every task, five roles
+
Plannerbreaks your task into steps
+
Tool Managerpicks from 100 real tools
+
Coderwrites the Python for each step
+
Auditorchecks it before anything runs
+
Verifierconfirms it actually worked
+
+
scroll to see it run
+
+ +
+ + +
+
+
Live Demo
+

Pick any task. Watch it run.

+

These are real categories from the tool registry — dev tooling, cloud, business, communication, creative, data, media, and system automation.

+
+
+
+
+
+
NPM-AutoCode-AI — v3
+
MCP LINKED
+
+
+
+ +
+
Navigation
+
💬 Agent
+
📋 History
+
🛠 Tools
+
⚙ Settings
+
👤 Founder
+
📖 Docs
+
+
MCP Link Connected
+
+
+
+
Ready0%
+
+
+
+
Hi! Describe any task — I'll plan it, write the code, audit it, run it, and confirm it actually worked. Pick one below to see it live.
+
+
Execution Logs
+
+ + + +
+
+
+
+
+ + + + + + + + + + + + + + +
+
+
+ +
+ + +
+
+
MCP Architecture
+

Connect it to Claude or Grok.
No API key required.

+

NPM-AutoCode-AI hosts its own MCP endpoint. Link it once from the app, paste the link into your Claude or Grok connector settings, and the LLM you already talk to becomes the brain — planning, coding, and auditing — while this app is the hands that actually execute on your machine.

+
+ +
+
🖥
Your Desktop
executes locally
+ +
🛰
MCP Server(Hosted by NPMAI ECOSYSTEM)
Connect your web browser LLMs to your desktop
+ +
🤖
Claude / Grok and others
thinks, writes, reviews
+
+
+ Code + results flow back →← Prompts + tasks flow out +
+ +
+
01

Get your link, once

Log in from the app's MCP tab. That's the only time you'll ever need to log in — everything else in the app works without an account.

+
02

Paste it into your connector

Drop the link into Claude or Grok's custom connector settings. From then on, just talk to your normal LLM chat like always.

+
03

Ask for anything

"Clear my Downloads folder," "email my team the report" — the LLM detects it needs a real action and calls into your link automatically.

+
04

It's audited before it runs

Every generated script passes an Auditor review, plus a separate safety check, before it's ever sent to your desktop to execute.

+
05

Runs on your machine, not the cloud

Execution happens locally — your files, your credentials, your machine. The MCP server only ever relays code and results.

+
06

You confirm it, not just the exit code

A Verifier role checks the actual output against the task — not just whether the script ran without crashing.

+
+
+ +
+ + +
+
+
Capabilities
+

Built to compete.
Free to use.

+

Everything a coding agent should do — on your own machine, with a GUI, for everyone.

+

A System which can automate everything without needed of API_Key.

+

A System by which you can automate your Local Machine from AI Websites like claude.com, grok.com

+
+
+
+ 🧠

Five-Role Agent Pipeline

+

Planner, Tool Manager, Coder, Auditor, and Verifier — each with its own job and its own fallback chain. Not a chatbot. A real agent loop.

+ Plan → Select → Code → Audit → Execute → Verify +
+
+ 🔒

Independent Security Audit

+

Every generated script is reviewed by a separate Auditor role before execution, plus a second non-LLM safety check that can't be talked around.

+ Security First +
+
+ 🔄

Auto-Debug Loop (12 retries)

+

Execution fails? The agent captures the error, feeds it back to the Coder, and retries with the prior attempt as context — up to 12 times per step.

+ Self-Healing +
+
+ 📁

Workspace Awareness

+

On first run, the agent scans Desktop, Downloads, Documents, Pictures, Videos, and Music — it knows your files before you type a path.

+ Workspace Scanner +
+
+ 🔗

MCP Link, Not an API Key

+

Connect Claude or Grok directly from the sidebar. Login happens once, only for this — everything else in the app stays account-free.

+ Bring Your Own LLM +
+
+

Real Subprocess Execution

+

Code runs as an actual child process — not exec(). Output streams live to the log panel. Kill any script instantly, mid-run.

+ Real Isolation +
+
+
+ +
+ + +
+
+
Tool Registry
+

1371 Tools. Zero Setup.

+

Every tool below is available automatically. Just describe the task — the agent picks what it needs.

+
+
+
+ 🌱 GitTool · commit · branch · rebase + ⭐ GitHubTool · issues · PRs · releases + 🦊 GitLabTool · pipelines · merge requests + 🐳 DockerTool · build · compose · registry + ☁ AWSS3Tool · buckets · presigned URLs + 💳 StripeTool · payments · invoices · payouts + 📧 SMTPAdvancedTool · HTML email · attachments + 📓 NotionAdvancedTool · pages · databases + 🎥 ZoomTool · meetings · recordings + 📞 TwilioTool · SMS · voice · WhatsApp + 🌱 GitTool · commit · branch · rebase + ⭐ GitHubTool · issues · PRs · releases + 🦊 GitLabTool · pipelines · merge requests + 🐳 DockerTool · build · compose · registry + ☁ AWSS3Tool · buckets · presigned URLs + 💳 StripeTool · payments · invoices · payouts + 📧 SMTPAdvancedTool · HTML email · attachments + 📓 NotionAdvancedTool · pages · databases + 🎥 ZoomTool · meetings · recordings + 📞 TwilioTool · SMS · voice · WhatsApp +
+
+
+
+ 🖥 TerminalTool · run commands · pipe output + 📦 PackageManagerTool · npm · pip · cargo + 🧩 VSCodeTool · open · edit · extensions + 🛠 MakefileTool · targets · build steps + 🧱 CMakeTool · configure · build · test + 🐞 DebuggerTool · breakpoints · stack traces + 🎙 SpeechAITool · transcribe · diarize · clone + 👁 ComputerVisionTool · detect · classify · OCR + 🎬 FFmpegTool · convert · compress · trim + 🖼 ImageAdvancedTool · resize · batch edit + 🖥 TerminalTool · run commands · pipe output + 📦 PackageManagerTool · npm · pip · cargo + 🧩 VSCodeTool · open · edit · extensions + 🛠 MakefileTool · targets · build steps + 🧱 CMakeTool · configure · build · test + 🐞 DebuggerTool · breakpoints · stack traces + 🎙 SpeechAITool · transcribe · diarize · clone + 👁 ComputerVisionTool · detect · classify · OCR + 🎬 FFmpegTool · convert · compress · trim + 🖼 ImageAdvancedTool · resize · batch edit +
+
+
+ +
+ + +
+
+
Agent Architecture
+

How the agent thinks

+

Every task moves through these stages in order — each one runs a different role for the right job.

+
+
+
01
🔍
Scan
Reads your file system before anything else
+
02
🗺
Plan
Breaks the task into ordered, atomic steps
+
03
🧰
Select Tools
Picks exactly what each step needs from the registry
+
04
Generate
Writes real Python for each step
+
05
🔒
Audit
Reviewed for anything destructive — BLOCK or ALLOW
+
06
Execute
Real subprocess. Streams live. Kill anytime.
+
07
Verify
Confirms the task actually completed
+
+
+ +
+ + +
+
+
The Builder
+

Meet the Founder

+
+
+
+
+ Sonu Kumar +
+
+
Sonu Kumar 🇮🇳
+
Founder · NPMAI ECOSYSTEM · Bihar Viral Boy
+
+ Age 15 + Bihar, India + Kota, Rajasthan + 434K+ Followers + TEDx Speaker + Allen 100% Scholar +
+ +
+
+
+
4M+
npmai PyPI downloads
+
300K+
npmai-agents downloads
+
433K+
Social followers
+
TEDx
Speaker at 13
+
100%
Allen scholarship
+
10
Research papers
+
+
+
🧒
Age 9
Started Teaching Nursery Kids
While in 4th grade, taught nursery children — planting the seed for education equity.
+
🔥
Age 11 — "Bihar Viral Boy"
Challenged Bihar's Chief Minister
Raised education & liquor ban issues. Went viral nationally. Received support from Cabinet Ministers, Bollywood actors & ALLEN Institute founder.
+
🎤
Age 13
TEDx Speaker
One of India's youngest TEDx speakers — rural education and using tech to bridge India's urban-rural divide.
+
💻
Age 13-15
Founded NPMAI ECOSYSTEM
Self-taught, zero CS background. Built the entire ecosystem on free cloud infrastructure.
+
📄
2026
10 Research Papers Published
Spanning AI/agent architecture, chemistry, mathematics, and constitutional/political science.
+
+
+
+
+ +
+ + +
+
+
Get Started
+

Download free.
Run instantly.

+

No API keys required to start. No local GPU needed. Unzip and run.

+
+
1

Download ZIP

+
2

Unzip folder

+
3

Run .exe

+
4

Start automating

+
+ ↓ For Windows:- Download NPM-AutoCode-AI v3

+ ↓ For Linux:- Download NPM-AutoCode-AI v3

+ ↓ For Macos:- Download NPM-AutoCode-AI v3 +
Windows · Linux · Macos · Free Forever · Open Source · NPMAI Ecosystem
+
57,000+ downloads — determined as per the LLM requests received on our npmai_LLM server; that's how we identified this figure.
+
+
+ + + +
+ + + + diff --git a/README.md b/README.md index ca4c925..3c82e8a 100644 --- a/README.md +++ b/README.md @@ -1,52 +1,262 @@ -# NPM-AutoCode-AI -NPM AutoCode AI +
-Install app by clicking on this link:- https://github.com/sonuramashishnpm/NPM-AutoCode-AI/releases/download/v1.0/NPM_AutoCode_AI.zip + -🚀 Project Overview +Typing SVG -NPM AutoCode AI is a Python desktop application that allows users to automatically generate and execute Python code using AI. It integrates NPMAI, a custom LLM ecosystem, to understand natural language task descriptions and return clean, ready-to-run Python scripts. +
-This project demonstrates agentic AI automation, seamless integration of LLMs, and GUI-based Python automation for end-users. +[![Platform](https://img.shields.io/badge/Platform-Windows%20%7C%20macOS%20%7C%20Linux-6C63FF?style=for-the-badge&logo=windowsterminal&logoColor=white)](https://npmcodeai.netlify.app) +[![Built with PySide6](https://img.shields.io/badge/GUI-PySide6-41CD52?style=for-the-badge&logo=qt&logoColor=white)](https://pypi.org/project/PySide6/) +[![Engine](https://img.shields.io/badge/Engine-npmai--agents-2AFFA0?style=for-the-badge&logo=pypi&logoColor=black)](https://pypi.org/project/npmai-agents/) +[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg?style=for-the-badge)](LICENSE) +[![Ask DeepWiki](https://img.shields.io/badge/Ask-DeepWiki-blueviolet?style=for-the-badge)](https://deepwiki.com/npmaiecosystem/NPM-AutoCode-AI) -🧠 Features +### ⬇️ [**[DOWNLOAD FOR YOUR OS → npmcodeai.netlify.app](https://npmcodeai.netlify.app/)**](https://npmcodeai.netlify.app) ⬇️ +*Windows `.exe` · macOS `.app` · Linux binary — all built straight from this repo, every release.* -Natural Language Code Generation: Describe your task in plain English and let AI generate Python code. +
-Safe Code Execution: Runs generated code in an isolated local environment. +--- -Real-Time Logs: View step-by-step updates in the GUI while code is generated and executed. +## 📜 Table of Contents +- [[What is this, really?](https://claude.ai/chat/95ed3814-731c-4c7c-9b4b-8ec42b76fc82#-what-is-this-really)](#-what-is-this-really) +- [[The Idea: MCP Without the Toll Booth](https://claude.ai/chat/95ed3814-731c-4c7c-9b4b-8ec42b76fc82#-the-idea-mcp-without-the-toll-booth)](#-the-idea-mcp-without-the-toll-booth) +- [[Architecture](https://claude.ai/chat/95ed3814-731c-4c7c-9b4b-8ec42b76fc82#-architecture)](#-architecture) +- [[The 6-Role Agent Pipeline](https://claude.ai/chat/95ed3814-731c-4c7c-9b4b-8ec42b76fc82#-the-6-role-agent-pipeline)](#-the-6-role-agent-pipeline) +- [[1,371 Tools, 10 Domains](https://claude.ai/chat/95ed3814-731c-4c7c-9b4b-8ec42b76fc82#-1371-tools-10-domains)](#-1371-tools-10-domains) +- [[Features](https://claude.ai/chat/95ed3814-731c-4c7c-9b4b-8ec42b76fc82#-features)](#-features) +- [[Download & Install](https://claude.ai/chat/95ed3814-731c-4c7c-9b4b-8ec42b76fc82#-download--install)](#-download--install) +- [[Configure LLMs (Optional Upgrade Path)](https://claude.ai/chat/95ed3814-731c-4c7c-9b4b-8ec42b76fc82#-configure-llms-optional-upgrade-path)](#-configure-llms-optional-upgrade-path) +- [[Credential Vault](https://claude.ai/chat/95ed3814-731c-4c7c-9b4b-8ec42b76fc82#-credential-vault)](#-credential-vault) +- [[Tech Stack](https://claude.ai/chat/95ed3814-731c-4c7c-9b4b-8ec42b76fc82#-tech-stack)](#-tech-stack) +- [[Roadmap](https://claude.ai/chat/95ed3814-731c-4c7c-9b4b-8ec42b76fc82#-roadmap)](#-roadmap) +- [[Contributing](https://claude.ai/chat/95ed3814-731c-4c7c-9b4b-8ec42b76fc82#-contributing)](#-contributing) +- [[License](https://claude.ai/chat/95ed3814-731c-4c7c-9b4b-8ec42b76fc82#-license)](#-license) -Progress Feedback: Visual progress bar for task status. +--- -Custom AI Backend: Uses npmai.Ollama to call your Render + Colab hosted LLMs — no local LLM setup required. +## 🧠 What is this, really? -🛠️ Technical Details +**NPM-AutoCode-AI** is a desktop app: you type a task in plain English — +*"scrape this page and email me a summary"*, *"resize every image in this folder"*, *"push this fix to GitHub"* — +and it plans, writes, safety-audits, and runs the Python for you. Live, in front of you, on your own machine. -Language: Python 3.12+ +Under the hood, it's powered by **[`[npmai-agents](https://pypi.org/project/npmai-agents/)`](https://pypi.org/project/npmai-agents/)** — the actual open-source +reasoning engine (published on PyPI) that gives the app its **1,371 tools**, its safety auditing, and its +multi-role planning pipeline. NPM-AutoCode-AI is the face; `npmai-agents` is the brain. -GUI Framework: PySide6 +--- -AI/LLM Integration: NPMAI ecosystem (Ollama wrapper) +## 💡 The Idea: MCP Without the Toll Booth -Prompt Handling: LangChain Core (PromptTemplate, StrOutputParser) +
+ +
-Concurrency: QThread for non-blocking code generation & execution +Most **MCP (Model Context Protocol)** setups work like this: your LLM calls a *remote* server, that server calls +a *paid* API for every tool action, and you get billed per call, per token, per automation. That's fine for a SaaS — +but it's a toll booth on every single thing your AI does for you. -Safe Execution: exec in isolated local namespace with real-time logs +**NPM-AutoCode-AI inverts the model:** -⚡ Key Highlights +- The **MCP link** (`mcp_link.py`) is just a thin, authless *job relay* over **Supabase Realtime** — no tunnels, no + open ports, no third party sitting in the middle of your automation billing you per step. +- The **execution itself happens locally**, on your own desktop, using your own CPU/RAM — not a metered cloud sandbox. +- The **default reasoning models are free, local-first Ollama models** (`llama3.2`, `codellama:7b-instruct`, + `qwen2.5-coder:7b`, `granite3.3:2b`) — the entire Planner → Tool Manager → Coder → Auditor → Verifier → Chatter + pipeline runs **out of the box with zero API key**. +- Only if *you* want a stronger brain (GPT, Claude, Gemini...) do you ever touch an API key — and even then, it's + **your** key, stored **locally, encrypted**, never proxied through us. -Designed and built by Sonu Kumar Ramashish, a 14-year-old AI & automation prodigy. +So instead of *"pay per automation,"* it's: **connect any MCP-aware LLM to your own machine, and let it drive tools +that already live on your computer, for the cost of electricity.** -Part of the NPMAI ecosystem, which includes AI-driven web apps and automation tools. +--- -Demonstrates full-stack AI integration: LLMs, prompt engineering, GUI, and Python execution. +## 🏗️ Architecture -Highly modular: CodeWorker can be reused for other AI code-generation tasks. +```mermaid +sequenceDiagram + participant You as You (Agent tab / Chat) + participant App as NPM-AutoCode-AI (Desktop) + participant Brain as npmai-agents Engine + participant Supa as Supabase Realtime + participant LLM as Any MCP-aware LLM Client -Developer:- -Author: Sonu Kumar Ramashish (a.k.a. “Bihar Viral Boy”) -Age: 14 | Student | TEDx Speaker | Tech & AI Enthusiast -Location: Bihar / Kota, India -Public Reach: 398K+ Facebook followers + You->>App: "Resize every PNG in ~/photos to 800px" + App->>Brain: AgentBrain.plan(task) + Brain->>Brain: Planner -> Tool Manager -> Coder + Brain->>Brain: Auditor checks the generated code + Brain->>App: Safe code, ready to run + App->>App: Executor runs it locally (your CPU, your disk) + Brain->>Brain: Verifier confirms the step actually worked + App-->>You: Live logs + progress + result + + rect rgba(42,255,160,0.08) + Note over LLM,App: Optional - remote-control path via MCP Link + LLM->>Supa: INSERT job row (mcp_jobs) + Supa-->>App: Realtime push (no polling, no open ports) + App->>App: Executor runs the job locally + App->>Supa: INSERT result row (mcp_job_results) + Supa-->>LLM: Result delivered back + end +``` + +**Why this matters:** the bottom half of that diagram is the part most products don't give you. Any MCP-compatible +LLM — running anywhere — can hand a job to *your* desktop app through Supabase's realtime channel, and the job +executes with your local Python, your local files, your local tools. No always-on server bill for you to run, +no per-call metering for the automation itself. + +--- + +## 🧬 The 6-Role Agent Pipeline + +`AgentBrain` (the core of `npmai-agents`) doesn't use one model for everything — it splits the job across +**six specialized roles**, each independently swappable from **Configure LLMs**: + +| Stage | Job | Default (free, local) | +|---|---|---| +| 🧭 **Planner** | Breaks your task into atomic steps | `llama3.2:3b` | +| 🧰 **Tool Manager** | Picks which of the 1,371 tools apply | `llama3.2` | +| 👨‍💻 **Coder** | Writes the actual Python for each step | `codellama:7b-instruct` | +| 🛡 **Auditor** | Reviews the code for risk *before* it ever runs | `qwen2.5-coder:7b` | +| ✅ **Verifier** | Confirms a step genuinely completed | `llama3.2:3b` | +| 💬 **Chatter** | Handles plain conversation, non-task replies | `granite3.3:2b` | + +Every one of these runs against **free, local Ollama models by default**. Want GPT-4o doing the planning and +Claude doing the coding instead? One click each in **Configure LLMs** — nothing else changes. + +--- + +## 🧰 1,371 Tools, 10 Domains + +`npmai-agents` ships **1,371 tools across 121 classes**, organized into 10 domain files so the Tool Manager can +route intelligently instead of guessing: + +
+ +| Domain | Domain | Domain | +|---|---|---| +| 🖥 Developer & CLI | 💼 Business | ☁️ Cloud & DevOps | +| 📡 Communication (extended) | 🎨 Creative | 📊 Data & Research | +| 🎬 Media | 🗂 Productivity | 🔐 Security & AI | 🔧 System & Hardware | + +
+ +That's why the credentials system exists at all: most of these tools work with **zero setup** (local file ops, +system tasks, data processing) — only the ones that talk to a *third-party service* (GitHub, Twilio, Stripe, +AWS...) ever need a key, and even those keys stay on your machine. + +--- + +## ✨ Features + +- 🗣 **Natural language → working Python** — describe the task, watch it get built. +- 🛡 **Safety-audited before execution** — the Auditor role flags risky code (file deletion, remote access) before + anything runs. +- 🔁 **Auto-debug loop** — errors get fed back to the Coder role and retried, not just dumped on you. +- 📈 **Live logs + progress bar** — full visibility while the agent works, nothing happens silently. +- 🔑 **Bring-your-own-key, never forced** — every LLM provider and every tool credential is opt-in and locally + encrypted (`CredStore`). +- 🌐 **Remote-trigger via MCP Link** — connect any MCP-aware LLM client to your desktop through Supabase Realtime. +- 🖥 **True cross-platform** — native builds for Windows, macOS, and Linux from the same codebase. + +--- + +## 📦 Download & Install + +
+ +### 👉 **[[npmcodeai.netlify.app](https://npmcodeai.netlify.app/)](https://npmcodeai.netlify.app)** — pick your OS, download, run. + +| 🪟 Windows | 🍎 macOS | 🐧 Linux | +|:---:|:---:|:---:| +| `.exe`, no install needed | `.app` bundle | portable binary | + +*Every build on that page is produced straight from this repo's GitHub Actions pipeline — same source, three +native binaries, no cross-compiling shortcuts.* + +
+ +**Building from source instead?** + +```bash +git clone https://github.com/npmaiecosystem/NPM-AutoCode-AI.git +cd NPM-AutoCode-AI/Desktop_App +pip install -r requirements.txt +python app.py +``` + +--- + +## ⚙️ Configure LLMs (Optional Upgrade Path) + +Everything works day-one with the free local defaults above. If you want to swap in a hosted model for any of +the 6 roles — OpenAI, Anthropic, Gemini, Groq, Mistral, Cohere, Azure OpenAI, AWS Bedrock, HuggingFace, or a +local llama.cpp server — click **⚙ Configure LLMs** on the Agent tab: + +1. **① Provider Credentials** — add only the fields that provider actually needs (most: just an API key). +2. **② Assign Provider + Model per Stage** — pick which provider handles Planner / Tool Manager / Coder / + Auditor / Verifier / Chatter, individually. + +Nothing here is required. It's a ceiling to raise if you want it, not a floor you have to clear to get started. + +--- + +## 🔐 Credential Vault + +Any tool that needs a third-party credential (GitHub, Twilio, Stripe, AWS, Notion, and 25+ more) stores it through +a **locally encrypted `CredStore`** — never synced to us, never proxied through a middleman. Settings even has a +generic **"+ Add Credential Group"** button so you can wire up a tool that isn't hardcoded yet: name the group, +add whatever key/value pairs that tool's docs ask for, done. + +--- + +## 🛠 Tech Stack + +
+ +![Python](https://img.shields.io/badge/Python-3.11+-3776AB?style=flat-square&logo=python&logoColor=white) +![PySide6](https://img.shields.io/badge/PySide6-Qt%20for%20Python-41CD52?style=flat-square&logo=qt&logoColor=white) +![LangChain](https://img.shields.io/badge/LangChain-Prompt%20Orchestration-1C3C3C?style=flat-square) +![Supabase](https://img.shields.io/badge/Supabase-Realtime%20Job%20Relay-3ECF8E?style=flat-square&logo=supabase&logoColor=white) +![PyInstaller](https://img.shields.io/badge/PyInstaller-3--OS%20Builds-FFD43B?style=flat-square&logo=python&logoColor=black) +![GitHub Actions](https://img.shields.io/badge/GitHub%20Actions-CI%2FCD-2088FF?style=flat-square&logo=githubactions&logoColor=white) + +
+ +- **GUI:** PySide6 (custom animated widgets — glow cards, pulse buttons, cosmic background) +- **Engine:** `npmai-agents` (PyPI) — `AgentBrain`, `Executor`, `CredStore`, `Workspace` +- **Default inference:** `npmai` (Ollama-backed), fully local/free +- **Remote link:** `mcp_link.py` + Supabase Realtime (Postgres changes -> live push, no polling) +- **Packaging:** PyInstaller, one native build per OS via a matrix GitHub Actions workflow, auto-published to + GitHub Releases and mirrored on [[npmcodeai.netlify.app](https://npmcodeai.netlify.app/)](https://npmcodeai.netlify.app) + +--- + +## 🗺 Roadmap + +- [ ] Expand the MCP Link job protocol beyond single-shot code execution (multi-step remote sessions) +- [ ] More hardcoded credential templates in Settings (currently GitHub/SMTP/Notion/Stripe/AWS + generic groups) +- [ ] In-app model download manager for the local Ollama defaults +- [ ] Signed installers for Windows/macOS (currently unsigned binaries) + +--- + +## 🤝 Contributing + +Issues and PRs welcome — especially new tools for `npmai-agents`, new hardcoded credential templates, or +platform-specific packaging fixes. Fork it, build it, send a PR. + +## 📄 License + +MIT — see [[LICENSE](https://claude.ai/chat/LICENSE)](LICENSE). + +
+ + + +**Built by the NPMAI ECOSYSTEM** — automation that runs on *your* machine, not someone else's meter. + +
diff --git a/npmai.png b/npmai.png new file mode 100644 index 0000000..ec2e599 Binary files /dev/null and b/npmai.png differ