""" trilium_context.py — Gestion opérationnelle du contexte LLM via Trilium Usage : python trilium_context.py new-conversation --projet SlidingAutomation --llm "Claude Sonnet" --titre "Session render engine" python trilium_context.py close-session --note-id --summary "..." python trilium_context.py generate-context --projet SlidingAutomation --llm-cible "Le Chat Large" [--note-id ] python trilium_context.py list-backlog --projet SlidingAutomation python trilium_context.py add-decision --projet SlidingAutomation --enonce "..." --justification "..." python trilium_context.py add-history --projet SlidingAutomation --type "Test effectué" --enonce "..." --detail "..." python trilium_context.py list-projects python trilium_context.py add-skill --titre "Mon skill" --fichier skill.md --portee universel python trilium_context.py add-skill --titre "Mon skill" --fichier skill.md --portee projet --projet SlidingAutomation python trilium_context.py list-skills [--projet SlidingAutomation] [--portee universel] """ import argparse import json import os import sys from datetime import datetime from trilium_api import (check_api, create_note, get_note_id, get_note, get_note_content, update_note_content, search_by_label, set_label, get_label_value, find_note_by_title) IDS_FILE = os.path.join(os.path.dirname(__file__), "trilium_ids.json") GREEN, YELLOW, RED, BOLD, RESET = "\033[32m", "\033[33m", "\033[31m", "\033[1m", "\033[0m" def ok(m): print(f" {GREEN}✓{RESET} {m}") def warn(m): print(f" {YELLOW}⚠{RESET} {m}") def err(m): print(f" {RED}✗{RESET} {m}") def head(m): print(f"\n{BOLD}{m}{RESET}") def load_ids() -> dict: if not os.path.exists(IDS_FILE): raise SystemExit("❌ trilium_ids.json introuvable. Lance d'abord : python trilium_init.py") with open(IDS_FILE) as f: return json.load(f) def estimate_tokens(text: str) -> int: return max(1, int(len(text) / 4)) # --------------------------------------------------------------------------- # Commandes # --------------------------------------------------------------------------- def cmd_new_conversation(args): ids = load_ids() date = datetime.now().strftime("%Y-%m-%d %H:%M") titre = f"[{args.llm}] {date} — {args.titre}" content = f"""

{titre}

Projet{args.projet}
LLM{args.llm}
Date{date}
Synthèse de clôtureÀ remplir en fin de session
""" result = create_note(ids["Conversations"], titre, content) note_id = result["note"]["noteId"] set_label(note_id, "type", "conversation") set_label(note_id, "projet", args.projet) set_label(note_id, "llm", args.llm) set_label(note_id, "statut", "en-cours") set_label(note_id, "date", date) ok(f"Conversation créée : {titre}") print(f"\n Note l'ID pour close-session : {BOLD}{note_id}{RESET}\n") return note_id def cmd_close_session(args): date = datetime.now().strftime("%Y-%m-%d %H:%M") content = get_note_content(args.note_id) # Remplace le placeholder de synthèse if "À remplir en fin de session" in content: content = content.replace( "À remplir en fin de session", args.summary.replace("<", "<").replace(">", ">") ) else: content += f"\n

Synthèse de clôture ({date})

{args.summary}

" update_note_content(args.note_id, content) set_label(args.note_id, "statut", "clos") set_label(args.note_id, "syntheseCloture", args.summary[:200]) ok(f"Session clôturée (ID: {args.note_id})") def cmd_generate_context(args): ids = load_ids() date = datetime.now().strftime("%d/%m/%Y") # Récupérer les données du projet projets = search_by_label("projet", args.projet) decisions = search_by_label("type", "decision") decisions = [d for d in decisions if get_label_value(d["noteId"], "projet") == args.projet and get_label_value(d["noteId"], "statut") == "active"] historique = search_by_label("type", "historiqueItem") historique = [h for h in historique if get_label_value(h["noteId"], "projet") == args.projet and get_label_value(h["noteId"], "encoreValide") == "true"] backlog = search_by_label("type", "backlogItem") backlog = [b for b in backlog if get_label_value(b["noteId"], "projet") == args.projet and get_label_value(b["noteId"], "statut") not in ("fait", "abandonné")] glossaire = search_by_label("type", "termeGlossaire") glossaire = [g for g in glossaire if get_label_value(g["noteId"], "projet") == args.projet] # Synthèse de la session précédente last_summary = "" if args.note_id: last_summary = get_label_value(args.note_id, "syntheseCloture") or "" # Construire le briefing lines = [ f"# REPRISE DE CONTEXTE — {args.projet} — v{args.version} — {date}", f"**LLM cible : {args.llm_cible}**", "", ] if projets: p = projets[0] content = get_note_content(p["noteId"]) # Extraction texte brut simplifié import re texte = re.sub(r"<[^>]+>", " ", content).strip() lines += ["## 1. Projet", texte[:400], ""] if last_summary: lines += ["## 2. Où on en était", last_summary, ""] if decisions: lines += ["## 3. Décisions actives (ne pas remettre en question)"] for d in decisions: lines.append(f"- {d.get('title', '?')}") lines.append("") if historique: lines += ["## 4. Historique — déjà testé / établi (ne pas refaire)"] for h in historique: type_h = get_label_value(h["noteId"], "typeHistorique") or "" lines.append(f"- [{type_h}] {h.get('title', '?')}") lines.append("") if glossaire: lines += ["## 5. Glossaire projet"] for g in glossaire: lines.append(f"- **{g.get('title','?')}** : {get_label_value(g['noteId'], 'definition') or '(voir note)'}") lines.append("") if backlog: lines += ["## 6. Backlog actif"] prio_ordre = {"haute": 0, "moyenne": 1, "basse": 2} backlog_sorted = sorted(backlog, key=lambda b: prio_ordre.get(get_label_value(b["noteId"], "priorite") or "basse", 99)) for b in backlog_sorted: prio = get_label_value(b["noteId"], "priorite") or "?" statut = get_label_value(b["noteId"], "statut") or "?" lines.append(f"- [{statut}] [{prio}] {b.get('title','?')}") lines.append("") lines += [ "---", "**Confirme la prise en compte du contexte en 3 lignes :**", "(a) L'objectif du projet selon ta compréhension", "(b) La prochaine action concrète", "(c) Une incertitude ou question que tu identifies", ] briefing = "\n".join(lines) tokens = estimate_tokens(briefing) titre = f"Reprise {args.projet} v{args.version} — {date}" # Créer la note Contexte Reprise dans Trilium ids = load_ids() result = create_note(ids["ContextesReprise"], titre, briefing.replace("\n", "
")) ctx_id = result["note"]["noteId"] set_label(ctx_id, "type", "contexteReprise") set_label(ctx_id, "projet", args.projet) set_label(ctx_id, "llmCible", args.llm_cible) set_label(ctx_id, "version", str(args.version)) set_label(ctx_id, "tokens", str(tokens)) if args.note_id: set_label(ctx_id, "conversationSource", args.note_id) head("CONTEXTE DE REPRISE GÉNÉRÉ") ok(f"Note Trilium : {titre} (ID: {ctx_id})") ok(f"Tokens estimés : ~{tokens:,}") print("\n" + "═"*60) print("\n BRIEFING À COPIER-COLLER :\n") print(briefing) print("\n" + "═"*60) def cmd_list_backlog(args): backlog = search_by_label("type", "backlogItem") items = [b for b in backlog if get_label_value(b["noteId"], "projet") == args.projet and get_label_value(b["noteId"], "statut") not in ("fait", "abandonné")] if not items: ok("Backlog vide (ou tout est fait).") return head(f"BACKLOG — {args.projet} ({len(items)} items actifs)") emoji = {"haute": "", "moyenne": "", "basse": ""} prio_ordre = {"haute": 0, "moyenne": 1, "basse": 2} for b in sorted(items, key=lambda x: prio_ordre.get( get_label_value(x["noteId"], "priorite") or "basse", 99)): prio = get_label_value(b["noteId"], "priorite") or "?" statut = get_label_value(b["noteId"], "statut") or "?" e = emoji.get(prio, "⚪") print(f" {e} [{statut}] {b.get('title','?')} [id: {b['noteId']}]") def cmd_add_decision(args): ids = load_ids() date = datetime.now().strftime("%Y-%m-%d") result = create_note( ids["Decisions"], args.enonce, f"

Justification : {args.justification}

Date : {date}

" ) nid = result["note"]["noteId"] set_label(nid, "type", "decision") set_label(nid, "projet", args.projet) set_label(nid, "statut", "active") set_label(nid, "llm", args.llm or "") ok(f"Décision créée : {args.enonce[:60]} (ID: {nid})") def cmd_add_history(args): ids = load_ids() date = datetime.now().strftime("%Y-%m-%d") result = create_note( ids["Historique"], args.enonce, f"

Type : {args.type_h}

" f"

Détail : {args.detail or '—'}

" f"

Date : {date}

" ) nid = result["note"]["noteId"] set_label(nid, "type", "historiqueItem") set_label(nid, "projet", args.projet) set_label(nid, "typeHistorique", args.type_h) set_label(nid, "encoreValide", "true") ok(f"Historique créé : {args.enonce[:60]} (ID: {nid})") def cmd_add_backlog(args): ids = load_ids() result = create_note(ids["Backlog"], args.titre) nid = get_note_id(result) set_label(nid, "type", "backlogItem") set_label(nid, "projet", args.projet) set_label(nid, "priorite", args.priorite) set_label(nid, "statut", "a faire") ok(f"Backlog item cree : {args.titre[:60]} (ID: {nid})") def cmd_list_projects(args): projets = search_by_label("type", "projet") head(f"PROJETS ({len(projets)} trouvés)") for p in projets: statut = get_label_value(p["noteId"], "statut") or "?" print(f" [{statut}] {p.get('title','?')} [id: {p['noteId']}]") def cmd_add_skill(args): if args.portee != "universel" and not args.projet: err("--projet requis sauf si --portee universel") sys.exit(1) ids = load_ids() existant = find_note_by_title(args.titre, ids["Skills"]) if existant: err(f"Un skill '{args.titre}' existe déjà (ID: {existant})") sys.exit(1) with open(args.fichier, encoding="utf-8") as f: contenu = f.read() safe = contenu.replace("<", "<").replace(">", ">") result = create_note(ids["Skills"], args.titre, f"
{safe}
") nid = get_note_id(result) set_label(nid, "type", "skill") set_label(nid, "portee", args.portee) if args.projet: set_label(nid, "projet", args.projet) ok(f"Skill créé : {args.titre} (ID: {nid})") def cmd_list_skills(args): skills = search_by_label("type", "skill") if args.projet: skills = [s for s in skills if get_label_value(s["noteId"], "projet") == args.projet or get_label_value(s["noteId"], "portee") == "universel"] if args.portee: skills = [s for s in skills if get_label_value(s["noteId"], "portee") == args.portee] if not skills: ok("Aucun skill trouvé pour ces filtres.") return head(f"SKILLS ({len(skills)} trouvés)") for s in skills: portee = get_label_value(s["noteId"], "portee") or "?" projet = get_label_value(s["noteId"], "projet") or "—" print(f" [{portee}] [{projet}] {s.get('title','?')} [id: {s['noteId']}]") # --------------------------------------------------------------------------- # Main # --------------------------------------------------------------------------- def main(): parser = argparse.ArgumentParser( description="Gestion du contexte LLM multi-modèles via Trilium", formatter_class=argparse.RawDescriptionHelpFormatter) sub = parser.add_subparsers(dest="command", required=True) p1 = sub.add_parser("new-conversation") p1.add_argument("--projet", required=True) p1.add_argument("--llm", required=True) p1.add_argument("--titre", required=True) p2 = sub.add_parser("close-session") p2.add_argument("--note-id", required=True) p2.add_argument("--summary", required=True) p3 = sub.add_parser("generate-context") p3.add_argument("--projet", required=True) p3.add_argument("--llm-cible", required=True) p3.add_argument("--note-id", default="") p3.add_argument("--version", type=int, default=1) p4 = sub.add_parser("list-backlog") p4.add_argument("--projet", required=True) p5 = sub.add_parser("add-decision") p5.add_argument("--projet", required=True) p5.add_argument("--enonce", required=True) p5.add_argument("--justification", default="") p5.add_argument("--llm", default="") p6 = sub.add_parser("add-history") p6.add_argument("--projet", required=True) p6.add_argument("--type", required=True, dest="type_h", choices=["Fait etabli", "Test effectue", "Hypothese invalidee", "Contrainte decouverte"]) p6.add_argument("--enonce", required=True) p6.add_argument("--detail", default="") p_ab = sub.add_parser("add-backlog", help="Ajoute un item au backlog") p_ab.add_argument("--projet", required=True) p_ab.add_argument("--titre", required=True) p_ab.add_argument("--priorite", default="moyenne", choices=["haute", "moyenne", "basse"]) sub.add_parser("list-projects") p_as = sub.add_parser("add-skill", help="Enregistre un skill depuis un fichier") p_as.add_argument("--titre", required=True) p_as.add_argument("--fichier", required=True, help="Chemin du fichier .md contenant le skill") p_as.add_argument("--portee", default="projet", help="universel | reference-technique | projet | ... (defaut: projet)") p_as.add_argument("--projet", default="", help="Requis sauf si --portee universel") p_ls = sub.add_parser("list-skills", help="Liste les skills enregistres") p_ls.add_argument("--projet", default="") p_ls.add_argument("--portee", default="") args = parser.parse_args() check_api() { "new-conversation": cmd_new_conversation, "close-session": cmd_close_session, "generate-context": cmd_generate_context, "list-backlog": cmd_list_backlog, "add-decision": cmd_add_decision, "add-history": cmd_add_history, "add-backlog": cmd_add_backlog, "list-projects": cmd_list_projects, "add-skill": cmd_add_skill, "list-skills": cmd_list_skills, }[args.command](args) if __name__ == "__main__": main()