@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,204 @@
1
+ """Avant de montrer une commande : vérification par l'analyseur de PowerShell et repérage des commandes risquées."""
2
+ from __future__ import annotations
3
+
4
+ import base64
5
+ import os
6
+ import re
7
+ import subprocess
8
+ import tempfile
9
+ from pathlib import Path
10
+
11
+ from .config import CREATE_NO_WINDOW
12
+ from .shell import decode_output, refreshed_env
13
+
14
+ # ---------------------------------------------------------------- vérification avant validation
15
+ # Chaque commande est analysée par le vrai analyseur de PowerShell (sans l'exécuter) avant de te
16
+ # la montrer : erreur de syntaxe, programme absent, `curl` (alias trompeur d'Invoke-WebRequest).
17
+ # En cas de problème, le modèle la corrige seul : tu ne vois que des commandes plausibles.
18
+ PRECHECK_PS = r"""$ErrorActionPreference = 'Stop'
19
+ try {
20
+ $src = [Text.Encoding]::UTF8.GetString([Convert]::FromBase64String('@B64@'))
21
+ $tokens = $null; $errs = $null
22
+ $ast = [System.Management.Automation.Language.Parser]::ParseInput($src, [ref]$tokens, [ref]$errs)
23
+ if ($errs.Count -gt 0) { 'SYNTAXE ' + $errs[0].Message; exit }
24
+ $defined = @($ast.FindAll({ $args[0] -is [System.Management.Automation.Language.FunctionDefinitionAst] }, $true) | ForEach-Object { $_.Name })
25
+ $names = @($ast.FindAll({ $args[0] -is [System.Management.Automation.Language.CommandAst] }, $true) | ForEach-Object { $_.GetCommandName() } | Where-Object { $_ } | Select-Object -Unique)
26
+ foreach ($n in $names) {
27
+ if ($defined -contains $n) { continue }
28
+ if ($n -eq 'curl' -or $n -eq 'wget') { 'ALIAS ' + $n; continue }
29
+ if (-not (Get-Command $n -ErrorAction SilentlyContinue)) { 'INTROUVABLE ' + $n }
30
+ }
31
+ # Paramètres inventés (« Get-NetFirewallProfile -Enabled ») : seulement pour les cmdlets et fonctions connues
32
+ $common = @('Verbose','Debug','ErrorAction','WarningAction','InformationAction','ErrorVariable','WarningVariable',
33
+ 'InformationVariable','OutVariable','OutBuffer','PipelineVariable','WhatIf','Confirm')
34
+ foreach ($c in $ast.FindAll({ $args[0] -is [System.Management.Automation.Language.CommandAst] }, $true)) {
35
+ $n = $c.GetCommandName()
36
+ if (-not $n -or $defined -contains $n) { continue }
37
+ # Lecteurs spéciaux (Cert:, HKLM:, Env:…) : leurs paramètres dynamiques ne sont pas connus ici
38
+ if ($c.Extent.Text -match '(?i)\b(Cert|HK\w+|Env|WSMan|Function|Variable|Alias|Registry)::?') { continue }
39
+ try {
40
+ $info = Get-Command $n -ErrorAction SilentlyContinue | Select-Object -First 1
41
+ if ($info -and $info.CommandType -eq 'Alias') { $info = $info.ResolvedCommand }
42
+ if (-not $info -or ($info.CommandType -ne 'Cmdlet' -and $info.CommandType -ne 'Function')) { continue }
43
+ $params = $info.Parameters
44
+ if (-not $params -or $params.Count -eq 0) { continue }
45
+ $known = @($params.Keys) + @($params.Values | ForEach-Object { $_.Aliases }) + $common
46
+ foreach ($p in $c.CommandElements) {
47
+ if ($p -isnot [System.Management.Automation.Language.CommandParameterAst] -or -not $p.ParameterName) { continue }
48
+ $pn = $p.ParameterName
49
+ if (@($known | Where-Object { $_ -like "$pn*" }).Count -eq 0) {
50
+ 'PARAMETRE ' + $info.Name + ' ' + $pn + ' ' + ((@($params.Keys | Where-Object { $common -notcontains $_ }) | Select-Object -First 25) -join ',')
51
+ }
52
+ }
53
+ } catch { }
54
+ }
55
+ } catch { }
56
+ """
57
+ PLACEHOLDER = re.compile(r"<[^<>|]{2,40}>")
58
+
59
+
60
+ def precheck(command: str, cwd: Path) -> list[tuple[str, str]]:
61
+ """Problèmes détectés sans exécuter la commande : [(résumé pour l'écran, explication pour le modèle)]."""
62
+ script = PRECHECK_PS.replace("@B64@", base64.b64encode(command.encode("utf-8")).decode("ascii"))
63
+ path = ""
64
+ try:
65
+ fd, path = tempfile.mkstemp(prefix="prmt_chk_", suffix=".ps1")
66
+ with os.fdopen(fd, "w", encoding="utf-8-sig", newline="\r\n") as f:
67
+ f.write(script)
68
+ proc = subprocess.run(
69
+ ["powershell.exe", "-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass", "-File", path],
70
+ cwd=str(cwd), env=refreshed_env(), stdin=subprocess.DEVNULL, capture_output=True,
71
+ creationflags=CREATE_NO_WINDOW, timeout=20,
72
+ )
73
+ out = decode_output(proc.stdout)
74
+ except (OSError, subprocess.SubprocessError):
75
+ return [] # la vérification ne doit jamais bloquer l'utilisateur
76
+ finally:
77
+ if path:
78
+ try:
79
+ os.remove(path)
80
+ except OSError:
81
+ pass
82
+ problems: list[tuple[str, str]] = []
83
+ for line in out.split("\n"):
84
+ kind, _, arg = line.strip().partition(" ")
85
+ if kind == "SYNTAXE":
86
+ if PLACEHOLDER.search(command):
87
+ problems.append(("valeur manquante (" + PLACEHOLDER.search(command).group(0) + ")",
88
+ "la commande contient un espace réservé comme <adresse_ip> : remplace-le par la vraie valeur "
89
+ "(demande-la à l'utilisateur si tu ne la connais pas)"))
90
+ else:
91
+ problems.append(("erreur de syntaxe", f"erreur de syntaxe PowerShell : {arg}"))
92
+ elif kind == "ALIAS":
93
+ problems.append((f"`{arg}` à remplacer par curl.exe",
94
+ f"dans PowerShell 5.1, `{arg}` est un alias d'Invoke-WebRequest qui n'accepte pas les options "
95
+ f"de {arg} : écris `curl.exe` à la place"))
96
+ elif kind == "INTROUVABLE":
97
+ problems.append((f"« {arg} » n'est pas installé",
98
+ f"« {arg} » n'est ni installé sur cette machine ni une commande PowerShell : utilise une autre "
99
+ "commande disponible, ou propose à l'utilisateur de l'installer"))
100
+ elif kind == "PARAMETRE":
101
+ cmd, _, rest = arg.partition(" ")
102
+ param, _, known = rest.partition(" ")
103
+ listed = ", ".join(f"-{k}" for k in known.split(",") if k)
104
+ problems.append((f"{cmd} n'a pas de paramètre -{param}",
105
+ f"{cmd} n'a pas de paramètre -{param} (paramètre inventé). Ses paramètres : {listed}. "
106
+ "Utilise seulement ceux-là, ou filtre le résultat avec Where-Object / Select-Object"))
107
+ return problems
108
+
109
+ # Commandes qui modifient / suppriment / exécutent du code : jamais autorisées automatiquement.
110
+ DANGEROUS_ANYWHERE = {
111
+ "remove-item", "format-volume", "clear-disk", "initialize-disk", "stop-computer", "restart-computer",
112
+ "set-executionpolicy", "invoke-expression", "iex", "stop-process", "set-content", "out-file", "add-content",
113
+ "new-item", "move-item", "rename-item", "copy-item", "clear-content", "remove-itemproperty", "set-itemproperty",
114
+ "new-itemproperty", "start-process", "diskpart", "bcdedit", "vssadmin", "wbadmin", "takeown", "icacls",
115
+ "cipher", "taskkill", "shutdown", "set-mppreference", "add-mppreference", "remove-mppreference",
116
+ "set-netfirewallprofile", "new-netfirewallrule", "remove-netfirewallrule", "disable-netfirewallrule",
117
+ "set-service", "stop-service", "start-service", "restart-service", "new-service", "remove-service",
118
+ "new-localuser", "set-localuser", "remove-localuser", "add-localgroupmember", "remove-localgroupmember",
119
+ "uninstall-package", "install-package", "install-module", "clear-recyclebin", "set-acl", "invoke-command",
120
+ }
121
+ DANGEROUS_PREFIX = {
122
+ "rm", "ri", "del", "erase", "rd", "rmdir", "mv", "mi", "move", "ren", "rni", "cp", "cpi", "copy", "ni", "md",
123
+ "mkdir", "sc", "sc.exe", "ac", "clc", "sp", "rp", "spps", "kill", "saps", "start", "format", "reg", "reg.exe",
124
+ "net", "net.exe", "netsh", "schtasks", "attrib", "fsutil", "wmic", "powershell", "pwsh", "cmd", "winget",
125
+ "choco", "pip", "npm", "msiexec", "runas", "bitsadmin", "certutil", "mshta", "rundll32", "regsvr32",
126
+ }
127
+ SEGMENT_SPLIT = re.compile(r"\|\||&&|[|;\n]")
128
+ FILE_REDIRECT = re.compile(r"(?<!&)>{1,2}(?!&)\s*(?!\s*(?:\$null\b|nul\b))", re.I)
129
+ # Programme ou script désigné par son chemin (« .\\outil.exe », « C:\\x\\y.ps1 ») ou chargé avec « . script » :
130
+ # son nom ne dit rien de ce qu'il fait, il n'est donc jamais autorisé pour la session.
131
+ PATH_CALL = re.compile(r"""(?:^|[|;{(\n])\s*(?:\.\s|['"]?(?:\.{1,2}|~|[A-Za-z]:)?[\\/])""")
132
+
133
+
134
+ def segments(cmd: str) -> list[str]:
135
+ return [s.strip() for s in SEGMENT_SPLIT.split(cmd) if s.strip()]
136
+
137
+
138
+ def prefix_of(segment: str) -> str:
139
+ m = re.match(r"""^[&.(\s]*['"]?([^\s'"(){}]+)""", segment)
140
+ if not m:
141
+ return ""
142
+ token = m.group(1).replace("\\", "/").rsplit("/", 1)[-1].lower()
143
+ return token[:-4] if token.endswith(".exe") and token not in ("sc.exe", "reg.exe", "net.exe") else token
144
+
145
+
146
+ CMD_POSITION = re.compile(r"(?:^|[|;{(\n])\s*([^\s|;{}()]+)")
147
+ NEVER_AUTO = re.compile(r"`|\[[\w.]+\]::|&", re.I)
148
+
149
+
150
+ def command_words(cmd: str) -> list[str]:
151
+ """Tous les noms de commande, y compris ceux cachés dans { } et ( ) (ex. Where-Object { notepad })."""
152
+ words = []
153
+ for m in CMD_POSITION.finditer(cmd):
154
+ token = m.group(1)
155
+ if token[0] in "$'\"-0123456789@[.":
156
+ continue # variable, texte, paramètre, nombre… : pas une commande
157
+ word = prefix_of(token)
158
+ if word and word not in words:
159
+ words.append(word)
160
+ return words
161
+
162
+
163
+ def can_auto_allow(cmd: str) -> bool:
164
+ """Une commande n'est autorisable automatiquement que si elle est simple et non risquée."""
165
+ cleaned = re.sub(r"[0-9*]?>&[0-9]", "", cmd) # 2>&1 n'est pas un opérateur d'appel
166
+ return (not is_dangerous(cmd) and not NEVER_AUTO.search(cleaned) and not PATH_CALL.search(cmd)
167
+ and bool(command_words(cmd)))
168
+
169
+
170
+ def is_dangerous(cmd: str) -> bool:
171
+ words = {w.lower() for w in re.findall(r"[A-Za-z][\w.-]*", cmd)}
172
+ if words & DANGEROUS_ANYWHERE:
173
+ return True
174
+ if any(prefix_of(s) in DANGEROUS_PREFIX for s in segments(cmd)):
175
+ return True
176
+ # Aussi dans les blocs : « Get-ChildItem | ForEach-Object { rm $_ } » supprime autant que « rm »
177
+ if any(w in DANGEROUS_PREFIX for w in command_words(cmd)):
178
+ return True
179
+ return bool(FILE_REDIRECT.search(cmd))
180
+
181
+
182
+ # Portée d'un « ne plus demander » : pour une lecture reconnue, il vaut pour le nom de la commande ; pour tout le
183
+ # reste (python, git, curl, nmap…), seulement pour la ligne exacte. Sinon, après « python --version », n'importe
184
+ # quel « python script.py » partait sans validation.
185
+ READ_VERBS = ("get-", "test-", "resolve-", "select-", "where-", "sort-", "measure-", "format-", "group-", "compare-",
186
+ "convertto-", "convertfrom-", "find-", "join-", "split-")
187
+ READ_NAMES = {
188
+ "foreach-object", "out-string", "out-host", "write-output", "write-host", "ipconfig", "whoami", "hostname", "ping",
189
+ "tracert", "pathping", "nslookup", "netstat", "arp", "getmac", "systeminfo", "tasklist", "ver", "where", "findstr",
190
+ "sort", "more", "type", "dir", "ls", "cat", "gc", "gci", "gi", "gp", "gps", "gsv", "gcm", "gm", "select", "echo",
191
+ "pwd", "tree", "ft", "fl", "fw", "measure", "group", "sls", "oh", "gl",
192
+ }
193
+
194
+
195
+ def is_read_name(word: str) -> bool:
196
+ return word in READ_NAMES or word.startswith(READ_VERBS)
197
+
198
+
199
+ def session_scope(cmd: str) -> tuple[str, list[str]]:
200
+ """Ce que couvrirait un « ne plus demander » : ("noms", [commandes]) ou ("ligne", [la commande exacte])."""
201
+ words = command_words(cmd)
202
+ if words and all(is_read_name(w) for w in words):
203
+ return "noms", words
204
+ return "ligne", [" ".join(cmd.split())]
@@ -0,0 +1,190 @@
1
+ """Exécution PowerShell : décodage des sorties, environnement, script lancé, résultat renvoyé au modèle."""
2
+ from __future__ import annotations
3
+
4
+ import os
5
+ import re
6
+
7
+ from .config import MAX_TOOL_CHARS
8
+
9
+
10
+ def _codepages() -> tuple[str, str]:
11
+ try:
12
+ import ctypes
13
+ k32 = ctypes.windll.kernel32
14
+ return f"cp{k32.GetOEMCP()}", f"cp{k32.GetACP()}"
15
+ except Exception:
16
+ return "cp850", "cp1252"
17
+
18
+
19
+ OEM_CP, ANSI_CP = _codepages()
20
+ FRENCH_CHARS = set("àâäçéèêëîïôöùûüÿœÀÂÄÇÉÈÊËÎÏÔÖÙÛÜŒ«»’\xa0°")
21
+
22
+
23
+ def _decode_line(raw: bytes) -> str:
24
+ # Une même sortie mélange les encodages : PowerShell et la plupart des outils écrivent en OEM
25
+ # (cp850), certains messages d'erreur (findstr…) en ANSI (cp1252), python/git en UTF-8.
26
+ try:
27
+ return raw.decode("utf-8")
28
+ except UnicodeDecodeError:
29
+ pass
30
+ candidates = [raw.decode(OEM_CP, "replace"), raw.decode(ANSI_CP, "replace")]
31
+ return max(candidates, key=lambda s: sum(c in FRENCH_CHARS for c in s))
32
+
33
+
34
+ def decode_output(data: bytes) -> str:
35
+ try:
36
+ text = data.decode("utf-8")
37
+ except UnicodeDecodeError:
38
+ text = "\n".join(_decode_line(line) for line in data.split(b"\n"))
39
+ text = text.replace("\r\n", "\n").replace("\xa0", " ")
40
+ if "\r" in text:
41
+ # Barres de progression (winget, curl…) : un \r seul réécrit la ligne, comme dans un terminal
42
+ text = "\n".join(_last_segment(line) for line in text.split("\n"))
43
+ return text
44
+
45
+
46
+ def _last_segment(line: str) -> str:
47
+ parts = [p for p in line.split("\r") if p.strip()]
48
+ return parts[-1] if parts else ""
49
+
50
+
51
+ def refreshed_env() -> dict[str, str]:
52
+ """Environnement avec le PATH relu dans le registre : un logiciel installé pendant la session
53
+ (winget, installateur…) est trouvé sans relancer prmt, comme dans un nouveau terminal."""
54
+ env = dict(os.environ)
55
+ try:
56
+ import winreg
57
+ found: list[str] = []
58
+ for root, key in ((winreg.HKEY_LOCAL_MACHINE, r"SYSTEM\CurrentControlSet\Control\Session Manager\Environment"),
59
+ (winreg.HKEY_CURRENT_USER, "Environment")):
60
+ try:
61
+ with winreg.OpenKey(root, key) as k:
62
+ value, _ = winreg.QueryValueEx(k, "Path")
63
+ found += [os.path.expandvars(p) for p in value.split(";") if p.strip()]
64
+ except OSError:
65
+ pass
66
+ current = env.get("PATH", "").split(";")
67
+ known = {p.rstrip("\\").lower() for p in current}
68
+ extra = [p for p in found if p.rstrip("\\").lower() not in known]
69
+ if extra:
70
+ env["PATH"] = ";".join(current + extra)
71
+ except Exception:
72
+ pass
73
+ env["PATH"] = windows_first(env.get("PATH", ""), env.get("SystemRoot") or env.get("windir") or "C:\\Windows")
74
+ return env
75
+
76
+
77
+ def windows_first(path: str, root: str) -> str:
78
+ """PATH avec les dossiers de Windows en tête, comme dans le moteur de la console : « whoami » ou « ipconfig »
79
+ sont alors ceux de Windows, ceux que la politique a vérifiés, et pas un programme du même nom placé avant."""
80
+ head = [f"{root}\\System32", root, f"{root}\\System32\\Wbem", f"{root}\\System32\\WindowsPowerShell\\v1.0"]
81
+ lowered = {h.lower() for h in head}
82
+ return ";".join(head + [p for p in path.split(";") if p and p.rstrip("\\").lower() not in lowered])
83
+
84
+
85
+ CWD_MARK = "__PRMT_CWD__="
86
+
87
+
88
+ MODULE_NAME = re.compile(r"^[A-Za-z0-9._-]{1,80}$")
89
+ CONSTRAINED = "$ExecutionContext.SessionState.LanguageMode = 'ConstrainedLanguage'"
90
+
91
+
92
+ def build_script(cmd: str, modules: tuple[str, ...] | list[str] = (), constrained: bool = False) -> str:
93
+ # Le marqueur final permet de suivre les "cd" d'une commande à l'autre.
94
+ # $OutputEncoding = encodage console : sinon PowerShell 5.1 transmet les pipes natifs
95
+ # (ex. arp -a | findstr) en ASCII et les accents deviennent des « ? ».
96
+ # constrained : seconde barrière, comme dans la console. Les modules de fonctions sont chargés juste avant,
97
+ # sinon ils ne se chargeraient plus (mesuré : Get-NetTCPConnection échoue en mode contraint sans cela).
98
+ imports = "".join(f"Import-Module -Name '{m}' -ErrorAction SilentlyContinue\n" for m in modules if MODULE_NAME.match(m))
99
+ return (
100
+ "$ProgressPreference='SilentlyContinue'\n"
101
+ "$OutputEncoding = [Console]::OutputEncoding\n"
102
+ + imports + (CONSTRAINED + "\n" if constrained else "") +
103
+ f"{cmd}\n"
104
+ "$__ok = $?; $__code = $LASTEXITCODE\n"
105
+ # Retour à la ligne avant le marqueur : certaines commandes (curl…) ne finissent pas par un \n
106
+ f"Write-Output (\"`n{CWD_MARK}\" + (Get-Location).ProviderPath)\n"
107
+ "if (-not $__ok) { if ($__code) { exit $__code } else { exit 1 } }\n"
108
+ )
109
+
110
+
111
+ # Les erreurs PowerShell citent le fichier .ps1 temporaire et soulignent la ligne fautive :
112
+ # on ne garde que le message (plus lisible pour l'utilisateur, moins de bruit pour le modèle).
113
+ ERR_LOCATION = re.compile(r"^(?:Au caractère|At) .*prmt_\w+\.ps1:\d+\s*(?::|char:)\s*\d+\s*$")
114
+ ERR_DETAIL = re.compile(r"^\s+\+ (?:CategoryInfo|FullyQualifiedErrorId)\s*:")
115
+
116
+
117
+ def tidy_output(text: str) -> str:
118
+ lines: list[str] = []
119
+ in_location = in_detail = False
120
+ for line in text.split("\n"):
121
+ if ERR_LOCATION.match(line):
122
+ in_location = True
123
+ continue
124
+ if in_location and line.startswith("+ "):
125
+ continue
126
+ in_location = False
127
+ if ERR_DETAIL.match(line):
128
+ in_detail = True
129
+ continue
130
+ # PowerShell coupe les longues lignes de détail (~120 colonnes) : on saute leur suite
131
+ if in_detail and line.startswith(" ") and line.strip():
132
+ continue
133
+ in_detail = False
134
+ line = line.rstrip()
135
+ if not line and (not lines or not lines[-1]):
136
+ continue # pas de lignes vides en double
137
+ lines.append(line)
138
+ return "\n".join(lines).strip("\n")
139
+
140
+
141
+ def for_model(code: int | None, output: str, note: str = "") -> str:
142
+ out = output.strip() or "(aucune sortie)"
143
+ if len(out) > MAX_TOOL_CHARS:
144
+ cut = len(out) - 3300
145
+ out = out[:1800] + f"\n[… {cut} caractères coupés …]\n" + out[-1500:]
146
+ head = f"Code de sortie : {code}" if code is not None else "Commande non terminée"
147
+ return f"{head}{(' (' + note + ')') if note else ''}\n{out}{hint_for(code, out)}"
148
+
149
+
150
+ # Indices ajoutés au résultat envoyé au modèle (pas à l'écran) : un modèle de 8B suit bien mieux
151
+ # une consigne placée dans la sortie d'une commande que dans le prompt système.
152
+ WINGET_HINT = ("\n\n(Indice : le nom n'a pas suffi. Lance maintenant `winget search <un seul mot-clé>`, "
153
+ "puis réessaie avec `winget install -e --id <Id exact copié depuis la colonne Id> "
154
+ "--accept-package-agreements --accept-source-agreements`.)")
155
+
156
+
157
+ # Mesuré : après une commande fausse, le modèle relance l'outil 5/6 avec cet indice (2/6 sans),
158
+ # et répond toujours directement quand l'échec EST la réponse (processus absent, port fermé).
159
+ ERROR_HINT = ("\n\n(Indice : si cette sortie répond déjà à la question, réponds. Si la commande était fausse "
160
+ "(syntaxe, option, nom), rappelle directement l'outil avec une commande corrigée, sans demander la permission par écrit.)")
161
+
162
+
163
+ # Smart App Control (Windows 11) bloque au hasard les programmes qu'on vient de compiler : pas une erreur du code.
164
+ APP_CONTROL = re.compile(r"os error 4551|contr[ôo]le d.application a bloqu|application control policy has blocked", re.I)
165
+ APP_CONTROL_HINT = ("\n\n(Indice : c'est Windows (Smart App Control) qui a bloqué l'exécution du programme compilé, pas une "
166
+ "erreur du code : la compilation a réussi. Ne réessaie pas de le lancer. Vérifie le code avec "
167
+ "`cargo check` et `cargo clippy`, et dis à l'utilisateur que Smart App Control a bloqué l'exécution.)")
168
+
169
+
170
+ def hint_for(code: int | None, out: str) -> str:
171
+ if "No package found matching input criteria" in out or "Multiple packages found matching input criteria" in out:
172
+ return WINGET_HINT
173
+ if APP_CONTROL.search(out):
174
+ return APP_CONTROL_HINT
175
+ if code not in (0, None):
176
+ return ERROR_HINT
177
+ return ""
178
+
179
+
180
+ # --------------------------------------------------------------------------- Mode « ! » (commande lancée par l'utilisateur)
181
+ SHELL_TAG = "[Commande lancée par l'utilisateur avec !]"
182
+
183
+
184
+ def shell_message(command: str, code: int | None, output: str, note: str) -> str:
185
+ """Message ajouté à la conversation après « ! commande » (sans les indices destinés au modèle)."""
186
+ out = output.strip() or "(aucune sortie)"
187
+ if len(out) > MAX_TOOL_CHARS:
188
+ out = out[:1800] + f"\n[… {len(out) - 3300} caractères coupés …]\n" + out[-1500:]
189
+ head = f"Code de sortie : {code}" if code is not None else "Commande non terminée"
190
+ return f"{SHELL_TAG} `{command}`\n{head}{(' (' + note + ')') if note else ''}\n{out}"
@@ -0,0 +1,190 @@
1
+ """Textes fixes de l'interface : aide, spinner, feuille de style."""
2
+ from __future__ import annotations
3
+
4
+ from .theme import AGENT_COLOR, DIM, MEMORY_COLOR, OR, OR_CLAIR, ORANGE, PLAN_COLOR, RED, SHELL_COLOR
5
+
6
+ # --------------------------------------------------------------------------- Application
7
+ SPINNER = ["·", "✢", "✳", "✶", "✻", "✽", "✻", "✶", "✳", "✢"]
8
+ VERBS = [
9
+ "Réflexion", "Analyse", "Décryptage", "Forge", "Attise les braises", "Allume le feu",
10
+ "Énumération", "Investigation", "Rétro-ingénierie", "Recherche de failles", "Déchiffrement",
11
+ ]
12
+
13
+ HELP = """\
14
+ **Commandes**
15
+ - `/aide` : afficher cette aide
16
+ - `/etat` : état de la plateforme, mesuré à l'instant (Ollama, modèle, PowerShell, politique, périmètre, activité)
17
+ - `/clear` : nouvelle conversation (efface la mémoire de Prométhée et les autorisations de la session)
18
+ - `/modele` : lister les modèles · `/modele deephat` : changer de modèle
19
+ - `/copier` : copier la dernière réponse · `/copier code` : seulement les blocs de code
20
+ - `/sortie` : afficher la sortie complète de la dernière commande exécutée
21
+ - `/politique` : politique d'exécution et liste blanche · `/politique stricte` · `/politique souple`
22
+ - `/permissions` : autorisations de la session · `/permissions reset` : tout redemander ·
23
+ `/permissions autoriser <commande>` · `/permissions retirer <commande>`
24
+ - `/perimetre` : cibles réseau déclarées · `/perimetre ajouter <hôte>` · `/perimetre retirer <hôte>`
25
+ - `/journal` : dernières actions (commandes exécutées ou refusées, fichiers écrits, recherches web) · `/journal refus`
26
+ - `/recherche` : recherche web, active ou non, et SearXNG répond-il · `/recherche on` · `/recherche off`
27
+ - `/theme terminal` : garder le fond de ton terminal · `/theme promethee` : le fond de Prométhée
28
+ - `/sauver` : enregistrer la conversation (dossier Documents\\Promethee)
29
+ - `/compact` : résumer la conversation pour libérer le contexte (automatique à 80 %)
30
+ - `/reprendre` : reprendre une conversation précédente (aussi : `prmt -c` la dernière, `prmt -r` pour choisir)
31
+ - `/memoire` : voir la mémoire · `/memoire ouvrir` : la modifier dans le Bloc-notes
32
+ - `/plan` : activer ou couper le mode plan (comme `Shift+Tab`)
33
+ - `/quitter` : quitter
34
+ - Tape `/` : la liste des commandes s'affiche (`↑↓` choisir, `Tab` compléter, `Entrée` lancer, `Esc` fermer).
35
+
36
+ **Mode commande `!`**
37
+ - `! commande` : exécute ta commande PowerShell. Une lecture part **directement** ; une commande sensible, risquée
38
+ ou hors politique te demande **une confirmation** ; un refus de la politique vaut aussi pour toi.
39
+ - Sa sortie est ajoutée à la conversation : Prométhée peut ensuite l'analyser (« explique ce résultat »).
40
+
41
+ **Mémoire `#` (fichiers PROMETHEE.md)**
42
+ - `# ma VM Kali est en 192.168.56.10` : enregistre l'information, dans la mémoire globale ou dans celle du dossier courant.
43
+ - Prométhée relit sa mémoire à chaque message : plus besoin de répéter ton contexte (niveau, machines, préférences).
44
+ - Mémoire globale : `Documents\\Promethee\\PROMETHEE.md` · mémoire d'un dossier : `PROMETHEE.md` dans ce dossier.
45
+
46
+ **Fichiers joints `@`**
47
+ - `analyse @auth.log` : le contenu du fichier est joint à ta question. En tapant `@`, la liste des fichiers du dossier
48
+ s'affiche (`Tab` pour compléter, `\\` pour entrer dans un sous-dossier). `@dossier` joint la liste de son contenu.
49
+
50
+ **Agents (`@agent-nom`, `/agents`)**
51
+ - Un agent est un assistant spécialisé qui travaille **dans son propre contexte** (ses longues sorties de commandes
52
+ n'encombrent pas la conversation) puis renvoie seulement son **rapport**. Tu valides toujours ses commandes.
53
+ - `@agent-analyste-logs analyse @auth.log` : confie directement la tâche à l'agent. Prométhée peut aussi déléguer
54
+ de lui-même si tu le lui demandes (« demande à l'explorateur de trouver… »).
55
+ - `/agents` : liste des agents, **créer** un agent (tu le décris, Prométhée l'écrit), le modifier, le supprimer.
56
+ Intégrés : `explorateur` (cherche dans les fichiers), `analyste-logs` (analyse de journaux).
57
+ - Fichiers : `Documents\\Promethee\\agents\\nom.md` (globaux) ou `.promethee\\agents\\nom.md` dans un dossier.
58
+ - `Ctrl+O` : afficher ou replier le détail du travail des agents.
59
+
60
+ **IA de code (`/code`)**
61
+ - Quand tu demandes d'écrire, corriger ou expliquer du code, Prométhée passe la main à une IA de code
62
+ (`qwen3.5:9b` par défaut) pour cette réponse, puis la reprend : même conversation, mêmes outils, tu valides
63
+ toujours. Elle vérifie son code (compilation, exécution) avant de dire que ça marche.
64
+ - `/code` : état · `/code off` / `/code on` : couper / rétablir la bascule · `/code <modèle>` : changer d'IA de code.
65
+
66
+ **Commandes personnalisées (`/commandes`)**
67
+ - Un fichier `nom.md` dans `Documents\\Promethee\\commandes` (ou `.promethee\\commandes` d'un dossier) devient la
68
+ commande `/nom` : son texte est envoyé à Prométhée, `$ARGUMENTS` remplacé par ce que tu tapes après (`$1`, `$2`…
69
+ pour chaque mot). Elles apparaissent dans le menu `/`.
70
+ - `/commandes` : les lister, en **créer** une (tu la décris, Prométhée l'écrit), la modifier, la supprimer.
71
+
72
+ **Retour en arrière, mémoire du dossier, contexte**
73
+ - `Esc` `Esc` (saisie vide) : choisir un de tes messages ; la conversation repart juste avant, et le message revient
74
+ dans la saisie pour le modifier (les commandes déjà exécutées ne sont pas annulées).
75
+ - `/init` : Prométhée explore le dossier courant et écrit sa mémoire `PROMETHEE.md` (tu valides le fichier).
76
+ - `/contexte` : ce qui remplit le contexte du modèle (consignes, mémoire, messages, sorties de commandes…).
77
+
78
+ **Recherche**
79
+ - Prométhée cherche des fichiers (`chercher_fichiers`, par nom) ou du texte dans des fichiers (`chercher_texte`)
80
+ **sans te demander** : c'est de la lecture seule, comme `lire_fichier`.
81
+
82
+ **Recherche web (`/recherche`)**
83
+ - Quand une question demande une information récente ou précise (dernière version, actualité, CVE, prix) ou un lieu,
84
+ Prométhée cherche de lui-même sur internet par SearXNG, installé chez toi. Tu vois la requête et les sources
85
+ numérotées, et pour un lieu son adresse, ses coordonnées et un lien OpenStreetMap. La réponse cite ses sources : [1].
86
+ - Rien ne s'exécute sur ta machine : la réponse qui suit une recherche ne peut proposer aucune commande.
87
+ - `/recherche` : état · `/recherche off` / `/recherche on` : couper / rétablir. Le réglage est partagé avec la console
88
+ quand elle est reliée. SearXNG absent : `npm run searxng` dans le dossier de la console.
89
+
90
+ **Mode plan (`Shift+Tab`)**
91
+ - Prométhée propose un plan numéroté **sans rien exécuter**. Tu le relis, tu l'affines, puis tu choisis de l'exécuter :
92
+ il repasse alors en mode normal, et tu valides toujours chaque commande.
93
+ - Pendant l'exécution d'un plan ou d'un audit, la **liste des tâches** s'affiche au-dessus de la saisie et se coche
94
+ d'après les commandes réellement exécutées.
95
+
96
+ **Exécution de commandes**
97
+ - Prométhée peut lancer des commandes PowerShell, mais **tu valides chacune** avant qu'elle s'exécute.
98
+ - `1` Oui · `2` Oui et ne plus demander (session) · `3` / `Esc` Non. « Ne plus demander » vaut pour le nom d'une
99
+ commande de lecture reconnue (`ipconfig`, `Get-Process`…) ; pour toute autre, seulement pour la ligne exacte.
100
+ - Les commandes qui modifient ou suppriment quelque chose sont signalées en rouge et **toujours** soumises à validation.
101
+ - `Esc` pendant une commande : l'arrêter. Délai maximum : 5 minutes.
102
+
103
+ **Politique d'exécution (`/politique`)**
104
+ - Avant de t'être montrée, chaque commande est analysée par le moteur de la console web (le vrai analyseur de
105
+ PowerShell, sans l'exécuter) : liste blanche, arguments, dossiers d'écriture, cibles réseau.
106
+ - `● lecture` · `▲ sensible` (écrit, contacte le réseau, commande débloquée) · `◆ hors politique` · `✕ refusée`.
107
+ - **stricte** (par défaut) : une commande hors liste blanche est refusée, sans t'être proposée. Prométhée reçoit
108
+ la raison et cherche une commande de lecture, ou te dit quoi autoriser.
109
+ - **souple** : ce que l'analyse ne sait pas vérifier (scripts, compilateurs, programmes lancés par leur chemin)
110
+ t'est montré comme hors politique et tu le valides. Restent refusés dans les deux modes : commande destructrice
111
+ non débloquée, cible hors périmètre, protections de Windows, chemin réseau.
112
+ - `/permissions autoriser cargo` ajoute une commande à ta liste ; `/permissions autoriser Remove-Item` débloque une
113
+ commande destructrice. La liste est partagée avec la console web.
114
+ - Les tests de ports et les scans ne sont permis que sur cette machine et sur les cibles de `/perimetre` : des
115
+ systèmes qui t'appartiennent ou pour lesquels tu as une autorisation écrite.
116
+ - Si la console n'est pas reliée, la barre d'état affiche « validation manuelle » : tu valides chaque commande, sans
117
+ liste blanche. Chaque action est inscrite dans `Documents\\Promethee\\journal.jsonl` (`/journal`).
118
+
119
+ **Fichiers**
120
+ - Prométhée lit les fichiers sans demander (lecture seule), mais te montre un **diff coloré** avant chaque création
121
+ ou modification : `1` Oui · `2` Oui pour toutes les modifications de la session · `3` / `Esc` Non.
122
+ - Les fichiers hors de ton dossier utilisateur sont signalés en rouge et toujours soumis à validation.
123
+ - Les fichiers relus ou exécutés plus tard aussi, même après « oui pour toutes » : mémoire `PROMETHEE.md`, agents et
124
+ commandes de Prométhée, profil PowerShell, dossier `AppData`, fichiers et dossiers cachés (`.ssh`, `.git`, `.env`).
125
+
126
+ **Contrôles automatiques (interpréteur)**
127
+ - Avant de répondre, ta demande est classée : *action* (Prométhée exécute) ou *explication* (il répond sans rien lancer).
128
+ - S'il écrit des commandes au lieu de les exécuter, ou annonce une étape sans la faire, il est relancé automatiquement.
129
+ - Chaque commande est vérifiée avant de t'être montrée (syntaxe, programme installé, `curl` → `curl.exe`) et corrigée si besoin.
130
+ - Après la réponse, les IP et ports cités qui n'apparaissent dans aucune sortie de commande te sont signalés.
131
+ - Pour un audit (« fais un audit de sécurité de mon site … »), il suit une méthode en 5 étapes puis fait un rapport.
132
+
133
+ **Raccourcis**
134
+ - `Entrée` : envoyer · `Ctrl+J` ou `\\` puis `Entrée` : nouvelle ligne
135
+ - `↑` / `↓` : historique de tes messages
136
+ - `Esc` : interrompre la réponse · `Esc` `Esc` (saisie vide) : revenir à un message précédent
137
+ - `Ctrl+C` : copier la sélection, sinon interrompre ; deux fois de suite pour quitter
138
+ - `PgUp` / `PgDn` ou molette : faire défiler la conversation
139
+ """
140
+
141
+ # Les couleurs viennent de theme.py. Le fond, les panneaux ($surface) et les ascenseurs suivent le thème Textual
142
+ # choisi par /theme : peints par l'application, ou laissés au terminal.
143
+ CSS = f"""
144
+ Screen {{ layout: vertical; }}
145
+ #chat {{ height: 1fr; padding: 0 1; scrollbar-size-vertical: 1; }}
146
+ Logo {{ height: auto; width: auto; margin: 1 0 0 1; }}
147
+ Accueil {{
148
+ border: round {OR}; padding: 0 1; margin: 1 0 0 0; width: auto; max-width: 100%; height: auto; background: $surface;
149
+ }}
150
+ .etat {{
151
+ border: round {OR}; border-title-color: {OR_CLAIR}; border-title-style: bold; border-subtitle-color: {DIM};
152
+ padding: 0 1; margin: 1 0 0 0; width: auto; max-width: 100%; height: auto; background: $surface;
153
+ }}
154
+ .user {{ margin: 1 0 0 0; height: auto; color: {DIM}; }}
155
+ AssistantMessage {{ margin: 1 0 0 0; height: auto; }}
156
+ AssistantMessage .bullet {{ width: 2; height: 1; color: {ORANGE}; }}
157
+ AssistantMessage .body {{ width: 1fr; height: auto; }}
158
+ AssistantMessage Markdown {{ margin: 0; padding: 0; height: auto; }}
159
+ ToolCallView {{ margin: 1 0 0 0; height: auto; }}
160
+ ToolCallView Static {{ height: auto; }}
161
+ SearchView {{ margin: 1 0 0 0; height: auto; }}
162
+ SearchView Static {{ height: auto; }}
163
+ AgentView {{ margin: 1 0 0 0; height: auto; }}
164
+ AgentView Static {{ height: auto; }}
165
+ AgentView .agent-body {{ height: auto; padding: 0 0 0 2; }}
166
+ AgentView .agent-body ToolCallView {{ margin: 0; }}
167
+ .notice {{ margin: 1 0 0 0; padding: 0 0 0 2; height: auto; }}
168
+ .notice-md {{ margin: 1 0 0 2; padding: 0; height: auto; }}
169
+ #bottom {{ height: auto; }}
170
+ #spinner {{ height: 1; padding: 0 2; display: none; }}
171
+ #spinner.-active {{ display: block; }}
172
+ #input-box {{ border: round {DIM}; height: auto; margin: 0 1; padding: 0 1; background: $surface; }}
173
+ #input-box:focus-within {{ border: round {ORANGE}; }}
174
+ #input-box.-plan, #input-box.-plan:focus-within {{ border: round {PLAN_COLOR}; }}
175
+ #input-box.-shell, #input-box.-shell:focus-within {{ border: round {SHELL_COLOR}; }}
176
+ #input-box.-shell #prompt-char {{ color: {SHELL_COLOR}; }}
177
+ #input-box.-memory, #input-box.-memory:focus-within {{ border: round {MEMORY_COLOR}; }}
178
+ #input-box.-memory #prompt-char {{ color: {MEMORY_COLOR}; }}
179
+ #input-box.-agent, #input-box.-agent:focus-within {{ border: round {AGENT_COLOR}; }}
180
+ #input-box.-agent #prompt-char {{ color: {AGENT_COLOR}; }}
181
+ #completions {{ height: auto; padding: 0 2; display: none; }}
182
+ #tasks {{ height: auto; padding: 0 2; margin: 1 0 0 0; display: none; }}
183
+ #prompt-char {{ width: 2; height: 1; color: {ORANGE}; text-style: bold; }}
184
+ PromptInput {{ height: auto; max-height: 12; width: 1fr; border: none; padding: 0; background: transparent; }}
185
+ ChoicePrompt {{ border: round {ORANGE}; height: auto; margin: 0 1; padding: 0 1; background: $surface; }}
186
+ ChoicePrompt.-danger {{ border: round {RED}; }}
187
+ #status-line {{ height: 1; padding: 0 2; }}
188
+ #hints {{ width: 1fr; color: {DIM}; }}
189
+ #status {{ width: auto; color: {DIM}; }}
190
+ """