Refactor: Update configuration and MCP server for port 8888, enhance bash safety rules

Update configuration in .claude/CLAUDE.md with expanded stack (Python, R, bash), detailed linting/test commands, and stricter bash safety rules (no uncontrolled eval, prefer shellcheck). Adjust MCP servers to use configurable port 8888 instead of hardcoded 1248, and improve code formatting in agent_lm.py and server.py. Update README.md with concrete repository URL for cloning/subtree operations, and add missing tool requirements (aichat, jq, argc).
This commit is contained in:
2026-03-26 12:27:17 +01:00
parent c776733402
commit 2215c72a46
4 changed files with 147 additions and 45 deletions

View File

@@ -1,22 +1,45 @@
# Projet CrazyClaude ## Instructions pour Claude
### Profil utilisateur
Programmeur expérimenté. Connaît parfaitement son projet, ses outils, et son environnement.
### Comportement attendu
- Faire ce qui est demandé, directement.
- Ne pas expliquer des choses triviales ou évidentes.
- Ne pas répéter ce qui vient d'être dit.
- Faire confiance au jugement de l'utilisateur.
- Si quelque chose ne fonctionne pas, chercher le bug — ne pas réexpliquer comment utiliser l'outil.
- Avant de demander la validation d'un plan, toujours afficher la totalité du plan dans le chat. L'utilisateur est responsable du code et décide en toute connaissance de cause.
## Stack ## Stack
- Rust (edition 2024), Go 1.24 - Rust (edition 2024), Go 1.24
- Tests : cargo test, go test - Python 3.12, R 4.4, bash (scripts shell)
- Lint : clippy (Rust), golangci-lint (Go) - Tests :
- Rust : `cargo test`
- Go : `go test`
- Python : `pytest` (avec coverage)
- R : `testthat` (via `Rscript -e "testthat::test_dir('tests')"`)
- bash : `bats` (Bash Automated Testing System)
- Lint :
- Rust : `clippy`
- Go : `golangci-lint`
- Python : `ruff` (ou `flake8` + `black`)
- R : `lintr`
- bash : `shellcheck`
## Heuristiques de délégation ## Heuristiques de délégation
Délègue à `qwen3-worker` si la tâche est **atomique** et satisfait TOUS les critères : Délègue à `qwen3-worker` si la tâche est **atomique** et satisfait TOUS les critères :
- ≤ 3 fichiers concernés - ≤ 3 fichiers concernés
- Pas de modification d'API publique - Pas de modification dAPI publique
- Pas de dépendance externe non encore importée - Pas de dépendance externe non encore importée
- Tâches typiques : génération de tests unitaires, reformatage, documentation inline, - Tâches typiques : génération de tests unitaires, reformatage, documentation inline,
conversion de types simples, scaffolding de stubs conversion de types simples, scaffolding de stubs
- Pour le bash, éviter les commandes destructrices (voir garde-fous)
Traite toi-même si : Traite toi-même si :
- Refactoring cross-module - Refactoring cross-module
- Conception d'architecture - Conception darchitecture
- Debugging avec contexte multi-fichiers - Debugging avec contexte multi-fichiers
- Modification de traits/interfaces publics - Modification de traits/interfaces publics
@@ -24,3 +47,4 @@ Traite toi-même si :
- Ne jamais passer `rm -rf` sans confirmation explicite - Ne jamais passer `rm -rf` sans confirmation explicite
- Ne jamais committer sans que les tests passent - Ne jamais committer sans que les tests passent
- Toujours vérifier `git diff` avant un commit - Toujours vérifier `git diff` avant un commit
- Pour le bash : ne jamais exécuter de commande avec `eval` non contrôlé ; privilégier lutilisation de `shellcheck` pour détecter les erreurs courantes

View File

@@ -3,6 +3,7 @@
Agent loop local (~120 lignes) pour interagir avec LM Studio via l'API OpenAI-compatible. Agent loop local (~120 lignes) pour interagir avec LM Studio via l'API OpenAI-compatible.
Usage : python3 .claude/mcp/qwen3-mcp/agent_lm.py --task "..." --files file1.py file2.go Usage : python3 .claude/mcp/qwen3-mcp/agent_lm.py --task "..." --files file1.py file2.go
""" """
import argparse import argparse
import json import json
import os import os
@@ -11,8 +12,10 @@ from pathlib import Path
import requests import requests
LMSTUDIO_URL = "http://localhost:1248/v1/chat/completions" SERVER = "http://localhost"
LMSTUDIO_MODELS_URL = "http://localhost:1248/v1/models" PORT = 8888 # 1248
LMSTUDIO_URL = f"{SERVER}:{PORT}/v1/chat/completions"
LMSTUDIO_MODELS_URL = f"{SERVER}:{PORT}/v1/models"
DEFAULT_MODEL = "qwen/qwen3-coder-next" DEFAULT_MODEL = "qwen/qwen3-coder-next"
SYSTEM_PROMPT = """Tu es un assistant de codage spécialisé. Tu effectues des tâches atomiques sur des fichiers source. SYSTEM_PROMPT = """Tu es un assistant de codage spécialisé. Tu effectues des tâches atomiques sur des fichiers source.
@@ -49,7 +52,11 @@ def read_files(paths: list[str]) -> str:
def call_lmstudio(task: str, file_context: str, model: str) -> str: def call_lmstudio(task: str, file_context: str, model: str) -> str:
"""Envoie la tâche à LM Studio et retourne la réponse.""" """Envoie la tâche à LM Studio et retourne la réponse."""
user_message = f"TÂCHE: {task}\n\nFICHIERS:\n{file_context}" if file_context else f"TÂCHE: {task}" user_message = (
f"TÂCHE: {task}\n\nFICHIERS:\n{file_context}"
if file_context
else f"TÂCHE: {task}"
)
payload = { payload = {
"model": model, "model": model,
@@ -90,13 +97,14 @@ def run_lint(file_path: str) -> tuple[bool, str]:
if file_path.endswith(".rs"): if file_path.endswith(".rs"):
result = subprocess.run( result = subprocess.run(
["cargo", "clippy", "--quiet"], ["cargo", "clippy", "--quiet"], capture_output=True, text=True, timeout=60
capture_output=True, text=True, timeout=60
) )
elif file_path.endswith(".go"): elif file_path.endswith(".go"):
result = subprocess.run( result = subprocess.run(
["golangci-lint", "run", file_path], ["golangci-lint", "run", file_path],
capture_output=True, text=True, timeout=60 capture_output=True,
text=True,
timeout=60,
) )
else: else:
return True, "no linter for this file type" return True, "no linter for this file type"
@@ -106,10 +114,21 @@ def run_lint(file_path: str) -> tuple[bool, str]:
return passed, output return passed, output
def agent_loop(task: str, input_files: list[str], output_file: str | None, model: str, max_retries: int = 2) -> dict: def agent_loop(
task: str,
input_files: list[str],
output_file: str | None,
model: str,
max_retries: int = 2,
) -> dict:
"""Boucle principale : génère, écrit, lint, corrige (max_retries fois).""" """Boucle principale : génère, écrit, lint, corrige (max_retries fois)."""
file_context = read_files(input_files) if input_files else "" file_context = read_files(input_files) if input_files else ""
result = {"status": "failure", "files_modified": [], "summary": "", "lint": "skipped"} result = {
"status": "failure",
"files_modified": [],
"summary": "",
"lint": "skipped",
}
for attempt in range(max_retries + 1): for attempt in range(max_retries + 1):
response = call_lmstudio(task, file_context, model) response = call_lmstudio(task, file_context, model)
@@ -133,7 +152,9 @@ def agent_loop(task: str, input_files: list[str], output_file: str | None, model
if lint_ok: if lint_ok:
result["status"] = "success" result["status"] = "success"
result["summary"] = f"Attempt {attempt + 1}: task completed successfully" result["summary"] = (
f"Attempt {attempt + 1}: task completed successfully"
)
break break
elif attempt < max_retries: elif attempt < max_retries:
task = f"{task}\n\nCORRECTION REQUISE (tentative {attempt + 1}):\n{lint_output}" task = f"{task}\n\nCORRECTION REQUISE (tentative {attempt + 1}):\n{lint_output}"
@@ -149,10 +170,16 @@ def main():
parser = argparse.ArgumentParser(description="Agent loop local pour LM Studio") parser = argparse.ArgumentParser(description="Agent loop local pour LM Studio")
parser.add_argument("--task", required=True, help="Description de la tâche") parser.add_argument("--task", required=True, help="Description de la tâche")
parser.add_argument("--files", nargs="*", default=[], help="Fichiers source à lire") parser.add_argument("--files", nargs="*", default=[], help="Fichiers source à lire")
parser.add_argument("--output", help="Fichier de sortie (défaut: premier fichier input)") parser.add_argument(
"--output", help="Fichier de sortie (défaut: premier fichier input)"
)
parser.add_argument("--model", default=DEFAULT_MODEL, help="Modèle LM Studio") parser.add_argument("--model", default=DEFAULT_MODEL, help="Modèle LM Studio")
parser.add_argument("--list-models", action="store_true", help="Liste les modèles disponibles") parser.add_argument(
parser.add_argument("--json", action="store_true", dest="json_output", help="Sortie JSON") "--list-models", action="store_true", help="Liste les modèles disponibles"
)
parser.add_argument(
"--json", action="store_true", dest="json_output", help="Sortie JSON"
)
args = parser.parse_args() args = parser.parse_args()
if args.list_models: if args.list_models:

View File

@@ -3,25 +3,38 @@
MCP server stdio exposant un outil `qwen3_task`. MCP server stdio exposant un outil `qwen3_task`.
Appelé par Claude Code via : claude mcp add --transport stdio qwen3 -- .claude/venv/bin/python3 .claude/mcp/qwen3-mcp/server.py Appelé par Claude Code via : claude mcp add --transport stdio qwen3 -- .claude/venv/bin/python3 .claude/mcp/qwen3-mcp/server.py
""" """
import sys
import json import json
import sys
import requests import requests
LMSTUDIO_URL = "http://localhost:1248/v1/chat/completions" SERVER = "http://localhost"
PORT = 8888 # 1248
LMSTUDIO_URL = f"{SERVER}:{PORT}/v1/chat/completions"
QWEN3_MODEL = "qwen/qwen3-coder-next" QWEN3_MODEL = "qwen/qwen3-coder-next"
TOOLS = [{ TOOLS = [
"name": "qwen3_task", {
"description": "Délègue une tâche de codage atomique à Qwen3-Coder via LM Studio.", "name": "qwen3_task",
"inputSchema": { "description": "Délègue une tâche de codage atomique à Qwen3-Coder via LM Studio.",
"type": "object", "inputSchema": {
"properties": { "type": "object",
"task": {"type": "string", "description": "Description précise de la tâche"}, "properties": {
"files": {"type": "array", "items": {"type": "string"}, "description": "Chemins des fichiers concernés"} "task": {
"type": "string",
"description": "Description précise de la tâche",
},
"files": {
"type": "array",
"items": {"type": "string"},
"description": "Chemins des fichiers concernés",
},
},
"required": ["task"],
}, },
"required": ["task"]
} }
}] ]
def send(obj: dict): def send(obj: dict):
@@ -65,15 +78,17 @@ def handle(req: dict):
req_id = req.get("id") req_id = req.get("id")
if method == "initialize": if method == "initialize":
send({ send(
"jsonrpc": "2.0", {
"id": req_id, "jsonrpc": "2.0",
"result": { "id": req_id,
"protocolVersion": "2024-11-05", "result": {
"capabilities": {"tools": {}}, "protocolVersion": "2024-11-05",
"serverInfo": {"name": "qwen3-mcp", "version": "1.0.0"}, "capabilities": {"tools": {}},
}, "serverInfo": {"name": "qwen3-mcp", "version": "1.0.0"},
}) },
}
)
elif method == "notifications/initialized": elif method == "notifications/initialized":
pass # notification, pas de réponse pass # notification, pas de réponse
elif method == "tools/list": elif method == "tools/list":
@@ -85,13 +100,40 @@ def handle(req: dict):
if name == "qwen3_task": if name == "qwen3_task":
try: try:
text = call_qwen3(args["task"], args.get("files", [])) text = call_qwen3(args["task"], args.get("files", []))
send({"jsonrpc": "2.0", "id": req_id, "result": {"content": [{"type": "text", "text": text}]}}) send(
{
"jsonrpc": "2.0",
"id": req_id,
"result": {"content": [{"type": "text", "text": text}]},
}
)
except Exception as e: except Exception as e:
send({"jsonrpc": "2.0", "id": req_id, "result": {"content": [{"type": "text", "text": f"ERROR: {e}"}], "isError": True}}) send(
{
"jsonrpc": "2.0",
"id": req_id,
"result": {
"content": [{"type": "text", "text": f"ERROR: {e}"}],
"isError": True,
},
}
)
else: else:
send({"jsonrpc": "2.0", "id": req_id, "error": {"code": -32601, "message": f"Outil inconnu : {name}"}}) send(
{
"jsonrpc": "2.0",
"id": req_id,
"error": {"code": -32601, "message": f"Outil inconnu : {name}"},
}
)
elif req_id is not None: elif req_id is not None:
send({"jsonrpc": "2.0", "id": req_id, "error": {"code": -32601, "message": f"Méthode inconnue : {method}"}}) send(
{
"jsonrpc": "2.0",
"id": req_id,
"error": {"code": -32601, "message": f"Méthode inconnue : {method}"},
}
)
for line in sys.stdin: for line in sys.stdin:

View File

@@ -25,7 +25,7 @@ Avec **Jujutsu** :
```sh ```sh
# Depuis la racine de votre projet # Depuis la racine de votre projet
jj git clone <url-du-depot> .claude-crazy jj git clone https://gargoton.petite-maison-orange.fr/eric/CrazyClaude.git .claude-crazy
``` ```
Puis déplacez ou liez le contenu : Puis déplacez ou liez le contenu :
@@ -42,7 +42,7 @@ cp -r .claude-crazy/.claude ./.claude
> vous pouvez aussi ajouter ce dépôt comme subtree : > vous pouvez aussi ajouter ce dépôt comme subtree :
> >
> ```sh > ```sh
> git subtree add --prefix .claude <url-du-depot> main --squash > git subtree add --prefix .claude https://gargoton.petite-maison-orange.fr/eric/CrazyClaude.git main --squash
> ``` > ```
### 2. Créer le virtualenv Python ### 2. Créer le virtualenv Python
@@ -142,7 +142,7 @@ echo ".claude/logs/" >> .gitignore
Avec Jujutsu (workflow colocalisé git) : Avec Jujutsu (workflow colocalisé git) :
```sh ```sh
git subtree pull --prefix .claude <url-du-depot> main --squash git subtree pull --prefix .claude https://gargoton.petite-maison-orange.fr/eric/CrazyClaude.git main --squash
``` ```
Ou si vous avez cloné séparément, tirez les changements puis recopiez : Ou si vous avez cloné séparément, tirez les changements puis recopiez :
@@ -151,3 +151,12 @@ Ou si vous avez cloné séparément, tirez les changements puis recopiez :
cd .claude-crazy && jj git fetch && jj new main cd .claude-crazy && jj git fetch && jj new main
cp -r .claude/* ../.claude/ cp -r .claude/* ../.claude/
``` ```
## Outils necessaire
### aichat
### jq
### argc