#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 : \.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()