@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,518 @@
1
+ """/agents et /commandes : lister, voir, créer (à la main ou décrits à Prométhée), modifier, supprimer.
2
+
3
+ Un agent et une commande personnalisée sont tous deux un fichier Markdown que tu peux écrire toi-même ou
4
+ faire écrire par Prométhée. Leurs écrans suivent le même déroulé : il est écrit une fois, dans Definitions.
5
+ Ce qui les distingue (libellés, champs, façon de s'en servir) est décrit par un « genre » : AGENTS, COMMANDES.
6
+ """
7
+ from __future__ import annotations
8
+
9
+ import json
10
+ import os
11
+ import subprocess
12
+ from pathlib import Path
13
+
14
+ from rich.text import Text
15
+ from textual import work
16
+
17
+ from ..agents import (
18
+ AGENTS_DIR, AGENT_GEN_FIX, AGENT_GEN_SYSTEM, AGENT_SCHEMA, AGENT_TEMPLATE, AGENT_TOOL_NAMES, LOCAL_AGENTS,
19
+ SCOPE_LABELS, TOOL_LABELS, agent_command_problems, agent_markdown, agent_slug, find_agent, list_agents,
20
+ unique_agent_path,
21
+ )
22
+ from ..commandes import (
23
+ COMMANDS_DIR, COMMAND_GEN_SYSTEM, COMMAND_SCHEMA, COMMAND_TEMPLATE, LOCAL_COMMANDS, builtin_command_names,
24
+ command_markdown, find_custom_command, list_custom_commands,
25
+ )
26
+ from ..config import CREATE_NO_WINDOW
27
+ from ..consignes import READ_TOOL, TOOL_NAME
28
+ from ..fichiers import new_file_format, write_text_file
29
+ from ..theme import AGENT_COLOR, DIM, GREEN, RED, YELLOW
30
+
31
+ CREER = ("creer", "nouveau", "new", "create", "ajouter")
32
+ APERCU_LIGNES = 14 # lignes de consignes montrées avant de créer
33
+
34
+
35
+ def portee(item: dict) -> str:
36
+ return SCOPE_LABELS.get(item["scope"], item["scope"])
37
+
38
+
39
+ def resume(item: dict) -> str:
40
+ return " ".join(item["description"].split())
41
+
42
+
43
+ # --------------------------------------------------------------------------- genres
44
+ class Genre:
45
+ """Ce qui distingue un agent d'une commande personnalisée dans les écrans de gestion."""
46
+
47
+ cle = "" # « agent » ou « commande » (valeur de _draft_kind)
48
+ dossier: Path # définitions globales
49
+ local: Path # définitions d'un dossier, relatif au dossier courant
50
+ schema: dict = {} # forme de la réponse demandée au modèle
51
+ consigne = "" # consigne de génération
52
+ jetons = 0 # longueur maximale de la réponse
53
+ ferme_menu = False # après « l'utiliser », fermer le menu d'autocomplétion (une commande commence par /)
54
+ # Libellés (le genre grammatical change d'un genre à l'autre)
55
+ occupe = inconnu = titre = nouveau = titre_creation = ou_global = ou_dossier = ou_lieux = comment = decrire = ""
56
+ question = oui = oui_modifier = non_cree = annulee = impossible = demande = sans_texte = verification = ""
57
+ correction: str | None = None # texte du spinner pendant la correction (None : pas de changement de texte)
58
+ invite = "" # ce qui s'affiche quand tu vas décrire la définition à créer
59
+
60
+ def lister(self, cwd: Path) -> list[dict]:
61
+ raise NotImplementedError
62
+
63
+ def trouver(self, nom: str, cwd: Path) -> dict | None:
64
+ raise NotImplementedError
65
+
66
+ def markdown(self, item: dict) -> str:
67
+ raise NotImplementedError
68
+
69
+
70
+ class GenreAgent(Genre):
71
+ cle, dossier, local = "agent", AGENTS_DIR, LOCAL_AGENTS
72
+ schema, consigne, jetons = AGENT_SCHEMA, AGENT_GEN_SYSTEM, 1000
73
+ occupe = "Attends la fin de la réponse avant de gérer les agents (ou Esc pour l'interrompre)."
74
+ inconnu = "Agent inconnu : {arg}."
75
+ titre, nouveau, titre_creation = "Agents", "Créer un nouvel agent", "Nouvel agent"
76
+ ou_global = "Agent global : disponible partout"
77
+ ou_dossier = "Agent de ce dossier : seulement quand tu travailles dans {dossier}"
78
+ ou_lieux = " global : {globaux}\n ce dossier : {local}"
79
+ comment, decrire = "Comment le créer ?", "Le décrire : Prométhée écrit l'agent pour toi (recommandé)"
80
+ question, oui, oui_modifier = "Créer cet agent ?", "Oui, le créer", "Oui, puis le modifier dans le Bloc-notes"
81
+ non_cree, annulee = "Agent non créé.", "Création de l'agent annulée."
82
+ impossible = "Impossible de créer l'agent : {error}. Réessaie (/agents) ou écris-le toi-même."
83
+ demande = "Description de l'agent voulu : « {description} »"
84
+ sans_texte = "le modèle n'a pas écrit de consignes"
85
+ verification, correction = "Vérification des commandes de l'agent", "Prométhée corrige l'agent"
86
+ invite = ("Décris l'agent à créer : son rôle, ce qu'il doit faire et quand l'utiliser. Par exemple : "
87
+ "« un agent qui audite le pare-feu Windows et liste les règles trop permissives ».")
88
+
89
+ def lister(self, cwd: Path) -> list[dict]:
90
+ return list_agents(cwd)
91
+
92
+ def trouver(self, nom: str, cwd: Path) -> dict | None:
93
+ return find_agent(nom, cwd)
94
+
95
+ def markdown(self, item: dict) -> str:
96
+ return agent_markdown(item)
97
+
98
+ def entete(self, items: list[dict], cwd: Path) -> Text:
99
+ return Text.assemble((f" {len(items)} agent{'s' if len(items) > 1 else ''}", "bold"),
100
+ (" · tape @agent-nom pour confier une tâche à un agent\n", DIM),
101
+ (f" globaux : {AGENTS_DIR}\n de ce dossier : {cwd / LOCAL_AGENTS}", DIM))
102
+
103
+ def ligne(self, item: dict) -> str:
104
+ desc = resume(item)
105
+ return f"{item['name']} · {portee(item)} · {desc[:60] + '…' if len(desc) > 60 else desc}"
106
+
107
+ def titre_fiche(self, item: dict) -> str:
108
+ return f"Agent {item['name']}"
109
+
110
+ def fiche(self, item: dict) -> Text:
111
+ path = item.get("path")
112
+ body = Text()
113
+ body.append(f" {resume(item) or '(sans description)'}\n", style=DIM)
114
+ body.append(f" outils : {', '.join(TOOL_LABELS.get(t, t) for t in item['tools'])}", style=DIM)
115
+ if item.get("model"):
116
+ body.append(f"\n modèle : {item['model']}", style=DIM)
117
+ body.append(f"\n fichier : {path}" if path else "\n agent intégré à Prométhée", style=DIM)
118
+ return body
119
+
120
+ def options(self, item: dict) -> list[tuple[str, str]]:
121
+ options = [("use", f"L'utiliser : écrire @agent-{item['name']} dans la saisie"), ("show", "Voir ses consignes")]
122
+ if item.get("path"):
123
+ options += [("edit", "Le modifier dans le Bloc-notes"), ("delete", "Le supprimer")]
124
+ else:
125
+ options.append(("copy", "Le personnaliser (copie modifiable dans tes agents globaux)"))
126
+ return options + [("back", "Retour à la liste"), ("no", "Fermer (Esc)")]
127
+
128
+ def saisie(self, item: dict) -> str:
129
+ return f"@agent-{item['name']} "
130
+
131
+ def montrer(self, item: dict) -> str:
132
+ tools = ", ".join(f"`{t}`" for t in item["tools"])
133
+ return (f"**Agent {item['name']}** ({portee(item)})\n\n*{resume(item)}*\n\nOutils : {tools}\n\n"
134
+ f"```text\n{item['prompt']}\n```")
135
+
136
+ def ouvert(self, item: dict) -> str:
137
+ return f"Agent {item['name']} ouvert dans le Bloc-notes. Les changements comptent dès sa prochaine utilisation."
138
+
139
+ def suppression(self, item: dict) -> tuple[str, str]:
140
+ return "Supprimer un agent", f"Supprimer l'agent {item['name']} ?"
141
+
142
+ def supprime(self, item: dict, cwd: Path) -> str:
143
+ back = find_agent(item["name"], cwd)
144
+ return f"Agent {item['name']} supprimé ({item['path']})." + (
145
+ " L'agent intégré du même nom est de nouveau utilisé." if back and back["scope"] == "intégré" else "")
146
+
147
+ def gabarit(self, name: str) -> tuple[str, str]:
148
+ """(contenu du modèle prérempli, message une fois le fichier ouvert). {path} est remplacé par le chemin."""
149
+ return agent_markdown(dict(AGENT_TEMPLATE, name=name)), (
150
+ "Modèle d'agent créé ({path}). Remplis-le dans le Bloc-notes (nom, description, outils, consignes) puis "
151
+ "enregistre : il sera disponible tout de suite.")
152
+
153
+ gabarit_nom = AGENT_TEMPLATE["name"]
154
+
155
+ def construire(self, data: dict, prompt: str, scope: str) -> dict:
156
+ return {"name": agent_slug(str(data.get("nom") or "")) or "mon-agent", "description": " ".join(str(data.get("description") or "").split()),
157
+ "tools": [t for t in AGENT_TOOL_NAMES if t in (data.get("outils") or [])] or [TOOL_NAME, READ_TOOL],
158
+ "model": "", "prompt": prompt, "scope": scope, "path": None, "warnings": []}
159
+
160
+ def nom_libre(self, item: dict, folder: Path) -> tuple[str, Path]:
161
+ return unique_agent_path(folder, item["name"])
162
+
163
+ def apercu(self, item: dict) -> Text:
164
+ body = Text()
165
+ body.append(f" @agent-{item['name']}\n", style=f"bold {AGENT_COLOR}")
166
+ body.append(f" {item['description']}\n", style=DIM)
167
+ body.append(f" outils : {', '.join(TOOL_LABELS.get(t, t) for t in item['tools'])}\n\n", style=DIM)
168
+ return body
169
+
170
+ def cree(self, item: dict, edit: bool) -> str:
171
+ name = item["name"]
172
+ if edit:
173
+ return f"Agent {name} créé et ouvert dans le Bloc-notes. Utilise-le avec @agent-{name}."
174
+ return (f"Agent {name} créé ({item['path']}). Utilise-le avec @agent-{name} ou demande à Prométhée "
175
+ "de lui confier une tâche.")
176
+
177
+
178
+ class GenreCommande(Genre):
179
+ cle, dossier, local = "commande", COMMANDS_DIR, LOCAL_COMMANDS
180
+ schema, consigne, jetons = COMMAND_SCHEMA, COMMAND_GEN_SYSTEM, 800
181
+ ferme_menu = True
182
+ occupe = "Attends la fin de la réponse avant de gérer les commandes (ou Esc pour l'interrompre)."
183
+ inconnu = "Commande personnalisée inconnue : {arg}."
184
+ titre, nouveau, titre_creation = "Commandes personnalisées", "Créer une nouvelle commande", "Nouvelle commande"
185
+ ou_global = "Commande globale : disponible partout"
186
+ ou_dossier = "Commande de ce dossier : seulement quand tu travailles dans {dossier}"
187
+ ou_lieux = " globales : {globaux}\n ce dossier : {local}"
188
+ comment, decrire = "Comment la créer ?", "La décrire : Prométhée l'écrit pour toi (recommandé)"
189
+ question, oui, oui_modifier = "Créer cette commande ?", "Oui, la créer", "Oui, puis la modifier dans le Bloc-notes"
190
+ non_cree, annulee = "Commande non créée.", "Création de la commande annulée."
191
+ impossible = "Impossible de créer la commande : {error}. Réessaie (/commandes) ou écris-la toi-même."
192
+ demande = "Description de la commande voulue : « {description} »"
193
+ sans_texte = "le modèle n'a pas écrit le texte de la commande"
194
+ verification = "Vérification des commandes citées"
195
+ invite = ("Décris la commande à créer : ce qu'elle doit faire et ce que tu tapes après son nom. Par "
196
+ "exemple : « une commande qui scanne les ports courants d'une adresse IP et explique "
197
+ "chaque service trouvé ».")
198
+
199
+ def lister(self, cwd: Path) -> list[dict]:
200
+ return list_custom_commands(cwd)
201
+
202
+ def trouver(self, nom: str, cwd: Path) -> dict | None:
203
+ return find_custom_command(nom.lstrip("/"), cwd)
204
+
205
+ def markdown(self, item: dict) -> str:
206
+ return command_markdown(item)
207
+
208
+ @staticmethod
209
+ def usage(item: dict) -> str:
210
+ return f"/{item['name']}" + (f" {item['argument']}" if item.get("argument") else "")
211
+
212
+ def entete(self, items: list[dict], cwd: Path) -> Text:
213
+ s = "s" if len(items) > 1 else ""
214
+ return Text.assemble((f" {len(items)} commande{s} personnalisée{s}", "bold"),
215
+ (" · tape /nom pour en lancer une (elles apparaissent dans le menu /)\n", DIM),
216
+ (f" globales : {COMMANDS_DIR}\n de ce dossier : {cwd / LOCAL_COMMANDS}", DIM))
217
+
218
+ def ligne(self, item: dict) -> str:
219
+ desc = resume(item)
220
+ return f"/{item['name']} · {portee(item)} · {desc[:60] + '…' if len(desc) > 60 else desc}"
221
+
222
+ def titre_fiche(self, item: dict) -> str:
223
+ return f"Commande /{item['name']}"
224
+
225
+ def fiche(self, item: dict) -> Text:
226
+ body = Text()
227
+ body.append(f" {self.usage(item)}\n", style=f"bold {AGENT_COLOR}")
228
+ body.append(f" {resume(item) or '(sans description)'}\n", style=DIM)
229
+ body.append(f" fichier : {item['path']}", style=DIM)
230
+ return body
231
+
232
+ def options(self, item: dict) -> list[tuple[str, str]]:
233
+ return [("use", f"L'utiliser : écrire /{item['name']} dans la saisie"), ("show", "Voir son texte"),
234
+ ("edit", "La modifier dans le Bloc-notes"), ("delete", "La supprimer"), ("back", "Retour à la liste"),
235
+ ("no", "Fermer (Esc)")]
236
+
237
+ def saisie(self, item: dict) -> str:
238
+ return f"/{item['name']} "
239
+
240
+ def montrer(self, item: dict) -> str:
241
+ return (f"**Commande {self.usage(item)}** ({portee(item)})\n\n*{resume(item)}*\n\n"
242
+ f"```text\n{item['prompt']}\n```")
243
+
244
+ def ouvert(self, item: dict) -> str:
245
+ return f"Commande /{item['name']} ouverte dans le Bloc-notes. Les changements comptent dès sa prochaine utilisation."
246
+
247
+ def suppression(self, item: dict) -> tuple[str, str]:
248
+ return "Supprimer", f"Supprimer la commande /{item['name']} ?"
249
+
250
+ def supprime(self, item: dict, cwd: Path) -> str:
251
+ return f"La commande /{item['name']} : supprimée ({item['path']})."
252
+
253
+ def gabarit(self, name: str) -> tuple[str, str]:
254
+ return command_markdown(COMMAND_TEMPLATE), (
255
+ f"Modèle de commande créé ({{path}}) : le nom du fichier est le nom de la commande (/{name}). "
256
+ "Écris son texte puis enregistre.")
257
+
258
+ gabarit_nom = COMMAND_TEMPLATE["name"]
259
+
260
+ def construire(self, data: dict, prompt: str, scope: str) -> dict:
261
+ argument = str(data.get("argument") or "").strip()
262
+ if argument and "$ARGUMENTS" not in prompt:
263
+ prompt += "\n\nCible : $ARGUMENTS"
264
+ return {"name": agent_slug(str(data.get("nom") or "")) or "ma-commande", "description": " ".join(str(data.get("description") or "").split()),
265
+ "argument": argument, "prompt": prompt, "scope": scope, "path": None, "warnings": []}
266
+
267
+ def nom_libre(self, item: dict, folder: Path) -> tuple[str, Path]:
268
+ name = item["name"]
269
+ if name in builtin_command_names():
270
+ name = f"{name}-perso" # une commande personnalisée ne peut pas prendre le nom d'une commande intégrée
271
+ return unique_agent_path(folder, name)
272
+
273
+ def apercu(self, item: dict) -> Text:
274
+ body = Text()
275
+ body.append(f" {self.usage(item)}\n", style=f"bold {AGENT_COLOR}")
276
+ body.append(f" {item['description']}\n\n", style=DIM)
277
+ return body
278
+
279
+ def cree(self, item: dict, edit: bool) -> str:
280
+ name = item["name"]
281
+ if edit:
282
+ return f"Commande /{name} créée et ouverte dans le Bloc-notes."
283
+ return f"Commande /{name} créée ({item['path']}). Lance-la avec {self.usage(item)}."
284
+
285
+
286
+ AGENTS, COMMANDES = GenreAgent(), GenreCommande()
287
+ GENRES = {genre.cle: genre for genre in (AGENTS, COMMANDES)}
288
+
289
+
290
+ # --------------------------------------------------------------------------- écrans
291
+ class Definitions:
292
+ """Partie de PrometheeApp : /agents et /commandes : lister, créer, modifier, supprimer."""
293
+
294
+ def cmd_agents(self, arg: str = "") -> None:
295
+ """/agents : la liste des agents, puis créer, voir, modifier ou supprimer."""
296
+ self._gerer(AGENTS, arg)
297
+
298
+ def cmd_commands(self, arg: str = "") -> None:
299
+ """/commandes : la liste des commandes personnalisées, puis créer, voir, modifier ou supprimer."""
300
+ self._gerer(COMMANDES, arg)
301
+
302
+ def _gerer(self, genre: Genre, arg: str = "") -> None:
303
+ if self.generating:
304
+ self.add_notice(genre.occupe, YELLOW)
305
+ return
306
+ if arg:
307
+ if agent_slug(arg) in CREER:
308
+ self._creer(genre)
309
+ return
310
+ item = genre.trouver(arg, self.cwd)
311
+ if item is not None:
312
+ self._fiche(genre, item)
313
+ return
314
+ self.add_notice(genre.inconnu.format(arg=arg), YELLOW)
315
+ items = genre.lister(self.cwd)
316
+ options = [("new", genre.nouveau)] + [(f"i{i}", genre.ligne(item)) for i, item in enumerate(items)]
317
+ options.append(("no", "Fermer (Esc)"))
318
+
319
+ def chosen(decision: str) -> None:
320
+ if decision == "new":
321
+ self._creer(genre)
322
+ elif decision.startswith("i"):
323
+ self._fiche(genre, items[int(decision[1:])])
324
+
325
+ self.ask(genre.titre, genre.entete(items, self.cwd), "Que veux-tu faire ?", options, chosen, esc="fermer")
326
+
327
+ def _fiche(self, genre: Genre, item: dict) -> None:
328
+ """Une définition : s'en servir, voir son texte, la modifier, la supprimer."""
329
+ def chosen(decision: str) -> None:
330
+ if decision == "use":
331
+ self.input.load_text(genre.saisie(item))
332
+ self.input.move_cursor(self.input.document.end)
333
+ if genre.ferme_menu:
334
+ self.hide_completions()
335
+ elif decision == "show":
336
+ self.add_notice_md(genre.montrer(item))
337
+ elif decision == "edit":
338
+ self.open_in_notepad(item["path"], genre.ouvert(item))
339
+ elif decision == "delete":
340
+ self._supprimer(genre, item)
341
+ elif decision == "copy":
342
+ self._personnaliser(item)
343
+ elif decision == "back":
344
+ self._gerer(genre)
345
+
346
+ self.ask(genre.titre_fiche(item), genre.fiche(item), "Que veux-tu faire ?", genre.options(item), chosen, esc="fermer")
347
+
348
+ def _personnaliser(self, agent: dict) -> None:
349
+ """Agent intégré : en faire une copie modifiable dans les agents globaux (elle le remplace)."""
350
+ name, target = agent["name"], AGENTS_DIR / f"{agent['name']}.md"
351
+ try:
352
+ if not target.exists():
353
+ write_text_file(target, agent_markdown(agent), *new_file_format(target))
354
+ except OSError as err:
355
+ self.add_notice(f"Impossible d'écrire {target} : {err}", f"bold {RED}")
356
+ return
357
+ self.open_in_notepad(target, f"Copie de l'agent {name} créée ({target}) : elle remplace l'agent "
358
+ "intégré. Modifie-la puis enregistre.")
359
+
360
+ def _supprimer(self, genre: Genre, item: dict) -> None:
361
+ path: Path = item["path"]
362
+
363
+ def chosen(decision: str) -> None:
364
+ if decision != "yes":
365
+ return
366
+ try:
367
+ path.unlink()
368
+ except OSError as err:
369
+ self.add_notice(f"Impossible de supprimer {path} : {err}", f"bold {RED}")
370
+ return
371
+ self.add_notice(genre.supprime(item, self.cwd), GREEN)
372
+
373
+ title, question = genre.suppression(item)
374
+ self.ask(title, Text(f" {path}", style="bold"), question, [("yes", "Oui, supprimer ce fichier"), ("no", "Non (Esc)")],
375
+ chosen, danger=True)
376
+
377
+ def open_in_notepad(self, path: Path, message: str) -> None:
378
+ try:
379
+ subprocess.Popen(["notepad.exe", str(path)], creationflags=CREATE_NO_WINDOW)
380
+ except OSError as err:
381
+ self.add_notice(f"Impossible d'ouvrir {path} : {err}", f"bold {RED}")
382
+ return
383
+ self.add_notice(message, GREEN)
384
+
385
+ # ------------------------------------------------------------------ création
386
+ def _dossier(self, genre: Genre, scope: str) -> Path:
387
+ return genre.dossier if scope == "global" else self.cwd / genre.local
388
+
389
+ def _creer(self, genre: Genre) -> None:
390
+ """Créer une définition : où (globale / ce dossier), puis comment (la décrire / l'écrire soi-même)."""
391
+ local = self.cwd / genre.local
392
+ options = [("global", genre.ou_global)]
393
+ if os.path.normcase(str(local)) != os.path.normcase(str(genre.dossier)):
394
+ options.append(("dossier", genre.ou_dossier.format(dossier=self.cwd.name or self.cwd)))
395
+ options.append(("no", "Annuler (Esc)"))
396
+
397
+ def where(decision: str) -> None:
398
+ if decision in ("global", "dossier"):
399
+ self.call_after_refresh(self._creer_comment, genre, decision)
400
+
401
+ self.ask(genre.titre_creation, Text(genre.ou_lieux.format(globaux=genre.dossier, local=local), style=DIM),
402
+ "Où l'enregistrer ?", options, where)
403
+
404
+ def _creer_comment(self, genre: Genre, scope: str) -> None:
405
+ def how(decision: str) -> None:
406
+ if decision == "gen":
407
+ self.set_agent_draft(scope, genre.cle)
408
+ elif decision == "manual":
409
+ name, path = unique_agent_path(self._dossier(genre, scope), genre.gabarit_nom)
410
+ content, message = genre.gabarit(name)
411
+ try:
412
+ write_text_file(path, content, *new_file_format(path))
413
+ except OSError as err:
414
+ self.add_notice(f"Impossible d'écrire {path} : {err}", f"bold {RED}")
415
+ return
416
+ self.open_in_notepad(path, message.format(path=path))
417
+
418
+ options = [("gen", genre.decrire), ("manual", "L'écrire moi-même dans le Bloc-notes (modèle prérempli)"),
419
+ ("no", "Annuler (Esc)")]
420
+ self.ask(genre.titre_creation, None, genre.comment, options, how)
421
+
422
+ def set_agent_draft(self, scope: str | None, kind: str = "agent") -> None:
423
+ """En attente de la description d'un nouvel agent ou d'une nouvelle commande (la saisie devient violette)."""
424
+ self._agent_draft = scope
425
+ self._draft_kind = kind
426
+ self.query_one("#input-box").set_class(scope is not None, "-agent")
427
+ if scope is not None:
428
+ self.add_notice(GENRES[kind].invite, AGENT_COLOR)
429
+ self.set_hints()
430
+
431
+ @work(thread=True, exclusive=True, group="generate")
432
+ def generate_agent(self, description: str, scope: str) -> None:
433
+ self._generer(AGENTS, description, scope)
434
+
435
+ @work(thread=True, exclusive=True, group="generate")
436
+ def generate_command(self, description: str, scope: str) -> None:
437
+ self._generer(COMMANDES, description, scope)
438
+
439
+ def _generer(self, genre: Genre, description: str, scope: str) -> None:
440
+ """Fait écrire la définition par le modèle (fil de travail). Les commandes PowerShell qu'il cite sont
441
+ vérifiées sur la machine : si l'une est fausse, il a une occasion de corriger."""
442
+ payload: dict = {"model": self.model, "stream": True, "format": genre.schema,
443
+ "options": {"temperature": 0.3, "num_predict": genre.jetons},
444
+ "messages": [{"role": "system", "content": genre.consigne},
445
+ {"role": "user", "content": genre.demande.format(description=description)}]}
446
+ if self.supports_thinking:
447
+ payload["think"] = False
448
+ item, error, fixed = None, None, ""
449
+ try:
450
+ for attempt in range(2):
451
+ raw = self._summarize(payload) if not self.cancel_event.is_set() else ""
452
+ if self.cancel_event.is_set():
453
+ break
454
+ data = json.loads(raw or "{}")
455
+ prompt = str(data.get("consignes") or "").strip()
456
+ if not prompt:
457
+ if item is not None:
458
+ break # la correction a échoué : on garde la première version
459
+ raise ValueError(genre.sans_texte)
460
+ item = genre.construire(data, prompt, scope)
461
+ # Les commandes qu'il cite existent-elles, avec ces paramètres ? Sinon, une demande de correction.
462
+ self.call_from_thread(self._spinner, True, "check", genre.verification)
463
+ problems = agent_command_problems(item["prompt"], self.cwd)
464
+ item["warnings"] = [short for short, _ in problems]
465
+ if not problems or attempt:
466
+ break
467
+ if genre.correction:
468
+ self.call_from_thread(self._spinner, True, "check", genre.correction)
469
+ payload["messages"] = payload["messages"][:2] + [
470
+ {"role": "assistant", "content": raw},
471
+ {"role": "user", "content": AGENT_GEN_FIX.format(problems=" ; ".join(d for _, d in problems))}]
472
+ fixed = ", ".join(short for short, _ in problems)
473
+ except Exception as err: # JSON invalide, Ollama injoignable…
474
+ error = str(err)
475
+ if item is not None:
476
+ item["fixed"] = fixed
477
+ self.call_from_thread(self._fin_generation, genre, item, error, scope)
478
+
479
+ def _fin_generation(self, genre: Genre, item: dict | None, error: str | None, scope: str) -> None:
480
+ """Montre la définition écrite par le modèle : tu la crées, tu la crées puis la modifies, ou tu la refuses."""
481
+ self.generating = False
482
+ self._spinner(False)
483
+ if self.cancel_event.is_set():
484
+ self.add_notice(genre.annulee, DIM)
485
+ return
486
+ if item is None:
487
+ self.add_notice(genre.impossible.format(error=error), f"bold {RED}")
488
+ return
489
+ name, path = genre.nom_libre(item, self._dossier(genre, scope))
490
+ item.update(name=name, path=path)
491
+ body = genre.apercu(item)
492
+ lines = item["prompt"].split("\n")
493
+ for line in lines[:APERCU_LIGNES]:
494
+ body.append(f" {line}\n")
495
+ if len(lines) > APERCU_LIGNES:
496
+ body.append(f" … +{len(lines) - APERCU_LIGNES} lignes\n", style=f"italic {DIM}")
497
+ body.append(f"\n fichier : {path}", style=DIM)
498
+ if item.get("fixed") and not item.get("warnings"):
499
+ body.append(f"\n ✓ commandes corrigées automatiquement ({item['fixed']})", style=GREEN)
500
+ for warning in item.get("warnings") or []:
501
+ body.append(f"\n ⚠ À vérifier : {warning}", style=YELLOW)
502
+
503
+ def chosen(decision: str) -> None:
504
+ if decision not in ("yes", "edit"):
505
+ self.add_notice(genre.non_cree, DIM)
506
+ return
507
+ try:
508
+ write_text_file(path, genre.markdown(item), *new_file_format(path))
509
+ except OSError as err:
510
+ self.add_notice(f"Impossible d'écrire {path} : {err}", f"bold {RED}")
511
+ return
512
+ if decision == "edit":
513
+ self.open_in_notepad(path, genre.cree(item, True))
514
+ else:
515
+ self.add_notice(genre.cree(item, False), GREEN)
516
+
517
+ self.ask(genre.titre_creation, body, genre.question,
518
+ [("yes", genre.oui), ("edit", genre.oui_modifier), ("no", "Non (Esc)")], chosen)