@nosserb/prmt 0.0.0-stage → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +191 -2
- package/bin/prmt.mjs +174 -0
- package/dist/assets/AuditModule-BOO_iRy8.js +1 -0
- package/dist/assets/Audits-yUBm0EpW.js +1 -0
- package/dist/assets/BrainPage-FIWO_9Nc.js +3 -0
- package/dist/assets/BrainPage-XQCEGRc5.css +1 -0
- package/dist/assets/Carte-CPuiR1Ha.css +1 -0
- package/dist/assets/Carte-xycu1mQn.js +1 -0
- package/dist/assets/Dashboard-DKvCQcX8.js +2 -0
- package/dist/assets/Dashboard-XdhC6_hM.css +1 -0
- package/dist/assets/Field-C7c2mCbb.js +2 -0
- package/dist/assets/Journal-BLIIlh80.css +1 -0
- package/dist/assets/Journal-BaelFjwV.js +1 -0
- package/dist/assets/Landing-B2jxV3zs.css +1 -0
- package/dist/assets/Landing-Dr612Zdu.js +26 -0
- package/dist/assets/Register-DA_Aq5f8.js +1 -0
- package/dist/assets/Reports-8wfDNSxH.js +1 -0
- package/dist/assets/Reports-B8GXN95N.css +1 -0
- package/dist/assets/SavoirPage-BTxbH3LY.css +1 -0
- package/dist/assets/SavoirPage-Dp3eUCHp.js +1 -0
- package/dist/assets/SettingsPage-BElIP46c.css +1 -0
- package/dist/assets/SettingsPage-BiQbnw3o.js +1 -0
- package/dist/assets/Shell-BAyHVljp.css +1 -0
- package/dist/assets/Shell-CQRlx9-V.js +2 -0
- package/dist/assets/Table-DUmGel5e.js +1 -0
- package/dist/assets/Tabs-CfhbELZk.js +1 -0
- package/dist/assets/Terminal-Bv1IJfi9.js +3 -0
- package/dist/assets/Terminal-CEfj38NV.css +1 -0
- package/dist/assets/arrow-right-Cx3pAEhl.js +1 -0
- package/dist/assets/auditViews-BMza2Sdz.js +1 -0
- package/dist/assets/auditViews-BePWj26h.css +1 -0
- package/dist/assets/bricolage-grotesque-latin-ext-opsz-normal-IcJDqblK.woff2 +0 -0
- package/dist/assets/bricolage-grotesque-latin-opsz-normal-Cre6nC2_.woff2 +0 -0
- package/dist/assets/bricolage-grotesque-vietnamese-opsz-normal-D9N6E8K1.woff2 +0 -0
- package/dist/assets/charts-BcijSJ4J.js +1 -0
- package/dist/assets/circle-stop-C33BAcKW.js +1 -0
- package/dist/assets/command-ChA49Xk7.js +1 -0
- package/dist/assets/command-Ck4pdQCv.css +1 -0
- package/dist/assets/download-CeuKOenD.js +1 -0
- package/dist/assets/eye-CXXHBBpr.js +1 -0
- package/dist/assets/geist-cyrillic-ext-wght-normal-DjL33-gN.woff2 +0 -0
- package/dist/assets/geist-cyrillic-wght-normal-BEAKL7Jp.woff2 +0 -0
- package/dist/assets/geist-latin-ext-wght-normal-DC-KSUi6.woff2 +0 -0
- package/dist/assets/geist-latin-wght-normal-BgDaEnEv.woff2 +0 -0
- package/dist/assets/geist-vietnamese-wght-normal-6IgcOCM7.woff2 +0 -0
- package/dist/assets/habillage-B5mMBadn.js +1 -0
- package/dist/assets/index-BH1etemR.css +1 -0
- package/dist/assets/index-COSiM-R9.js +14 -0
- package/dist/assets/jetbrains-mono-cyrillic-ext-wght-normal-EocZY2iu.woff2 +0 -0
- package/dist/assets/jetbrains-mono-cyrillic-wght-normal-D73BlboJ.woff2 +0 -0
- package/dist/assets/jetbrains-mono-greek-wght-normal-Bw9x6K1M.woff2 +0 -0
- package/dist/assets/jetbrains-mono-latin-ext-wght-normal-DBQx-q_a.woff2 +0 -0
- package/dist/assets/jetbrains-mono-latin-wght-normal-B9CIFXIH.woff2 +0 -0
- package/dist/assets/jetbrains-mono-vietnamese-wght-normal-Bt-aOZkq.woff2 +0 -0
- package/dist/assets/layers-2x-Bpkbi35X.png +0 -0
- package/dist/assets/layers-BWBAp2CZ.png +0 -0
- package/dist/assets/markdown-DF87LQh1.css +1 -0
- package/dist/assets/markdown-o5sMhPHN.js +4 -0
- package/dist/assets/marker-icon-hN30_KVU.png +0 -0
- package/dist/assets/play-Daw4K_73.js +1 -0
- package/dist/assets/plus-D_QHm8ud.js +1 -0
- package/dist/assets/trash-DLpHM-s8.js +1 -0
- package/dist/assets/web-6j1-aY67.js +1 -0
- package/dist/favicon.svg +1 -0
- package/dist/index.html +18 -0
- package/docs/ANALYSE.md +87 -0
- package/docs/SECURITE.md +188 -0
- package/lib/scripts/lier.js +15 -0
- package/lib/scripts/searxng.js +91 -0
- package/lib/server/app.js +444 -0
- package/lib/server/audits/modules/local.js +429 -0
- package/lib/server/audits/modules/network.js +331 -0
- package/lib/server/audits/modules/web.js +302 -0
- package/lib/server/audits/net.js +141 -0
- package/lib/server/audits/registry.js +202 -0
- package/lib/server/audits/types.js +58 -0
- package/lib/server/brain/brain.js +624 -0
- package/lib/server/brain/conversations.js +82 -0
- package/lib/server/brain/ollama.js +197 -0
- package/lib/server/brain/prompt.js +188 -0
- package/lib/server/brain/provider.js +1 -0
- package/lib/server/cli/bridge.js +292 -0
- package/lib/server/cli/pont.js +62 -0
- package/lib/server/config.js +68 -0
- package/lib/server/engine/analyzer.js +205 -0
- package/lib/server/engine/catalog.js +229 -0
- package/lib/server/engine/decoder.js +91 -0
- package/lib/server/engine/engine.js +194 -0
- package/lib/server/engine/paths.js +129 -0
- package/lib/server/engine/policy.js +397 -0
- package/lib/server/engine/relax.js +10 -0
- package/lib/server/engine/runner.js +193 -0
- package/lib/server/errors.js +23 -0
- package/lib/server/http/guard.js +75 -0
- package/lib/server/http/router.js +154 -0
- package/lib/server/http/static.js +76 -0
- package/lib/server/index.js +40 -0
- package/lib/server/journal.js +139 -0
- package/lib/server/lien.js +33 -0
- package/lib/server/savoir/base.js +284 -0
- package/lib/server/savoir/lecture.js +406 -0
- package/lib/server/savoir/memoire.js +115 -0
- package/lib/server/search/categories.js +179 -0
- package/lib/server/search/fiche.js +326 -0
- package/lib/server/search/lieux.js +134 -0
- package/lib/server/search/overpass.js +216 -0
- package/lib/server/search/searxng.js +223 -0
- package/lib/server/search/tiles.js +103 -0
- package/lib/server/status.js +157 -0
- package/lib/server/stores.js +141 -0
- package/lib/server/telephone/acces.js +457 -0
- package/lib/server/telephone/certificat.js +79 -0
- package/lib/server/telephone/qr.js +308 -0
- package/lib/shared/analysis.js +40 -0
- package/lib/shared/schemas.js +105 -0
- package/lib/shared/types.js +10 -0
- package/package.json +81 -4
- package/searxng/settings.yml +35 -0
- package/tui/Modelfile +23 -0
- package/tui/README.md +103 -0
- package/tui/prmt_tui.py +65 -0
- package/tui/promethee/__init__.py +1 -0
- package/tui/promethee/__main__.py +4 -0
- package/tui/promethee/accueil.py +66 -0
- package/tui/promethee/agents.py +220 -0
- package/tui/promethee/app/__init__.py +4 -0
- package/tui/promethee/app/application.py +299 -0
- package/tui/promethee/app/clavier.py +99 -0
- package/tui/promethee/app/commandes_slash.py +164 -0
- package/tui/promethee/app/contexte.py +188 -0
- package/tui/promethee/app/definitions.py +518 -0
- package/tui/promethee/app/ecran.py +177 -0
- package/tui/promethee/app/envoi.py +141 -0
- package/tui/promethee/app/historique.py +373 -0
- package/tui/promethee/app/modele.py +108 -0
- package/tui/promethee/app/notes.py +63 -0
- package/tui/promethee/app/outils.py +589 -0
- package/tui/promethee/app/plateforme.py +409 -0
- package/tui/promethee/app/saisie.py +100 -0
- package/tui/promethee/app/taches.py +92 -0
- package/tui/promethee/app/tour.py +539 -0
- package/tui/promethee/app/validation.py +210 -0
- package/tui/promethee/appels.py +35 -0
- package/tui/promethee/commandes.py +154 -0
- package/tui/promethee/completion.py +140 -0
- package/tui/promethee/config.py +12 -0
- package/tui/promethee/consignes.py +278 -0
- package/tui/promethee/conversation.py +94 -0
- package/tui/promethee/etat.py +253 -0
- package/tui/promethee/fichiers.py +354 -0
- package/tui/promethee/journal.py +116 -0
- package/tui/promethee/logo.py +132 -0
- package/tui/promethee/memoire.py +50 -0
- package/tui/promethee/ollama.py +52 -0
- package/tui/promethee/plan.py +91 -0
- package/tui/promethee/politique.py +429 -0
- package/tui/promethee/preferences.py +65 -0
- package/tui/promethee/presse_papiers.py +44 -0
- package/tui/promethee/recherche.py +581 -0
- package/tui/promethee/securite.py +204 -0
- package/tui/promethee/shell.py +190 -0
- package/tui/promethee/textes.py +190 -0
- package/tui/promethee/theme.py +92 -0
- package/tui/promethee/widgets.py +493 -0
- package/tui/requirements.txt +1 -0
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
"""Validation par l'utilisateur et exécution des commandes."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import os
|
|
5
|
+
import subprocess
|
|
6
|
+
import tempfile
|
|
7
|
+
import threading
|
|
8
|
+
import time
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
from rich.text import Text
|
|
12
|
+
from textual.containers import Vertical
|
|
13
|
+
|
|
14
|
+
from ..config import CMD_TIMEOUT, CREATE_NO_WINDOW
|
|
15
|
+
from ..consignes import WRITE_TOOL
|
|
16
|
+
from ..fichiers import render_diff
|
|
17
|
+
from ..securite import can_auto_allow, is_dangerous, session_scope
|
|
18
|
+
from ..shell import CWD_MARK, build_script, decode_output, refreshed_env, tidy_output
|
|
19
|
+
from ..theme import DIM, RED
|
|
20
|
+
from ..widgets import ChoicePrompt, PermissionPrompt
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class Validation:
|
|
24
|
+
"""Partie de PrometheeApp : validation par l'utilisateur et exécution des commandes."""
|
|
25
|
+
|
|
26
|
+
# ------------------------------------------------------------------ validation des commandes et fichiers
|
|
27
|
+
def _ask_permission(self, command: str, explanation: str, verdict=None) -> str:
|
|
28
|
+
"""Affiche la demande de validation et attend la réponse (appelé depuis le thread de l'agent)."""
|
|
29
|
+
return self._wait_choice(lambda done: self.show_permission(command, explanation, done, verdict))
|
|
30
|
+
|
|
31
|
+
def _wait_choice(self, show) -> str:
|
|
32
|
+
answered = threading.Event()
|
|
33
|
+
box: dict[str, str] = {}
|
|
34
|
+
|
|
35
|
+
def done(decision: str) -> None:
|
|
36
|
+
box["decision"] = decision
|
|
37
|
+
answered.set()
|
|
38
|
+
|
|
39
|
+
self.call_from_thread(show, done)
|
|
40
|
+
while not answered.wait(0.1):
|
|
41
|
+
if self.cancel_event.is_set():
|
|
42
|
+
return "no"
|
|
43
|
+
self.call_from_thread(self.hide_permission)
|
|
44
|
+
return box.get("decision", "no")
|
|
45
|
+
|
|
46
|
+
def show_permission(self, command: str, explanation: str, callback, verdict=None, direct: bool = False) -> None:
|
|
47
|
+
"""verdict : celui de la politique (None : validation manuelle). direct : commande « ! » tapée par toi."""
|
|
48
|
+
# Une commande destructrice que tu as débloquée reste signalée en rouge
|
|
49
|
+
dangerous = is_dangerous(command) or (verdict is not None and any(r.code == "unlocked" for r in verdict.raisons))
|
|
50
|
+
# Option « ne plus demander » seulement pour les commandes simples, non risquées et, si la politique répond,
|
|
51
|
+
# classées en lecture. Elle vaut pour le nom d'une lecture reconnue, sinon pour la ligne exacte.
|
|
52
|
+
scope, items = session_scope(command) if can_auto_allow(command) and not direct else ("noms", [])
|
|
53
|
+
if direct:
|
|
54
|
+
prompt = PermissionPrompt(command, explanation, dangerous, [], callback, verdict=verdict,
|
|
55
|
+
title="Exécuter ta commande", question="Tu l'as tapée toi-même : l'exécuter ?",
|
|
56
|
+
refusal="Non (Esc)")
|
|
57
|
+
else:
|
|
58
|
+
prompt = PermissionPrompt(command, explanation, dangerous, items, callback, verdict=verdict, scope=scope)
|
|
59
|
+
if self._agent_name:
|
|
60
|
+
prompt.title += f" · demandé par l'agent {self._agent_name}"
|
|
61
|
+
self.show_prompt(prompt)
|
|
62
|
+
|
|
63
|
+
def show_edit_permission(self, tool: str, path: Path, old: str | None, new: str, explanation: str,
|
|
64
|
+
outside: bool, callback, sensitive: str = "") -> None:
|
|
65
|
+
diff, _, _ = render_diff(old, new, max_lines=max(8, self.size.height - 20))
|
|
66
|
+
body = Text()
|
|
67
|
+
body.append(f" {path}\n", style="bold")
|
|
68
|
+
if explanation:
|
|
69
|
+
body.append(f" {explanation}\n", style=DIM)
|
|
70
|
+
body.append("\n")
|
|
71
|
+
body.append_text(diff)
|
|
72
|
+
if outside:
|
|
73
|
+
body.append("\n\n ⚠ Ce fichier est en dehors de ton dossier utilisateur : vérifie bien le chemin.",
|
|
74
|
+
style=f"bold {RED}")
|
|
75
|
+
elif sensitive:
|
|
76
|
+
body.append(f"\n\n ⚠ Fichier sensible ({sensitive}) : il te sera toujours montré avant d'être écrit.",
|
|
77
|
+
style=f"bold {RED}")
|
|
78
|
+
if old is None:
|
|
79
|
+
title, question = "Créer un fichier", "Créer ce fichier ?"
|
|
80
|
+
elif tool == WRITE_TOOL:
|
|
81
|
+
title, question = "Remplacer le contenu d'un fichier", "Remplacer tout le contenu de ce fichier ?"
|
|
82
|
+
else:
|
|
83
|
+
title, question = "Modifier un fichier", "Appliquer ces modifications ?"
|
|
84
|
+
options = [("yes", "Oui")]
|
|
85
|
+
if not outside and not sensitive:
|
|
86
|
+
options.append(("always", "Oui, et autoriser toutes les modifications de fichiers pendant cette session"))
|
|
87
|
+
options.append(("no", "Non, et dire à Prométhée quoi faire autrement (Esc)"))
|
|
88
|
+
if self._agent_name:
|
|
89
|
+
title += f" · demandé par l'agent {self._agent_name}"
|
|
90
|
+
self.show_prompt(ChoicePrompt(title, body, question, options, callback, danger=outside or bool(sensitive),
|
|
91
|
+
command=str(path)))
|
|
92
|
+
|
|
93
|
+
def ask(self, title: str, body: Text | None, question: str, options: list[tuple[str, str]], on_choice,
|
|
94
|
+
esc: str = "annuler", enter: str = "valider", danger: bool = False) -> ChoicePrompt:
|
|
95
|
+
"""Pose une question à choix sous la conversation. L'invite se referme, puis on_choice reçoit la réponse
|
|
96
|
+
(« no » si tu fais Esc). esc et enter disent, dans l'aide du bas, ce que font ces deux touches."""
|
|
97
|
+
def chosen(decision: str) -> None:
|
|
98
|
+
if self._perm is prompt:
|
|
99
|
+
self.hide_permission()
|
|
100
|
+
on_choice(decision)
|
|
101
|
+
|
|
102
|
+
prompt = ChoicePrompt(title, body, question, options, chosen, danger=danger)
|
|
103
|
+
prompt.hint = f"↑↓ choisir · Entrée {enter} (ou tape le numéro) · Esc {esc}"
|
|
104
|
+
self.show_prompt(prompt)
|
|
105
|
+
return prompt
|
|
106
|
+
|
|
107
|
+
def show_prompt(self, prompt: ChoicePrompt) -> None:
|
|
108
|
+
self._perm = prompt
|
|
109
|
+
self._comp_items = []
|
|
110
|
+
self.render_completions()
|
|
111
|
+
self._spinner(False)
|
|
112
|
+
self.query_one("#input-box").display = False
|
|
113
|
+
self.query_one("#bottom", Vertical).mount(prompt, before="#status-line")
|
|
114
|
+
self.call_after_refresh(prompt.focus)
|
|
115
|
+
self.set_hints()
|
|
116
|
+
|
|
117
|
+
def hide_permission(self) -> None:
|
|
118
|
+
prompt, self._perm = self._perm, None
|
|
119
|
+
if prompt is not None:
|
|
120
|
+
prompt.remove()
|
|
121
|
+
self.query_one("#input-box").display = True
|
|
122
|
+
self.input.focus()
|
|
123
|
+
self.set_hints()
|
|
124
|
+
|
|
125
|
+
def is_allowed(self, command: str, verdict=None) -> bool:
|
|
126
|
+
"""La commande peut-elle partir sans te la redemander ? Jamais si la politique ne la classe pas en lecture."""
|
|
127
|
+
if not can_auto_allow(command) or (verdict is not None and not verdict.lecture):
|
|
128
|
+
return False
|
|
129
|
+
scope, items = session_scope(command)
|
|
130
|
+
return all(w in self.allowed for w in items) if scope == "noms" else items[0] in self.allowed_lines
|
|
131
|
+
|
|
132
|
+
def allow(self, command: str) -> None:
|
|
133
|
+
if can_auto_allow(command):
|
|
134
|
+
scope, items = session_scope(command)
|
|
135
|
+
(self.allowed if scope == "noms" else self.allowed_lines).update(items)
|
|
136
|
+
|
|
137
|
+
# ------------------------------------------------------------------ exécution (thread)
|
|
138
|
+
def _execute(self, command: str, verdict=None) -> tuple[int | None, str, str]:
|
|
139
|
+
# Script dans un .ps1 temporaire (UTF-8 avec BOM pour PowerShell 5.1) lancé avec -File :
|
|
140
|
+
# avec -EncodedCommand, PowerShell renvoie les erreurs et Write-Host en XML (CLIXML).
|
|
141
|
+
# verdict : une commande autorisée par la liste blanche s'exécute en mode de langage contraint.
|
|
142
|
+
constrained = verdict is not None and verdict.mode_contraint
|
|
143
|
+
script_path = ""
|
|
144
|
+
try:
|
|
145
|
+
fd, script_path = tempfile.mkstemp(prefix="prmt_", suffix=".ps1")
|
|
146
|
+
with os.fdopen(fd, "w", encoding="utf-8-sig", newline="\r\n") as f:
|
|
147
|
+
f.write(build_script(command, verdict.modules if constrained else (), constrained))
|
|
148
|
+
proc = subprocess.Popen(
|
|
149
|
+
["powershell.exe", "-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass", "-File", script_path],
|
|
150
|
+
cwd=str(self.cwd),
|
|
151
|
+
env=refreshed_env(),
|
|
152
|
+
stdin=subprocess.DEVNULL,
|
|
153
|
+
stdout=subprocess.PIPE,
|
|
154
|
+
stderr=subprocess.STDOUT,
|
|
155
|
+
creationflags=CREATE_NO_WINDOW,
|
|
156
|
+
)
|
|
157
|
+
return self._wait(proc)
|
|
158
|
+
except OSError as err:
|
|
159
|
+
return None, f"Impossible de lancer PowerShell : {err}", ""
|
|
160
|
+
finally:
|
|
161
|
+
if script_path:
|
|
162
|
+
try:
|
|
163
|
+
os.remove(script_path)
|
|
164
|
+
except OSError:
|
|
165
|
+
pass
|
|
166
|
+
|
|
167
|
+
def _wait(self, proc: subprocess.Popen) -> tuple[int | None, str, str]:
|
|
168
|
+
self._proc = proc
|
|
169
|
+
start = time.monotonic()
|
|
170
|
+
chunks: list[bytes] = []
|
|
171
|
+
note = ""
|
|
172
|
+
try:
|
|
173
|
+
while True:
|
|
174
|
+
try:
|
|
175
|
+
out, _ = proc.communicate(timeout=0.25)
|
|
176
|
+
chunks.append(out or b"")
|
|
177
|
+
break
|
|
178
|
+
except subprocess.TimeoutExpired:
|
|
179
|
+
if self.cancel_event.is_set():
|
|
180
|
+
note = "arrêtée par toi"
|
|
181
|
+
elif time.monotonic() - start > CMD_TIMEOUT:
|
|
182
|
+
note = f"arrêtée après {CMD_TIMEOUT // 60} min (délai dépassé)"
|
|
183
|
+
else:
|
|
184
|
+
continue
|
|
185
|
+
self._kill_proc()
|
|
186
|
+
try:
|
|
187
|
+
out, _ = proc.communicate(timeout=5)
|
|
188
|
+
except subprocess.TimeoutExpired:
|
|
189
|
+
out = b"" # un processus enfant garde la sortie ouverte : on n'attend pas davantage
|
|
190
|
+
chunks.append(out or b"")
|
|
191
|
+
break
|
|
192
|
+
finally:
|
|
193
|
+
self._proc = None
|
|
194
|
+
output = decode_output(b"".join(chunks))
|
|
195
|
+
# Suivi du dossier courant (cd / Set-Location) : on retire le marqueur où qu'il soit
|
|
196
|
+
idx = output.rfind(CWD_MARK)
|
|
197
|
+
if idx != -1:
|
|
198
|
+
end = output.find("\n", idx)
|
|
199
|
+
new_cwd = Path(output[idx + len(CWD_MARK): end if end != -1 else len(output)].strip())
|
|
200
|
+
if new_cwd.is_dir():
|
|
201
|
+
self.cwd = new_cwd
|
|
202
|
+
output = output[:idx] + (output[end + 1:] if end != -1 else "")
|
|
203
|
+
output = tidy_output(output)
|
|
204
|
+
code = None if note else proc.returncode
|
|
205
|
+
return code, output, note
|
|
206
|
+
|
|
207
|
+
def _kill_proc(self) -> None:
|
|
208
|
+
proc = self._proc
|
|
209
|
+
if proc is not None and proc.poll() is None:
|
|
210
|
+
subprocess.run(["taskkill", "/T", "/F", "/PID", str(proc.pid)], capture_output=True, creationflags=CREATE_NO_WINDOW)
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"""Appels d'outils du modèle : lecture des arguments, tri des appels lancés à l'aveugle."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import json
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
from .agents import AGENT_TOOL
|
|
8
|
+
from .consignes import READ_TOOL, SEARCH_TOOL_NAMES
|
|
9
|
+
from .fichiers import resolve_path
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def call_args(call: dict) -> dict:
|
|
13
|
+
args = (call.get("function") or {}).get("arguments") or {}
|
|
14
|
+
if isinstance(args, str):
|
|
15
|
+
try:
|
|
16
|
+
args = json.loads(args)
|
|
17
|
+
except json.JSONDecodeError:
|
|
18
|
+
return {}
|
|
19
|
+
return args if isinstance(args, dict) else {}
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def prune_calls(calls: list[dict], cwd: Path) -> list[dict]:
|
|
23
|
+
"""Appels lancés « à l'aveugle » dans un même tour, que le modèle 8B fait souvent :
|
|
24
|
+
- lancer_agent en plus de ses propres outils : il confie alors à l'agent une tâche hors sujet, puis croit son
|
|
25
|
+
rapport plutôt que la vraie sortie → on garde seulement ses propres outils ;
|
|
26
|
+
- chercher_fichiers ET lire_fichier de fichiers inventés (« fichier1.txt ») avant d'avoir vu le résultat
|
|
27
|
+
→ on garde la recherche et les lectures de fichiers qui existent : il lira les vrais au tour suivant."""
|
|
28
|
+
names = [(c.get("function") or {}).get("name") for c in calls]
|
|
29
|
+
if AGENT_TOOL in names and any(n != AGENT_TOOL for n in names):
|
|
30
|
+
calls = [c for c, n in zip(calls, names) if n != AGENT_TOOL]
|
|
31
|
+
names = [n for n in names if n != AGENT_TOOL]
|
|
32
|
+
if any(n in SEARCH_TOOL_NAMES for n in names) and READ_TOOL in names:
|
|
33
|
+
calls = [c for c, n in zip(calls, names) if n != READ_TOOL
|
|
34
|
+
or resolve_path(str(call_args(c).get("chemin") or "?"), cwd).is_file()]
|
|
35
|
+
return calls
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
"""Commandes « / » : la liste intégrée, les commandes personnalisées et /init."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import os
|
|
5
|
+
import re
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import NamedTuple
|
|
8
|
+
|
|
9
|
+
from .agents import agent_slug
|
|
10
|
+
from .config import SAVE_DIR
|
|
11
|
+
from .fichiers import read_text_file
|
|
12
|
+
|
|
13
|
+
# --------------------------------------------------------------------------- Commandes personnalisées (comme .claude/commands)
|
|
14
|
+
# Un fichier nom.md = une commande /nom : son texte est envoyé à Prométhée, $ARGUMENTS remplacé par ce qui suit /nom.
|
|
15
|
+
COMMANDS_DIR = SAVE_DIR / "commandes" # commandes globales
|
|
16
|
+
LOCAL_COMMANDS = Path(".promethee") / "commandes" # commandes d'un dossier
|
|
17
|
+
COMMAND_SCHEMA = {"type": "object", "properties": {
|
|
18
|
+
"nom": {"type": "string"},
|
|
19
|
+
"description": {"type": "string"},
|
|
20
|
+
"argument": {"type": "string"},
|
|
21
|
+
"consignes": {"type": "string"},
|
|
22
|
+
}, "required": ["nom", "description", "argument", "consignes"]}
|
|
23
|
+
COMMAND_GEN_SYSTEM = """Tu écris des commandes personnalisées pour Prométhée, un assistant de cybersécurité local sous Windows 11 (PowerShell 5.1). Une commande est un message type que l'utilisateur lance en tapant /nom suivi d'un argument. À partir de sa description, écris :
|
|
24
|
+
- nom : court, en minuscules, mots séparés par des tirets (par exemple « scan-reseau », « revue-script »).
|
|
25
|
+
- description : une phrase : ce que fait la commande.
|
|
26
|
+
- argument : ce que l'utilisateur tape après /nom (par exemple « <adresse ou plage IP> », « <fichier> »), ou "" s'il n'y en a pas.
|
|
27
|
+
- consignes : le message envoyé à Prométhée, à l'impératif, en le tutoyant, en 3 à 10 lignes : ce qu'il doit faire étape par étape et ce que doit contenir sa réponse. C'est Prométhée qui agit lui-même avec ses outils (executer_commande pour les commandes, lire_fichier, chercher_fichiers, chercher_texte) : écris « Lance … avec executer_commande », jamais des consignes pour un humain (« Exécutez … »). Écris $ARGUMENTS à l'endroit où va l'argument de l'utilisateur.
|
|
28
|
+
Cite seulement des commandes PowerShell qui existent vraiment, avec leurs vrais paramètres, entre backticks.
|
|
29
|
+
Réponds uniquement en JSON."""
|
|
30
|
+
COMMAND_TEMPLATE = {"name": "ma-commande", "description": "Ce que fait cette commande.", "argument": "<cible>",
|
|
31
|
+
"prompt": "Fais … sur $ARGUMENTS.\n\n1. …\n2. …\n\nTermine par un résumé : …"}
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class Commande(NamedTuple):
|
|
35
|
+
"""Une commande « / » intégrée. action : la méthode de PrometheeApp qui la traite (elle reçoit ce qui suit le nom)."""
|
|
36
|
+
nom: str
|
|
37
|
+
alias: tuple[str, ...]
|
|
38
|
+
description: str
|
|
39
|
+
action: str
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
# Le seul endroit où une commande intégrée est déclarée : le menu « / », l'aiguillage (app/commandes_slash.py) et les
|
|
43
|
+
# noms réservés (une commande personnalisée ne peut pas les prendre) partent tous de cette liste.
|
|
44
|
+
COMMANDS = [
|
|
45
|
+
Commande("/aide", ("help", "?", "h"), "afficher l'aide", "cmd_aide"),
|
|
46
|
+
Commande("/etat", ("état", "status", "statut"), "état de la plateforme : IA, moteur, périmètre, activité", "cmd_etat"),
|
|
47
|
+
Commande("/clear", ("nouveau", "reset", "new"), "nouvelle conversation", "clear_conversation"),
|
|
48
|
+
Commande("/compact", ("compacter", "resumer", "résumer"), "résumer la conversation pour libérer le contexte", "cmd_compact"),
|
|
49
|
+
Commande("/reprendre", ("resume", "continuer", "sessions", "historique"), "reprendre une conversation précédente", "cmd_resume"),
|
|
50
|
+
Commande("/memoire", ("memory", "mémoire"), "voir la mémoire PROMETHEE.md (/memoire ouvrir : la modifier)", "cmd_memoire"),
|
|
51
|
+
Commande("/plan", (), "activer ou couper le mode plan (aussi Shift+Tab)", "cmd_plan"),
|
|
52
|
+
Commande("/agents", ("agent",), "créer, voir et gérer les agents (sous-agents)", "cmd_agents"),
|
|
53
|
+
Commande("/code", (), "IA de code : bascule automatique (on / off) et modèle utilisé", "cmd_code"),
|
|
54
|
+
Commande("/commandes", ("commands",), "créer, voir et gérer tes commandes personnalisées", "cmd_commands"),
|
|
55
|
+
Commande("/init", (), "écrire la mémoire PROMETHEE.md de ce dossier (Prométhée l'explore)", "cmd_init"),
|
|
56
|
+
Commande("/contexte", ("context",), "ce qui remplit le contexte du modèle", "cmd_context"),
|
|
57
|
+
Commande("/modele", ("model", "modèle", "models", "modeles", "modèles"), "lister les modèles ou en changer", "cmd_modele"),
|
|
58
|
+
Commande("/copier", ("copy", "cp"), "copier la dernière réponse (/copier code : les blocs de code)", "cmd_copy"),
|
|
59
|
+
Commande("/sortie", ("output",), "sortie complète de la dernière commande", "cmd_sortie"),
|
|
60
|
+
Commande("/politique", ("policy",), "politique d'exécution : stricte ou souple, liste blanche", "cmd_politique"),
|
|
61
|
+
Commande("/permissions", ("perms", "autorisations"), "autorisations de la session, débloquer ou ajouter une commande", "cmd_permissions"),
|
|
62
|
+
Commande("/perimetre", ("périmètre", "scope", "cibles"), "cibles réseau que tu as le droit de tester", "cmd_perimetre"),
|
|
63
|
+
Commande("/journal", ("log", "logs"), "dernières actions : commandes exécutées, refusées, fichiers écrits", "cmd_journal"),
|
|
64
|
+
Commande("/recherche", ("web", "internet", "searxng"), "recherche web par SearXNG : état, on, off", "cmd_recherche"),
|
|
65
|
+
Commande("/theme", ("thème", "fond"), "fond de l'interface : promethee ou terminal", "cmd_theme"),
|
|
66
|
+
Commande("/sauver", ("save", "enregistrer"), "exporter la conversation en Markdown", "cmd_save"),
|
|
67
|
+
Commande("/quitter", ("exit", "quit", "bye", "q"), "quitter Prométhée", "cmd_quitter"),
|
|
68
|
+
]
|
|
69
|
+
MENU_ROWS = 8
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def find_command(name: str) -> Commande | None:
|
|
73
|
+
"""La commande intégrée qui porte ce nom ou cet alias (sans la barre), ou None."""
|
|
74
|
+
return next((c for c in COMMANDS if name == c.nom[1:] or name in c.alias), None)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def builtin_command_names() -> set[str]:
|
|
78
|
+
return {c.nom[1:] for c in COMMANDS} | {a for c in COMMANDS for a in c.alias}
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def parse_command_file(text: str, path: Path, scope: str = "global") -> dict | None:
|
|
82
|
+
"""En-tête optionnel (description, argument) puis le texte envoyé à Prométhée."""
|
|
83
|
+
meta: dict[str, str] = {}
|
|
84
|
+
body = text.lstrip("\ufeff") # BOM éventuel
|
|
85
|
+
match = re.match(r"\s*---[ \t]*\r?\n(.*?)\r?\n---[ \t]*(?:\r?\n|$)(.*)$", body, re.S)
|
|
86
|
+
if match:
|
|
87
|
+
for line in match.group(1).splitlines():
|
|
88
|
+
key, sep, value = line.partition(":")
|
|
89
|
+
if sep:
|
|
90
|
+
meta[key.strip().lower()] = value.strip().strip("\"'")
|
|
91
|
+
body = match.group(2)
|
|
92
|
+
name = agent_slug(meta.get("nom") or meta.get("name") or path.stem)
|
|
93
|
+
prompt = body.strip()
|
|
94
|
+
if not name or not prompt:
|
|
95
|
+
return None
|
|
96
|
+
return {"name": name, "description": meta.get("description", ""),
|
|
97
|
+
"argument": meta.get("argument") or meta.get("argument-hint") or "", "prompt": prompt,
|
|
98
|
+
"scope": scope, "path": path}
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def command_markdown(command: dict) -> str:
|
|
102
|
+
lines = ["---", f"description: {' '.join(command['description'].split())}"]
|
|
103
|
+
if command.get("argument"):
|
|
104
|
+
lines.append(f"argument: {command['argument']}")
|
|
105
|
+
return "\n".join(lines) + "\n---\n\n" + command["prompt"].strip() + "\n"
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def list_custom_commands(cwd: Path) -> list[dict]:
|
|
109
|
+
"""Commandes globales puis celles du dossier (même nom : celle du dossier gagne). Les noms des commandes
|
|
110
|
+
intégrées (/aide, /clear…) restent réservés."""
|
|
111
|
+
found: dict[str, dict] = {}
|
|
112
|
+
folders = [("global", COMMANDS_DIR)]
|
|
113
|
+
local = cwd / LOCAL_COMMANDS
|
|
114
|
+
if os.path.normcase(str(local)) != os.path.normcase(str(COMMANDS_DIR)):
|
|
115
|
+
folders.append(("dossier", local))
|
|
116
|
+
reserved = builtin_command_names()
|
|
117
|
+
for scope, folder in folders:
|
|
118
|
+
try:
|
|
119
|
+
files = sorted(folder.glob("*.md"))
|
|
120
|
+
except OSError:
|
|
121
|
+
continue
|
|
122
|
+
for path in files:
|
|
123
|
+
try:
|
|
124
|
+
command = parse_command_file(read_text_file(path)[0], path, scope)
|
|
125
|
+
except (OSError, ValueError):
|
|
126
|
+
continue
|
|
127
|
+
if command and command["name"] not in reserved:
|
|
128
|
+
found[command["name"]] = command
|
|
129
|
+
return list(found.values())
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def find_custom_command(name: str, cwd: Path) -> dict | None:
|
|
133
|
+
slug = agent_slug(name)
|
|
134
|
+
return next((c for c in list_custom_commands(cwd) if c["name"] == slug), None)
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def expand_command(command: dict, args: str) -> str:
|
|
138
|
+
"""$ARGUMENTS = tout ce qui suit /nom ; $1, $2… = ses mots (les guillemets groupent)."""
|
|
139
|
+
text = command["prompt"]
|
|
140
|
+
words = [a or b for a, b in re.findall(r'"([^"]*)"|(\S+)', args)]
|
|
141
|
+
text = re.sub(r"\$([1-9])", lambda m: words[int(m.group(1)) - 1] if int(m.group(1)) <= len(words) else "", text)
|
|
142
|
+
if "$ARGUMENTS" in text:
|
|
143
|
+
return text.replace("$ARGUMENTS", args.strip())
|
|
144
|
+
return text + (f"\n\n{args.strip()}" if args.strip() else "")
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
# --------------------------------------------------------------------------- /init : mémoire du dossier
|
|
148
|
+
INIT_PROMPT = """Analyse le dossier courant ({cwd}) pour écrire sa mémoire : le fichier {path} (comme CLAUDE.md pour Claude Code), que tu reliras à chaque conversation dans ce dossier.
|
|
149
|
+
1. Liste ses fichiers avec chercher_fichiers (motif *), seul, et attends son résultat avant de lire quoi que ce soit.
|
|
150
|
+
2. Ensuite, lis avec lire_fichier les 5 fichiers les plus utiles DE CETTE LISTE (documentation, notes, scripts), en recopiant exactement leur chemin. Ne lis aucun fichier qui n'y figure pas.
|
|
151
|
+
3. Écris {path} avec ecrire_fichier : un titre, puis des puces courtes : à quoi sert ce dossier, ses fichiers et sous-dossiers importants (seulement ceux de la liste), les commandes ou outils qui apparaissent dans les fichiers lus, et les précautions qu'ils mentionnent. Rien d'inventé : pas de fichier, de commande ni de conseil général qui ne viennent pas de ce que tu as lu.{existing}"""
|
|
152
|
+
INIT_EXISTING = ("\nCe fichier existe déjà : lis-le d'abord, puis complète-le avec modifier_fichier, sans supprimer ce que "
|
|
153
|
+
"l'utilisateur y a écrit.")
|
|
154
|
+
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
"""Menus d'autocomplétion ( / et @ ) et fichiers joints avec @chemin."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import os
|
|
5
|
+
import re
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
from .agents import list_agents
|
|
9
|
+
from .commandes import COMMANDS, list_custom_commands
|
|
10
|
+
from .fichiers import read_text_file, resolve_path, show_path
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def command_completions(typed: str) -> list[tuple[str, str, str]]:
|
|
14
|
+
"""(texte à insérer, libellé, description) des commandes qui commencent par ce qui est tapé."""
|
|
15
|
+
word = typed[1:].lower()
|
|
16
|
+
return [(c.nom, c.nom, c.description) for c in COMMANDS
|
|
17
|
+
if c.nom[1:].startswith(word) or any(a.startswith(word) for a in c.alias)]
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def custom_command_completions(typed: str, cwd: Path) -> list[tuple[str, str, str]]:
|
|
21
|
+
word = typed[1:].lower()
|
|
22
|
+
items = []
|
|
23
|
+
for command in list_custom_commands(cwd):
|
|
24
|
+
if command["name"].startswith(word):
|
|
25
|
+
label = f"/{command['name']}" + (f" {command['argument']}" if command.get("argument") else "")
|
|
26
|
+
desc = " ".join(command["description"].split())
|
|
27
|
+
items.append((f"/{command['name']}", label, "perso · " + (desc[:70] + "…" if len(desc) > 70 else desc)))
|
|
28
|
+
return items
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
AT_TOKEN = re.compile(r'(?:^|(?<=\s))@("[^"]*|[^\s"]*)$') # « @chemin » en cours de frappe, juste avant le curseur
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def human_size(n: int) -> str:
|
|
35
|
+
if n < 1024:
|
|
36
|
+
return f"{n} o"
|
|
37
|
+
if n < 1024 ** 2:
|
|
38
|
+
return f"{n / 1024:.0f} Ko"
|
|
39
|
+
return f"{n / 1024 ** 2:.1f} Mo"
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def file_completions(token: str, cwd: Path, limit: int = 40) -> list[tuple[str, str, str]]:
|
|
43
|
+
raw = token.lstrip('"').replace("/", "\\")
|
|
44
|
+
folder, _, prefix = raw.rpartition("\\")
|
|
45
|
+
base = resolve_path(folder + "\\", cwd) if folder else cwd
|
|
46
|
+
entries = []
|
|
47
|
+
try:
|
|
48
|
+
with os.scandir(base) as it:
|
|
49
|
+
for i, entry in enumerate(it):
|
|
50
|
+
if i > 3000:
|
|
51
|
+
break
|
|
52
|
+
name = entry.name
|
|
53
|
+
if not name.lower().startswith(prefix.lower()) or (name.startswith(".") and not prefix.startswith(".")):
|
|
54
|
+
continue
|
|
55
|
+
try:
|
|
56
|
+
is_dir = entry.is_dir()
|
|
57
|
+
except OSError:
|
|
58
|
+
continue
|
|
59
|
+
entries.append((not is_dir, name.lower(), name, is_dir, entry))
|
|
60
|
+
except OSError:
|
|
61
|
+
return []
|
|
62
|
+
items = []
|
|
63
|
+
for _, _, name, is_dir, entry in sorted(entries)[:limit]:
|
|
64
|
+
value = (folder + "\\" if folder else "") + name + ("\\" if is_dir else "")
|
|
65
|
+
if is_dir:
|
|
66
|
+
desc = "dossier"
|
|
67
|
+
else:
|
|
68
|
+
try:
|
|
69
|
+
desc = human_size(entry.stat().st_size)
|
|
70
|
+
except OSError:
|
|
71
|
+
desc = ""
|
|
72
|
+
if " " in value: # chemin avec espaces : entre guillemets (laissés ouverts pour un dossier)
|
|
73
|
+
value = f'"{value}' if is_dir else f'"{value}"'
|
|
74
|
+
items.append((value, "@" + value.strip('"'), desc))
|
|
75
|
+
return items
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def agent_completions(token: str, cwd: Path) -> list[tuple[str, str, str]]:
|
|
79
|
+
"""Agents proposés en tête du menu « @ » (comme @agent-… dans Claude Code)."""
|
|
80
|
+
typed = token.lower()
|
|
81
|
+
if '"' in typed or "\\" in typed:
|
|
82
|
+
return []
|
|
83
|
+
items = []
|
|
84
|
+
for agent in list_agents(cwd):
|
|
85
|
+
value = f"agent-{agent['name']}"
|
|
86
|
+
if value.startswith(typed):
|
|
87
|
+
desc = " ".join(agent["description"].split())
|
|
88
|
+
items.append((value, "@" + value, "agent · " + (desc[:70] + "…" if len(desc) > 70 else desc)))
|
|
89
|
+
return items
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
# --------------------------------------------------------------------------- Fichiers joints avec @chemin
|
|
93
|
+
AT_REF = re.compile(r'(?:^|(?<=\s))@(?:"([^"]+)"?|([^\s"]+))')
|
|
94
|
+
MAX_ATTACH_CHARS = 8000
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def expand_attachments(text: str, cwd: Path) -> tuple[str, list[tuple[str, str, bool]]]:
|
|
98
|
+
"""Ajoute au message le contenu des fichiers cités avec @chemin.
|
|
99
|
+
Renvoie (texte pour le modèle, [(chemin affiché, résumé, réussi)])."""
|
|
100
|
+
blocks: list[str] = []
|
|
101
|
+
notes: list[tuple[str, str, bool]] = []
|
|
102
|
+
seen: set[str] = set()
|
|
103
|
+
for match in AT_REF.finditer(text):
|
|
104
|
+
raw = (match.group(1) or match.group(2) or "").strip()
|
|
105
|
+
path = resolve_path(raw, cwd)
|
|
106
|
+
if not path.exists(): # « regarde @log.txt, stp » : ponctuation collée
|
|
107
|
+
path = resolve_path(raw.rstrip(",.;:!?)»'"), cwd)
|
|
108
|
+
if not path.exists():
|
|
109
|
+
if re.search(r"[.\\/]", raw):
|
|
110
|
+
notes.append((raw, "introuvable, non joint", False))
|
|
111
|
+
continue
|
|
112
|
+
key = os.path.normcase(str(path))
|
|
113
|
+
if key in seen:
|
|
114
|
+
continue
|
|
115
|
+
seen.add(key)
|
|
116
|
+
shown = show_path(path, cwd)
|
|
117
|
+
if path.is_dir():
|
|
118
|
+
try:
|
|
119
|
+
names = sorted(e.name + ("\\" if e.is_dir() else "") for e in os.scandir(path))
|
|
120
|
+
except OSError as err:
|
|
121
|
+
notes.append((shown, f"illisible ({err})", False))
|
|
122
|
+
continue
|
|
123
|
+
blocks.append(f"[Contenu du dossier {path} : {len(names)} éléments]\n" + "\n".join(names[:200])
|
|
124
|
+
+ ("\n[…]" if len(names) > 200 else ""))
|
|
125
|
+
notes.append((shown, f"{len(names)} éléments listés", True))
|
|
126
|
+
continue
|
|
127
|
+
try:
|
|
128
|
+
content = read_text_file(path)[0]
|
|
129
|
+
except (OSError, ValueError) as err:
|
|
130
|
+
notes.append((shown, f"non joint : {err}", False))
|
|
131
|
+
continue
|
|
132
|
+
n = content.count("\n") + (0 if content.endswith("\n") or not content else 1)
|
|
133
|
+
if len(content) > MAX_ATTACH_CHARS:
|
|
134
|
+
content = (content[:MAX_ATTACH_CHARS] + f"\n[… coupé : {n} lignes au total. Pour la suite, utilise "
|
|
135
|
+
"lire_fichier avec debut et fin …]")
|
|
136
|
+
blocks.append(f"[Fichier joint : {path}]\n```\n{content.rstrip()}\n```")
|
|
137
|
+
notes.append((shown, f"{n} ligne{'s' if n > 1 else ''}", True))
|
|
138
|
+
if not blocks:
|
|
139
|
+
return text, notes
|
|
140
|
+
return text + "\n\n" + "\n\n".join(blocks), notes
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"""Réglages fixes de la TUI : modèle par défaut, dossier de données, limites d'exécution."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
DEFAULT_MODEL = "promethee"
|
|
7
|
+
SAVE_DIR = Path.home() / "Documents" / "Promethee"
|
|
8
|
+
|
|
9
|
+
MAX_STEPS = 12 # nombre max d'allers-retours modèle -> commande par message (audit ≈ 6-8)
|
|
10
|
+
CMD_TIMEOUT = 300 # secondes avant d'arrêter une commande bloquée
|
|
11
|
+
MAX_TOOL_CHARS = 3500 # taille max d'une sortie renvoyée au modèle
|
|
12
|
+
CREATE_NO_WINDOW = 0x08000000
|