klapcontext 0.3.3__tar.gz → 0.5.0__tar.gz

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 (49) hide show
  1. {klapcontext-0.3.3/src/klapcontext.egg-info → klapcontext-0.5.0}/PKG-INFO +31 -4
  2. {klapcontext-0.3.3 → klapcontext-0.5.0}/README.md +30 -3
  3. {klapcontext-0.3.3 → klapcontext-0.5.0}/src/klapcontext/__init__.py +1 -1
  4. {klapcontext-0.3.3 → klapcontext-0.5.0}/src/klapcontext/agent/compiler.py +5 -2
  5. {klapcontext-0.3.3 → klapcontext-0.5.0}/src/klapcontext/agent_context.py +6 -0
  6. klapcontext-0.5.0/src/klapcontext/agent_setup.py +264 -0
  7. klapcontext-0.5.0/src/klapcontext/cli.py +190 -0
  8. {klapcontext-0.3.3 → klapcontext-0.5.0}/src/klapcontext/context_builder.py +8 -2
  9. {klapcontext-0.3.3 → klapcontext-0.5.0}/src/klapcontext/detector.py +26 -16
  10. klapcontext-0.5.0/src/klapcontext/exploration.py +118 -0
  11. {klapcontext-0.3.3 → klapcontext-0.5.0}/src/klapcontext/frameworks/laravel.py +4 -1
  12. {klapcontext-0.3.3 → klapcontext-0.5.0}/src/klapcontext/portal.py +36 -1
  13. {klapcontext-0.3.3 → klapcontext-0.5.0/src/klapcontext.egg-info}/PKG-INFO +31 -4
  14. {klapcontext-0.3.3 → klapcontext-0.5.0}/src/klapcontext.egg-info/SOURCES.txt +4 -0
  15. klapcontext-0.5.0/tests/test_agent_setup.py +122 -0
  16. klapcontext-0.5.0/tests/test_exploration.py +68 -0
  17. {klapcontext-0.3.3 → klapcontext-0.5.0}/tests/test_klapcontext.py +1 -0
  18. klapcontext-0.3.3/src/klapcontext/cli.py +0 -82
  19. {klapcontext-0.3.3 → klapcontext-0.5.0}/LICENSE +0 -0
  20. {klapcontext-0.3.3 → klapcontext-0.5.0}/pyproject.toml +0 -0
  21. {klapcontext-0.3.3 → klapcontext-0.5.0}/setup.cfg +0 -0
  22. {klapcontext-0.3.3 → klapcontext-0.5.0}/src/klapcontext/agent/__init__.py +0 -0
  23. {klapcontext-0.3.3 → klapcontext-0.5.0}/src/klapcontext/agent/capabilities.py +0 -0
  24. {klapcontext-0.3.3 → klapcontext-0.5.0}/src/klapcontext/agent/impact.py +0 -0
  25. {klapcontext-0.3.3 → klapcontext-0.5.0}/src/klapcontext/agent/intents.py +0 -0
  26. {klapcontext-0.3.3 → klapcontext-0.5.0}/src/klapcontext/agent/models.py +0 -0
  27. {klapcontext-0.3.3 → klapcontext-0.5.0}/src/klapcontext/agent/planner.py +0 -0
  28. {klapcontext-0.3.3 → klapcontext-0.5.0}/src/klapcontext/agent/verification.py +0 -0
  29. {klapcontext-0.3.3 → klapcontext-0.5.0}/src/klapcontext/agent/workflows.py +0 -0
  30. {klapcontext-0.3.3 → klapcontext-0.5.0}/src/klapcontext/evidence.py +0 -0
  31. {klapcontext-0.3.3 → klapcontext-0.5.0}/src/klapcontext/frameworks/__init__.py +0 -0
  32. {klapcontext-0.3.3 → klapcontext-0.5.0}/src/klapcontext/frameworks/base.py +0 -0
  33. {klapcontext-0.3.3 → klapcontext-0.5.0}/src/klapcontext/frameworks/fastapi.py +0 -0
  34. {klapcontext-0.3.3 → klapcontext-0.5.0}/src/klapcontext/frameworks/generic_php.py +0 -0
  35. {klapcontext-0.3.3 → klapcontext-0.5.0}/src/klapcontext/frameworks/node.py +0 -0
  36. {klapcontext-0.3.3 → klapcontext-0.5.0}/src/klapcontext/git.py +0 -0
  37. {klapcontext-0.3.3 → klapcontext-0.5.0}/src/klapcontext/graphify.py +0 -0
  38. {klapcontext-0.3.3 → klapcontext-0.5.0}/src/klapcontext/providers/__init__.py +0 -0
  39. {klapcontext-0.3.3 → klapcontext-0.5.0}/src/klapcontext/providers/base.py +0 -0
  40. {klapcontext-0.3.3 → klapcontext-0.5.0}/src/klapcontext/providers/code_intelligence.py +0 -0
  41. {klapcontext-0.3.3 → klapcontext-0.5.0}/src/klapcontext/providers/graphify.py +0 -0
  42. {klapcontext-0.3.3 → klapcontext-0.5.0}/src/klapcontext/providers/javascript_intelligence.py +0 -0
  43. {klapcontext-0.3.3 → klapcontext-0.5.0}/src/klapcontext/providers/python_intelligence.py +0 -0
  44. {klapcontext-0.3.3 → klapcontext-0.5.0}/src/klapcontext/semantic.py +0 -0
  45. {klapcontext-0.3.3 → klapcontext-0.5.0}/src/klapcontext/system.py +0 -0
  46. {klapcontext-0.3.3 → klapcontext-0.5.0}/src/klapcontext.egg-info/dependency_links.txt +0 -0
  47. {klapcontext-0.3.3 → klapcontext-0.5.0}/src/klapcontext.egg-info/entry_points.txt +0 -0
  48. {klapcontext-0.3.3 → klapcontext-0.5.0}/src/klapcontext.egg-info/requires.txt +0 -0
  49. {klapcontext-0.3.3 → klapcontext-0.5.0}/src/klapcontext.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: klapcontext
3
- Version: 0.3.3
3
+ Version: 0.5.0
4
4
  Summary: Make your repository understandable to humans and AI.
5
5
  Author: Willy Cordon
6
6
  License-Expression: Apache-2.0
@@ -78,9 +78,29 @@ klap init
78
78
  klap open
79
79
  ```
80
80
 
81
- `klap init` genera el análisis Graphify y escribe artefactos locales bajo
82
- `.klap/`. KlapContext agrega esa carpeta a `.git/info/exclude`; nunca modifica
83
- tu `.gitignore`.
81
+ `klap init` genera el análisis Graphify, escribe artefactos locales bajo
82
+ `.klap/` y prepara un `AGENTS.md` privado para que los agentes descubran el
83
+ contexto progresivamente. Ambos se agregan a `.git/info/exclude`; KlapContext
84
+ nunca modifica tu `.gitignore`.
85
+
86
+ Para compartir las instrucciones de onboarding con el equipo:
87
+
88
+ ```bash
89
+ klap init --shared
90
+ ```
91
+
92
+ El modo compartido genera un `AGENTS.md` versionable, sin rutas personales ni
93
+ secretos. Si el archivo ya existe, KlapContext preserva su contenido y sólo
94
+ administra el bloque entre `KLAPCONTEXT:START` y `KLAPCONTEXT:END`. Un archivo
95
+ versionado nunca se modifica silenciosamente en modo privado.
96
+
97
+ El portal incluye un árbol navegable de archivos y símbolos extraídos del grafo,
98
+ recorridos de llamadas y cambios recientes de Git. El contexto del agente y
99
+ `klap context` usan las mismas rutas y símbolos. El indicador de cobertura cuenta
100
+ archivos de código con al menos un nodo en Graphify: **no mide exhaustividad de
101
+ funciones ni garantiza que se hayan resuelto rutas dinámicas**. Los recorridos
102
+ son caminos del grafo, no afirmaciones de que una ruta HTTP los ejecuta; sus
103
+ ramas pueden requerir inspección del código original.
84
104
 
85
105
  ## Comandos
86
106
 
@@ -92,6 +112,7 @@ tu `.gitignore`.
92
112
  | `klap open [ruta]` | Abre el portal humano estático. |
93
113
  | `klap agent [ruta]` | Muestra la configuración MCP local de Graphify. |
94
114
  | `klap context "tarea" [ruta]` | Compila un contexto focalizado reutilizando `.klap/context.json` si existe. |
115
+ | `klap doctor --agent [ruta]` | Comprueba AGENTS.md, freshness, Graphify y configuración MCP. |
95
116
  | `klap --version` | Muestra la versión instalada. |
96
117
 
97
118
  ## Salidas
@@ -102,9 +123,15 @@ tu `.gitignore`.
102
123
  ├── agent-context.md # briefing para agentes de IA
103
124
  ├── index.html # portal humano, sin servidor
104
125
  ├── state.json # metadatos de freshness
126
+ ├── mcp/ # configuración local reutilizable para clientes MCP
105
127
  └── graphify/ # grafo, informe y visualizaciones de Graphify
106
128
  ```
107
129
 
130
+ `klap agent` genera ejemplos locales para Codex, Claude Code, Cursor y clientes
131
+ MCP genéricos. No instala agentes ni modifica configuraciones globales. Usá
132
+ `klap doctor --agent --probe-mcp` para comprobar que Graphify MCP puede iniciar;
133
+ eso no equivale a verificar una conexión real de un cliente.
134
+
108
135
  Las afirmaciones incluyen evidencia y estados `CONFIRMED`, `INFERRED` o
109
136
  `UNKNOWN`. Si KlapContext no puede determinar algo con confianza, lo expone en
110
137
  lugar de inventarlo.
@@ -44,9 +44,29 @@ klap init
44
44
  klap open
45
45
  ```
46
46
 
47
- `klap init` genera el análisis Graphify y escribe artefactos locales bajo
48
- `.klap/`. KlapContext agrega esa carpeta a `.git/info/exclude`; nunca modifica
49
- tu `.gitignore`.
47
+ `klap init` genera el análisis Graphify, escribe artefactos locales bajo
48
+ `.klap/` y prepara un `AGENTS.md` privado para que los agentes descubran el
49
+ contexto progresivamente. Ambos se agregan a `.git/info/exclude`; KlapContext
50
+ nunca modifica tu `.gitignore`.
51
+
52
+ Para compartir las instrucciones de onboarding con el equipo:
53
+
54
+ ```bash
55
+ klap init --shared
56
+ ```
57
+
58
+ El modo compartido genera un `AGENTS.md` versionable, sin rutas personales ni
59
+ secretos. Si el archivo ya existe, KlapContext preserva su contenido y sólo
60
+ administra el bloque entre `KLAPCONTEXT:START` y `KLAPCONTEXT:END`. Un archivo
61
+ versionado nunca se modifica silenciosamente en modo privado.
62
+
63
+ El portal incluye un árbol navegable de archivos y símbolos extraídos del grafo,
64
+ recorridos de llamadas y cambios recientes de Git. El contexto del agente y
65
+ `klap context` usan las mismas rutas y símbolos. El indicador de cobertura cuenta
66
+ archivos de código con al menos un nodo en Graphify: **no mide exhaustividad de
67
+ funciones ni garantiza que se hayan resuelto rutas dinámicas**. Los recorridos
68
+ son caminos del grafo, no afirmaciones de que una ruta HTTP los ejecuta; sus
69
+ ramas pueden requerir inspección del código original.
50
70
 
51
71
  ## Comandos
52
72
 
@@ -58,6 +78,7 @@ tu `.gitignore`.
58
78
  | `klap open [ruta]` | Abre el portal humano estático. |
59
79
  | `klap agent [ruta]` | Muestra la configuración MCP local de Graphify. |
60
80
  | `klap context "tarea" [ruta]` | Compila un contexto focalizado reutilizando `.klap/context.json` si existe. |
81
+ | `klap doctor --agent [ruta]` | Comprueba AGENTS.md, freshness, Graphify y configuración MCP. |
61
82
  | `klap --version` | Muestra la versión instalada. |
62
83
 
63
84
  ## Salidas
@@ -68,9 +89,15 @@ tu `.gitignore`.
68
89
  ├── agent-context.md # briefing para agentes de IA
69
90
  ├── index.html # portal humano, sin servidor
70
91
  ├── state.json # metadatos de freshness
92
+ ├── mcp/ # configuración local reutilizable para clientes MCP
71
93
  └── graphify/ # grafo, informe y visualizaciones de Graphify
72
94
  ```
73
95
 
96
+ `klap agent` genera ejemplos locales para Codex, Claude Code, Cursor y clientes
97
+ MCP genéricos. No instala agentes ni modifica configuraciones globales. Usá
98
+ `klap doctor --agent --probe-mcp` para comprobar que Graphify MCP puede iniciar;
99
+ eso no equivale a verificar una conexión real de un cliente.
100
+
74
101
  Las afirmaciones incluyen evidencia y estados `CONFIRMED`, `INFERRED` o
75
102
  `UNKNOWN`. Si KlapContext no puede determinar algo con confianza, lo expone en
76
103
  lugar de inventarlo.
@@ -1,3 +1,3 @@
1
1
  """KlapContext local engineering-context generator."""
2
2
 
3
- __version__ = "0.3.3"
3
+ __version__ = "0.5.0"
@@ -34,8 +34,11 @@ def compile_context(root: Path, query: str, *, detail: str = "standard", max_tok
34
34
  context = build(root, {"nodes": []})
35
35
  model = context["system_model"]; limit = min(DETAIL_LIMITS[detail], max(1, max_tokens // 350)); terms = _terms(query, plan.area)
36
36
  entries = _rank(model["entry_points"], terms, limit); flows = _rank(model["main_flows"], terms, limit); files = _rank(model["important_files"], terms, limit)
37
- read_first = ([{"path": item.get("file"), "reason": "Punto de entrada relacionado", "relevance": item["relevance"]} for item in entries if item.get("file")] + files)[:limit]
38
- result = {"intent": plan.intent, "confidence": plan.confidence, "query": query, "area": plan.area, "detail": detail, "max_tokens": max_tokens, "entry_points": entries, "flows": flows, "symbols": _rank(model["semantic_model"]["components"], terms, limit), "files": files, "dependencies": _rank(model["dependencies"], terms, limit), "tests": _rank(context["tests"], terms, limit), "read_first": read_first, "evidence": [evidence for group in entries + flows for evidence in group.get("evidence", [])][:limit], "uncertainties": model["unknowns"], "strategy": plan.strategy}
37
+ exploration = model.get("exploration", {})
38
+ graph_files = _rank(exploration.get("files", []), terms, limit)
39
+ graph_paths = _rank(exploration.get("call_paths", []), terms, limit)
40
+ read_first = ([{"path": item.get("file"), "reason": "Punto de entrada relacionado", "relevance": item["relevance"]} for item in entries if item.get("file")] + [{"path": item["path"], "reason": "Archivo o símbolo relacionado en el grafo", "relevance": item["relevance"]} for item in graph_files] + files)[:limit]
41
+ result = {"intent": plan.intent, "confidence": plan.confidence, "query": query, "area": plan.area, "detail": detail, "max_tokens": max_tokens, "entry_points": entries, "flows": flows, "graph_call_paths": graph_paths, "symbols": _rank(model["semantic_model"]["components"], terms, limit), "files": files + graph_files, "coverage": exploration.get("coverage", {}), "recent_changes": _rank(model.get("recent_changes", []), terms, min(limit, 3)), "dependencies": _rank(model["dependencies"], terms, limit), "tests": _rank(context["tests"], terms, limit), "read_first": read_first, "evidence": [evidence for group in entries + flows for evidence in group.get("evidence", [])][:limit], "uncertainties": model["unknowns"], "strategy": plan.strategy}
39
42
  result["verification"] = reconcile(result["evidence"])
40
43
  result = sanitize(result)
41
44
  result["estimated_tokens"] = min(max_tokens, len(json.dumps(result, ensure_ascii=False)) // 4)
@@ -5,9 +5,15 @@ def items(values, key="name"):
5
5
  return "\n".join(f"- {value.get(key) or value.get('path')}" for value in values) if values else "Sin evidencia disponible."
6
6
  def render(context: dict) -> str:
7
7
  s=context.get("system_model",{}); purpose=s.get("purpose",{}).get("text") or "No fue posible determinar el propósito con confianza."
8
+ exploration=s.get("exploration",{}); coverage=exploration.get("coverage",{})
8
9
  sections=[("Propósito del Proyecto",purpose),("Cómo Funciona el Sistema",s.get("project_story",{}).get("text") or "No fue posible determinarlo con confianza."),("Superficies de Ejecución","\n".join(f"- {x['label']}: {x['count']}" for x in s.get("runtime_surfaces",[])) or "Sin superficies específicas detectadas."),("Rutas",items(s.get("routes",[]))),("Procesos Programados",items(s.get("scheduled_processes",[]))),("Jobs de Cola",items(s.get("queue_jobs",[]))),("Flujos Principales","\n".join(f"- **{x['name']}**: " + " → ".join(step["name"] for step in x["steps"]) for x in s.get("main_flows",[])) or "No se detectaron flujos principales con confianza."),("Dependencias Importantes","\n".join(f"- {x['package']} {x['version']} ({x['scope']})" for x in s.get("dependencies",[])[:12]) or "Sin dependencias específicas detectadas."),("Sistemas Externos",items(s.get("external_systems",[]))),("Almacenes de Datos",items(s.get("datastores",[]))),("Por Dónde Empezar","\n".join(f"- **{x['intent']}**: {', '.join(x['paths'])}" for x in s.get("start_here",[])) or "Sin recomendaciones detectadas."),("Incertidumbres","\n".join(f"- {x}" for x in s.get("unknowns",[])) or "Sin incertidumbres relevantes registradas.")]
9
10
  lines=["# KlapContext — Contexto del Proyecto"]
10
11
  for title,body in sections: lines.extend(["",f"## {title}",body])
12
+ lines.extend(["", "## Cobertura de la exploración", f"- Archivos de código con nodos: {coverage.get('code_files_with_graph_nodes', 0)}/{coverage.get('eligible_code_files', 0)}; estado: {coverage.get('status', 'UNKNOWN')}.", "- Un archivo representado en el grafo no implica que todas sus funciones estén resueltas.", "", "## Estructura y recorridos de llamadas", "- Consulta `.klap/context.json` → `system_model.exploration.files` para localizar archivos y símbolos por ruta."])
13
+ for path in exploration.get("call_paths", [])[:8]:
14
+ lines.append("- " + " → ".join(f"{step['name']} ({step['file']})" for step in path["steps"]) + " [recorrido del grafo; entrada no confirmada]")
15
+ lines.extend(["", "## Cambios recientes en Git"])
16
+ lines.extend(f"- {change['date']} {change['commit'][:10]}: {change['subject']}" for change in s.get("recent_changes", [])[:6])
11
17
  lines.extend(["","## Instrucciones para el Agente","1. Lee este contexto antes de explorar ampliamente el repositorio.","2. Usa Graphify MCP para dependencias y rutas de llamadas.","3. Consulta paths o vecinos relevantes antes de abrir muchos archivos.","4. Lee el código real antes de modificarlo.","5. Trata el contexto inferido como guía, no como verdad absoluta."])
12
18
  text="\n".join(lines)+"\n"; return text+f"\nTokens estimados: {len(text.split()) * 4 // 3}\n"
13
19
  def write(context: dict, output: Path) -> str:
@@ -0,0 +1,264 @@
1
+ """Safe, idempotent onboarding for AGENTS.md-compatible coding agents."""
2
+ from __future__ import annotations
3
+
4
+ import json
5
+ import os
6
+ import subprocess
7
+ import sys
8
+ import tempfile
9
+ from dataclasses import asdict, dataclass
10
+ from pathlib import Path
11
+
12
+ from .git import commit, dirty_files, is_repository, run_git
13
+ from .graphify import command_prefix
14
+
15
+ START = "<!-- KLAPCONTEXT:START -->"
16
+ END = "<!-- KLAPCONTEXT:END -->"
17
+ AGENT_FILE = "AGENTS.md"
18
+
19
+
20
+ @dataclass
21
+ class AgentSetupResult:
22
+ status: str
23
+ path: str = AGENT_FILE
24
+ mode: str = "private"
25
+ created: bool = False
26
+ updated: bool = False
27
+ tracked: bool = False
28
+ mcp_configured: bool = False
29
+ message: str = ""
30
+
31
+ def as_dict(self) -> dict:
32
+ return asdict(self)
33
+
34
+
35
+ def is_tracked(root: Path, relative: str = AGENT_FILE) -> bool:
36
+ return is_repository(root) and run_git(root, "ls-files", "--error-unmatch", relative) is not None
37
+
38
+
39
+ def marker_state(text: str) -> str:
40
+ starts, ends = text.count(START), text.count(END)
41
+ if starts == ends == 0:
42
+ return "absent"
43
+ if starts == ends == 1 and text.index(START) < text.index(END):
44
+ return "valid"
45
+ return "malformed"
46
+
47
+
48
+ def context_freshness(root: Path) -> dict:
49
+ state_file = root / ".klap" / "state.json"
50
+ if not state_file.exists():
51
+ return {"status": "MISSING", "reason": "No existe .klap/state.json"}
52
+ try:
53
+ state = json.loads(state_file.read_text(encoding="utf-8"))
54
+ except (OSError, json.JSONDecodeError):
55
+ return {"status": "INVALID", "reason": "state.json no es válido"}
56
+ current = commit(root)
57
+ ignored = (".klap/", "graphify-out/", AGENT_FILE)
58
+ relevant = [path for path in dirty_files(root) if not path.replace("\\", "/").startswith(ignored)]
59
+ if state.get("git_commit") != current:
60
+ return {"status": "STALE", "reason": "El commit analizado no coincide con HEAD", "changed_files": relevant}
61
+ if relevant:
62
+ return {"status": "STALE", "reason": "Hay cambios relevantes sin confirmar", "changed_files": relevant}
63
+ return {"status": "CURRENT", "reason": "El contexto coincide con el código confirmado", "changed_files": []}
64
+
65
+
66
+ def generate_mcp_configs(root: Path) -> dict:
67
+ """Write local examples only; never mutate global agent configuration."""
68
+ graph = (root / ".klap" / "graphify" / "graph.json").resolve()
69
+ directory = root / ".klap" / "mcp"
70
+ directory.mkdir(parents=True, exist_ok=True)
71
+ prefix = command_prefix()
72
+ available = bool(prefix and graph.exists())
73
+ command = sys.executable
74
+ args = ["-m", "graphify.serve", str(graph)]
75
+ generic = {"mcpServers": {"graphify": {"command": command, "args": args}}}
76
+ (directory / "generic.json").write_text(json.dumps(generic, indent=2) + "\n", encoding="utf-8")
77
+ codex = "[mcp_servers.graphify]\n" + f'command = {json.dumps(command)}\nargs = {json.dumps(args)}\n'
78
+ (directory / "codex.toml").write_text(codex, encoding="utf-8")
79
+ instructions = f"""# Graphify MCP setup
80
+
81
+ Generated local configuration examples. They contain no credentials.
82
+
83
+ - Generic / Claude Code project config: `.klap/mcp/generic.json`
84
+ - Cursor project config: copy the same JSON to `.cursor/mcp.json` only when you explicitly want project configuration.
85
+ - Codex project config snippet: `.klap/mcp/codex.toml`
86
+
87
+ The configured stdio command is:
88
+
89
+ `{command} {' '.join(args)}`
90
+
91
+ Run `klap doctor --agent --probe-mcp` to verify that the server can start. A configuration file alone does not prove a client connection.
92
+ """
93
+ (directory / "README.md").write_text(instructions, encoding="utf-8")
94
+ return {"available": available, "directory": ".klap/mcp", "generic": ".klap/mcp/generic.json", "codex": ".klap/mcp/codex.toml"}
95
+
96
+
97
+ def _overview(context: dict) -> str:
98
+ project = context.get("project", {})
99
+ system = context.get("system_model", {})
100
+ stack = context.get("stack", {})
101
+ purpose = system.get("purpose", {}).get("text")
102
+ languages = ", ".join(stack.get("languages", []))
103
+ frameworks = ", ".join(stack.get("frameworks", []))
104
+ facts = [f"Repository `{project.get('name', 'project')}`"]
105
+ if languages:
106
+ facts.append(f"uses {languages}")
107
+ if frameworks:
108
+ facts.append(f"with {frameworks}")
109
+ summary = " ".join(facts) + "."
110
+ if purpose:
111
+ summary += " " + purpose.strip()
112
+ flows = system.get("main_flows", [])
113
+ if flows:
114
+ summary += f" KlapContext detected {len(flows)} main execution flow(s)."
115
+ return summary
116
+
117
+
118
+ def render_agent_block(context: dict, *, shared: bool) -> str:
119
+ shared_note = "\nIf `.klap/` is missing, run `klap init` before relying on generated context.\n" if shared else ""
120
+ return f"""{START}
121
+
122
+ ## KlapContext — Engineering Context
123
+
124
+ This repository has been analyzed by KlapContext.
125
+
126
+ ### Project overview
127
+
128
+ {_overview(context)}
129
+ {shared_note}
130
+ ### Before starting a task
131
+
132
+ 1. Run `klap status`; if relevant context is stale, run `klap update`.
133
+ 2. Read `.klap/agent-context.md` progressively; do not load the entire graph.
134
+ 3. Use Graphify MCP for dependencies, call paths, callers and impact analysis.
135
+ 4. Identify the relevant execution flow before modifying code.
136
+ 5. Read the actual source code and related tests before making changes.
137
+ 6. Treat inferred relationships as hypotheses until verified.
138
+
139
+ ### Context resources
140
+
141
+ - `.klap/agent-context.md`
142
+ - `.klap/context.json`
143
+ - `.klap/index.html`
144
+ - `.klap/graphify/graph.json`
145
+ - `.klap/mcp/README.md`
146
+
147
+ ### Working rules
148
+
149
+ - Respect existing architecture, conventions and contracts.
150
+ - Prefer focused Graphify queries over broad repository searches.
151
+ - Never modify code based only on generated context; verify the source first.
152
+ - Preserve existing contracts unless the task explicitly requires changing them.
153
+
154
+ {END}"""
155
+
156
+
157
+ class AgentInstructionsManager:
158
+ def __init__(self, root: Path):
159
+ self.root = root.resolve()
160
+ self.path = self.root / AGENT_FILE
161
+
162
+ def apply(self, context: dict, *, shared: bool = False, allow_tracked: bool = False) -> AgentSetupResult:
163
+ mode = "shared" if shared else "private"
164
+ tracked = is_tracked(self.root)
165
+ if tracked and not (shared or allow_tracked):
166
+ return AgentSetupResult("authorization_required", mode=mode, tracked=True, message="AGENTS.md está versionado; se requiere autorización explícita.")
167
+ existing = self.path.read_text(encoding="utf-8") if self.path.exists() else ""
168
+ state = marker_state(existing)
169
+ if state == "malformed":
170
+ return AgentSetupResult("conflict", mode=mode, tracked=tracked, message="Marcadores KLAPCONTEXT incompletos o duplicados; no se modificó AGENTS.md.")
171
+ block = render_agent_block(context, shared=shared)
172
+ if state == "valid":
173
+ start = existing.index(START)
174
+ end = existing.index(END, start) + len(END)
175
+ updated = existing[:start] + block + existing[end:]
176
+ elif existing.strip():
177
+ updated = existing.rstrip() + "\n\n" + block + "\n"
178
+ else:
179
+ updated = block + "\n"
180
+ if updated == existing:
181
+ if shared:
182
+ self._include_shared_agents()
183
+ return AgentSetupResult("unchanged", mode=mode, tracked=tracked, mcp_configured=True, message="AGENTS.md ya está actualizado.")
184
+ self._atomic_write(updated, existing if self.path.exists() else None)
185
+ if shared:
186
+ self._include_shared_agents()
187
+ elif not tracked and is_repository(self.root):
188
+ self._exclude_private_agents()
189
+ return AgentSetupResult("configured", mode=mode, created=not bool(existing), updated=bool(existing), tracked=tracked, mcp_configured=True, message="AGENTS.md configurado de forma segura.")
190
+
191
+ def _atomic_write(self, content: str, previous: str | None) -> None:
192
+ if previous is not None:
193
+ backup = self.root / ".klap" / "backups" / AGENT_FILE
194
+ backup.parent.mkdir(parents=True, exist_ok=True)
195
+ backup.write_text(previous, encoding="utf-8")
196
+ fd, name = tempfile.mkstemp(prefix=".AGENTS.", suffix=".tmp", dir=self.root)
197
+ try:
198
+ with os.fdopen(fd, "w", encoding="utf-8", newline="\n") as handle:
199
+ handle.write(content)
200
+ handle.flush()
201
+ os.fsync(handle.fileno())
202
+ os.replace(name, self.path)
203
+ finally:
204
+ if os.path.exists(name):
205
+ os.unlink(name)
206
+
207
+ def _exclude_private_agents(self) -> None:
208
+ git_dir = run_git(self.root, "rev-parse", "--git-dir")
209
+ if not git_dir:
210
+ return
211
+ exclude = (self.root / git_dir / "info" / "exclude").resolve()
212
+ existing = exclude.read_text(encoding="utf-8") if exclude.exists() else ""
213
+ if AGENT_FILE not in {line.strip() for line in existing.splitlines()}:
214
+ exclude.write_text(existing.rstrip() + ("\n" if existing.strip() else "") + AGENT_FILE + "\n", encoding="utf-8")
215
+
216
+ def _include_shared_agents(self) -> None:
217
+ git_dir = run_git(self.root, "rev-parse", "--git-dir")
218
+ if not git_dir:
219
+ return
220
+ exclude = (self.root / git_dir / "info" / "exclude").resolve()
221
+ if not exclude.exists():
222
+ return
223
+ lines = exclude.read_text(encoding="utf-8").splitlines()
224
+ filtered = [line for line in lines if line.strip() != AGENT_FILE]
225
+ if filtered != lines:
226
+ exclude.write_text("\n".join(filtered).rstrip() + ("\n" if filtered else ""), encoding="utf-8")
227
+
228
+ def validate(self) -> dict:
229
+ if not self.path.exists():
230
+ return {"status": "missing", "valid": False, "message": "AGENTS.md no existe"}
231
+ text = self.path.read_text(encoding="utf-8")
232
+ state = marker_state(text)
233
+ paths = [".klap/agent-context.md", ".klap/context.json", ".klap/index.html", ".klap/graphify/graph.json"]
234
+ missing = [path for path in paths if not (self.root / path).exists()]
235
+ return {"status": state, "valid": state == "valid" and not missing, "missing_paths": missing, "message": "Configuración válida" if state == "valid" and not missing else "Configuración incompleta"}
236
+
237
+
238
+ def probe_graphify_mcp(root: Path, timeout: float = 2.0) -> dict:
239
+ graph = root / ".klap" / "graphify" / "graph.json"
240
+ prefix = command_prefix()
241
+ if not prefix or not graph.exists():
242
+ return {"startable": False, "connection_verified": False, "reason": "Graphify o graph.json no disponible"}
243
+ command = [sys.executable, "-m", "graphify.serve", str(graph)]
244
+ process = subprocess.Popen(command, cwd=root, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
245
+ try:
246
+ process.wait(timeout=timeout)
247
+ return {"startable": process.returncode == 0, "connection_verified": False, "reason": "El proceso terminó sin una sesión MCP"}
248
+ except subprocess.TimeoutExpired:
249
+ process.terminate()
250
+ try:
251
+ process.wait(timeout=1)
252
+ except subprocess.TimeoutExpired:
253
+ process.kill()
254
+ return {"startable": True, "connection_verified": False, "reason": "El servidor inició; no se verificó conexión de un cliente"}
255
+
256
+
257
+ def doctor_agent(root: Path, *, probe_mcp: bool = False) -> dict:
258
+ manager = AgentInstructionsManager(root)
259
+ agents = manager.validate()
260
+ freshness = context_freshness(root)
261
+ graph = root / ".klap" / "graphify" / "graph.json"
262
+ mcp_files = [root / ".klap" / "mcp" / "generic.json", root / ".klap" / "mcp" / "codex.toml"]
263
+ mcp_probe = probe_graphify_mcp(root) if probe_mcp else {"startable": bool(command_prefix() and graph.exists()), "connection_verified": False, "reason": "Use --probe-mcp para comprobar el arranque"}
264
+ return {"agents": agents, "context": {"available": (root / ".klap" / "agent-context.md").exists() and (root / ".klap" / "context.json").exists()}, "freshness": freshness, "graph": {"available": graph.exists()}, "mcp": {"configured": all(path.exists() for path in mcp_files), **mcp_probe}}
@@ -0,0 +1,190 @@
1
+ from __future__ import annotations
2
+ import argparse, json, os, sys, webbrowser
3
+ from datetime import datetime, timezone
4
+ from pathlib import Path
5
+
6
+ from . import __version__
7
+ from .agent_context import write as write_agent
8
+ from .agent_setup import AgentInstructionsManager, context_freshness, doctor_agent, generate_mcp_configs, is_tracked
9
+ from .context_builder import build
10
+ from .git import commit, dirty_files, exclude_klap, is_repository
11
+ from .graphify import GraphifyError
12
+ from .portal import write as write_portal
13
+ from .providers import default_provider
14
+
15
+
16
+ def root_path(value: str | None) -> Path:
17
+ return Path(value or os.getcwd()).resolve()
18
+
19
+
20
+ def generate_all(root: Path, update: bool = False, *, shared: bool = False, allow_tracked_agents: bool = False) -> dict:
21
+ if not is_repository(root):
22
+ raise RuntimeError(f"Not a Git repository: {root}")
23
+ exclude_klap(root)
24
+ klap = root / ".klap"
25
+ klap.mkdir(exist_ok=True)
26
+ provider = default_provider()
27
+ graph_path = provider.generate(root, update)
28
+ files = provider.copy_outputs(root, klap / "graphify")
29
+ context = build(root, provider.load_graph(graph_path))
30
+ (klap / "context.json").write_text(json.dumps(context, indent=2) + "\n", encoding="utf-8")
31
+ agent = write_agent(context, klap / "agent-context.md")
32
+ mcp = generate_mcp_configs(root)
33
+ setup = AgentInstructionsManager(root).apply(context, shared=shared, allow_tracked=allow_tracked_agents)
34
+ context["agent_onboarding"] = {**setup.as_dict(), "mcp": mcp}
35
+ (klap / "context.json").write_text(json.dumps(context, indent=2) + "\n", encoding="utf-8")
36
+ write_portal(context, agent, files, klap / "index.html")
37
+ state = {
38
+ "schema_version": "0.3",
39
+ "generated_at": datetime.now(timezone.utc).isoformat(),
40
+ "git_commit": commit(root),
41
+ "provider": {"name": provider.name, "version": provider.version()},
42
+ "klap_version": __version__,
43
+ "agent_onboarding": setup.as_dict(),
44
+ }
45
+ (klap / "state.json").write_text(json.dumps(state, indent=2) + "\n", encoding="utf-8")
46
+ return context
47
+
48
+
49
+ def _permission(args, root: Path, action: str) -> tuple[bool, bool]:
50
+ shared = getattr(args, "shared", False)
51
+ allow = getattr(args, "allow_tracked_agents", False)
52
+ if not shared and is_tracked(root) and not allow and sys.stdin.isatty():
53
+ allow = input(f"AGENTS.md está versionado. ¿{action} la sección KlapContext? [y/N] ").strip().lower() in {"y", "yes", "s", "si", "sí"}
54
+ return shared, allow
55
+
56
+
57
+ def cmd_init(args):
58
+ root = root_path(args.path)
59
+ print("KlapContext\n")
60
+ shared, allow = _permission(args, root, "Agregar o actualizar")
61
+ try:
62
+ context = generate_all(root, shared=shared, allow_tracked_agents=allow) if shared or allow else generate_all(root)
63
+ except (RuntimeError, GraphifyError) as error:
64
+ print(f"✗ {error}", file=sys.stderr)
65
+ return 1
66
+ print("✓ Git repository detected\n✓ Graphify available\n✓ Graph generated\n✓ Engineering context generated\n✓ Agent context generated\n✓ Human portal generated")
67
+ setup = context.get("agent_onboarding", {})
68
+ print(("✓ " if setup.get("status") in {"configured", "unchanged"} else "! ") + setup.get("message", "Agent onboarding not configured"))
69
+ coverage = context["system_model"]["exploration"]["coverage"]
70
+ print(f"Coverage: {coverage['code_files_with_graph_nodes']}/{coverage['eligible_code_files']} code files represented in graph ({coverage['status']}); graph nodes do not guarantee complete symbol analysis")
71
+ stack_items = [item for values in context["stack"].values() if isinstance(values, list) for item in values]
72
+ print(f"\nProject: {context['project']['name']}\nStack: {' / '.join(stack_items) or 'unknown'}\nContext: CURRENT\n\nHuman portal:\n.klap/index.html\n\nAgent context:\n.klap/agent-context.md")
73
+ return 0
74
+
75
+
76
+ def cmd_update(args):
77
+ root = root_path(args.path)
78
+ shared, allow = _permission(args, root, "Actualizar")
79
+ try:
80
+ context = generate_all(root, update=True, shared=shared, allow_tracked_agents=allow) if shared or allow else generate_all(root, update=True)
81
+ except (RuntimeError, GraphifyError) as error:
82
+ print(f"✗ {error}", file=sys.stderr)
83
+ return 1
84
+ coverage = context["system_model"]["exploration"]["coverage"]
85
+ print(f"✓ KlapContext updated; graph coverage: {coverage['code_files_with_graph_nodes']}/{coverage['eligible_code_files']} code files ({coverage['status']})")
86
+ return 0
87
+
88
+
89
+ def cmd_status(args):
90
+ root = root_path(args.path)
91
+ state_file = root / ".klap" / "state.json"
92
+ if not state_file.exists():
93
+ print("! No KlapContext found. Run: klap init")
94
+ return 1
95
+ try:
96
+ state = json.loads(state_file.read_text(encoding="utf-8"))
97
+ except (OSError, json.JSONDecodeError):
98
+ print("Status: INVALID\nstate.json no es válido")
99
+ return 2
100
+ current = commit(root)
101
+ ignored = (".klap/", "graphify-out/", "AGENTS.md")
102
+ relevant = [path for path in dirty_files(root) if not path.replace("\\", "/").startswith(ignored)]
103
+ fresh = state.get("git_commit") == current and not relevant
104
+ reason = "El contexto coincide con el código confirmado" if fresh else "El commit analizado o archivos relevantes cambiaron"
105
+ print(f"Status: {'CURRENT' if fresh else 'STALE'}\n{reason}")
106
+ return 0 if fresh else 2
107
+
108
+
109
+ def cmd_doctor(args):
110
+ result = doctor_agent(root_path(args.path), probe_mcp=getattr(args, "probe_mcp", False))
111
+ print("KlapContext — Agent Doctor\n")
112
+ checks = [
113
+ (result["agents"]["valid"], "AGENTS.md configured"),
114
+ (result["context"]["available"], "Engineering context available"),
115
+ (result["freshness"]["status"] == "CURRENT", "Context is current"),
116
+ (result["graph"]["available"], "Graphify graph available"),
117
+ (result["mcp"]["configured"], "MCP configuration generated"),
118
+ ]
119
+ for ok, label in checks:
120
+ print(("✓" if ok else "!") + " " + label)
121
+ print(("✓" if result["mcp"]["connection_verified"] else "!") + " MCP connection " + ("verified" if result["mcp"]["connection_verified"] else "not yet verified"))
122
+ if result["agents"].get("missing_paths"):
123
+ print("Missing paths: " + ", ".join(result["agents"]["missing_paths"]))
124
+ return 0 if all(ok for ok, _ in checks) else 2
125
+
126
+
127
+ def cmd_open(args):
128
+ page = root_path(args.path) / ".klap" / "index.html"
129
+ if not page.exists():
130
+ print("⚠ No human portal found. Run: klap init", file=sys.stderr)
131
+ return 1
132
+ webbrowser.open(page.as_uri())
133
+ print(f"Opened {page}")
134
+ return 0
135
+
136
+
137
+ def cmd_agent(args):
138
+ root = root_path(args.path)
139
+ graph = root / ".klap" / "graphify" / "graph.json"
140
+ if not graph.exists():
141
+ print("⚠ No Graphify graph found. Run: klap init", file=sys.stderr)
142
+ return 1
143
+ config = generate_mcp_configs(root)
144
+ print(f"Graphify MCP setup generated in {config['directory']}\nGeneric: {config['generic']}\nCodex: {config['codex']}\nRun: klap doctor --agent --probe-mcp")
145
+ return 0
146
+
147
+
148
+ def cmd_context(args):
149
+ from .agent.compiler import compile_context
150
+ result = compile_context(root_path(args.path), args.query, detail=args.detail, max_tokens=args.max_tokens)
151
+ if args.json:
152
+ print(json.dumps(result, ensure_ascii=False, indent=2))
153
+ else:
154
+ print(f"Intent: {result['intent']}\nÁrea: {result['area']}\nRead first:\n" + "\n".join(f"- {item.get('path', item.get('name'))}: {item['reason']}" for item in result["read_first"]))
155
+ return 0
156
+
157
+
158
+ def main(argv=None):
159
+ if hasattr(sys.stdout, "reconfigure"):
160
+ sys.stdout.reconfigure(encoding="utf-8", errors="replace")
161
+ if hasattr(sys.stderr, "reconfigure"):
162
+ sys.stderr.reconfigure(encoding="utf-8", errors="replace")
163
+ parser = argparse.ArgumentParser(prog="klap", description="Make your repository understandable to humans and AI.")
164
+ parser.add_argument("--version", action="version", version=__version__)
165
+ subs = parser.add_subparsers(dest="command", required=True)
166
+ for name, func, help_text in (("init", cmd_init, "Generate engineering context"), ("update", cmd_update, "Refresh engineering context"), ("status", cmd_status, "Show context freshness"), ("open", cmd_open, "Open the human portal"), ("agent", cmd_agent, "Show Graphify MCP setup")):
167
+ command = subs.add_parser(name, help=help_text)
168
+ command.add_argument("path", nargs="?", help="Repository root (defaults to current directory)")
169
+ if name in {"init", "update"}:
170
+ command.add_argument("--shared", action="store_true", help="Create versionable AGENTS.md instructions")
171
+ command.add_argument("--allow-tracked-agents", action="store_true", help="Authorize updating a tracked AGENTS.md")
172
+ command.set_defaults(func=func)
173
+ command = subs.add_parser("doctor", help="Diagnose project and agent readiness")
174
+ command.add_argument("path", nargs="?", default=".")
175
+ command.add_argument("--agent", action="store_true", help="Check AGENTS.md and generated context")
176
+ command.add_argument("--probe-mcp", action="store_true", help="Start Graphify MCP briefly and stop it")
177
+ command.set_defaults(func=cmd_doctor)
178
+ command = subs.add_parser("context", help="Compile compact context for an agent task")
179
+ command.add_argument("query")
180
+ command.add_argument("path", nargs="?", default=".")
181
+ command.add_argument("--detail", choices=("minimal", "standard", "deep"), default="standard")
182
+ command.add_argument("--max-tokens", type=int, default=5000)
183
+ command.add_argument("--json", action="store_true")
184
+ command.set_defaults(func=cmd_context)
185
+ args = parser.parse_args(argv)
186
+ return args.func(args)
187
+
188
+
189
+ if __name__ == "__main__":
190
+ raise SystemExit(main())