feat(core): moteur PromptKey — bibliothèque .md, variables, injection, raccourcis
- PromptStore/Frontmatter : un prompt = un fichier Markdown, en-tête au sous-ensemble strict du document d'architecture (§7), écriture UTF-8 - VariableEngine : variables typées + automatiques (selection, clipboard, date, app, cursor), résolution et profils de destination - Injector/ClipboardGuard : focus capturé/rendu, presse-papiers sauvegardé puis restauré conditionnellement, aucun envoi automatique par défaut - Search : classement flou insensible aux accents, pondéré par l'usage - UsageStats (usage.json), HotkeyManager (conflits signalés, jamais muets) - Settings : config.ini + profils par IA (claude, chatgpt, qwen, terminal…) - PromptKey.ahk : tray, instance unique, capture du contexte, surveillance de la bibliothèque, installation du pack au premier lancement
This commit is contained in:
+259
@@ -0,0 +1,259 @@
|
||||
#Requires AutoHotkey v2.0
|
||||
#SingleInstance Force
|
||||
Persistent()
|
||||
; PromptKey — Gestionnaire de prompts pour IA (Windows, AutoHotkey v2).
|
||||
; Document d'architecture : architecture-prompt-manager-autohotkey.md (Spark, v1.0).
|
||||
; Principe : insérer, ne pas envoyer (D5) ; zéro réseau (D7) ; un prompt = un fichier .md (D2).
|
||||
|
||||
#Include lib\Util.ahk
|
||||
#Include lib\Json.ahk
|
||||
#Include lib\Settings.ahk
|
||||
#Include lib\Frontmatter.ahk
|
||||
#Include lib\VariableEngine.ahk
|
||||
#Include lib\PromptStore.ahk
|
||||
#Include lib\Search.ahk
|
||||
#Include lib\ClipboardGuard.ahk
|
||||
#Include lib\Injector.ahk
|
||||
#Include lib\UsageStats.ahk
|
||||
#Include lib\HotkeyManager.ahk
|
||||
#Include gui\VarForm.ahk
|
||||
#Include gui\Picker.ahk
|
||||
#Include gui\Editor.ahk
|
||||
#Include gui\Manager.ahk
|
||||
#Include gui\SettingsGui.ahk
|
||||
|
||||
class App {
|
||||
static Settings := ""
|
||||
static Store := ""
|
||||
static Usage := ""
|
||||
static InjectorObj := ""
|
||||
static Hotkeys := HotkeyManager()
|
||||
static Picker := PromptPicker()
|
||||
static Manager := PromptManager()
|
||||
static Editor := PromptEditor()
|
||||
static PrefsGui := SettingsGui()
|
||||
static VarForm := VarForm()
|
||||
static LastCtx := ""
|
||||
static LastPromptId := ""
|
||||
static Started := false
|
||||
|
||||
static Init() {
|
||||
Util.LogFile := A_ScriptDir "\promptkey.log"
|
||||
App.Settings := Settings(A_ScriptDir "\config.ini")
|
||||
App.Store := PromptStore(App.Settings.LibraryPath)
|
||||
App.Store.Load()
|
||||
App.InstallStarterPackIfEmpty()
|
||||
App.Usage := UsageStats(App.Settings.LibraryPath "\usage.json")
|
||||
App.InjectorObj := Injector(App.Settings)
|
||||
App.Picker.Build((p, useDefaults) => App.PickerUse(p, useDefaults), (p) => App.Editor.Show(p))
|
||||
App.Manager.Build((p) => App.UsePrompt(p, App.LastCtxOrCapture(), Map()), (p) => App.Editor.Show(p))
|
||||
App.BuildTray()
|
||||
App.RegisterHotkeys()
|
||||
SetTimer(() => App.WatchLibrary(), 5000)
|
||||
App.Started := true
|
||||
Util.Log("PromptKey démarré — " App.Store.Prompts.Length " prompts, bibliothèque : " App.Settings.LibraryPath)
|
||||
TrayTip("Prêt — " App.Store.Prompts.Length " prompts. Raccourci : " App.Settings.PickerHotkey, "PromptKey")
|
||||
}
|
||||
|
||||
; Premier lancement : si la bibliothèque est vide et que le pack est présent, on le recopie.
|
||||
static InstallStarterPackIfEmpty() {
|
||||
if (App.Store.Prompts.Length > 0)
|
||||
return
|
||||
packRoot := A_ScriptDir "\packs\essentiels"
|
||||
if !DirExist(packRoot)
|
||||
return
|
||||
copied := 0
|
||||
Loop Files packRoot "\*.md", "FR" {
|
||||
; Chemin relatif : <Catégorie>\<fichier>.md
|
||||
rel := SubStr(A_LoopFilePath, StrLen(packRoot) + 2)
|
||||
dest := App.Settings.LibraryPath "\" rel
|
||||
SplitPath(dest, , &destDir)
|
||||
DirCreate(destDir)
|
||||
if !FileExist(dest) {
|
||||
FileCopy(A_LoopFilePath, dest)
|
||||
copied++
|
||||
}
|
||||
}
|
||||
if (copied > 0) {
|
||||
App.Store.Load()
|
||||
Util.Log("Pack de démarrage installé : " copied " prompts")
|
||||
}
|
||||
}
|
||||
|
||||
static BuildTray() {
|
||||
A_IconTip := "PromptKey — Gestionnaire de prompts"
|
||||
tray := A_TrayMenu
|
||||
tray.Delete()
|
||||
tray.Add("Ouvrir le Picker", (*) => App.OpenPicker())
|
||||
tray.Add("Gestionnaire", (*) => App.OpenManager())
|
||||
tray.Add("Nouveau prompt", (*) => App.NewPromptInteractive())
|
||||
tray.Add()
|
||||
tray.Add("Recharger la bibliothèque", (*) => App.RefreshAfterLibraryChange(true))
|
||||
tray.Add("Paramètres", (*) => App.PrefsGui.Show())
|
||||
tray.Add("Ouvrir le dossier de la bibliothèque", (*) => Run(App.Settings.LibraryPath))
|
||||
tray.Add()
|
||||
tray.Add("Quitter", (*) => ExitApp())
|
||||
tray.Default := "Ouvrir le Picker"
|
||||
tray.ClickCount := 1
|
||||
}
|
||||
|
||||
static RegisterHotkeys() {
|
||||
callbacks := Map(
|
||||
"picker", (*) => App.OpenPicker(),
|
||||
"manager", (*) => App.OpenManager(),
|
||||
"newPrompt", (*) => App.NewPromptInteractive(),
|
||||
"replay", (*) => App.Replay(),
|
||||
"usePrompt", (id, extra*) => App.UsePromptById(id)
|
||||
)
|
||||
App.Hotkeys.RegisterAll(App.Settings, App.Store.Prompts, callbacks)
|
||||
}
|
||||
|
||||
; Après un changement de bibliothèque (édition, rechargement) : raccourcis à jour.
|
||||
static RefreshAfterLibraryChange(announce := false) {
|
||||
App.Store.Load()
|
||||
App.RegisterHotkeys()
|
||||
if announce
|
||||
TrayTip("Bibliothèque rechargée : " App.Store.Prompts.Length " prompts.", "PromptKey")
|
||||
}
|
||||
|
||||
static WatchLibrary() {
|
||||
if (App.Started && App.Store.ChangedOnDisk()) {
|
||||
Util.Log("Changement détecté dans la bibliothèque — rechargement.")
|
||||
App.RefreshAfterLibraryChange()
|
||||
}
|
||||
}
|
||||
|
||||
; Capture du contexte d'invocation (§11.2) : fenêtre active, processus, sélection, presse-papiers.
|
||||
; L'ordre compte : lire le presse-papiers AVANT la capture de sélection (qui le restaure ensuite).
|
||||
static CaptureContext() {
|
||||
ctx := Map("hwnd", 0, "title", "", "process", "", "selection", "", "clipboardText", "")
|
||||
try ctx["hwnd"] := WinExist("A")
|
||||
if (ctx["hwnd"]) {
|
||||
try ctx["title"] := WinGetTitle("ahk_id " ctx["hwnd"])
|
||||
try ctx["process"] := WinGetProcessName("ahk_id " ctx["hwnd"])
|
||||
}
|
||||
ctx["clipboardText"] := ClipboardGuard.ClipboardText()
|
||||
ctx["selection"] := ClipboardGuard.CaptureSelection()
|
||||
return ctx
|
||||
}
|
||||
|
||||
static LastCtxOrCapture() {
|
||||
return App.LastCtx != "" ? App.LastCtx : App.CaptureContext()
|
||||
}
|
||||
|
||||
static OpenPicker() {
|
||||
App.LastCtx := App.CaptureContext()
|
||||
ctx := App.LastCtx
|
||||
ctx["profileName"] := App.ResolveProfileName("", ctx)
|
||||
App.Picker.Show(ctx)
|
||||
}
|
||||
|
||||
static OpenManager() {
|
||||
App.LastCtx := App.CaptureContext()
|
||||
App.Manager.Show()
|
||||
}
|
||||
|
||||
static NewPromptInteractive() {
|
||||
ctx := App.CaptureContext()
|
||||
App.LastCtx := ctx
|
||||
ib := InputBox("Titre du nouveau prompt :", "PromptKey — Nouveau prompt")
|
||||
if (ib.Result != "OK" || Trim(ib.Value) = "")
|
||||
return
|
||||
body := ctx["selection"] != "" ? ctx["selection"] : ""
|
||||
item := App.Store.CreateNew(Trim(ib.Value), "Sans catégorie", body)
|
||||
App.RefreshAfterLibraryChange()
|
||||
App.Editor.Show(item)
|
||||
}
|
||||
|
||||
static Replay() {
|
||||
if (App.LastPromptId = "") {
|
||||
TrayTip("Aucun prompt récent à rejouer.", "PromptKey")
|
||||
return
|
||||
}
|
||||
App.LastCtx := App.CaptureContext()
|
||||
App.UsePromptById(App.LastPromptId)
|
||||
}
|
||||
|
||||
static PickerUse(prompt, useDefaults) {
|
||||
opts := Map("useDefaults", useDefaults ? 1 : 0)
|
||||
App.UsePrompt(prompt, App.LastCtxOrCapture(), opts)
|
||||
}
|
||||
|
||||
static UsePromptById(id) {
|
||||
prompt := App.Store.ById(id)
|
||||
if (prompt = "")
|
||||
return
|
||||
App.UsePrompt(prompt, App.LastCtxOrCapture(), Map())
|
||||
}
|
||||
|
||||
; Résolution du profil (§12.3) : prompt > targets > auto-détection > défaut.
|
||||
static ResolveProfileName(prompt, ctx) {
|
||||
if (prompt != "" && prompt.Profile != "")
|
||||
return StrLower(prompt.Profile)
|
||||
if (prompt != "" && prompt.Targets.Length > 0)
|
||||
return StrLower(String(prompt.Targets[1]))
|
||||
proc := ctx.Has("process") ? ctx["process"] : ""
|
||||
title := ctx.Has("title") ? ctx["title"] : ""
|
||||
return App.Settings.DetectProfile(proc, title)
|
||||
}
|
||||
|
||||
static UsePrompt(prompt, ctx, opts) {
|
||||
useDefaults := opts.Has("useDefaults") ? opts["useDefaults"] : 0
|
||||
profileName := App.ResolveProfileName(prompt, ctx)
|
||||
defs := VariableEngine.ManualVars(prompt)
|
||||
lastVals := App.Usage.LastValues(prompt.Id)
|
||||
defaults := Map()
|
||||
for d in defs {
|
||||
defaults[d["name"]] := lastVals.Has(d["name"]) ? lastVals[d["name"]] : d["default"]
|
||||
}
|
||||
if (defs.Length = 0 || useDefaults) {
|
||||
values := Map()
|
||||
for d in defs
|
||||
values[d["name"]] := defaults[d["name"]]
|
||||
App.FinishUse(prompt, ctx, profileName, values, defs)
|
||||
return
|
||||
}
|
||||
App.VarForm.Show(prompt, defs, defaults, (values) => App.OnVarFormDone(prompt, ctx, profileName, values, defs))
|
||||
}
|
||||
|
||||
static OnVarFormDone(prompt, ctx, profileName, values, defs) {
|
||||
if (values = "") {
|
||||
; Annulation : rendre le focus à l'application d'origine, sans rien injecter (§10.4).
|
||||
if (ctx.Has("hwnd") && ctx["hwnd"] && WinExist("ahk_id " ctx["hwnd"]))
|
||||
try WinActivate("ahk_id " ctx["hwnd"])
|
||||
return
|
||||
}
|
||||
App.FinishUse(prompt, ctx, profileName, values, defs)
|
||||
}
|
||||
|
||||
static FinishUse(prompt, ctx, profileName, values, defs) {
|
||||
resolved := VariableEngine.Resolve(prompt.Body, values, ctx)
|
||||
if (resolved["unresolved"].Length > 0)
|
||||
TrayTip("Variables non résolues : " Join(resolved["unresolved"], ", "), "PromptKey")
|
||||
profile := App.Settings.GetProfile(profileName)
|
||||
finalText := VariableEngine.ApplyProfile(resolved["text"], profile)
|
||||
sendAfter := prompt.SendAfter != "" ? prompt.SendAfter : profile["sendAfter"]
|
||||
method := profile["methode"] != "" ? profile["methode"] : App.Settings.Method
|
||||
; Statistiques : exclure les variables de type password (§15).
|
||||
statsValues := Map()
|
||||
pwNames := Map()
|
||||
for d in defs {
|
||||
if (d["type"] = "password")
|
||||
pwNames[d["name"]] := true
|
||||
}
|
||||
for name, val in values {
|
||||
if !pwNames.Has(name)
|
||||
statsValues[name] := val
|
||||
}
|
||||
App.Usage.RecordUse(prompt.Id, statsValues)
|
||||
App.LastPromptId := prompt.Id
|
||||
App.InjectorObj.Deliver(finalText, Map(
|
||||
"hwnd", ctx.Has("hwnd") ? ctx["hwnd"] : 0,
|
||||
"method", method,
|
||||
"sendAfter", sendAfter,
|
||||
"sensitive", prompt.Sensitive,
|
||||
"cursorFromEnd", resolved["cursorFromEnd"]))
|
||||
}
|
||||
}
|
||||
|
||||
App.Init()
|
||||
Reference in New Issue
Block a user