@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,299 @@
|
|
|
1
|
+
"""L'application : état, mise en page, envoi des messages. Ses autres méthodes sont dans les modules voisins."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import argparse
|
|
5
|
+
import datetime as dt
|
|
6
|
+
import os
|
|
7
|
+
import subprocess
|
|
8
|
+
import threading
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
from textual import on
|
|
12
|
+
from textual.app import App, ComposeResult
|
|
13
|
+
from textual.binding import Binding
|
|
14
|
+
from textual.containers import Horizontal, Vertical, VerticalScroll
|
|
15
|
+
from textual.widget import Widget
|
|
16
|
+
from textual.widgets import Static, TextArea
|
|
17
|
+
|
|
18
|
+
from .clavier import Clavier
|
|
19
|
+
from .commandes_slash import CommandesSlash
|
|
20
|
+
from .contexte import Contexte
|
|
21
|
+
from .definitions import Definitions
|
|
22
|
+
from .ecran import Ecran
|
|
23
|
+
from .envoi import Envoi
|
|
24
|
+
from .historique import Historique
|
|
25
|
+
from .modele import Modele
|
|
26
|
+
from .notes import Notes
|
|
27
|
+
from .outils import Outils
|
|
28
|
+
from .plateforme import Plateforme
|
|
29
|
+
from .saisie import Saisie
|
|
30
|
+
from .taches import Taches
|
|
31
|
+
from .tour import Tour
|
|
32
|
+
from .validation import Validation
|
|
33
|
+
from .. import preferences
|
|
34
|
+
from ..accueil import Accueil
|
|
35
|
+
from ..config import DEFAULT_MODEL
|
|
36
|
+
from ..consignes import CODE_MODEL
|
|
37
|
+
from ..conversation import list_sessions, new_session_id
|
|
38
|
+
from ..logo import Logo
|
|
39
|
+
from ..politique import Politique
|
|
40
|
+
from ..textes import CSS, VERBS
|
|
41
|
+
from ..theme import FONDS, THEMES, YELLOW
|
|
42
|
+
from ..widgets import ChoicePrompt, PromptInput
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class PrometheeApp(Ecran, Saisie, Taches, Notes, Definitions, Contexte, Envoi, Tour, Outils, Validation, Historique, Modele,
|
|
46
|
+
Plateforme, CommandesSlash, Clavier, App):
|
|
47
|
+
TITLE = "Prométhée"
|
|
48
|
+
CSS = CSS
|
|
49
|
+
BINDINGS = [
|
|
50
|
+
Binding("ctrl+c", "ctrl_c", show=False, priority=True),
|
|
51
|
+
Binding("escape", "escape", show=False, priority=True),
|
|
52
|
+
Binding("ctrl+d", "ctrl_d", show=False, priority=True),
|
|
53
|
+
Binding("ctrl+q", "quit", show=False, priority=True),
|
|
54
|
+
Binding("pageup", "scroll_chat(-1)", show=False, priority=True),
|
|
55
|
+
Binding("pagedown", "scroll_chat(1)", show=False, priority=True),
|
|
56
|
+
Binding("shift+tab", "toggle_plan", show=False, priority=True),
|
|
57
|
+
Binding("tab", "complete", show=False, priority=True),
|
|
58
|
+
Binding("ctrl+o", "toggle_details", show=False, priority=True),
|
|
59
|
+
]
|
|
60
|
+
|
|
61
|
+
def __init__(self, model: str, start: str = "", politique: Politique | None = None) -> None:
|
|
62
|
+
super().__init__()
|
|
63
|
+
self.prefs = preferences.lire()
|
|
64
|
+
for theme in THEMES:
|
|
65
|
+
self.register_theme(theme)
|
|
66
|
+
self.theme = FONDS[self.prefs["fond"]]
|
|
67
|
+
self.model = model
|
|
68
|
+
self.summary = "" # résumé de la conversation après /compact (ajouté au prompt système)
|
|
69
|
+
self.allow_edits = False # « Oui pour toutes les modifications de fichiers » (session)
|
|
70
|
+
self.session_id = new_session_id()
|
|
71
|
+
self.session_created = dt.datetime.now().isoformat(timespec="seconds")
|
|
72
|
+
self.session_title = ""
|
|
73
|
+
self._start = start # "continue" (prmt -c) ou "resume" (prmt -r)
|
|
74
|
+
self.plan_mode = False # Shift+Tab : propose un plan sans rien exécuter
|
|
75
|
+
self._run_plan = False # la réponse en cours a-t-elle été demandée en mode plan ?
|
|
76
|
+
self.tasks: list[dict] = [] # liste de tâches affichée (plan accepté ou audit)
|
|
77
|
+
self.task_kind = ""
|
|
78
|
+
self._comp_items: list[tuple[str, str, str]] = [] # menu d'autocomplétion ( / ou @ )
|
|
79
|
+
self._comp_kind = ""
|
|
80
|
+
self._comp_index = 0
|
|
81
|
+
self._comp_dismissed: str | None = None
|
|
82
|
+
self._tool_parent: Widget | None = None # pendant un agent : ses outils s'affichent dans son cadre
|
|
83
|
+
self._agent_name = "" # agent en cours (son nom s'affiche dans les validations)
|
|
84
|
+
self.show_details = False # Ctrl+O : détail du travail des agents
|
|
85
|
+
self._agent_draft: str | None = None # /agents → créer : « global » ou « dossier », en attente de la description
|
|
86
|
+
self._draft_kind = "agent" # … d'un agent ou d'une commande personnalisée
|
|
87
|
+
self._esc_at = 0.0 # Esc Esc : revenir en arrière
|
|
88
|
+
self._memory_rejected: set[str] = set() # fichiers inexistants déjà signalés dans un PROMETHEE.md
|
|
89
|
+
self.code_model = CODE_MODEL # IA de code (/code pour la changer ou couper la bascule)
|
|
90
|
+
self.code_auto = True
|
|
91
|
+
self._is_code = False # dernière demande classée « programmation » par l'interpréteur
|
|
92
|
+
self._last_code = False # la réponse précédente venait de l'IA de code
|
|
93
|
+
self._turn_model: str | None = None # modèle qui répond en ce moment
|
|
94
|
+
self._loaded_model: str | None = None # dernier modèle utilisé (déjà en mémoire GPU)
|
|
95
|
+
self._caps: dict[str, list[str] | None] = {}
|
|
96
|
+
self._code_warned = False
|
|
97
|
+
self.model_desc = "" # taille et quantification, affichées à l'accueil et dans /etat
|
|
98
|
+
self.model_system = ""
|
|
99
|
+
self.supports_tools = False
|
|
100
|
+
self.supports_thinking = False
|
|
101
|
+
self.messages: list[dict] = []
|
|
102
|
+
self._generating = False
|
|
103
|
+
self._clear_after = False # /clear demandé pendant un travail : effacer dès qu'il se termine
|
|
104
|
+
self.cancel_event = threading.Event()
|
|
105
|
+
self._resp = None
|
|
106
|
+
self._proc: subprocess.Popen | None = None
|
|
107
|
+
self._perm: ChoicePrompt | None = None
|
|
108
|
+
self.cwd = Path.cwd()
|
|
109
|
+
# Politique d'exécution : liste blanche et périmètre partagés avec la console (voir politique.py)
|
|
110
|
+
self.politique = politique if politique is not None else Politique(self.prefs["politique"])
|
|
111
|
+
self._policy_warned = False # la panne du moteur de politique a déjà été signalée
|
|
112
|
+
self._policy_stop = False # la réponse en cours s'arrête après deux refus de la politique
|
|
113
|
+
self.ps_version = "" # version de PowerShell, mesurée au premier /etat
|
|
114
|
+
self.allowed: set[str] = set() # lectures reconnues autorisées pour la session, par nom
|
|
115
|
+
self.allowed_lines: set[str] = set() # autres commandes autorisées pour la session, ligne exacte
|
|
116
|
+
self.last_output = ""
|
|
117
|
+
self.num_ctx: int | None = None
|
|
118
|
+
self.ctx_used = 0
|
|
119
|
+
self.last_tps: float | None = None
|
|
120
|
+
self.last_answer = ""
|
|
121
|
+
self.model_state = "chargement…"
|
|
122
|
+
self.spin_start = 0.0
|
|
123
|
+
self.spin_tokens = 0
|
|
124
|
+
self.spin_verb = VERBS[0]
|
|
125
|
+
self.spin_thinking = False
|
|
126
|
+
self.spin_mode = "model" # "model" ou "command"
|
|
127
|
+
self._ctrl_c_at = 0.0
|
|
128
|
+
|
|
129
|
+
@property
|
|
130
|
+
def generating(self) -> bool:
|
|
131
|
+
"""Un travail est en cours (réponse du modèle, commande « ! », compactage, création d'un agent)."""
|
|
132
|
+
return self._generating
|
|
133
|
+
|
|
134
|
+
@generating.setter
|
|
135
|
+
def generating(self, value: bool) -> None:
|
|
136
|
+
self._generating = value
|
|
137
|
+
if not value and self._clear_after:
|
|
138
|
+
# Le travail interrompu par /clear vient de se terminer : on efface maintenant, après lui
|
|
139
|
+
self._clear_after = False
|
|
140
|
+
self.call_later(self.clear_conversation)
|
|
141
|
+
|
|
142
|
+
# ------------------------------------------------------------------ layout
|
|
143
|
+
def compose(self) -> ComposeResult:
|
|
144
|
+
chat = VerticalScroll(id="chat")
|
|
145
|
+
chat.can_focus = False
|
|
146
|
+
with chat:
|
|
147
|
+
yield from self._welcome_widgets()
|
|
148
|
+
with Vertical(id="bottom"):
|
|
149
|
+
yield Static(id="tasks")
|
|
150
|
+
yield Static(id="spinner")
|
|
151
|
+
with Horizontal(id="input-box"):
|
|
152
|
+
yield Static(">", id="prompt-char")
|
|
153
|
+
yield PromptInput(id="input", placeholder="Pose ta question à Prométhée… (/aide pour l'aide)")
|
|
154
|
+
yield Static(id="completions")
|
|
155
|
+
with Horizontal(id="status-line"):
|
|
156
|
+
yield Static(id="hints")
|
|
157
|
+
yield Static(id="status")
|
|
158
|
+
|
|
159
|
+
def _welcome_widgets(self):
|
|
160
|
+
yield Logo()
|
|
161
|
+
yield Accueil()
|
|
162
|
+
|
|
163
|
+
def on_mount(self) -> None:
|
|
164
|
+
self.input.focus()
|
|
165
|
+
self.watch(self.screen, "focused", self._keep_focus)
|
|
166
|
+
self.set_interval(0.1, self._tick_spinner)
|
|
167
|
+
self.set_hints()
|
|
168
|
+
self.update_status()
|
|
169
|
+
self.load_model()
|
|
170
|
+
self.demarrer_politique()
|
|
171
|
+
if self._start == "continue":
|
|
172
|
+
sessions = list_sessions(1)
|
|
173
|
+
if sessions:
|
|
174
|
+
self.call_after_refresh(self.load_session, sessions[0])
|
|
175
|
+
else:
|
|
176
|
+
self.add_notice("Aucune conversation enregistrée à reprendre.", YELLOW)
|
|
177
|
+
elif self._start == "resume":
|
|
178
|
+
self.call_after_refresh(self.cmd_resume, "")
|
|
179
|
+
|
|
180
|
+
def on_unmount(self) -> None:
|
|
181
|
+
self.cancel_event.set()
|
|
182
|
+
prompt, self._perm = self._perm, None
|
|
183
|
+
if prompt is not None:
|
|
184
|
+
prompt.resolve("no")
|
|
185
|
+
self._kill_proc()
|
|
186
|
+
self.politique.fermer()
|
|
187
|
+
|
|
188
|
+
def _keep_focus(self, focused) -> None:
|
|
189
|
+
# La saisie (ou la demande de validation) garde toujours le focus.
|
|
190
|
+
target = self._perm if self._perm is not None else self.input
|
|
191
|
+
if focused is not target:
|
|
192
|
+
self.call_after_refresh(target.focus)
|
|
193
|
+
|
|
194
|
+
def on_resize(self, event) -> None:
|
|
195
|
+
for logo in self.query(Logo):
|
|
196
|
+
logo.refresh(layout=True)
|
|
197
|
+
self.update_status() # la jauge de contexte ne s'affiche que si la largeur le permet
|
|
198
|
+
|
|
199
|
+
def _follow(self) -> None:
|
|
200
|
+
"""Suit le bas de la conversation, seulement quand elle dépasse la hauteur de l'écran.
|
|
201
|
+
|
|
202
|
+
(Ancrer un contenu plus court que l'écran le collerait en bas : l'accueil doit rester en haut.)
|
|
203
|
+
"""
|
|
204
|
+
chat = self.chat
|
|
205
|
+
if not chat.is_anchored and chat.max_scroll_y > 0:
|
|
206
|
+
chat.anchor()
|
|
207
|
+
|
|
208
|
+
@property
|
|
209
|
+
def chat(self) -> VerticalScroll:
|
|
210
|
+
return self.query_one("#chat", VerticalScroll)
|
|
211
|
+
|
|
212
|
+
@property
|
|
213
|
+
def input(self) -> PromptInput:
|
|
214
|
+
return self.query_one("#input", PromptInput)
|
|
215
|
+
|
|
216
|
+
@on(TextArea.Changed, "#input")
|
|
217
|
+
def _input_changed(self, event: TextArea.Changed) -> None:
|
|
218
|
+
# Modes « ! » (commande) et « # » (mémoire) : la barre de saisie change de couleur, comme dans Claude Code
|
|
219
|
+
text = self.input.text
|
|
220
|
+
shell, memory = text.startswith("!"), text.startswith("#")
|
|
221
|
+
box = self.query_one("#input-box")
|
|
222
|
+
if shell != box.has_class("-shell") or memory != box.has_class("-memory"):
|
|
223
|
+
box.set_class(shell, "-shell")
|
|
224
|
+
box.set_class(memory, "-memory")
|
|
225
|
+
self.query_one("#prompt-char", Static).update("!" if shell else "#" if memory else ">")
|
|
226
|
+
self.update_completions()
|
|
227
|
+
|
|
228
|
+
@on(TextArea.SelectionChanged, "#input")
|
|
229
|
+
def _input_cursor_moved(self, event: TextArea.SelectionChanged) -> None:
|
|
230
|
+
self.update_completions()
|
|
231
|
+
|
|
232
|
+
# ------------------------------------------------------------------ envoi
|
|
233
|
+
@on(PromptInput.Submitted)
|
|
234
|
+
async def on_submit(self, event: PromptInput.Submitted) -> None:
|
|
235
|
+
text = event.value.strip("\n").rstrip()
|
|
236
|
+
stripped = text.strip()
|
|
237
|
+
if stripped.startswith("/") and "\n" not in stripped:
|
|
238
|
+
self.input.remember(stripped)
|
|
239
|
+
self.input.clear()
|
|
240
|
+
if self._agent_draft is not None:
|
|
241
|
+
self.set_agent_draft(None)
|
|
242
|
+
await self.run_command(stripped)
|
|
243
|
+
return
|
|
244
|
+
if self.generating:
|
|
245
|
+
self.notify("Prométhée travaille encore. Esc pour l'interrompre.", severity="warning")
|
|
246
|
+
return
|
|
247
|
+
if self._agent_draft is not None:
|
|
248
|
+
if not stripped:
|
|
249
|
+
return
|
|
250
|
+
scope, kind = self._agent_draft, self._draft_kind
|
|
251
|
+
self.set_agent_draft(None)
|
|
252
|
+
self.input.remember(stripped)
|
|
253
|
+
self.input.clear()
|
|
254
|
+
self.add_user(stripped)
|
|
255
|
+
self.generating = True
|
|
256
|
+
self.cancel_event.clear()
|
|
257
|
+
if kind == "commande":
|
|
258
|
+
self._spinner(True, "check", "Prométhée écrit la commande")
|
|
259
|
+
self.generate_command(stripped, scope)
|
|
260
|
+
else:
|
|
261
|
+
self._spinner(True, "check", "Prométhée écrit l'agent")
|
|
262
|
+
self.generate_agent(stripped, scope)
|
|
263
|
+
return
|
|
264
|
+
if stripped.startswith("!"):
|
|
265
|
+
command = stripped[1:].strip()
|
|
266
|
+
if not command:
|
|
267
|
+
self.notify("Tape une commande après le « ! », par exemple : ! ipconfig", severity="warning")
|
|
268
|
+
return
|
|
269
|
+
self.input.remember(stripped)
|
|
270
|
+
self.input.clear()
|
|
271
|
+
view = await self._new_tool_view(command, "!", "Contrôle de la commande…")
|
|
272
|
+
self.generating = True
|
|
273
|
+
self.cancel_event.clear()
|
|
274
|
+
self._spinner(True, "check", "Contrôle de la commande")
|
|
275
|
+
self.run_shell(command, view)
|
|
276
|
+
return
|
|
277
|
+
if stripped.startswith("#"):
|
|
278
|
+
note = " ".join(stripped[1:].split())
|
|
279
|
+
if not note:
|
|
280
|
+
self.notify("Écris l'information après le « # », par exemple : # ma box est en 192.168.1.1",
|
|
281
|
+
severity="warning")
|
|
282
|
+
return
|
|
283
|
+
self.input.remember(stripped)
|
|
284
|
+
self.input.clear()
|
|
285
|
+
self.ask_memory_target(note)
|
|
286
|
+
return
|
|
287
|
+
self.input.remember(text)
|
|
288
|
+
self.input.clear()
|
|
289
|
+
self.send_message(text)
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
def main() -> None:
|
|
293
|
+
parser = argparse.ArgumentParser(prog="prmt", description="Prométhée : IA locale de cybersécurité")
|
|
294
|
+
parser.add_argument("-m", "--model", default=os.environ.get("PRMT_MODEL", DEFAULT_MODEL), help="modèle Ollama à utiliser")
|
|
295
|
+
group = parser.add_mutually_exclusive_group()
|
|
296
|
+
group.add_argument("-c", "--continue", dest="cont", action="store_true", help="reprendre la dernière conversation")
|
|
297
|
+
group.add_argument("-r", "--resume", action="store_true", help="choisir une conversation à reprendre")
|
|
298
|
+
args = parser.parse_args()
|
|
299
|
+
PrometheeApp(args.model, "continue" if args.cont else "resume" if args.resume else "").run()
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
"""Actions du clavier : Esc, Ctrl+C, Ctrl+D, défilement."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import threading
|
|
5
|
+
import time
|
|
6
|
+
|
|
7
|
+
from ..theme import DIM
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class Clavier:
|
|
11
|
+
"""Partie de PrometheeApp : actions du clavier : Esc, Ctrl+C, Ctrl+D, défilement."""
|
|
12
|
+
|
|
13
|
+
# ------------------------------------------------------------------ actions clavier
|
|
14
|
+
def action_escape(self) -> None:
|
|
15
|
+
if self._perm is not None:
|
|
16
|
+
self._perm.resolve("no")
|
|
17
|
+
return
|
|
18
|
+
if self._comp_items:
|
|
19
|
+
self.hide_completions()
|
|
20
|
+
return
|
|
21
|
+
if self._agent_draft is not None and not self.generating:
|
|
22
|
+
what = "de l'agent" if self._draft_kind == "agent" else "de la commande"
|
|
23
|
+
self.set_agent_draft(None)
|
|
24
|
+
self.input.clear()
|
|
25
|
+
self.add_notice(f"Création {what} annulée.", DIM)
|
|
26
|
+
return
|
|
27
|
+
if self.generating:
|
|
28
|
+
self.interrompre()
|
|
29
|
+
elif self.input.text:
|
|
30
|
+
self.input.clear()
|
|
31
|
+
else:
|
|
32
|
+
# Esc deux fois de suite (saisie vide) : revenir à un message précédent, comme dans Claude Code
|
|
33
|
+
now = time.monotonic()
|
|
34
|
+
if now - self._esc_at < 0.8:
|
|
35
|
+
self._esc_at = 0.0
|
|
36
|
+
self.open_rewind()
|
|
37
|
+
else:
|
|
38
|
+
self._esc_at = now
|
|
39
|
+
|
|
40
|
+
def interrompre(self) -> None:
|
|
41
|
+
"""Arrête le travail en cours : validation en attente, réponse du modèle, commande lancée."""
|
|
42
|
+
if self._perm is not None:
|
|
43
|
+
self._perm.resolve("no")
|
|
44
|
+
self.cancel_event.set()
|
|
45
|
+
resp = self._resp
|
|
46
|
+
if resp is not None:
|
|
47
|
+
threading.Thread(target=self._close_quietly, args=(resp,), daemon=True).start()
|
|
48
|
+
self._kill_proc()
|
|
49
|
+
|
|
50
|
+
@staticmethod
|
|
51
|
+
def _close_quietly(resp) -> None:
|
|
52
|
+
try:
|
|
53
|
+
resp.close()
|
|
54
|
+
except Exception:
|
|
55
|
+
pass
|
|
56
|
+
|
|
57
|
+
def action_ctrl_c(self) -> None:
|
|
58
|
+
selected = None
|
|
59
|
+
try:
|
|
60
|
+
selected = self.screen.get_selected_text()
|
|
61
|
+
except Exception:
|
|
62
|
+
pass
|
|
63
|
+
if not selected and self._perm is None and self.input.selected_text:
|
|
64
|
+
selected = self.input.selected_text
|
|
65
|
+
if selected:
|
|
66
|
+
self.copy_text(selected)
|
|
67
|
+
try:
|
|
68
|
+
self.screen.clear_selection()
|
|
69
|
+
except Exception:
|
|
70
|
+
pass
|
|
71
|
+
self.notify("Sélection copiée", timeout=2)
|
|
72
|
+
return
|
|
73
|
+
if self._perm is not None or self.generating:
|
|
74
|
+
self.action_escape()
|
|
75
|
+
return
|
|
76
|
+
if self.input.text:
|
|
77
|
+
self.input.clear()
|
|
78
|
+
return
|
|
79
|
+
now = time.monotonic()
|
|
80
|
+
if now - self._ctrl_c_at < 2:
|
|
81
|
+
self.exit()
|
|
82
|
+
return
|
|
83
|
+
self._ctrl_c_at = now
|
|
84
|
+
self.set_hints("Appuie encore sur Ctrl+C pour quitter", warn=True)
|
|
85
|
+
self.set_timer(2, self.set_hints)
|
|
86
|
+
|
|
87
|
+
def action_ctrl_d(self) -> None:
|
|
88
|
+
if self._perm is not None:
|
|
89
|
+
return
|
|
90
|
+
if self.input.text:
|
|
91
|
+
self.input.action_delete_right()
|
|
92
|
+
else:
|
|
93
|
+
self.exit()
|
|
94
|
+
|
|
95
|
+
def action_scroll_chat(self, direction: int) -> None:
|
|
96
|
+
if direction < 0:
|
|
97
|
+
self.chat.scroll_page_up()
|
|
98
|
+
else:
|
|
99
|
+
self.chat.scroll_page_down()
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
"""Aiguillage des commandes « / » et commandes simples (aide, modèle, copier, sauver, effacer)."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import datetime as dt
|
|
5
|
+
import inspect
|
|
6
|
+
import re
|
|
7
|
+
|
|
8
|
+
from ..agents import AGENT_TOOL
|
|
9
|
+
from ..appels import call_args
|
|
10
|
+
from ..commandes import expand_command, find_command, find_custom_command
|
|
11
|
+
from ..config import SAVE_DIR
|
|
12
|
+
from ..consignes import FILE_TOOL_NAMES, SEARCH_TOOL_NAMES
|
|
13
|
+
from ..conversation import new_session_id
|
|
14
|
+
from ..fichiers import search_desc
|
|
15
|
+
from ..presse_papiers import copy_windows
|
|
16
|
+
from ..recherche import OUTIL as WEB_TOOL
|
|
17
|
+
from ..textes import HELP
|
|
18
|
+
from ..theme import GREEN, PLAN_COLOR, RED, YELLOW
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class CommandesSlash:
|
|
22
|
+
"""Partie de PrometheeApp : aiguillage des commandes « / » et commandes simples (aide, modèle, copier, sauver, effacer)."""
|
|
23
|
+
|
|
24
|
+
# ------------------------------------------------------------------ commandes
|
|
25
|
+
async def run_command(self, cmd: str) -> None:
|
|
26
|
+
"""Lance une commande « / ». La liste, ses alias et la méthode qui traite chacune sont dans commandes.COMMANDS."""
|
|
27
|
+
name, _, arg = cmd[1:].partition(" ")
|
|
28
|
+
name, arg = name.lower(), arg.strip()
|
|
29
|
+
command = find_command(name)
|
|
30
|
+
if command is None:
|
|
31
|
+
custom = find_custom_command(name, self.cwd)
|
|
32
|
+
if custom is not None:
|
|
33
|
+
# Commande personnalisée : son texte part à Prométhée comme un message
|
|
34
|
+
if self.generating:
|
|
35
|
+
self.notify("Prométhée travaille encore. Esc pour l'interrompre.", severity="warning")
|
|
36
|
+
return
|
|
37
|
+
content = expand_command(custom, arg)
|
|
38
|
+
self.send_message(cmd, content, intent=content)
|
|
39
|
+
return
|
|
40
|
+
self.add_user(cmd)
|
|
41
|
+
self.add_notice(f"Commande inconnue : /{name}. Tape /aide pour la liste.", YELLOW)
|
|
42
|
+
return
|
|
43
|
+
if command.nom != "/clear":
|
|
44
|
+
self.add_user(cmd) # /clear efface l'écran : inutile d'y afficher la commande
|
|
45
|
+
result = getattr(self, command.action)(arg)
|
|
46
|
+
if inspect.isawaitable(result):
|
|
47
|
+
await result
|
|
48
|
+
|
|
49
|
+
def cmd_aide(self, arg: str = "") -> None:
|
|
50
|
+
self.add_notice_md(HELP)
|
|
51
|
+
|
|
52
|
+
def cmd_modele(self, arg: str = "") -> None:
|
|
53
|
+
if self.generating and arg:
|
|
54
|
+
self.add_notice("Attends la fin de la réponse avant de changer de modèle.", YELLOW)
|
|
55
|
+
else:
|
|
56
|
+
self.list_models(arg or None)
|
|
57
|
+
|
|
58
|
+
def cmd_sortie(self, arg: str = "") -> None:
|
|
59
|
+
if self.last_output:
|
|
60
|
+
self.add_notice_md(f"**Sortie complète de la dernière commande**\n\n```text\n{self.last_output}\n```")
|
|
61
|
+
else:
|
|
62
|
+
self.add_notice("Aucune commande exécutée pour l'instant.", YELLOW)
|
|
63
|
+
|
|
64
|
+
def cmd_compact(self, arg: str = "") -> None:
|
|
65
|
+
self.start_compact(arg)
|
|
66
|
+
|
|
67
|
+
def cmd_memoire(self, arg: str = "") -> None:
|
|
68
|
+
if arg.lower() in ("ouvrir", "open", "modifier", "edit", "editer", "éditer"):
|
|
69
|
+
self.open_memory()
|
|
70
|
+
else:
|
|
71
|
+
self.show_memory()
|
|
72
|
+
|
|
73
|
+
def cmd_plan(self, arg: str = "") -> None:
|
|
74
|
+
self.set_plan_mode(not self.plan_mode)
|
|
75
|
+
self.add_notice("Mode plan activé : Prométhée propose un plan sans rien exécuter (Shift+Tab ou /plan "
|
|
76
|
+
"pour quitter)." if self.plan_mode else "Mode plan désactivé.", PLAN_COLOR)
|
|
77
|
+
|
|
78
|
+
def cmd_quitter(self, arg: str = "") -> None:
|
|
79
|
+
self.exit()
|
|
80
|
+
|
|
81
|
+
async def clear_conversation(self, arg: str = "") -> None:
|
|
82
|
+
if self.generating:
|
|
83
|
+
# Le travail en cours est interrompu ; l'effacement se fait quand il a rendu la main (voir le
|
|
84
|
+
# setter de « generating »). Avant, sa fin remettait l'ancienne conversation en place après l'effacement.
|
|
85
|
+
self._clear_after = True
|
|
86
|
+
self.interrompre()
|
|
87
|
+
return
|
|
88
|
+
self.messages = []
|
|
89
|
+
self.summary = ""
|
|
90
|
+
# Une nouvelle conversation repart sans autorisation de session
|
|
91
|
+
self.allow_edits = False
|
|
92
|
+
self.allowed.clear()
|
|
93
|
+
self.allowed_lines.clear()
|
|
94
|
+
self.session_id = new_session_id()
|
|
95
|
+
self.session_created = dt.datetime.now().isoformat(timespec="seconds")
|
|
96
|
+
self.session_title = ""
|
|
97
|
+
self.last_answer = ""
|
|
98
|
+
self.ctx_used = 0
|
|
99
|
+
self.tasks = []
|
|
100
|
+
self.render_tasks()
|
|
101
|
+
if self._agent_draft is not None:
|
|
102
|
+
self.set_agent_draft(None)
|
|
103
|
+
await self.chat.remove_children()
|
|
104
|
+
await self.chat.mount_all(list(self._welcome_widgets()))
|
|
105
|
+
self.chat.anchor(False)
|
|
106
|
+
self.chat.scroll_home(animate=False)
|
|
107
|
+
self.update_status()
|
|
108
|
+
self.notify("Nouvelle conversation", timeout=2)
|
|
109
|
+
|
|
110
|
+
def copy_text(self, text: str) -> None:
|
|
111
|
+
if not copy_windows(text):
|
|
112
|
+
self.copy_to_clipboard(text) # repli : séquence OSC 52 du terminal
|
|
113
|
+
|
|
114
|
+
def cmd_copy(self, arg: str = "") -> None:
|
|
115
|
+
if not self.last_answer:
|
|
116
|
+
self.add_notice("Rien à copier pour l'instant.", YELLOW)
|
|
117
|
+
return
|
|
118
|
+
text = self.last_answer
|
|
119
|
+
if arg.lower().startswith("code"):
|
|
120
|
+
blocks = re.findall(r"```[^\n]*\n(.*?)```", text, flags=re.S)
|
|
121
|
+
if not blocks:
|
|
122
|
+
self.add_notice("Pas de bloc de code dans la dernière réponse.", YELLOW)
|
|
123
|
+
return
|
|
124
|
+
text = "\n\n".join(b.rstrip("\n") for b in blocks)
|
|
125
|
+
self.copy_text(text)
|
|
126
|
+
what = "Blocs de code copiés" if arg.lower().startswith("code") else "Dernière réponse copiée"
|
|
127
|
+
self.add_notice(f"{what} dans le presse-papiers ({len(text)} caractères).", GREEN)
|
|
128
|
+
|
|
129
|
+
def cmd_save(self, arg: str = "") -> None:
|
|
130
|
+
if not self.messages:
|
|
131
|
+
self.add_notice("La conversation est vide.", YELLOW)
|
|
132
|
+
return
|
|
133
|
+
now = dt.datetime.now()
|
|
134
|
+
path = SAVE_DIR / f"conversation-{now:%Y-%m-%d_%H-%M-%S}.md"
|
|
135
|
+
out = [f"# Conversation Prométhée ({self.model}), {now:%d/%m/%Y %H:%M}\n"]
|
|
136
|
+
for m in self.messages:
|
|
137
|
+
if m["role"] == "user":
|
|
138
|
+
out.append(f"## Toi\n\n{m['content']}\n")
|
|
139
|
+
elif m["role"] == "assistant":
|
|
140
|
+
if m.get("content", "").strip():
|
|
141
|
+
out.append(f"## Prométhée\n\n{m['content']}\n")
|
|
142
|
+
for call in m.get("tool_calls") or []:
|
|
143
|
+
name = (call.get("function") or {}).get("name") or "?"
|
|
144
|
+
args = call_args(call)
|
|
145
|
+
if name in FILE_TOOL_NAMES:
|
|
146
|
+
out.append(f"**{name}** : `{args.get('chemin', '')}`\n")
|
|
147
|
+
elif name in SEARCH_TOOL_NAMES:
|
|
148
|
+
out.append(f"**{name}** : {search_desc(name, args, str(args.get('dossier') or ''))}\n")
|
|
149
|
+
elif name == AGENT_TOOL:
|
|
150
|
+
out.append(f"**Agent {args.get('agent', '?')}** : {args.get('tache', '')}\n")
|
|
151
|
+
elif name == WEB_TOOL:
|
|
152
|
+
out.append(f"**Recherche web** : « {args.get('requete', '')} »\n")
|
|
153
|
+
else:
|
|
154
|
+
command = args.get("commande") or args.get("command") or args.get("cmd") or ""
|
|
155
|
+
out.append(f"**Commande exécutée :**\n\n```powershell\n{command}\n```\n")
|
|
156
|
+
elif m["role"] == "tool":
|
|
157
|
+
out.append(f"**Résultat :**\n\n```text\n{m['content']}\n```\n")
|
|
158
|
+
try:
|
|
159
|
+
SAVE_DIR.mkdir(parents=True, exist_ok=True)
|
|
160
|
+
path.write_text("\n".join(out), encoding="utf-8")
|
|
161
|
+
except OSError as err:
|
|
162
|
+
self.add_notice(f"Enregistrement impossible dans {path} : {err}", f"bold {RED}")
|
|
163
|
+
return
|
|
164
|
+
self.add_notice(f"Conversation enregistrée : {path}", GREEN)
|