@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
package/tui/README.md ADDED
@@ -0,0 +1,103 @@
1
+ # Prométhée : la TUI `prmt`
2
+
3
+ Interface de discussion dans le terminal pour une IA locale de cybersécurité (Ollama, modèle `promethee`). Elle peut exécuter des commandes PowerShell et écrire des fichiers sur ta machine. Chaque commande passe d'abord par une politique de sécurité, puis par ta validation.
4
+
5
+ Elle fait partie d'une plateforme à deux interfaces. L'autre est la console web, dans `C:\Users\<utilisateur>\prj\promethee-site`. Les deux partagent la même liste blanche, le même périmètre réseau et un journal que chacune sait lire.
6
+
7
+ ## Lancer
8
+
9
+ ```
10
+ prmt interface de discussion
11
+ prmt -m deephat avec un autre modèle
12
+ prmt -c reprendre la dernière conversation
13
+ prmt -r choisir une conversation à reprendre
14
+ ```
15
+
16
+ Le lanceur (`~\.local\bin\prmt.cmd`, copie dans `lanceur\`) appelle `.venv\Scripts\python.exe prmt_tui.py`. Dépendances : Python 3.13 et Textual, installés dans `.venv`.
17
+
18
+ ## Relier la console
19
+
20
+ La politique vient de la console. Pour que la TUI la trouve, la console écrit un fichier de liaison, `Documents\Promethee\plateforme.json`, à chaque démarrage de son serveur. Sans démarrer le serveur :
21
+
22
+ ```
23
+ cd C:\Users\<utilisateur>\prj\promethee-site
24
+ npm run lier
25
+ ```
26
+
27
+ Sans ce fichier, ou sans Node, la TUI fonctionne quand même. La barre d'état affiche alors « validation manuelle » : tu valides chaque commande, sans liste blanche. `/etat` dit pourquoi.
28
+
29
+ ## Ce qui se passe quand Prométhée propose une commande
30
+
31
+ 1. **Vérification.** L'analyseur de PowerShell la lit sans l'exécuter : erreur de syntaxe, programme absent, paramètre inventé. En cas de problème, le modèle la corrige et tu ne la vois pas.
32
+ 2. **Politique.** Le moteur de la console la classe.
33
+ - `● lecture` : elle t'est montrée, et tu peux choisir de ne plus être interrogé jusqu'à la fin de la session. Ce choix vaut pour le nom d'une lecture reconnue (`ipconfig`, `Get-Process`), sinon pour la ligne exacte.
34
+ - `▲ sensible` : elle écrit, contacte le réseau ou vient de ta liste personnelle. Elle t'est montrée à chaque fois, avec les fichiers écrits et les hôtes contactés.
35
+ - `✕ refusée` : elle ne t'est pas proposée et rien ne s'exécute. Le modèle reçoit la raison et te dit quoi taper pour l'autoriser.
36
+ 3. **Ta validation.** `1` Oui, `2` Oui et ne plus demander, `3` ou `Esc` Non.
37
+ 4. **Exécution**, 5 minutes au plus, `Esc` pour arrêter. Une commande de la liste blanche s'exécute en mode de langage contraint de PowerShell, qui bloque les appels directs à .NET (réglage « langage contraint » de la console, actif par défaut).
38
+ 5. **Journal.** Une ligne dans `Documents\Promethee\journal.jsonl`.
39
+
40
+ ## Politique : stricte ou souple
41
+
42
+ | | stricte (par défaut) | souple |
43
+ |---|---|---|
44
+ | Commande de la liste blanche | montrée | montrée |
45
+ | Commande hors liste (`cargo`, `python`, `.\outil.exe`, un script) | refusée | montrée comme `◆ hors politique`, en rouge |
46
+ | Commande destructrice non débloquée (`Remove-Item`, `Stop-Computer`, `winget install`…) | refusée | refusée |
47
+ | Scan ou test de port vers une cible non déclarée | refusé | refusé |
48
+ | Script `.ps1` lancé par son chemin | refusé | lu et analysé : refusé s'il contient une commande refusée dans les deux modes |
49
+ | Adresse écrite en clair dans une commande hors liste (`python scan.py 10.0.0.5`) | refusée | refusée si la cible n'est pas déclarée |
50
+ | Désactiver une protection de Windows, chemin réseau `\\serveur\partage` | refusé | refusé |
51
+
52
+ ```
53
+ /politique mode actuel et résumé de la liste blanche
54
+ /politique souple changer de mode (gardé d'un lancement à l'autre)
55
+ /permissions autoriser cargo ajouter une commande à ta liste
56
+ /permissions autoriser Remove-Item débloquer une commande destructrice
57
+ /permissions retirer cargo
58
+ /perimetre ajouter labo.test déclarer une cible, après attestation
59
+ /perimetre retirer labo.test
60
+ ```
61
+
62
+ Les tests de ports, les scans et les audits ne sont permis que sur cette machine et sur les cibles déclarées : des systèmes qui t'appartiennent ou pour lesquels tu as une autorisation écrite.
63
+
64
+ ## Commandes
65
+
66
+ `/aide` les décrit toutes. Les principales :
67
+
68
+ | | |
69
+ |---|---|
70
+ | `/etat` | état de la plateforme, mesuré à l'instant |
71
+ | `/journal`, `/journal refus` | dernières actions des deux interfaces |
72
+ | `/recherche`, `/recherche on`, `/recherche off` | recherche web par SearXNG (`npm run searxng` dans le dossier de la console) |
73
+ | `/modele`, `/modele <nom>` | modèles installés, en changer |
74
+ | `/code` | IA de code : bascule automatique et modèle |
75
+ | `/plan` ou `Shift+Tab` | proposer un plan sans rien exécuter |
76
+ | `/agents`, `/commandes` | agents et commandes personnalisées |
77
+ | `/memoire`, `/init` | mémoire `PROMETHEE.md` |
78
+ | `/compact`, `/reprendre`, `/clear` | contexte et conversations |
79
+ | `/theme terminal`, `/theme promethee` | fond de l'interface |
80
+ | `! commande` | exécuter ta propre commande |
81
+ | `# note` | ajouter une note à la mémoire |
82
+ | `@fichier`, `@agent-nom` | joindre un fichier, confier une tâche à un agent |
83
+
84
+ ## Fichiers
85
+
86
+ | | |
87
+ |---|---|
88
+ | `Documents\Promethee\sessions\` | conversations enregistrées |
89
+ | `Documents\Promethee\journal.jsonl` | journal des actions de la TUI |
90
+ | `Documents\Promethee\preferences.json` | politique, fond et recherche web choisis (sans console reliée) |
91
+ | `Documents\Promethee\plateforme.json` | fichier de liaison, écrit par la console |
92
+ | `Documents\Promethee\PROMETHEE.md` | mémoire globale |
93
+ | `Documents\Promethee\agents\`, `commandes\` | agents et commandes personnalisées |
94
+
95
+ ## Code et tests
96
+
97
+ Le code est dans le paquet `promethee\` (41 modules). `prmt_tui.py` reste le point d'entrée. L'architecture, l'audit de la version d'origine et les choix de la refonte sont dans [docs/ANALYSE.md](docs/ANALYSE.md).
98
+
99
+ ```
100
+ .venv\Scripts\python.exe -m unittest discover -s tests -t .
101
+ ```
102
+
103
+ 143 tests, sans écran, avec un faux Ollama, un faux SearXNG, un faux PowerShell et un dossier utilisateur jetable. La version d'avant la refonte est le commit `c290fc3`.
@@ -0,0 +1,65 @@
1
+ """Prométhée : interface de discussion dans le terminal (style Claude Code) pour Ollama.
2
+
3
+ Prométhée peut exécuter des commandes PowerShell sur la machine, mais CHAQUE commande doit
4
+ être validée par l'utilisateur (comme le mode par défaut de Claude Code).
5
+
6
+ Lancement : prmt (ou : python prmt_tui.py [-m MODELE])
7
+ Dépendance : textual (installé dans le venv .venv à côté de ce fichier)
8
+ """
9
+ from __future__ import annotations
10
+
11
+ import importlib
12
+
13
+ from promethee.app import PrometheeApp, main
14
+
15
+ # Le code vit dans le paquet « promethee ». Ce fichier reste le point d'entrée du lanceur prmt, et les noms
16
+ # d'avant le découpage (prmt_tui.is_dangerous, prmt_tui.build_script…) y restent accessibles.
17
+ _MODULES = (
18
+ "promethee.config",
19
+ "promethee.theme",
20
+ "promethee.ollama",
21
+ "promethee.consignes",
22
+ "promethee.securite",
23
+ "promethee.shell",
24
+ "promethee.fichiers",
25
+ "promethee.appels",
26
+ "promethee.conversation",
27
+ "promethee.memoire",
28
+ "promethee.agents",
29
+ "promethee.commandes",
30
+ "promethee.completion",
31
+ "promethee.plan",
32
+ "promethee.presse_papiers",
33
+ "promethee.logo",
34
+ "promethee.widgets",
35
+ "promethee.textes",
36
+ "promethee.app.ecran",
37
+ "promethee.app.saisie",
38
+ "promethee.app.taches",
39
+ "promethee.app.notes",
40
+ "promethee.app.definitions",
41
+ "promethee.app.contexte",
42
+ "promethee.app.envoi",
43
+ "promethee.app.tour",
44
+ "promethee.app.outils",
45
+ "promethee.app.validation",
46
+ "promethee.app.historique",
47
+ "promethee.app.modele",
48
+ "promethee.app.commandes_slash",
49
+ "promethee.app.clavier",
50
+ "promethee.app.application",
51
+ )
52
+
53
+
54
+ def __getattr__(name: str):
55
+ for module in _MODULES:
56
+ found = importlib.import_module(module)
57
+ if hasattr(found, name):
58
+ return getattr(found, name)
59
+ raise AttributeError(f"module 'prmt_tui' has no attribute {name!r}")
60
+
61
+
62
+ __all__ = ["PrometheeApp", "main"]
63
+
64
+ if __name__ == "__main__":
65
+ main()
@@ -0,0 +1 @@
1
+ """Prométhée : interface de discussion dans le terminal pour une IA locale de cybersécurité (Ollama)."""
@@ -0,0 +1,4 @@
1
+ """Lancement : python -m promethee [-m MODELE] [-c | -r]"""
2
+ from .app import main
3
+
4
+ main()
@@ -0,0 +1,66 @@
1
+ """Écran d'accueil : l'état de la session (modèle, politique, dossier, mémoire) puis les gestes à connaître."""
2
+ from __future__ import annotations
3
+
4
+ from rich.text import Text
5
+ from textual.widgets import Static
6
+
7
+ from .memoire import read_memory
8
+ from .theme import DIM, GREEN, ORANGE, RED, YELLOW
9
+
10
+
11
+ class Accueil(Static):
12
+ """Panneau d'accueil. Ses lignes d'état suivent l'application : rafraichir() après chaque changement."""
13
+
14
+ def on_mount(self) -> None:
15
+ self.rafraichir()
16
+
17
+ def rafraichir(self) -> None:
18
+ self.update(self.contenu())
19
+
20
+ def _modele(self) -> tuple[str, str, str]:
21
+ app = self.app
22
+ detail = app.model + (f" · {app.model_desc}" if app.model_desc else "")
23
+ if app.model_state:
24
+ bad = any(w in app.model_state for w in ("introuvable", "injoignable", "erreur"))
25
+ return ("✕" if bad else "◌", RED if bad else YELLOW, f"{detail} · {app.model_state}")
26
+ if not app.supports_tools:
27
+ return "▲", YELLOW, f"{detail} · prêt, sans commandes (ce modèle n'a pas d'outils)"
28
+ return "●", GREEN, f"{detail} · prêt" + (f" · contexte de {app.num_ctx:,} jetons".replace(",", " ") if app.num_ctx else "")
29
+
30
+ def _politique(self) -> tuple[str, str, str]:
31
+ politique = self.app.politique
32
+ if politique.etat == "prête":
33
+ if politique.mode == "stricte":
34
+ return "●", GREEN, "stricte · liste blanche et périmètre partagés avec la console"
35
+ return "▲", YELLOW, "souple · le hors-liste passe à ta validation, les planchers refusent toujours"
36
+ if politique.etat == "en attente":
37
+ return "◌", YELLOW, f"{politique.mode} · démarrage du moteur…"
38
+ return "▲", YELLOW, f"validation manuelle · {politique.raison or 'moteur de la console indisponible'}"
39
+
40
+ def contenu(self) -> Text:
41
+ app = self.app
42
+ t = Text()
43
+ t.append("✻ ", style=ORANGE)
44
+ t.append("Bienvenue dans Prométhée !", style="bold")
45
+ t.append("\n IA locale de cybersécurité · hors ligne · tes données restent sur ta machine", style=DIM)
46
+ t.append("\n")
47
+ for label, (sign, color, value) in (("modèle", self._modele()), ("politique", self._politique())):
48
+ t.append(f"\n {label:<10}", style=DIM)
49
+ t.append(f"{sign} ", style=color)
50
+ t.append(value)
51
+ t.append(f"\n {'commandes':<10}", style=DIM)
52
+ t.append("tu valides chaque commande", style=ORANGE)
53
+ t.append(" avant qu'elle s'exécute")
54
+ t.append(f"\n {'dossier':<10}", style=DIM)
55
+ t.append(str(app.cwd))
56
+ memory = read_memory(app.cwd)
57
+ if memory:
58
+ t.append(f"\n {'mémoire':<10}", style=DIM)
59
+ t.append(" + ".join(f"{label} ({text.count(chr(10)) + 1} lignes)" for label, _, text in memory))
60
+ t.append("\n")
61
+ t.append("\n /aide pour l'aide · /etat l'état de la plateforme · /modele pour changer de modèle", style=DIM)
62
+ t.append("\n ! commande pour l'exécuter toi-même · # note pour la mémoire · @fichier pour le joindre", style=DIM)
63
+ t.append("\n Shift+Tab : mode plan · /reprendre une conversation · /compact", style=DIM)
64
+ t.append("\n @agent-nom pour confier une tâche à un agent · /agents pour les gérer", style=DIM)
65
+ t.append("\n /commandes : tes commandes perso · /init : mémoire du dossier · Esc Esc : revenir en arrière", style=DIM)
66
+ return t
@@ -0,0 +1,220 @@
1
+ """Agents : définition, fichiers, consignes, génération."""
2
+ from __future__ import annotations
3
+
4
+ import os
5
+ import re
6
+ import unicodedata
7
+ from pathlib import Path
8
+
9
+ from .config import SAVE_DIR
10
+ from .consignes import EDIT_TOOL, READ_TOOL, SEARCH_FILES_TOOL, SEARCH_TEXT_TOOL, TOOL_NAME, WRITE_TOOL
11
+ from .fichiers import read_text_file
12
+ from .securite import precheck
13
+
14
+ # --------------------------------------------------------------------------- Agents (sous-agents, comme Claude Code)
15
+ # Un agent = un fichier Markdown : en-tête (nom, description, outils, modèle) + ses consignes. Il travaille dans son
16
+ # propre contexte (les longues sorties de commandes n'encombrent pas la conversation) et renvoie seulement son rapport.
17
+ # Mesuré avec l'outil lancer_agent en plus : 18/18 bons choix d'outil (ipconfig, fichiers, délégation demandée), et le
18
+ # modèle ne délègue pas de lui-même ce qu'il sait faire (« analyse ce journal » → lire_fichier, 3/3).
19
+ AGENT_TOOL = "lancer_agent"
20
+ AGENTS_DIR = SAVE_DIR / "agents" # agents globaux
21
+ LOCAL_AGENTS = Path(".promethee") / "agents" # agents d'un dossier (relatif au dossier courant)
22
+ AGENT_MENTION = re.compile(r"(?:^|(?<=\s))@agent-([a-z0-9][a-z0-9-]*)", re.I)
23
+ MAX_AGENT_STEPS = 14 # écrire, compiler, corriger, recompiler, tester… (10 ne suffisait pas pour du code)
24
+ AGENT_TOOL_NAMES = [TOOL_NAME, READ_TOOL, WRITE_TOOL, EDIT_TOOL, SEARCH_FILES_TOOL, SEARCH_TEXT_TOOL]
25
+ # Noms d'outils de Claude Code acceptés dans l'en-tête (pour reprendre un agent écrit pour Claude Code)
26
+ CLAUDE_TOOL_NAMES = {"bash": TOOL_NAME, "powershell": TOOL_NAME, "read": READ_TOOL, "write": WRITE_TOOL,
27
+ "edit": EDIT_TOOL, "multiedit": EDIT_TOOL, "grep": SEARCH_TEXT_TOOL, "glob": SEARCH_FILES_TOOL,
28
+ "ls": SEARCH_FILES_TOOL}
29
+ TOOL_LABELS = {TOOL_NAME: "commandes", READ_TOOL: "lecture", WRITE_TOOL: "création de fichiers",
30
+ EDIT_TOOL: "modification de fichiers", SEARCH_FILES_TOOL: "recherche de fichiers",
31
+ SEARCH_TEXT_TOOL: "recherche dans les fichiers"}
32
+ SCOPE_LABELS = {"intégré": "intégré", "global": "global", "dossier": "ce dossier"}
33
+
34
+ BUILTIN_AGENTS = [
35
+ {"name": "explorateur",
36
+ "description": "Cherche et lit des fichiers sur l'ordinateur (lecture seule) pour répondre à une question, puis "
37
+ "résume ce qu'il a trouvé. Utile pour fouiller un dossier ou de nombreux fichiers.",
38
+ "tools": [SEARCH_FILES_TOOL, SEARCH_TEXT_TOOL, READ_TOOL, TOOL_NAME],
39
+ "prompt": "Tu es l'explorateur : tu trouves l'information demandée dans les fichiers et les dossiers de "
40
+ "l'ordinateur, en LECTURE SEULE.\n"
41
+ "- Pour chercher : chercher_fichiers (par nom, par exemple *.log) et chercher_texte (un mot dans le "
42
+ "contenu des fichiers), sans validation. Une commande (Get-ChildItem, Get-Item) seulement si ces "
43
+ "outils ne suffisent pas.\n"
44
+ "- Pour lire un fichier : l'outil lire_fichier.\n"
45
+ "- Ne crée, ne modifie et ne supprime jamais rien.\n"
46
+ "- Arrête-toi dès que tu as la réponse. Ton rapport : ce que tu as trouvé, avec les chemins exacts, "
47
+ "court et factuel.\n"
48
+ "- Ne cite le contenu d'un fichier que si tu l'as lu avec lire_fichier, et recopie-le exactement. "
49
+ "N'invente jamais de contenu."},
50
+ {"name": "analyste-logs",
51
+ "description": "Analyse des journaux (auth.log, access.log, journaux d'événements Windows…) pour repérer les "
52
+ "attaques (force brute, scans, connexions suspectes) et en fait un rapport chiffré.",
53
+ "tools": [READ_TOOL, SEARCH_FILES_TOOL, SEARCH_TEXT_TOOL, TOOL_NAME],
54
+ "prompt": "Tu es l'analyste SOC : tu analyses les journaux indiqués et tu repères les événements suspects.\n"
55
+ "- Lis le fichier avec lire_fichier ; pour un gros fichier, Select-String -Pattern 'Failed|Invalid' ; "
56
+ "pour les journaux Windows, Get-WinEvent.\n"
57
+ "- Compte précisément : échecs de connexion par IP et par compte, connexions réussies, IP suspectes, "
58
+ "horaires. Ne compte pas de tête : fais compter PowerShell, par exemple "
59
+ "`Select-String -Path auth.log -Pattern 'Failed password' | ForEach-Object { if ($_.Line -match "
60
+ "'from (\\S+)') { $Matches[1] } } | Group-Object | Select-Object Count, Name` (nombre d'échecs par IP).\n"
61
+ "- Ne modifie rien.\n"
62
+ "- Ton rapport : résumé en une phrase, tableau des IP suspectes (IP, nombre d'échecs, comptes visés), "
63
+ "connexions réussies notables, gravité (faible / moyenne / élevée), recommandations. Uniquement des "
64
+ "valeurs lues dans les journaux."},
65
+ ]
66
+
67
+ AGENT_SYSTEM = """Tu es « {name} », un agent spécialisé de Prométhée (assistant de cybersécurité local). Prométhée t'a confié une tâche : fais-la seul, avec tes outils, puis écris ton rapport.
68
+
69
+ ## Ta spécialité
70
+ {prompt}
71
+
72
+ ## Règles
73
+ - Ordinateur de l'utilisateur : Windows 11, PowerShell 5.1. Dossier courant : {cwd}. Date : {date}.
74
+ - Tes outils : {tools}. L'utilisateur valide chacune de tes commandes et de tes écritures.
75
+ - Une commande à la fois ; si elle échoue, corrige-la (écris `curl.exe` et non `curl`, mets entre guillemets les chemins avec des espaces).
76
+ - Tu ne peux pas parler à l'utilisateur : ne pose pas de question et ne demande jamais de confirmation par écrit.
77
+ - N'invente rien : seulement ce que tu as lu dans les sorties de commandes et les fichiers.
78
+ - Quand tu as fini, réponds sans appeler d'outil : ce texte est ton rapport, transmis à Prométhée. Commence directement par les résultats."""
79
+ AGENT_REMINDER = ("\n\n(Rappel système : fais la tâche toi-même en appelant directement tes outils ; l'utilisateur "
80
+ "valide dans l'interface. Quand tu as fini, écris ton rapport sans appeler d'outil.)")
81
+ AGENT_REPORT_NOW = ("(Contrôle automatique : tu as atteint le nombre maximum d'étapes. N'appelle plus d'outil : écris "
82
+ "maintenant ton rapport avec ce que tu as trouvé.)")
83
+ AGENT_HINT = " ; ou lancer_agent pour confier la tâche à un agent."
84
+ # Sans elle, après un échange « @agent-… » dans l'historique, le modèle recopiait « @agent-… » au lieu de répondre
85
+ AGENT_DONE = "\n\n(Fin du rapport de l'agent. Réponds maintenant à l'utilisateur à partir de ce rapport.)"
86
+
87
+
88
+ def agent_slug(name: str) -> str:
89
+ """« Analyste Logs » → « analyste-logs » (minuscules, chiffres et tirets)."""
90
+ plain = unicodedata.normalize("NFKD", name).encode("ascii", "ignore").decode("ascii").lower()
91
+ return re.sub(r"-{2,}", "-", re.sub(r"[^a-z0-9]+", "-", plain)).strip("-")[:40]
92
+
93
+
94
+ def parse_agent(text: str, path: Path | None = None, scope: str = "global") -> dict | None:
95
+ """Fichier d'agent : en-tête « --- nom / description / outils / modele --- » puis les consignes."""
96
+ match = re.match(r"\ufeff?\s*---[ \t]*\r?\n(.*?)\r?\n---[ \t]*(?:\r?\n|$)(.*)$", text, re.S) # \ufeff : BOM éventuel
97
+ if not match:
98
+ return None
99
+ meta: dict[str, str] = {}
100
+ for line in match.group(1).splitlines():
101
+ key, sep, value = line.partition(":")
102
+ if sep:
103
+ meta[key.strip().lower()] = value.strip().strip("\"'")
104
+ name = agent_slug(meta.get("nom") or meta.get("name") or (path.stem if path else ""))
105
+ prompt = match.group(2).strip()
106
+ if not name or not prompt:
107
+ return None
108
+ tools: list[str] = []
109
+ for raw in re.split(r"[,;\s]+", meta.get("outils") or meta.get("tools") or ""):
110
+ tool = raw.strip().strip("[]\"'")
111
+ tool = tool if tool in AGENT_TOOL_NAMES else CLAUDE_TOOL_NAMES.get(tool.lower(), "")
112
+ if tool and tool not in tools:
113
+ tools.append(tool)
114
+ return {"name": name, "description": meta.get("description", ""), "tools": tools or list(AGENT_TOOL_NAMES),
115
+ "model": meta.get("modele") or meta.get("modèle") or meta.get("model") or "", "prompt": prompt,
116
+ "scope": scope, "path": path}
117
+
118
+
119
+ def agent_markdown(agent: dict) -> str:
120
+ lines = ["---", f"nom: {agent['name']}", f"description: {' '.join(agent['description'].split())}",
121
+ f"outils: {', '.join(agent['tools'])}"]
122
+ if agent.get("model"):
123
+ lines.append(f"modele: {agent['model']}")
124
+ return "\n".join(lines) + "\n---\n\n" + agent["prompt"].strip() + "\n"
125
+
126
+
127
+ def agent_dirs(cwd: Path) -> list[tuple[str, Path]]:
128
+ dirs = [("global", AGENTS_DIR)]
129
+ local = cwd / LOCAL_AGENTS
130
+ if os.path.normcase(str(local)) != os.path.normcase(str(AGENTS_DIR)):
131
+ dirs.append(("dossier", local))
132
+ return dirs
133
+
134
+
135
+ def list_agents(cwd: Path) -> list[dict]:
136
+ """Agents intégrés, puis globaux, puis ceux du dossier : un agent écrit remplace l'intégré du même nom."""
137
+ agents = {a["name"]: dict(a, scope="intégré", path=None, model="") for a in BUILTIN_AGENTS}
138
+ for scope, folder in agent_dirs(cwd):
139
+ try:
140
+ files = sorted(folder.glob("*.md"))
141
+ except OSError:
142
+ continue
143
+ for path in files:
144
+ try:
145
+ agent = parse_agent(read_text_file(path)[0], path, scope)
146
+ except (OSError, ValueError):
147
+ continue
148
+ if agent:
149
+ agents[agent["name"]] = agent
150
+ return list(agents.values())
151
+
152
+
153
+ def find_agent(name: str, cwd: Path) -> dict | None:
154
+ slug = agent_slug(name.removeprefix("agent-"))
155
+ return next((a for a in list_agents(cwd) if a["name"] == slug), None)
156
+
157
+
158
+ def agent_tool_def(agents: list[dict]) -> dict:
159
+ return {"type": "function", "function": {
160
+ "name": AGENT_TOOL,
161
+ "description": "Confie une tâche à un agent spécialisé, qui travaille dans son propre contexte puis renvoie "
162
+ "son rapport.",
163
+ "parameters": {"type": "object", "properties": {
164
+ "agent": {"type": "string", "enum": [a["name"] for a in agents], "description": "Nom de l'agent."},
165
+ "tache": {"type": "string", "description": "La tâche complète à confier (l'agent ne voit pas la "
166
+ "conversation)."},
167
+ }, "required": ["agent", "tache"]}}}
168
+
169
+
170
+ def agents_guide(agents: list[dict]) -> str:
171
+ lines = "\n".join(f"- `{a['name']}` : {' '.join(a['description'].split()) or '(sans description)'}" for a in agents)
172
+ return ("\n\n## Agents\nAvec l'outil `lancer_agent`, tu peux confier une tâche à un agent spécialisé : il travaille "
173
+ "seul, dans son propre contexte, puis te renvoie son rapport (l'utilisateur valide toujours ses commandes). "
174
+ f"Agents disponibles :\n{lines}\nUtilise un agent seulement si l'utilisateur le demande, ou si la tâche "
175
+ "correspond exactement à sa description. Sinon, fais-le toi-même avec les autres outils. Dans `tache`, "
176
+ "écris une consigne complète (l'agent ne voit pas la conversation).")
177
+
178
+
179
+ def unique_agent_path(folder: Path, name: str) -> tuple[str, Path]:
180
+ candidate, n = name, 2
181
+ while (folder / f"{candidate}.md").exists():
182
+ candidate, n = f"{name}-{n}", n + 1
183
+ return candidate, folder / f"{candidate}.md"
184
+
185
+
186
+ AGENT_GEN_SYSTEM = """Tu conçois des agents spécialisés pour Prométhée, un assistant de cybersécurité local sous Windows 11 (PowerShell 5.1). À partir de la description de l'utilisateur, écris la définition d'UN agent :
187
+ - nom : court, en minuscules, mots séparés par des tirets (par exemple « analyste-logs », « auditeur-web »).
188
+ - description : une ou deux phrases : ce que fait l'agent et QUAND l'utiliser (Prométhée s'en sert pour choisir l'agent).
189
+ - outils : seulement ceux dont il a besoin parmi executer_commande (commandes PowerShell), lire_fichier, ecrire_fichier, modifier_fichier, chercher_fichiers (trouver des fichiers par nom), chercher_texte (chercher dans leur contenu).
190
+ - consignes : ses instructions, à la deuxième personne (« Tu es… »), en 5 à 12 lignes : son rôle, sa méthode étape par étape, les commandes PowerShell ou outils à privilégier, ce qu'il ne doit pas faire, et le contenu de son rapport final.
191
+ Cite seulement des commandes PowerShell qui existent vraiment, avec leurs vrais paramètres, entre backticks ; ne mentionne pas de fichier de configuration qui n'existe pas sous Windows.
192
+ Réponds uniquement en JSON."""
193
+ AGENT_CMD = re.compile(r"[`'\"«]\s*([A-Z][a-z]+-[A-Z][A-Za-z]+(?:[ \t][^`'\"»\n]*)?)[`'\"»]")
194
+ AGENT_GEN_FIX = ("(Contrôle automatique : dans les consignes, {problems}. Réécris la définition complète en JSON, en "
195
+ "remplaçant ces commandes par des commandes PowerShell correctes.)")
196
+
197
+
198
+ def agent_command_problems(prompt: str, cwd: Path) -> list[tuple[str, str]]:
199
+ """Commandes PowerShell citées dans les consignes d'un agent : paramètres inventés, commandes inexistantes."""
200
+ commands: list[str] = []
201
+ for match in AGENT_CMD.finditer(prompt):
202
+ command = match.group(1).strip()
203
+ if command not in commands:
204
+ commands.append(command)
205
+ problems: list[tuple[str, str]] = []
206
+ for command in commands[:6]:
207
+ for short, detail in precheck(command, cwd):
208
+ if ("paramètre" in short or "pas installé" in short) and short not in (s for s, _ in problems):
209
+ problems.append((short, detail))
210
+ return problems
211
+ AGENT_SCHEMA = {"type": "object", "properties": {
212
+ "nom": {"type": "string"},
213
+ "description": {"type": "string"},
214
+ "outils": {"type": "array", "items": {"type": "string", "enum": AGENT_TOOL_NAMES}},
215
+ "consignes": {"type": "string"},
216
+ }, "required": ["nom", "description", "outils", "consignes"]}
217
+ AGENT_TEMPLATE = {"name": "mon-agent", "tools": [TOOL_NAME, READ_TOOL, SEARCH_FILES_TOOL, SEARCH_TEXT_TOOL],
218
+ "description": "Ce que fait cet agent, et quand Prométhée doit l'utiliser.",
219
+ "prompt": "Tu es … (le rôle de l'agent).\n\nMéthode :\n1. …\n2. …\n\nNe fais jamais : …\n\n"
220
+ "Ton rapport final : …"}
@@ -0,0 +1,4 @@
1
+ """L'application Textual. La classe PrometheeApp est assemblée à partir des modules de ce dossier."""
2
+ from .application import PrometheeApp, main
3
+
4
+ __all__ = ["PrometheeApp", "main"]