diff --git a/.gitignore b/.gitignore index 3a12a40..b68d54b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ reseau +context # ---> macOS # General diff --git a/.zed/settings.json b/.zed/settings.json new file mode 100644 index 0000000..3b0c2c6 --- /dev/null +++ b/.zed/settings.json @@ -0,0 +1,12 @@ +// Folder-specific settings +// +// For a full list of overridable settings, and general information on folder-specific settings, +// see the documentation: https://zed.dev/docs/configuring-zed#settings-files +{ + "inlay_hints": { + "enabled": true + }, + "colorize_brackets": true, + "project_name": "PMO Crazy Coder", + "autosave": "on_window_change", +} diff --git a/Dockerfile b/Dockerfile index e05aa76..ee6902d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,16 +1,61 @@ # Dockerfile FROM python:3.11-slim +# Ajouter le dépôt Debian Testing pour les paquets plus récents +RUN echo "deb http://deb.debian.org/debian testing main" \ + > /etc/apt/sources.list.d/testing.list + WORKDIR /app +# Installer les outils de développement Go et Rust +RUN apt-get update && \ + apt-get install -y \ + clang \ + llvm-dev \ + libclang-dev \ + pkg-config \ + build-essential \ + git \ + make \ + curl \ + nodejs \ + npm \ + pylint \ + black \ + flake8 \ + mypy \ + isort + +COPY tools tools + +# Installer la dernière version de Go +RUN bash tools/install_latest_go.sh +ENV PATH=/usr/local/go/bin:$PATH + +# Installer la dernière version de Rust +ENV RUSTUP_HOME=/usr/local/rustup +ENV CARGO_HOME=/usr/local/cargo +ENV PATH=/usr/local/cargo/bin:$PATH +RUN curl https://sh.rustup.rs -sSf \ + | sh -s -- -y --default-toolchain stable \ + && rustup install nightly \ + && rustup component add rustfmt clippy rust-src rust-docs \ + && cargo install cargo-edit cargo-watch cargo-outdated cargo-make cargo-binstall + +# Installer la dernière version de typscript +RUN npm install -g typescript tsc @types/node eslint prettier + +RUN cargo binstall --strategies crate-meta-data jj-cli + # Installer le SDK MCP Python -RUN pip install --no-cache-dir fastmcp +RUN pip install --no-cache-dir ruff fastmcp ipython # Copier le serveur -COPY src/* . +COPY src . # Exposer le port EXPOSE 6666 +ENV OLLAMA_ADDR="http://host.docker.internal:11434" # Lancer le serveur -CMD ["python", "./mon_mcp.py"] +CMD ["python", "./PMOCrazyCoder.py"] diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..52a2fcd --- /dev/null +++ b/Makefile @@ -0,0 +1,33 @@ +.PHONY: build run all debug + +# Variables +IMAGE_NAME = pmo_crazy_coder +TAG = latest +PORT = 7860 +DOCKER_RUN_COMMON = --rm -p $(PORT):$(PORT) -v ./bac_a_sable:/sandbox +FULL_IMAGE_NAME = $(IMAGE_NAME):$(TAG) + +all: run + +$(SANDBOX_VOLUME): + mkdir -p $(SANDBOX_VOLUME) + +stampdir: + mkdir -p stamp + +stamp/build_PMOCrazyCoder: stampdir Dockerfile + @echo building the PMOCrazyCoder server + @docker build -t $(FULL_IMAGE_NAME) . + @touch $@ + +build: stamp/build_PMOCrazyCoder +run: build + @echo Launching the PMOCrazyCoder MCP server + @docker run $(DOCKER_RUN_COMMON) \ + $(FULL_IMAGE_NAME) + +debug: build + @echo Launching the PMOCrazyCoder MCP server in debug mode + @docker run $(DOCKER_RUN_COMMON) \ + -it \ + $(FULL_IMAGE_NAME) /bin/bash diff --git a/Notes/jj_to_json.md b/Notes/jj_to_json.md new file mode 100644 index 0000000..5fa69ab --- /dev/null +++ b/Notes/jj_to_json.md @@ -0,0 +1,49 @@ +```bash +jj log | orla agent -m ollama:qwen3:0.6b \ + " +Transform the following `jj log` output into JSON according to the given schema. + +**Return only valid JSON, without any Markdown formatting :** + - no backticks, + - no code blocks, + - no extra text + +Schema: +{ + \"type\": \"array\", + \"items\": { + \"type\": \"object\", + \"properties\": { + \"hash\": {\"type\": \"string\"}, + \"author\": {\"type\": \"string\"}, + \"date\": {\"type\": \"string\"}, + \"message\": {\"type\": \"string\"} + }, + \"required\": [\"hash\", \"author\", \"date\", \"message\"] + } +} +" | grep -v '```' | jq . +``` + +```json +[ + { + "hash": "252b5de3", + "author": "nrrwwpms", + "date": "2026-01-25 17:36:18", + "message": "(no description set)" + }, + { + "hash": "9f9d9bc3", + "author": "stxkzlul", + "date": "2026-01-25 12:08:12", + "message": "Ajout de Dockerfile et configuration MCP" + }, + { + "hash": "7412b51a", + "author": "xnrxukn", + "date": "2026-01-25 07:53:42", + "message": "Initial commit" + } +] +``` diff --git a/opencode.json b/opencode.json index ad01805..d9c66a3 100644 --- a/opencode.json +++ b/opencode.json @@ -1,7 +1,7 @@ { "$schema": "https://opencode.ai/config.json", "mcp": { - "echo-server": { + "PMO-Crazy-coder": { "type": "remote", "url": "http://localhost:7860/mcp" } diff --git a/src/mon_mcp.py b/src/PMOCrazyCoder.py similarity index 89% rename from src/mon_mcp.py rename to src/PMOCrazyCoder.py index 6aed017..561fb9c 100644 --- a/src/mon_mcp.py +++ b/src/PMOCrazyCoder.py @@ -1,7 +1,5 @@ -from fastmcp import FastMCP - -# Création du serveur MCP -mcp = FastMCP("Serveur Echo MCP") +from crazy_mcp import mcp +from sandbox.mcp import install_project_tool # Déclaration d'un outil diff --git a/src/crazy_mcp/__init__.py b/src/crazy_mcp/__init__.py new file mode 100644 index 0000000..3c3ae06 --- /dev/null +++ b/src/crazy_mcp/__init__.py @@ -0,0 +1,3 @@ +from fastmcp import FastMCP + +mcp = FastMCP("PMO Crazy coder") diff --git a/src/json_extract/__init__.py b/src/json_extract/__init__.py new file mode 100644 index 0000000..489176c --- /dev/null +++ b/src/json_extract/__init__.py @@ -0,0 +1,50 @@ +import json +import re +from ollama import prompt + + +def structure_to_json(text, schema): + """ + Convert structured text to JSON using Ollama. + + Args: + text (str): The structured text to convert + schema (dict): The JSON schema to use for conversion + + Returns: + dict: The parsed JSON object + """ + # Format the schema as JSON string + schema_json = json.dumps(schema, indent=2) + + # Create the prompt following the pattern from your bash command + full_prompt = f"""Transform the following structured text into JSON according to the given schema. + +**Return only valid JSON, without any Markdown formatting :** + - no backticks, + - no code blocks, + - no extra text + +Input text: +{text} + +Schema: +{schema_json}""" + + # Send to Ollama + response = prompt("ollama:qwen3:0.6b", full_prompt, allow_pull=True) + + # Remove any markdown formatting (backticks, code blocks) that Ollama might include + # Only remove backticks at the beginning or end of the response, not those within the JSON + clean_response = response.strip() + while clean_response.startswith("`") or clean_response.startswith("```"): + clean_response = clean_response[1:].strip() + while clean_response.endswith("`") or clean_response.endswith("```"): + clean_response = clean_response[:-1].strip() + + # Try to parse the response as JSON + try: + return json.loads(clean_response) + except json.JSONDecodeError: + # If parsing fails, return the raw response + return {"error": "Failed to parse JSON", "raw_response": clean_response} diff --git a/src/jujutsu/__init__.py b/src/jujutsu/__init__.py new file mode 100644 index 0000000..4277f71 --- /dev/null +++ b/src/jujutsu/__init__.py @@ -0,0 +1 @@ +from .command import jj diff --git a/src/jujutsu/command.py b/src/jujutsu/command.py new file mode 100644 index 0000000..ca414fb --- /dev/null +++ b/src/jujutsu/command.py @@ -0,0 +1,87 @@ +import json +import subprocess +from typing import List, Optional + + +def jj( + subcommand: str, + options: Optional[List[str]] = None, + args: Optional[List[str]] = None, + template: Optional[str] = None, +) -> str: + """ + Lance une commande jj. + + Args: + subcommand: La sous-commande jj (ex: 'log', 'status', 'diff') + options: Liste des options (ex: ['--limit', '5']) + args: Liste des arguments additionnels + template: Template jj à utiliser (ex: 'builtin_log_compact') + + Returns: + str: sortie de la commande + """ + cmd = ["jj", subcommand] + + # Ajoute le template spécifié + if template: + cmd.extend(["--template", template]) + + if options: + cmd.extend(options) + if args: + cmd.extend(args) + + result = subprocess.run(cmd, capture_output=True, text=True) + + if result.returncode != 0: + raise RuntimeError( + f"Commande jj échouée: {' '.join(cmd)}\nstderr: {result.stderr}" + ) + + return result.stdout + + +def jj_log(options=None, args=None, template=None): + """ + Execute jj log command with optional arguments. + + Args: + options: List of options to pass to jj log (ex: ['--limit', '5']) + args: List of additional arguments + template: Template to use (ex: 'builtin_log_compact') + + Returns: + str: Output of the jj log command + """ + return jj("log", options, args, template) + + +def jj_clone(url, destination=None, options=None): + """ + Execute jj git clone command. + + Args: + url: The repository URL to clone + destination: The destination directory (optional) + options: List of additional options for jj git clone + + Returns: + str: Output of the jj git clone command + """ + cmd = ["jj", "git", "clone", url] + + if destination: + cmd.append(destination) + + if options: + cmd.extend(options) + + result = subprocess.run(cmd, capture_output=True, text=True) + + if result.returncode != 0: + raise RuntimeError( + f"Commande jj git clone échouée: {' '.join(cmd)}\nstderr: {result.stderr}" + ) + + return result.stdout diff --git a/src/ollama/__init__.py b/src/ollama/__init__.py new file mode 100644 index 0000000..f55e2d1 --- /dev/null +++ b/src/ollama/__init__.py @@ -0,0 +1,83 @@ +import os + +import requests + +# Get Ollama server address from environment variable, default to local address +ollama_addr = os.environ.get("OLLAMA_ADDR", "http://host.docker.internal:11434") + + +def prompt(model, text, allow_pull=False): + """ + Send a prompt to Ollama server and return the response. + + Args: + model (str): The model to use + text (str): The prompt text to send + allow_pull (bool): If True, automatically download the model if not available + + Returns: + str: The response from the Ollama server + """ + # Check if model is available + if allow_pull and not is_model_available(model): + # Try to pull the model + pull_result = pull_model(model) + if not pull_result.startswith("Model downloaded successfully"): + return pull_result # Return error from pull + + url = f"{ollama_addr}/api/generate" + + payload = {"model": model, "prompt": text, "stream": False} + + try: + response = requests.post(url, json=payload) + response.raise_for_status() + return response.json()["response"] + except requests.exceptions.RequestException as e: + return f"Error: {str(e)}" + + +def pull_model(model_name, force=False): + """ + Download a model from Ollama. + + Args: + model_name (str): The name of the model to download + force (bool): If True, forces download even if model already exists + + Returns: + str: Success message or error message + """ + url = f"{ollama_addr}/api/pull" + + payload = {"name": model_name} + if force: + payload["force"] = True + + try: + response = requests.post(url, json=payload) + response.raise_for_status() + return "Model downloaded successfully" + except requests.exceptions.RequestException as e: + return f"Error downloading model: {str(e)}" + + +def is_model_available(model_name): + """ + Check if a model is available in Ollama. + + Args: + model_name (str): The name of the model to check + + Returns: + bool: True if model is available, False otherwise + """ + url = f"{ollama_addr}/api/tags" + + try: + response = requests.get(url) + response.raise_for_status() + models = response.json().get("models", []) + return any(model.get("name") == model_name for model in models) + except requests.exceptions.RequestException: + return False diff --git a/src/sandbox/__init__.py b/src/sandbox/__init__.py new file mode 100644 index 0000000..57f89c3 --- /dev/null +++ b/src/sandbox/__init__.py @@ -0,0 +1,82 @@ +import os +import re +from pathlib import Path +import shutil + +from jujutsu.command import jj_clone + +__sandbox_root__ = "/sandbox" + +# Create the sandbox root directory if it doesn't exist +os.makedirs(__sandbox_root__, exist_ok=True) + + +def get_sandbox_root(): + """Return the sandbox root directory path.""" + return __sandbox_root__ + + +def install_project(url, project_name=None): + """ + Clone a Git project into the sandbox root directory. + + Args: + url: The Git repository URL to clone + project_name: Optional project name (if None, derived from URL) + + Returns: + str: The name of the cloned project + """ + # If no project name is provided, derive it from the URL + if project_name is None: + # Extract project name from URL (e.g., from 'https://github.com/user/repo.git' get 'repo') + project_name = re.sub(r".*/([^/]+?)(?:\.git)?$", r"\1", url) + + # Create the full path for the project + project_path = os.path.join(__sandbox_root__, project_name) + + # Clone the repository + jj_clone(url, destination=project_path) + + return project_name + + +def remove_project(project_name): + """ + Remove a project from the sandbox. + + Args: + project_name (str): The name of the project to remove + + Returns: + bool: True if the project was removed successfully, False otherwise + """ + project_path = os.path.join(__sandbox_root__, project_name) + + try: + if os.path.exists(project_path): + shutil.rmtree(project_path) + return True + return False + except (OSError, PermissionError): + return False + + +def sandboxed_project_list(): + """ + List all projects available in the sandbox. + + Returns: + list: A list of project names (strings) + """ + projects = [] + try: + with os.scandir(__sandbox_root__) as entries: + for entry in entries: + if entry.is_dir(): + projects.append(entry.name) + except (OSError, PermissionError): + # Return empty list if we can't access the directory + pass + + return projects diff --git a/src/sandbox/mcp.py b/src/sandbox/mcp.py new file mode 100644 index 0000000..7f49151 --- /dev/null +++ b/src/sandbox/mcp.py @@ -0,0 +1,51 @@ +from sandbox import install_project, sandboxed_project_list, remove_project +from crazy_mcp import mcp + + +@mcp.tool( + description="Clone a Git repository into the sandbox. Note: Access to the cloned project is only possible through MCP tools of this server." +) +def install_project_tool(url: str, project_name: str = None) -> str: + """ + Install a Git project into the sandbox environment. + + Args: + url: The Git repository URL to clone + project_name: Optional project name (if None, derived from URL) + + Returns: + str: Confirmation message with project name + """ + project_name = install_project(url, project_name) + return f"Project '{project_name}' installed successfully in sandbox" + + +@mcp.tool( + description="Remove a project from the sandbox. Note: Access to the cloned project is only possible through MCP tools of this server." +) +def remove_project_tool(project_name: str) -> str: + """ + Remove a project from the sandbox environment. + + Args: + project_name (str): The name of the project to remove + + Returns: + str: Confirmation message with project name + """ + success = remove_project(project_name) + if success: + return f"Project '{project_name}' removed successfully from sandbox" + else: + return f"Failed to remove project '{project_name}' from sandbox" + + +@mcp.tool(description="List all projects available in the sandbox") +def list_sandboxed_projects() -> list: + """ + Get a list of all projects available in the sandbox. + + Returns: + list: A list of project names + """ + return sandboxed_project_list() diff --git a/stamp/build_PMOCrazyCoder b/stamp/build_PMOCrazyCoder new file mode 100644 index 0000000..e69de29 diff --git a/tools/install_latest_go.sh b/tools/install_latest_go.sh new file mode 100644 index 0000000..1a48f72 --- /dev/null +++ b/tools/install_latest_go.sh @@ -0,0 +1,33 @@ +#!/bin/bash + +URL="https://go.dev/dl/" + +OS=$(uname -a | awk '{print $1}') +ARCH=$(uname -m) + +if [[ "$ARCH" == "x86_64" ]] ; then + ARCH="amd64" +fi + +if [[ "$ARCH" == "aarch64" ]] ; then + ARCH="arm64" +fi + +GOFILE=$(curl "$URL" \ + | grep 'class="download"' \ + | grep "\.tar\.gz" \ + | sed -E 's@^.*/dl/(go[1-9].+\.tar\.gz)".*$@\1@' \ + | grep -i "$OS" \ + | grep -i "$ARCH" \ + | head -1) + +GOURL=$(curl "${URL}${GOFILE}" \ + | sed -E 's@^.*href="(.*\.tar\.gz)".*$@\1@') + +echo "Install GO from : $GOURL" 1>&2 + +mkdir -p /usr/local +cd /usr/local + +curl "$GOURL" \ + | tar zxf -