@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.
Files changed (166) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +191 -2
  3. package/bin/prmt.mjs +174 -0
  4. package/dist/assets/AuditModule-BOO_iRy8.js +1 -0
  5. package/dist/assets/Audits-yUBm0EpW.js +1 -0
  6. package/dist/assets/BrainPage-FIWO_9Nc.js +3 -0
  7. package/dist/assets/BrainPage-XQCEGRc5.css +1 -0
  8. package/dist/assets/Carte-CPuiR1Ha.css +1 -0
  9. package/dist/assets/Carte-xycu1mQn.js +1 -0
  10. package/dist/assets/Dashboard-DKvCQcX8.js +2 -0
  11. package/dist/assets/Dashboard-XdhC6_hM.css +1 -0
  12. package/dist/assets/Field-C7c2mCbb.js +2 -0
  13. package/dist/assets/Journal-BLIIlh80.css +1 -0
  14. package/dist/assets/Journal-BaelFjwV.js +1 -0
  15. package/dist/assets/Landing-B2jxV3zs.css +1 -0
  16. package/dist/assets/Landing-Dr612Zdu.js +26 -0
  17. package/dist/assets/Register-DA_Aq5f8.js +1 -0
  18. package/dist/assets/Reports-8wfDNSxH.js +1 -0
  19. package/dist/assets/Reports-B8GXN95N.css +1 -0
  20. package/dist/assets/SavoirPage-BTxbH3LY.css +1 -0
  21. package/dist/assets/SavoirPage-Dp3eUCHp.js +1 -0
  22. package/dist/assets/SettingsPage-BElIP46c.css +1 -0
  23. package/dist/assets/SettingsPage-BiQbnw3o.js +1 -0
  24. package/dist/assets/Shell-BAyHVljp.css +1 -0
  25. package/dist/assets/Shell-CQRlx9-V.js +2 -0
  26. package/dist/assets/Table-DUmGel5e.js +1 -0
  27. package/dist/assets/Tabs-CfhbELZk.js +1 -0
  28. package/dist/assets/Terminal-Bv1IJfi9.js +3 -0
  29. package/dist/assets/Terminal-CEfj38NV.css +1 -0
  30. package/dist/assets/arrow-right-Cx3pAEhl.js +1 -0
  31. package/dist/assets/auditViews-BMza2Sdz.js +1 -0
  32. package/dist/assets/auditViews-BePWj26h.css +1 -0
  33. package/dist/assets/bricolage-grotesque-latin-ext-opsz-normal-IcJDqblK.woff2 +0 -0
  34. package/dist/assets/bricolage-grotesque-latin-opsz-normal-Cre6nC2_.woff2 +0 -0
  35. package/dist/assets/bricolage-grotesque-vietnamese-opsz-normal-D9N6E8K1.woff2 +0 -0
  36. package/dist/assets/charts-BcijSJ4J.js +1 -0
  37. package/dist/assets/circle-stop-C33BAcKW.js +1 -0
  38. package/dist/assets/command-ChA49Xk7.js +1 -0
  39. package/dist/assets/command-Ck4pdQCv.css +1 -0
  40. package/dist/assets/download-CeuKOenD.js +1 -0
  41. package/dist/assets/eye-CXXHBBpr.js +1 -0
  42. package/dist/assets/geist-cyrillic-ext-wght-normal-DjL33-gN.woff2 +0 -0
  43. package/dist/assets/geist-cyrillic-wght-normal-BEAKL7Jp.woff2 +0 -0
  44. package/dist/assets/geist-latin-ext-wght-normal-DC-KSUi6.woff2 +0 -0
  45. package/dist/assets/geist-latin-wght-normal-BgDaEnEv.woff2 +0 -0
  46. package/dist/assets/geist-vietnamese-wght-normal-6IgcOCM7.woff2 +0 -0
  47. package/dist/assets/habillage-B5mMBadn.js +1 -0
  48. package/dist/assets/index-BH1etemR.css +1 -0
  49. package/dist/assets/index-COSiM-R9.js +14 -0
  50. package/dist/assets/jetbrains-mono-cyrillic-ext-wght-normal-EocZY2iu.woff2 +0 -0
  51. package/dist/assets/jetbrains-mono-cyrillic-wght-normal-D73BlboJ.woff2 +0 -0
  52. package/dist/assets/jetbrains-mono-greek-wght-normal-Bw9x6K1M.woff2 +0 -0
  53. package/dist/assets/jetbrains-mono-latin-ext-wght-normal-DBQx-q_a.woff2 +0 -0
  54. package/dist/assets/jetbrains-mono-latin-wght-normal-B9CIFXIH.woff2 +0 -0
  55. package/dist/assets/jetbrains-mono-vietnamese-wght-normal-Bt-aOZkq.woff2 +0 -0
  56. package/dist/assets/layers-2x-Bpkbi35X.png +0 -0
  57. package/dist/assets/layers-BWBAp2CZ.png +0 -0
  58. package/dist/assets/markdown-DF87LQh1.css +1 -0
  59. package/dist/assets/markdown-o5sMhPHN.js +4 -0
  60. package/dist/assets/marker-icon-hN30_KVU.png +0 -0
  61. package/dist/assets/play-Daw4K_73.js +1 -0
  62. package/dist/assets/plus-D_QHm8ud.js +1 -0
  63. package/dist/assets/trash-DLpHM-s8.js +1 -0
  64. package/dist/assets/web-6j1-aY67.js +1 -0
  65. package/dist/favicon.svg +1 -0
  66. package/dist/index.html +18 -0
  67. package/docs/ANALYSE.md +87 -0
  68. package/docs/SECURITE.md +188 -0
  69. package/lib/scripts/lier.js +15 -0
  70. package/lib/scripts/searxng.js +91 -0
  71. package/lib/server/app.js +444 -0
  72. package/lib/server/audits/modules/local.js +429 -0
  73. package/lib/server/audits/modules/network.js +331 -0
  74. package/lib/server/audits/modules/web.js +302 -0
  75. package/lib/server/audits/net.js +141 -0
  76. package/lib/server/audits/registry.js +202 -0
  77. package/lib/server/audits/types.js +58 -0
  78. package/lib/server/brain/brain.js +624 -0
  79. package/lib/server/brain/conversations.js +82 -0
  80. package/lib/server/brain/ollama.js +197 -0
  81. package/lib/server/brain/prompt.js +188 -0
  82. package/lib/server/brain/provider.js +1 -0
  83. package/lib/server/cli/bridge.js +292 -0
  84. package/lib/server/cli/pont.js +62 -0
  85. package/lib/server/config.js +68 -0
  86. package/lib/server/engine/analyzer.js +205 -0
  87. package/lib/server/engine/catalog.js +229 -0
  88. package/lib/server/engine/decoder.js +91 -0
  89. package/lib/server/engine/engine.js +194 -0
  90. package/lib/server/engine/paths.js +129 -0
  91. package/lib/server/engine/policy.js +397 -0
  92. package/lib/server/engine/relax.js +10 -0
  93. package/lib/server/engine/runner.js +193 -0
  94. package/lib/server/errors.js +23 -0
  95. package/lib/server/http/guard.js +75 -0
  96. package/lib/server/http/router.js +154 -0
  97. package/lib/server/http/static.js +76 -0
  98. package/lib/server/index.js +40 -0
  99. package/lib/server/journal.js +139 -0
  100. package/lib/server/lien.js +33 -0
  101. package/lib/server/savoir/base.js +284 -0
  102. package/lib/server/savoir/lecture.js +406 -0
  103. package/lib/server/savoir/memoire.js +115 -0
  104. package/lib/server/search/categories.js +179 -0
  105. package/lib/server/search/fiche.js +326 -0
  106. package/lib/server/search/lieux.js +134 -0
  107. package/lib/server/search/overpass.js +216 -0
  108. package/lib/server/search/searxng.js +223 -0
  109. package/lib/server/search/tiles.js +103 -0
  110. package/lib/server/status.js +157 -0
  111. package/lib/server/stores.js +141 -0
  112. package/lib/server/telephone/acces.js +457 -0
  113. package/lib/server/telephone/certificat.js +79 -0
  114. package/lib/server/telephone/qr.js +308 -0
  115. package/lib/shared/analysis.js +40 -0
  116. package/lib/shared/schemas.js +105 -0
  117. package/lib/shared/types.js +10 -0
  118. package/package.json +81 -4
  119. package/searxng/settings.yml +35 -0
  120. package/tui/Modelfile +23 -0
  121. package/tui/README.md +103 -0
  122. package/tui/prmt_tui.py +65 -0
  123. package/tui/promethee/__init__.py +1 -0
  124. package/tui/promethee/__main__.py +4 -0
  125. package/tui/promethee/accueil.py +66 -0
  126. package/tui/promethee/agents.py +220 -0
  127. package/tui/promethee/app/__init__.py +4 -0
  128. package/tui/promethee/app/application.py +299 -0
  129. package/tui/promethee/app/clavier.py +99 -0
  130. package/tui/promethee/app/commandes_slash.py +164 -0
  131. package/tui/promethee/app/contexte.py +188 -0
  132. package/tui/promethee/app/definitions.py +518 -0
  133. package/tui/promethee/app/ecran.py +177 -0
  134. package/tui/promethee/app/envoi.py +141 -0
  135. package/tui/promethee/app/historique.py +373 -0
  136. package/tui/promethee/app/modele.py +108 -0
  137. package/tui/promethee/app/notes.py +63 -0
  138. package/tui/promethee/app/outils.py +589 -0
  139. package/tui/promethee/app/plateforme.py +409 -0
  140. package/tui/promethee/app/saisie.py +100 -0
  141. package/tui/promethee/app/taches.py +92 -0
  142. package/tui/promethee/app/tour.py +539 -0
  143. package/tui/promethee/app/validation.py +210 -0
  144. package/tui/promethee/appels.py +35 -0
  145. package/tui/promethee/commandes.py +154 -0
  146. package/tui/promethee/completion.py +140 -0
  147. package/tui/promethee/config.py +12 -0
  148. package/tui/promethee/consignes.py +278 -0
  149. package/tui/promethee/conversation.py +94 -0
  150. package/tui/promethee/etat.py +253 -0
  151. package/tui/promethee/fichiers.py +354 -0
  152. package/tui/promethee/journal.py +116 -0
  153. package/tui/promethee/logo.py +132 -0
  154. package/tui/promethee/memoire.py +50 -0
  155. package/tui/promethee/ollama.py +52 -0
  156. package/tui/promethee/plan.py +91 -0
  157. package/tui/promethee/politique.py +429 -0
  158. package/tui/promethee/preferences.py +65 -0
  159. package/tui/promethee/presse_papiers.py +44 -0
  160. package/tui/promethee/recherche.py +581 -0
  161. package/tui/promethee/securite.py +204 -0
  162. package/tui/promethee/shell.py +190 -0
  163. package/tui/promethee/textes.py +190 -0
  164. package/tui/promethee/theme.py +92 -0
  165. package/tui/promethee/widgets.py +493 -0
  166. 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)