@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,278 @@
1
+ """Ce que le modèle reçoit : outils déclarés, consignes, rappels et relances mesurés sur le modèle 8B."""
2
+ from __future__ import annotations
3
+
4
+ import re
5
+ from pathlib import Path
6
+
7
+ # --------------------------------------------------------------------------- Outil : exécution de commandes
8
+ TOOL_NAME = "executer_commande"
9
+ TOOL = {
10
+ "type": "function",
11
+ "function": {
12
+ "name": TOOL_NAME,
13
+ "description": "Exécute une commande PowerShell sur l'ordinateur de l'utilisateur (Windows 11) et renvoie "
14
+ "sa sortie. L'utilisateur valide chaque commande avant son exécution.",
15
+ "parameters": {
16
+ "type": "object",
17
+ "properties": {
18
+ "commande": {"type": "string", "description": "La commande PowerShell à exécuter."},
19
+ "explication": {"type": "string", "description": "En une phrase courte : ce que fait la commande et pourquoi."},
20
+ },
21
+ "required": ["commande", "explication"],
22
+ },
23
+ },
24
+ }
25
+
26
+ # --------------------------------------------------------------------------- Outils : fichiers
27
+ READ_TOOL, WRITE_TOOL, EDIT_TOOL = "lire_fichier", "ecrire_fichier", "modifier_fichier"
28
+ FILE_TOOL_NAMES = {READ_TOOL, WRITE_TOOL, EDIT_TOOL}
29
+ _PATH_PARAM = {"type": "string", "description": "Chemin du fichier : absolu, ou relatif au dossier courant."}
30
+ _WHY_PARAM = {"type": "string", "description": "En une phrase courte : ce que change cette écriture et pourquoi."}
31
+ FILE_TOOLS = [
32
+ {"type": "function", "function": {
33
+ "name": READ_TOOL,
34
+ "description": "Lit un fichier texte de l'ordinateur de l'utilisateur et renvoie son contenu avec les numéros "
35
+ "de ligne. À utiliser avant de modifier un fichier.",
36
+ "parameters": {"type": "object", "properties": {
37
+ "chemin": _PATH_PARAM,
38
+ "debut": {"type": "integer", "description": "Optionnel : première ligne à lire (1 = début du fichier)."},
39
+ "fin": {"type": "integer", "description": "Optionnel : dernière ligne à lire."},
40
+ }, "required": ["chemin"]}}},
41
+ {"type": "function", "function": {
42
+ "name": WRITE_TOOL,
43
+ "description": "Crée un fichier texte, ou remplace entièrement son contenu. L'utilisateur voit le contenu "
44
+ "(ou les différences) et valide avant l'écriture.",
45
+ "parameters": {"type": "object", "properties": {
46
+ "chemin": _PATH_PARAM,
47
+ "contenu": {"type": "string", "description": "Le contenu complet du fichier."},
48
+ "explication": _WHY_PARAM,
49
+ }, "required": ["chemin", "contenu", "explication"]}}},
50
+ {"type": "function", "function": {
51
+ "name": EDIT_TOOL,
52
+ "description": "Modifie une partie d'un fichier existant en remplaçant un extrait exact par un nouveau texte. "
53
+ "L'utilisateur voit les différences et valide avant l'écriture.",
54
+ "parameters": {"type": "object", "properties": {
55
+ "chemin": _PATH_PARAM,
56
+ "ancien_texte": {"type": "string", "description": "Extrait EXACT du fichier à remplacer, copié depuis "
57
+ "lire_fichier (sans les numéros de ligne), assez long pour n'apparaître qu'une fois."},
58
+ "nouveau_texte": {"type": "string", "description": "Le texte qui remplace cet extrait."},
59
+ "explication": _WHY_PARAM,
60
+ }, "required": ["chemin", "ancien_texte", "nouveau_texte", "explication"]}}},
61
+ ]
62
+ # --------------------------------------------------------------------------- Outils : recherche (lecture seule)
63
+ # Comme Glob / Grep dans Claude Code : sans validation. Mesuré : « trouve les .log » → chercher_fichiers 3/3 (sans eux,
64
+ # il essayait de « lire » le dossier 3/3), « quels fichiers contiennent password » → chercher_texte 3/3, et les autres
65
+ # demandes gardent le bon outil (15/15).
66
+ SEARCH_FILES_TOOL, SEARCH_TEXT_TOOL = "chercher_fichiers", "chercher_texte"
67
+ SEARCH_TOOL_NAMES = {SEARCH_FILES_TOOL, SEARCH_TEXT_TOOL}
68
+ _FOLDER_PARAM = {"type": "string", "description": "Optionnel : dossier où chercher (par défaut le dossier courant)."}
69
+ SEARCH_TOOLS = [
70
+ {"type": "function", "function": {
71
+ "name": SEARCH_FILES_TOOL,
72
+ "description": "Cherche des fichiers par leur nom (motif comme *.log ou rapport*.txt) dans un dossier et ses "
73
+ "sous-dossiers. Lecture seule, sans validation.",
74
+ "parameters": {"type": "object", "properties": {
75
+ "motif": {"type": "string", "description": "Motif du nom de fichier, par exemple *.log ou *config*."},
76
+ "dossier": _FOLDER_PARAM,
77
+ }, "required": ["motif"]}}},
78
+ {"type": "function", "function": {
79
+ "name": SEARCH_TEXT_TOOL,
80
+ "description": "Cherche un texte (ou une expression régulière) dans le contenu des fichiers d'un dossier et de "
81
+ "ses sous-dossiers ; renvoie les lignes trouvées avec le fichier et le numéro de ligne. Lecture "
82
+ "seule, sans validation.",
83
+ "parameters": {"type": "object", "properties": {
84
+ "texte": {"type": "string", "description": "Le texte ou l'expression régulière à chercher."},
85
+ "dossier": _FOLDER_PARAM,
86
+ "fichiers": {"type": "string", "description": "Optionnel : seulement les fichiers dont le nom correspond à "
87
+ "ce motif, par exemple *.log."},
88
+ }, "required": ["texte"]}}},
89
+ ]
90
+ TOOLS = [TOOL] + FILE_TOOLS + SEARCH_TOOLS
91
+
92
+ TOOL_GUIDE = """
93
+
94
+ ## Exécution de commandes
95
+ Tu as accès à l'ordinateur de l'utilisateur grâce à l'outil `executer_commande` (PowerShell 5.1 sous Windows 11).
96
+ Dossier courant : {cwd}. Date : {date}. L'utilisateur valide chaque commande avant son exécution.
97
+ Toi, tu n'as pas de navigateur, mais l'ordinateur est connecté à Internet : une commande peut s'en servir si c'est utile (par exemple `curl.exe -s -4 https://ifconfig.me` donne l'adresse IP publique IPv4, et `-6` au lieu de `-4` l'IPv6).
98
+
99
+ Quand utiliser l'outil :
100
+ - Dès que la question porte sur CETTE machine (son IP, ses ports, ses fichiers, ses processus, sa configuration, ses logs…) : exécute la commande toi-même avec l'outil au lieu d'expliquer à l'utilisateur comment faire.
101
+ - Quand l'utilisateur te demande d'agir (créer, tester, lancer, scanner une cible autorisée…).
102
+ - Pas pour une question de cours ou une explication générale : là, réponds directement.
103
+
104
+ Sécurité :
105
+ - Chaque commande que tu proposes est d'abord contrôlée par la politique de sécurité de Prométhée (liste blanche, périmètre), puis affichée à l'utilisateur, qui la valide ou la refuse.
106
+ - Donc ne refuse jamais toi-même une action que l'utilisateur te demande explicitement (même supprimer, modifier ou installer quelque chose sur sa machine) : propose la commande exacte avec l'outil. Si la politique la refuse, le résultat de l'outil te dit pourquoi et ce que l'utilisateur peut autoriser : transmets-le-lui, sans chercher un détour.
107
+ - Ne demande JAMAIS de confirmation par écrit (« voulez-vous vraiment… ? ») : l'interface affiche déjà la commande à l'utilisateur pour qu'il la valide. Appelle directement l'outil.
108
+ - Quand l'utilisateur te donne une commande précise à exécuter, appelle immédiatement l'outil avec cette commande exacte, sans la recopier en texte.
109
+ - De ta propre initiative, en revanche, utilise seulement des commandes en lecture seule : ne modifie ni ne supprime rien sans qu'on te l'ait demandé.
110
+
111
+ Comment l'utiliser :
112
+ - Une commande à la fois, qui fait exactement ce qui est demandé (rien de plus).
113
+ - Si le résultat ne répond pas à la question (mauvaise commande, sortie vide, erreur), exécute directement une meilleure commande avec l'outil.
114
+ - Après le résultat, réponds à la question de l'utilisateur avec les VRAIES données de la sortie (valeurs, noms, chiffres). Ne réexplique pas la commande.
115
+ - N'invente jamais d'information absente de la sortie.
116
+ - Si l'utilisateur refuse une commande, arrête-toi et attends ses instructions.
117
+
118
+ Fichiers (utilise ces outils plutôt que des commandes PowerShell pour lire ou écrire un fichier) :
119
+ - `lire_fichier` : lire un fichier ; le contenu revient avec les numéros de ligne. Lis toujours un fichier avant de le modifier.
120
+ - `ecrire_fichier` : créer un fichier (script, notes, rapport…) ou remplacer tout son contenu. Sans chemin précis, crée-le dans le dossier courant.
121
+ - `modifier_fichier` : changer une partie d'un fichier ; `ancien_texte` doit être copié exactement depuis `lire_fichier`, sans les numéros de ligne.
122
+ - L'utilisateur voit les différences et valide chaque écriture.
123
+
124
+ Installer un logiciel sous Windows (avec winget, jamais avec une URL de téléchargement devinée) :
125
+ - Lance `winget install <nom du logiciel> --accept-package-agreements --accept-source-agreements`, sans --id : winget trouve lui-même le paquet à partir de son nom.
126
+ - Si winget répond « No package found » ou « Multiple packages found », lance `winget search <mot-clé>`, puis réessaie avec `--id <Id exact lu dans la colonne Id>`.
127
+ - Mets entre guillemets tout chemin qui contient des espaces.
128
+
129
+ Recherche (lecture seule, sans validation : préfère-les aux commandes pour chercher) :
130
+ - `chercher_fichiers` : trouver des fichiers par leur nom (motif *.log, *config*…).
131
+ - `chercher_texte` : trouver un mot ou une expression dans le contenu des fichiers."""
132
+
133
+ # Rappel ajouté (invisible) au dernier message de l'utilisateur : sans lui, ce modèle de 8B
134
+ # oublie l'outil dans les longues conversations (mesuré : 2-3/5 sans, 5/5 avec).
135
+ # Il cite aussi les outils fichiers : sans eux, le modèle créait les fichiers avec `echo … > fichier` (4/4).
136
+ TOOL_REMINDER = ("\n\n(Rappel système : si ma demande consiste à agir sur ma machine ou à l'observer, appelle "
137
+ "directement l'outil adapté (executer_commande, ou lire_fichier / ecrire_fichier / modifier_fichier "
138
+ "pour un fichier, chercher_fichiers / chercher_texte pour chercher) ; je validerai dans l'interface.)")
139
+
140
+ # ---------------------------------------------------------------- interpréteur de la demande
141
+ # Avant de répondre, un appel très court (≈0,3 s) classe la demande : « action » (exécuter) ou
142
+ # « explication » (répondre directement). Mesuré : 18/19 demandes bien classées. Sans lui, le modèle
143
+ # exécutait des commandes pour des questions de cours (« explique netstat » → netstat lancé 4/4).
144
+ ROUTER_SYSTEM = """Tu es un classificateur. L'utilisateur parle à un assistant de cybersécurité qui peut exécuter des commandes sur son PC Windows. Classe sa DERNIÈRE demande :
145
+ - "action" : il veut que l'assistant FASSE quelque chose maintenant ou obtienne une information réelle (sur son PC, son réseau, un site, un fichier) : afficher, lire ou ouvrir un fichier, vérifier, chercher, scanner, auditer, tester, installer, créer, modifier, corriger, supprimer, lancer. Une relance courte (« install le », « vas-y », « c'est pas ça ») après une action est aussi une action.
146
+ - "explication" : il veut apprendre ou comprendre : un cours, une définition, « comment faire », « c'est quoi », « explique », « donne-moi les commandes », un exemple de code à lire, un avis, ou une simple discussion (« merci »).
147
+ Indique aussi "code" : true si la demande porte sur la programmation : écrire, compléter, corriger, relire ou expliquer un programme, un script ou un extrait de code (Rust, Python, C, JavaScript, script PowerShell…), ou une erreur de compilation ; une relance courte après une demande de code (« corrige-le », « relance-le ») aussi. false pour tout le reste : commandes système ou réseau à lancer, cybersécurité, cours général, discussion.
148
+ Réponds uniquement en JSON."""
149
+ ROUTER_SCHEMA = {"type": "object", "properties": {"type": {"type": "string", "enum": ["action", "explication"]},
150
+ "code": {"type": "boolean"}}, "required": ["type", "code"]}
151
+
152
+ # --------------------------------------------------------------------------- IA de code (bascule automatique)
153
+ # Quand l'interpréteur reconnaît une demande de programmation, la réponse est confiée à un modèle meilleur en code
154
+ # (même conversation, mêmes outils, même validation), puis Prométhée reprend la main au message suivant.
155
+ CODE_MODEL = "qwen3.5:9b"
156
+ CODE_SYSTEM = """
157
+
158
+ ## Mode code
159
+ Pour cette demande, tu tournes sur {model}, un modèle meilleur en programmation : tu restes Prométhée et tu réponds en français.
160
+ - Rust (cargo, rustc) et Python sont déjà installés sur ce PC : ne les installe pas.
161
+ - Quand l'utilisateur veut un programme ou un script, écris-le dans un fichier avec ecrire_fichier (ou modifier_fichier), pas seulement dans ta réponse.
162
+ - Puis vérifie-le toi-même avec executer_commande et corrige jusqu'à zéro erreur : Rust → `cargo new nom --vcs none`, le code dans nom\\src\\main.rs, puis `cargo check --manifest-path nom\\Cargo.toml` (un fichier .rs seul : `rustc --edition 2021 fichier.rs`) ; Python → `python -m py_compile fichier.py` puis exécution ; script PowerShell → exécution.
163
+ - Puis lance-le vraiment : s'il lit un fichier, crée un petit fichier d'exemple avec ecrire_fichier et lance le programme dessus ; vérifie que la sortie est juste.
164
+ - N'écris jamais qu'un code compile ou fonctionne sans l'avoir vu dans une sortie de commande."""
165
+ EXPLAIN_REMINDER = "\n\n(Rappel système : ceci est une question de cours ou une discussion. Réponds directement, sans exécuter de commande.)"
166
+ # Retour à Prométhée après l'IA de code : sur « merci ! », il refaisait toute la demande de code (3/4 ; 0/4 avec ce rappel)
167
+ RETURN_REMINDER = ("\n\n(Rappel système : ceci est une question de cours ou une discussion. Réponds directement, sans exécuter "
168
+ "de commande. Ta réponse précédente a terminé la demande d'avant : ne la refais pas, réponds seulement "
169
+ "à ce dernier message, en une ou deux phrases si c'est un remerciement ou une simple discussion.)")
170
+ # IA de code qui annonce son plan (« Allons-y : d'abord créer le projet ») sans rien lancer : relancée
171
+ CODE_START = re.compile(r"allons-y|commençons|je vais (?:maintenant )?(?:écrire|créer|commencer|coder)|"
172
+ r"d'abord,? (?:je |cré|écri)", re.I)
173
+
174
+ # Méthode ajoutée à la demande quand on demande un audit : le modèle suit alors de vraies étapes
175
+ # au lieu d'inventer des commandes (mesuré : 8/8 démarrent l'audit avec l'outil).
176
+ AUDIT_RE = re.compile(r"audit|pentest|test d.intrusion|vuln[ée]rabilit", re.I)
177
+ AUDIT_PLAYBOOK = """
178
+
179
+ (Méthode pour un audit autorisé d'un site web ou d'une machine : exécute toi-même chaque étape avec l'outil, une commande à la fois, au lieu de me donner une liste de commandes.
180
+ 1. `Resolve-DnsName <domaine>` : adresses IP et serveurs DNS.
181
+ 2. `nmap -Pn -sV --top-ports 100 <cible>` : ports ouverts et versions des services.
182
+ 3. `curl.exe -sI https://<domaine>` : en-têtes HTTP (serveur, HSTS, CSP, X-Frame-Options, cookies).
183
+ 4. `nmap -Pn -p 443 --script ssl-enum-ciphers <domaine>` : configuration TLS.
184
+ 5. `curl.exe -s https://<domaine>/robots.txt` : chemins déclarés.
185
+ Après chaque résultat, passe directement à l'étape suivante. À la fin seulement, fais le rapport : constats réels, risques, recommandations.)"""
186
+
187
+ # Relance quand, pour une demande d'action, le modèle écrit des commandes au lieu de les exécuter.
188
+ NUDGE = ("(Contrôle automatique : je t'ai demandé d'agir, pas de m'écrire des commandes ou des appels d'outils en texte. "
189
+ "Fais-le maintenant toi-même en appelant vraiment l'outil : executer_commande pour une commande, "
190
+ "lire_fichier / ecrire_fichier / modifier_fichier pour un fichier.)")
191
+ # Relance quand le modèle annonce l'étape suivante (« Passons au scan… ») mais rend la main sans l'exécuter.
192
+ CONTINUE = ("(Contrôle automatique : la tâche n'est pas terminée. Exécute maintenant l'étape suivante avec l'outil "
193
+ "adapté. S'il ne reste vraiment plus rien à exécuter, fais le rapport final.)")
194
+ ANNOUNCE = re.compile(r"passons|prochaine étape|étape suivante|lançons|exécutons|"
195
+ r"je vais (?:maintenant )?(?:lancer|exécuter|scanner|vérifier|analyser|compiler|tester)|"
196
+ r"\bje (?:compile|teste|recompile)\b", re.I)
197
+ MAX_CONTINUES = 6
198
+ # Pendant un plan accepté ou un audit, il demande parfois « Je l'exécute ? » au lieu de continuer
199
+ ASK_CONFIRM = re.compile(r"(?:je l'exécute|je continue|on continue|je lance|voulez-vous|veux-tu|dois-je|souhaitez-vous|"
200
+ r"est-elle correcte|est-ce correct)[^?\n]{0,80}\?|veuillez (?:valider|confirmer)|"
201
+ r"valide[rz]? (?:cette|la) commande|confirme[rz]? (?:cette|la) commande", re.I)
202
+ NO_CONFIRM = ("(Contrôle automatique : ne me demande pas de confirmation par écrit, je valide déjà chaque commande dans "
203
+ "l'interface. Exécute maintenant l'étape suivante du plan avec l'outil adapté. S'il ne reste plus rien à "
204
+ "exécuter, fais le bilan.)")
205
+ # Un agent qui vient de modifier du code conclut souvent « ça devrait marcher » sans recompiler (vu en Rust) :
206
+ # relancé jusqu'à 2 fois pour vérifier d'abord.
207
+ CODE_EXTS = {".rs", ".c", ".h", ".cpp", ".cc", ".hpp", ".go", ".java", ".cs", ".py", ".js", ".ts"}
208
+ VERIFY = ("(Contrôle automatique : tu as modifié {path} mais tu n'as rien relancé depuis : tu ne sais pas encore si ça "
209
+ "marche. Vérifie maintenant avec executer_commande ({how}), corrige s'il reste des erreurs, et seulement "
210
+ "ensuite fais ton rapport.)")
211
+
212
+
213
+ # IA de code : « ça compile » n'est pas « ça marche » (vu : programme Rust compilé, jamais lancé, qui ne trouvait aucune
214
+ # ligne car il cherchait « Failed password from » au lieu de « Failed password for root from »).
215
+ COMPILE_ONLY = re.compile(r"^\s*(?:cd\s[^;]+;\s*)?(?:cargo\s+(?:check|build|clippy|fmt)|rustc\s+(?!--explain|--version)|"
216
+ r"python(?:\.exe)?\s+-m\s+(?:py_compile|compileall)|gcc\s|g\+\+\s|clang\s|javac\s|go\s+(?:build|vet)|tsc\b)",
217
+ re.I)
218
+ INSPECT_CMD = re.compile(r"^\s*(?:Get-ChildItem|Get-Content|Get-Item|Test-Path|Resolve-Path|Select-String|dir|ls|type|cat)\b",
219
+ re.I)
220
+ EMPTY_REPORT = ("(Contrôle automatique : ta réponse est vide. Fais maintenant ton rapport final en français : ce que tu as "
221
+ "écrit, le résultat de la dernière compilation ou exécution, et ce qui ne marche pas encore s'il reste un "
222
+ "problème.)")
223
+ RUN_CHECK = ("(Contrôle automatique : ça compile, mais tu n'as pas encore lancé le programme : tu ne sais pas s'il donne le "
224
+ "bon résultat. S'il lit un fichier, crée d'abord un petit fichier d'exemple avec ecrire_fichier. Lance-le avec "
225
+ "executer_commande (cargo run --manifest-path … -- arguments, python fichier.py arguments…), compare sa sortie "
226
+ "au résultat attendu et corrige si besoin, puis fais ton rapport. Si ce n'est pas un programme qu'on peut "
227
+ "lancer, dis-le simplement.)")
228
+
229
+
230
+ def verify_nudge(path: str, cwd: Path) -> str:
231
+ """Relance « vérifie ton code », avec la commande adaptée au fichier modifié."""
232
+ file = Path(path) if Path(path).is_absolute() else cwd / path
233
+ ext = file.suffix.lower()
234
+ if ext == ".rs":
235
+ toml = next((d / "Cargo.toml" for d in file.parents if (d / "Cargo.toml").is_file()), None)
236
+ how = (f"`cargo check --manifest-path \"{toml}\"`, puis cargo run ou cargo test" if toml else
237
+ f"ce fichier n'est dans aucun projet cargo : compile-le avec `rustc --edition 2021 \"{file}\"`, "
238
+ "puis lance l'exécutable produit")
239
+ elif ext == ".py":
240
+ how = f"`python -m py_compile \"{file}\"`, puis lance-le sur un petit exemple"
241
+ else:
242
+ how = "la commande qui le compile ou le lance"
243
+ return VERIFY.format(path=path, how=how)
244
+ # Rapport « compilé correctement, aucune erreur » alors que sa dernière commande a échoué (vu en Rust : projet jamais
245
+ # créé, deux commandes en erreur, puis succès annoncé) : relancé une fois, puis averti dans le rapport.
246
+ SUCCESS_CLAIM = re.compile(r"(?:compil|fonctionn|march|exécut|réussi|termin)\w*(?:(?!\b(?:pas|plus|jamais)\b)[^.\n]){0,40}?"
247
+ r"(?:correctement|sans (?:erreur|problème)|avec succès)|aucune erreur|tout fonctionne|"
248
+ r"ça (?:marche|fonctionne)(?! pas)", re.I)
249
+ FAILED_CLAIM = ("(Contrôle automatique : ta dernière commande `{cmd}` a échoué (code {code}) : tu ne peux pas écrire que ça "
250
+ "marche. Lis son erreur, corrige et relance ; ou, si tu t'arrêtes, dis honnêtement ce qui ne marche pas.)")
251
+ AUDIT_STEPS = 5 # nombre d'étapes de la méthode d'audit
252
+ REPORT_RE = re.compile(r"rapport|recommandation", re.I)
253
+
254
+ # Contrôle de la réponse finale : les adresses IP et numéros (ports, PID…) cités doivent apparaître
255
+ # dans les sorties des commandes. Ex. réel : le modèle a « listé » les ports 19444 à 19459 qui n'existent pas.
256
+ VALUE_RE = re.compile(r"(?<![\w.:])(?:\d{1,3}(?:\.\d{1,3}){3}|\d{3,5})(?![\w.:]*\d)")
257
+
258
+
259
+ def unverified_values(answer: str, references: str) -> list[str]:
260
+ missing: list[str] = []
261
+ for value in dict.fromkeys(VALUE_RE.findall(answer)):
262
+ if not re.search(rf"(?<![\d.]){re.escape(value)}(?![\d])", references):
263
+ missing.append(value)
264
+ return missing
265
+
266
+
267
+ CODE_BLOCK = re.compile(r"```[ \t]*(?:powershell|ps1?|pwsh|cmd|bat|bash|sh|shell|console)?[ \t]*\n\s*\S", re.I)
268
+ INLINE_CMD = re.compile(r"`(?:nmap|curl(?:\.exe)?|wget|netstat|ipconfig|ping|tracert|nslookup|whoami|systeminfo|tasklist|"
269
+ r"winget|[A-Z][a-z]+-[A-Z][A-Za-z]+)\b[^`]*`")
270
+
271
+
272
+ # Appel d'outil écrit en texte (JSON) au lieu d'être vraiment appelé
273
+ TOOL_TEXT = re.compile(r'"(?:commande|chemin|ancien_texte|nouveau_texte|contenu)"\s*:|'
274
+ r"\b(?:executer_commande|lire_fichier|ecrire_fichier|modifier_fichier)\b|<tool_call>")
275
+
276
+
277
+ def writes_commands(text: str) -> bool:
278
+ return bool(CODE_BLOCK.search(text) or INLINE_CMD.search(text) or TOOL_TEXT.search(text))
@@ -0,0 +1,94 @@
1
+ """Compactage de la conversation et sessions enregistrées."""
2
+ from __future__ import annotations
3
+
4
+ import datetime as dt
5
+ import json
6
+ import os
7
+ import re
8
+
9
+ from .config import SAVE_DIR
10
+ from .shell import SHELL_TAG
11
+
12
+ # --------------------------------------------------------------------------- Compactage de la conversation
13
+ AUTO_COMPACT_PCT = 80 # compactage automatique quand le contexte dépasse ce pourcentage
14
+ COMPACT_SYSTEM = """Tu résumes une conversation entre un utilisateur et Prométhée (assistant de cybersécurité qui exécute des commandes et modifie des fichiers sur le PC Windows de l'utilisateur), pour que Prométhée puisse la continuer sans l'historique complet.
15
+ Écris en français, en puces courtes, 250 mots maximum, avec ces parties :
16
+ - Objectif : ce que veut l'utilisateur, demandes en cours.
17
+ - Faits établis : informations obtenues avec leurs VALEURS EXACTES (adresses IP, ports, chemins, noms, versions, résultats de commandes).
18
+ - Fait : commandes lancées, fichiers créés ou modifiés.
19
+ - Reste à faire.
20
+ Recopie les valeurs telles quelles. N'invente rien : ce qui n'est pas dans la conversation ne doit pas apparaître."""
21
+
22
+
23
+ def conversation_transcript(messages: list[dict], summary: str = "", max_chars: int = 18_000) -> str:
24
+ parts = [f"[Résumé d'une partie plus ancienne]\n{summary}"] if summary else []
25
+ for m in messages:
26
+ content = (m.get("content") or "").strip()
27
+ if m["role"] == "user":
28
+ parts.append(("Commande lancée par l'utilisateur : " + content[len(SHELL_TAG):].strip()[:1500])
29
+ if content.startswith(SHELL_TAG) else f"Utilisateur : {content}")
30
+ elif m["role"] == "assistant":
31
+ if content:
32
+ parts.append(f"Prométhée : {content}")
33
+ for call in m.get("tool_calls") or []:
34
+ fn = call.get("function") or {}
35
+ args = fn.get("arguments") or {}
36
+ if isinstance(args, dict):
37
+ shown = args.get("commande") or args.get("chemin") or json.dumps(args, ensure_ascii=False)
38
+ else:
39
+ shown = str(args)
40
+ parts.append(f"Prométhée utilise {fn.get('name', '?')} : {str(shown)[:300]}")
41
+ elif m["role"] == "tool":
42
+ parts.append(f"Résultat : {content[:1200]}" + (" […]" if len(content) > 1200 else ""))
43
+ text = "\n\n".join(parts)
44
+ if len(text) > max_chars: # on garde le début (contexte) et surtout la fin (le plus récent)
45
+ text = text[:3000] + "\n\n[… partie du milieu coupée …]\n\n" + text[-(max_chars - 3000):]
46
+ return text
47
+
48
+
49
+ # --------------------------------------------------------------------------- Sessions (reprise)
50
+ SESSIONS_DIR = SAVE_DIR / "sessions"
51
+
52
+
53
+ def new_session_id() -> str:
54
+ return dt.datetime.now().strftime("%Y-%m-%d_%H-%M-%S") + "-" + os.urandom(2).hex()
55
+
56
+
57
+ def list_sessions(limit: int = 9) -> list[dict]:
58
+ if not SESSIONS_DIR.is_dir():
59
+ return []
60
+ files = sorted(SESSIONS_DIR.glob("*.json"), key=lambda p: p.stat().st_mtime, reverse=True)
61
+ sessions = []
62
+ for path in files:
63
+ try:
64
+ data = json.loads(path.read_text(encoding="utf-8"))
65
+ except (OSError, ValueError):
66
+ continue
67
+ if data.get("messages") or data.get("summary"):
68
+ sessions.append(data)
69
+ if len(sessions) >= limit:
70
+ break
71
+ return sessions
72
+
73
+
74
+ def describe_when(iso: str) -> str:
75
+ try:
76
+ when = dt.datetime.fromisoformat(iso)
77
+ except (TypeError, ValueError):
78
+ return "?"
79
+ today = dt.date.today()
80
+ if when.date() == today:
81
+ return f"aujourd'hui {when:%H:%M}"
82
+ if when.date() == today - dt.timedelta(days=1):
83
+ return f"hier {when:%H:%M}"
84
+ return f"{when:%d/%m %H:%M}"
85
+
86
+
87
+ def parse_tool_result(content: str) -> tuple[int | None, str]:
88
+ """Code de sortie et sortie d'un résultat d'outil enregistré (pour réafficher une conversation)."""
89
+ content = re.split(r"\n\n\((?:Indice|Rappel)", content, maxsplit=1)[0]
90
+ head, _, body = content.partition("\n")
91
+ match = re.match(r"Code de sortie : (-?\d+)", head)
92
+ if match:
93
+ return int(match.group(1)), body
94
+ return None, body
@@ -0,0 +1,253 @@
1
+ """État de la plateforme, affiché par /etat : IA locale, moteur d'exécution, périmètre, activité, connexions.
2
+
3
+ Tout vient de mesures faites au moment de la commande : appels à Ollama, résumé rendu par la politique,
4
+ lecture des journaux. Rien n'est inventé : une mesure qui échoue s'affiche comme un échec, avec sa raison.
5
+ """
6
+ from __future__ import annotations
7
+
8
+ import datetime as dt
9
+ import json
10
+ import subprocess
11
+ import time
12
+ import urllib.error
13
+ import urllib.request
14
+ from pathlib import Path
15
+
16
+ from rich.text import Text
17
+
18
+ from . import journal
19
+ from .agents import list_agents
20
+ from .commandes import list_custom_commands
21
+ from .config import CMD_TIMEOUT, CREATE_NO_WINDOW
22
+ from .conversation import SESSIONS_DIR
23
+ from .memoire import read_memory
24
+ from .ollama import OLLAMA, api
25
+ from .shell import decode_output, refreshed_env
26
+ from .theme import DIM, GREEN, OR_CLAIR, RED, YELLOW
27
+
28
+ # état d'une ligne → (signe, couleur). Le signe change avec l'état : la couleur n'est jamais le seul repère.
29
+ SIGNES = {"ok": ("●", GREEN), "attention": ("▲", YELLOW), "probleme": ("✕", RED), "inactif": ("○", DIM), "info": ("·", DIM)}
30
+ Ligne = tuple[str, str] # (état, texte)
31
+ Section = tuple[str, list[Ligne]] # (titre, lignes)
32
+
33
+
34
+ def pluriel(n: int, mot: str, pluriel_mot: str | None = None) -> str:
35
+ return f"{n} {mot if n < 2 else (pluriel_mot or mot + 's')}"
36
+
37
+
38
+ def nombre(n: int) -> str:
39
+ return f"{n:,}".replace(",", " ")
40
+
41
+
42
+ def depuis(quand: dt.datetime, maintenant: dt.datetime | None = None) -> str:
43
+ """« il y a 3 min », « il y a 2 h », « hier », « le 04/11 »."""
44
+ now = maintenant or dt.datetime.now().astimezone()
45
+ seconds = (now - quand).total_seconds()
46
+ if seconds < 90:
47
+ return "à l'instant"
48
+ if seconds < 3600:
49
+ return f"il y a {int(seconds // 60)} min"
50
+ if seconds < 86_400 and quand.date() == now.date():
51
+ return f"il y a {int(seconds // 3600)} h"
52
+ if (now.date() - quand.date()).days == 1:
53
+ return "hier"
54
+ return f"le {quand:%d/%m}"
55
+
56
+
57
+ def version_powershell() -> str:
58
+ """Version de PowerShell installée, ou "" s'il ne répond pas."""
59
+ try:
60
+ proc = subprocess.run(["powershell.exe", "-NoProfile", "-NonInteractive", "-Command", "$PSVersionTable.PSVersion.ToString()"],
61
+ env=refreshed_env(), stdin=subprocess.DEVNULL, capture_output=True,
62
+ creationflags=CREATE_NO_WINDOW, timeout=20)
63
+ except (OSError, subprocess.SubprocessError):
64
+ return ""
65
+ return decode_output(proc.stdout).strip().split("\n")[0].strip() if proc.returncode == 0 else ""
66
+
67
+
68
+ def console_en_ligne(port: int, delai: float = 1.5) -> str:
69
+ """Version de la console web si son serveur répond sur cette machine, sinon ""."""
70
+ try:
71
+ with urllib.request.urlopen(f"http://127.0.0.1:{port}/api/session", timeout=delai) as resp:
72
+ data = json.loads(resp.read().decode("utf-8"))
73
+ except (OSError, ValueError, urllib.error.URLError):
74
+ return ""
75
+ return str(data.get("version") or "") if isinstance(data, dict) else ""
76
+
77
+
78
+ # --------------------------------------------------------------------------- sections
79
+ def section_ia(app) -> Section:
80
+ lignes: list[Ligne] = []
81
+ start = time.monotonic()
82
+ try:
83
+ version = api("/api/version", timeout=4).get("version") or "?"
84
+ except Exception as err:
85
+ return "IA locale", [("probleme", f"Ollama ne répond pas sur {OLLAMA} ({err}). Lance l'application Ollama.")]
86
+ latency = round((time.monotonic() - start) * 1000)
87
+ try:
88
+ installed = api("/api/tags", timeout=6).get("models") or []
89
+ loaded = api("/api/ps", timeout=6).get("models") or []
90
+ except Exception as err:
91
+ return "IA locale", [("attention", f"Ollama {version} répond, mais pas sa liste de modèles ({err}).")]
92
+ lignes.append(("ok", f"Ollama {version} · répond en {latency} ms · {pluriel(len(installed), 'modèle installé', 'modèles installés')}"))
93
+
94
+ def charge(name: str) -> str:
95
+ full = name if ":" in name else f"{name}:latest"
96
+ entry = next((m for m in loaded if m.get("name") in (name, full)), None)
97
+ if entry is None:
98
+ return "non chargé"
99
+ return "en mémoire GPU" if entry.get("size_vram") else "chargé sur le processeur (carte graphique non détectée)"
100
+
101
+ names = {str(m.get("name") or "") for m in installed}
102
+
103
+ def installe(name: str) -> bool:
104
+ return name in names or f"{name}:latest" in names
105
+
106
+ if app.model_state:
107
+ lignes.append(("probleme" if "introuvable" in app.model_state or "injoignable" in app.model_state or "erreur" in app.model_state
108
+ else "attention", f"{app.model} · {app.model_state}"))
109
+ else:
110
+ where = charge(app.model)
111
+ parts = [app.model] + ([app.model_desc] if app.model_desc else []) + [where,
112
+ "commandes : oui" if app.supports_tools else "commandes : non (modèle sans outils)"]
113
+ if app.num_ctx:
114
+ pct = min(100, round(100 * app.ctx_used / app.num_ctx))
115
+ parts.append(f"contexte {pct} % ({nombre(app.ctx_used)} / {nombre(app.num_ctx)} jetons)")
116
+ if app.last_tps:
117
+ parts.append(f"{app.last_tps:.0f} jetons/s")
118
+ lignes.append(("attention" if "processeur" in where or not app.supports_tools else "ok", " · ".join(parts)))
119
+ if app.code_model != app.model:
120
+ if not installe(app.code_model):
121
+ lignes.append(("attention", f"IA de code {app.code_model} · non installée (/code <modèle> pour en choisir une autre)"))
122
+ else:
123
+ lignes.append(("ok" if app.code_auto else "inactif", f"IA de code {app.code_model} · {charge(app.code_model)} · "
124
+ + ("bascule automatique" if app.code_auto else "bascule coupée (/code on)")))
125
+ return "IA locale", lignes
126
+
127
+
128
+ def section_moteur(app, resume: dict | None) -> Section:
129
+ lignes: list[Ligne] = []
130
+ version = app.ps_version
131
+ lignes.append(("ok", f"PowerShell {version} · {CMD_TIMEOUT // 60} min au plus par commande") if version
132
+ else ("probleme", "PowerShell ne répond pas : aucune commande ne peut être exécutée"))
133
+ politique = app.politique
134
+ if resume is None:
135
+ raison = politique.raison or "moteur de la console indisponible"
136
+ lignes.append(("attention", f"validation manuelle : tu valides chaque commande, sans liste blanche ({raison})"))
137
+ return "Moteur d'exécution", lignes
138
+ lignes.append(("ok", "mode de langage contraint : PowerShell bloque lui-même les appels .NET des commandes autorisées")
139
+ if resume.get("constrained") else
140
+ ("attention", "mode de langage contraint coupé dans les réglages de la console : il ne reste que la politique"))
141
+ counts = resume.get("counts") or {}
142
+ unlocked = [str(u) for u in resume.get("unlocked") or []]
143
+ custom = [str(c.get("name")) for c in resume.get("custom") or [] if isinstance(c, dict)]
144
+ lignes.append(("ok" if politique.mode == "stricte" else "attention",
145
+ f"politique {politique.mode} · moteur de la console {politique.version or '?'} · "
146
+ + ("tout refus de la liste blanche est appliqué" if politique.mode == "stricte"
147
+ else "le hors-liste passe à ta validation, les planchers refusent toujours")))
148
+ lignes.append(("info", f"liste blanche : {pluriel(int(counts.get('read', 0)), 'lecture')}, "
149
+ f"{pluriel(int(counts.get('sensitive', 0)), 'sensible')}, "
150
+ f"{pluriel(int(counts.get('destructive', 0)), 'destructrice')} ({pluriel(len(unlocked), 'débloquée')}), "
151
+ f"{pluriel(int(counts.get('forbidden', 0)), 'interdite')}"))
152
+ if unlocked:
153
+ lignes.append(("attention", "débloquées : " + ", ".join(unlocked)))
154
+ if custom:
155
+ lignes.append(("info", "liste personnelle (arguments non contrôlés) : " + ", ".join(custom)))
156
+ roots = [str(r) for r in resume.get("roots") or []]
157
+ if roots:
158
+ lignes.append(("info", "écriture permise dans : " + ", ".join(roots)))
159
+ return "Moteur d'exécution", lignes
160
+
161
+
162
+ def section_perimetre(resume: dict | None) -> Section:
163
+ if resume is None:
164
+ return "Périmètre réseau", [("inactif", "non contrôlé : la politique n'est pas disponible")]
165
+ targets = [t for t in resume.get("targets") or [] if isinstance(t, dict)]
166
+ if not targets:
167
+ return "Périmètre réseau", [("inactif", "aucune cible déclarée : seuls les tests sur cette machine sont permis "
168
+ "(/perimetre ajouter <hôte>)")]
169
+ lignes = []
170
+ for t in targets:
171
+ note = f" · {t['note']}" if t.get("note") else ""
172
+ sub = " · sous-domaines compris" if t.get("includeSubdomains") else ""
173
+ lignes.append(("ok", f"{t.get('host')}{sub}{note} · attestée le {str(t.get('attestedAt') or '')[:10]}"))
174
+ return "Périmètre réseau", lignes
175
+
176
+
177
+ def section_activite(fichiers: list[Path]) -> Section:
178
+ entries = journal.lire(fichiers)
179
+ compte = journal.bilan(entries)
180
+ if not entries:
181
+ return "Activité", [("inactif", "journal vide : il se remplit dès la première commande")]
182
+
183
+ def ligne(c: dict) -> str:
184
+ return (f"{pluriel(c['executees'], 'exécutée')} · {c['echecs']} en échec · "
185
+ f"{pluriel(c['refusees'], 'refusée')} par la politique · {pluriel(c['annulees'], 'annulée')} par toi")
186
+
187
+ lignes: list[Ligne] = [("info", f"aujourd'hui : {ligne(compte['jour'])}"),
188
+ ("info", f"{compte['jours']} derniers jours : {ligne(compte['periode'])} "
189
+ f"(TUI {compte['tui']}, console {compte['console']})")]
190
+ last = compte["derniere"]
191
+ if last is not None:
192
+ when = journal._moment(last)
193
+ label = {"ok": "réussie", "error": "en échec", "refused": "refusée", "cancelled": "annulée", "info": ""}[last["outcome"]]
194
+ lignes.append(("info", f"dernière : {depuis(when) if when else '?'} · {last['title'][:70]} ({label})"))
195
+ return "Activité (TUI et console)", lignes
196
+
197
+
198
+ def section_connexions(app) -> Section:
199
+ lien = app.politique.lien
200
+ lignes: list[Ligne] = []
201
+ if lien is None:
202
+ lignes.append(("inactif", f"console web non reliée : {app.politique.raison}"))
203
+ else:
204
+ port = int(lien.get("port") or 4317)
205
+ version = console_en_ligne(port)
206
+ lignes.append(("ok", f"console web {version} en ligne : http://127.0.0.1:{port}") if version
207
+ else ("inactif", f"console web arrêtée (« npm start » dans {lien.get('racine') or 'son dossier'})"))
208
+ lignes.append(("info", f"réglages et périmètre partagés : {lien.get('donnees')}"))
209
+ lignes.append(("info", f"journal de la TUI : {journal.FICHIER}"))
210
+ return "Connexions", lignes
211
+
212
+
213
+ def section_session(app) -> Section:
214
+ n = sum(1 for m in app.messages if m.get("role") == "user")
215
+ title = f"« {app.session_title} » · " if app.session_title else ""
216
+ lignes: list[Ligne] = [("info", f"{title}{pluriel(n, 'message')} · dossier {app.cwd}")]
217
+ memory = read_memory(app.cwd)
218
+ try:
219
+ saved = sum(1 for _ in SESSIONS_DIR.glob("*.json"))
220
+ except OSError:
221
+ saved = 0
222
+ lignes.append(("info", ("mémoire " + " + ".join(f"{label} ({pluriel(text.count(chr(10)) + 1, 'ligne')})" for label, _, text in memory)
223
+ if memory else "mémoire vide")
224
+ + f" · {pluriel(len(list_agents(app.cwd)), 'agent')} · "
225
+ + pluriel(len(list_custom_commands(app.cwd)), 'commande perso', 'commandes perso')
226
+ + f" · {pluriel(saved, 'conversation enregistrée', 'conversations enregistrées')}"))
227
+ auto = sorted(app.allowed) + sorted(app.allowed_lines)
228
+ if auto or app.allow_edits:
229
+ lignes.append(("attention", "sans validation pour cette session : " + ", ".join(auto + (["écritures de fichiers"] if app.allow_edits else []))))
230
+ return "Session", lignes
231
+
232
+
233
+ def collecter(app) -> list[Section]:
234
+ """Mesure l'état de la plateforme (appelé depuis un fil de travail : plusieurs appels réseau et un PowerShell)."""
235
+ if not app.ps_version:
236
+ app.ps_version = version_powershell()
237
+ resume = app.politique.resume()
238
+ fichiers = [journal.FICHIER]
239
+ if app.politique.lien is not None:
240
+ fichiers.append(Path(str(app.politique.lien["donnees"])) / "journal.jsonl")
241
+ return [section_ia(app), section_moteur(app, resume), section_perimetre(resume), section_activite(fichiers),
242
+ section_connexions(app), section_session(app)]
243
+
244
+
245
+ def rendre(sections: list[Section]) -> Text:
246
+ text = Text()
247
+ for i, (titre, lignes) in enumerate(sections):
248
+ text.append(("\n\n" if i else "") + titre, style=f"bold {OR_CLAIR}")
249
+ for etat, contenu in lignes:
250
+ signe, couleur = SIGNES[etat]
251
+ text.append(f"\n {signe} ", style=couleur)
252
+ text.append(contenu, style=DIM if etat in ("info", "inactif") else "")
253
+ return text