auto-context-cli 1.0.4__tar.gz → 1.0.5__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.
- {auto_context_cli-1.0.4 → auto_context_cli-1.0.5}/PKG-INFO +2 -2
- {auto_context_cli-1.0.4 → auto_context_cli-1.0.5}/README.md +1 -1
- {auto_context_cli-1.0.4 → auto_context_cli-1.0.5}/pyproject.toml +1 -1
- {auto_context_cli-1.0.4 → auto_context_cli-1.0.5}/src/auto_context/__init__.py +1 -1
- {auto_context_cli-1.0.4 → auto_context_cli-1.0.5}/src/auto_context/commands/init_cmd.py +11 -0
- {auto_context_cli-1.0.4 → auto_context_cli-1.0.5}/src/auto_context/commands/scan_cmd.py +8 -3
- {auto_context_cli-1.0.4 → auto_context_cli-1.0.5}/src/auto_context/main.py +1 -1
- auto_context_cli-1.0.5/src/auto_context/scanner.py +421 -0
- {auto_context_cli-1.0.4 → auto_context_cli-1.0.5}/src/auto_context/templates/context.template.md +1 -2
- auto_context_cli-1.0.4/src/auto_context/scanner.py +0 -297
- {auto_context_cli-1.0.4 → auto_context_cli-1.0.5}/.gitignore +0 -0
- {auto_context_cli-1.0.4 → auto_context_cli-1.0.5}/LICENSE +0 -0
- {auto_context_cli-1.0.4 → auto_context_cli-1.0.5}/src/auto_context/commands/__init__.py +0 -0
- {auto_context_cli-1.0.4 → auto_context_cli-1.0.5}/src/auto_context/commands/import_cmd.py +0 -0
- {auto_context_cli-1.0.4 → auto_context_cli-1.0.5}/src/auto_context/commands/mcp.py +0 -0
- {auto_context_cli-1.0.4 → auto_context_cli-1.0.5}/src/auto_context/commands/memory.py +0 -0
- {auto_context_cli-1.0.4 → auto_context_cli-1.0.5}/src/auto_context/commands/share.py +0 -0
- {auto_context_cli-1.0.4 → auto_context_cli-1.0.5}/src/auto_context/commands/update_cmd.py +0 -0
- {auto_context_cli-1.0.4 → auto_context_cli-1.0.5}/src/auto_context/config.py +0 -0
- {auto_context_cli-1.0.4 → auto_context_cli-1.0.5}/src/auto_context/security.py +0 -0
- {auto_context_cli-1.0.4 → auto_context_cli-1.0.5}/src/auto_context/templates/AGENTS.template.md +0 -0
- {auto_context_cli-1.0.4 → auto_context_cli-1.0.5}/src/auto_context/templates/GEMINI.template.md +0 -0
- {auto_context_cli-1.0.4 → auto_context_cli-1.0.5}/src/auto_context/templates/context.local.template.md +0 -0
- {auto_context_cli-1.0.4 → auto_context_cli-1.0.5}/src/auto_context/templates/maintain_context.template.md +0 -0
- {auto_context_cli-1.0.4 → auto_context_cli-1.0.5}/src/auto_context/templates/mcp_config.template.json +0 -0
- {auto_context_cli-1.0.4 → auto_context_cli-1.0.5}/src/auto_context/templates/profile.template.md +0 -0
- {auto_context_cli-1.0.4 → auto_context_cli-1.0.5}/src/auto_context/templates/rule.template.md +0 -0
- {auto_context_cli-1.0.4 → auto_context_cli-1.0.5}/src/auto_context/templates/rules/code-standards.md +0 -0
- {auto_context_cli-1.0.4 → auto_context_cli-1.0.5}/src/auto_context/templates/rules/hub-satellite-architecture.md +0 -0
- {auto_context_cli-1.0.4 → auto_context_cli-1.0.5}/src/auto_context/templates/rules/readme-docs-sync.md +0 -0
- {auto_context_cli-1.0.4 → auto_context_cli-1.0.5}/src/auto_context/templates/rules/security-boundaries.md +0 -0
- {auto_context_cli-1.0.4 → auto_context_cli-1.0.5}/src/auto_context/templates/skill.template.md +0 -0
- {auto_context_cli-1.0.4 → auto_context_cli-1.0.5}/src/auto_context/templates/skills/browser-testing/SKILL.md +0 -0
- {auto_context_cli-1.0.4 → auto_context_cli-1.0.5}/src/auto_context/templates/skills/docker-ops/SKILL.md +0 -0
- {auto_context_cli-1.0.4 → auto_context_cli-1.0.5}/src/auto_context/templates/skills/git-workflow/SKILL.md +0 -0
- {auto_context_cli-1.0.4 → auto_context_cli-1.0.5}/src/auto_context/templates/skills/server-diagnostics/SKILL.md +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: auto-context-cli
|
|
3
|
-
Version: 1.0.
|
|
3
|
+
Version: 1.0.5
|
|
4
4
|
Summary: Autonomous context & instruction manager for AI coding assistants
|
|
5
5
|
Author: Arghya Biswas
|
|
6
6
|
License: MIT
|
|
@@ -20,7 +20,7 @@ Description-Content-Type: text/markdown
|
|
|
20
20
|
|
|
21
21
|
[](https://pypi.org/project/auto-context-cli/)
|
|
22
22
|
[](https://pypi.org/project/auto-context-cli/)
|
|
23
|
-
[](https://github.com/Cyber-arghya/auto-context/releases/tag/v1.0.
|
|
23
|
+
[](https://github.com/Cyber-arghya/auto-context/releases/tag/v1.0.5)
|
|
24
24
|
[](https://opensource.org/licenses/MIT)
|
|
25
25
|
[](#-quick-one-line-install)
|
|
26
26
|
[](#-supported-ai-assistants)
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
[](https://pypi.org/project/auto-context-cli/)
|
|
8
8
|
[](https://pypi.org/project/auto-context-cli/)
|
|
9
|
-
[](https://github.com/Cyber-arghya/auto-context/releases/tag/v1.0.
|
|
9
|
+
[](https://github.com/Cyber-arghya/auto-context/releases/tag/v1.0.5)
|
|
10
10
|
[](https://opensource.org/licenses/MIT)
|
|
11
11
|
[](#-quick-one-line-install)
|
|
12
12
|
[](#-supported-ai-assistants)
|
|
@@ -102,6 +102,17 @@ def detect_stack(project_dir: Path) -> tuple[str, str]:
|
|
|
102
102
|
|
|
103
103
|
return stack, "Standard"
|
|
104
104
|
|
|
105
|
+
# Fallback: check immediate child directories if current folder has no manifest
|
|
106
|
+
try:
|
|
107
|
+
for child in project_dir.iterdir():
|
|
108
|
+
if child.is_dir() and not child.name.startswith((".", "node_modules", "venv", "__pycache__")):
|
|
109
|
+
for filename, stack, kind in config.STACK_INDICATORS:
|
|
110
|
+
if (child / filename).exists():
|
|
111
|
+
sub_stack, sub_fw = detect_stack(child)
|
|
112
|
+
return f"{sub_stack} (in {child.name}/)", sub_fw
|
|
113
|
+
except Exception:
|
|
114
|
+
pass
|
|
115
|
+
|
|
105
116
|
return "General / Multi-stack", "Not detected"
|
|
106
117
|
|
|
107
118
|
|
|
@@ -59,15 +59,20 @@ def scan(
|
|
|
59
59
|
for svc, port, src in ports:
|
|
60
60
|
ports_table += f"| **{svc}** | `{port}` | {src} |\n"
|
|
61
61
|
else:
|
|
62
|
-
|
|
62
|
+
is_web = any(w in (stack + framework).lower() for w in ("next", "react", "vue", "express", "fastapi", "flask", "django", "node", "web", "nest", "svelte"))
|
|
63
|
+
if is_web:
|
|
64
|
+
ports_table += "| **App Port** | `3000` (Default) | Inferred web port |\n"
|
|
65
|
+
else:
|
|
66
|
+
ports_table += "| **Network Ports** | `None` | CLI / Library (No local server ports detected) |\n"
|
|
63
67
|
|
|
64
|
-
# Build runbook table
|
|
68
|
+
# Build runbook table (Strictly verified executable commands, zero hallucinated fallbacks)
|
|
65
69
|
runbook_table = "| Task | Executable Command |\n| :--- | :--- |\n"
|
|
66
70
|
if commands:
|
|
67
71
|
for action, cmd in commands:
|
|
68
72
|
runbook_table += f"| {action} | `{cmd}` |\n"
|
|
69
73
|
else:
|
|
70
|
-
runbook_table += "|
|
|
74
|
+
runbook_table += "| Project Runbook | No startup scripts declared in manifests |\n"
|
|
75
|
+
runbook_table += "| Rescan Codebase | `init-context scan` |\n"
|
|
71
76
|
|
|
72
77
|
# Build satellites index section
|
|
73
78
|
satellites_index = ""
|
|
@@ -0,0 +1,421 @@
|
|
|
1
|
+
"""
|
|
2
|
+
scanner.py — Stack-Adaptive Codebase Introspection & Brownfield Discovery Engine.
|
|
3
|
+
|
|
4
|
+
Discovers ports, routes, database schemas, services, and runbooks using lightweight
|
|
5
|
+
filesystem convention heuristics (0 LLM tokens, 100% crash-proof, idempotency-guaranteed).
|
|
6
|
+
"""
|
|
7
|
+
import hashlib
|
|
8
|
+
import json
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
import re
|
|
11
|
+
from typing import Optional
|
|
12
|
+
try:
|
|
13
|
+
import tomllib
|
|
14
|
+
except ImportError:
|
|
15
|
+
try:
|
|
16
|
+
import tomli as tomllib # type: ignore
|
|
17
|
+
except ImportError:
|
|
18
|
+
tomllib = None
|
|
19
|
+
|
|
20
|
+
from rich.console import Console
|
|
21
|
+
|
|
22
|
+
console = Console()
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
# ── Idempotency & Hash Helpers ────────────────────────────────────────────────
|
|
26
|
+
|
|
27
|
+
def get_content_hash(text: str) -> str:
|
|
28
|
+
"""Compute SHA-256 hash of text content."""
|
|
29
|
+
return hashlib.sha256(text.encode("utf-8")).hexdigest()
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def write_idempotent(path: Path, content: str, label: str = "File") -> bool:
|
|
33
|
+
"""
|
|
34
|
+
Write content to path only if content has changed (Content-Hash Guard).
|
|
35
|
+
Returns True if a write occurred, False if content was already identical.
|
|
36
|
+
"""
|
|
37
|
+
if path.exists():
|
|
38
|
+
try:
|
|
39
|
+
existing = path.read_text(encoding="utf-8")
|
|
40
|
+
if get_content_hash(existing) == get_content_hash(content):
|
|
41
|
+
console.print(f" [dim]— Up to date:[/] {path.name}")
|
|
42
|
+
return False
|
|
43
|
+
except Exception:
|
|
44
|
+
pass
|
|
45
|
+
|
|
46
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
47
|
+
path.write_text(content, encoding="utf-8")
|
|
48
|
+
console.print(f" [green][✓] Synchronized:[/] {path.name}")
|
|
49
|
+
return True
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _get_project_search_roots(project_dir: Path) -> list[Path]:
|
|
53
|
+
"""
|
|
54
|
+
Returns [project_dir] plus any immediate child directories containing manifests.
|
|
55
|
+
Guards against scanning dependencies or virtual environments.
|
|
56
|
+
"""
|
|
57
|
+
roots = [project_dir]
|
|
58
|
+
ignore_names = {
|
|
59
|
+
"node_modules", ".git", ".venv", "venv", "__pycache__",
|
|
60
|
+
".agents", ".gemini", "dist", "build", "site-packages", ".idea", ".vscode"
|
|
61
|
+
}
|
|
62
|
+
manifest_names = {
|
|
63
|
+
"package.json", "pyproject.toml", "Makefile", "manage.py",
|
|
64
|
+
"Cargo.toml", "go.mod", "docker-compose.yml", "compose.yaml", "requirements.txt"
|
|
65
|
+
}
|
|
66
|
+
try:
|
|
67
|
+
for child in project_dir.iterdir():
|
|
68
|
+
if child.is_dir() and child.name not in ignore_names and not child.name.startswith("."):
|
|
69
|
+
if any((child / m).exists() for m in manifest_names):
|
|
70
|
+
roots.append(child)
|
|
71
|
+
except Exception:
|
|
72
|
+
pass
|
|
73
|
+
return roots
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
# ── Port & Service Extractor ──────────────────────────────────────────────────
|
|
77
|
+
|
|
78
|
+
def discover_ports_and_services(project_dir: Path) -> list[tuple[str, str, str]]:
|
|
79
|
+
"""
|
|
80
|
+
Extract network ports and service bindings from configs without leaking secrets.
|
|
81
|
+
Returns list of (Service, Port, SourceFile).
|
|
82
|
+
"""
|
|
83
|
+
results: list[tuple[str, str, str]] = []
|
|
84
|
+
seen_ports: set[str] = set()
|
|
85
|
+
|
|
86
|
+
for d in _get_project_search_roots(project_dir):
|
|
87
|
+
# 1. Check docker-compose.yml / compose.yaml
|
|
88
|
+
for compose_name in ("docker-compose.yml", "docker-compose.yaml", "compose.yml", "compose.yaml"):
|
|
89
|
+
compose_file = d / compose_name
|
|
90
|
+
if compose_file.exists():
|
|
91
|
+
try:
|
|
92
|
+
text = compose_file.read_text(encoding="utf-8", errors="ignore")
|
|
93
|
+
matches = re.findall(r'["\']?(\d{2,5}):(\d{2,5})["\']?', text)
|
|
94
|
+
for host_port, container_port in matches:
|
|
95
|
+
if host_port not in seen_ports and int(host_port) > 80:
|
|
96
|
+
seen_ports.add(host_port)
|
|
97
|
+
src_label = compose_name if d == project_dir else f"{d.name}/{compose_name}"
|
|
98
|
+
results.append(("Container Port", host_port, src_label))
|
|
99
|
+
except Exception:
|
|
100
|
+
pass
|
|
101
|
+
|
|
102
|
+
# 2. Check .env.example / .env
|
|
103
|
+
for env_name in (".env.example", ".env", ".env.local"):
|
|
104
|
+
env_file = d / env_name
|
|
105
|
+
if env_file.exists():
|
|
106
|
+
try:
|
|
107
|
+
for line in env_file.read_text(encoding="utf-8", errors="ignore").splitlines():
|
|
108
|
+
line = line.strip()
|
|
109
|
+
if line.startswith("#") or "=" not in line:
|
|
110
|
+
continue
|
|
111
|
+
key, val = line.split("=", 1)
|
|
112
|
+
key = key.strip()
|
|
113
|
+
val = val.strip().strip('"').strip("'")
|
|
114
|
+
if "PORT" in key.upper() and val.isdigit() and val not in seen_ports:
|
|
115
|
+
seen_ports.add(val)
|
|
116
|
+
src_label = env_name if d == project_dir else f"{d.name}/{env_name}"
|
|
117
|
+
results.append((key, val, src_label))
|
|
118
|
+
except Exception:
|
|
119
|
+
pass
|
|
120
|
+
|
|
121
|
+
# 3. Check Dockerfile
|
|
122
|
+
dockerfile = d / "Dockerfile"
|
|
123
|
+
if dockerfile.exists():
|
|
124
|
+
try:
|
|
125
|
+
for line in dockerfile.read_text(encoding="utf-8", errors="ignore").splitlines():
|
|
126
|
+
if line.strip().upper().startswith("EXPOSE "):
|
|
127
|
+
port = line.strip().split()[1]
|
|
128
|
+
if port.isdigit() and port not in seen_ports:
|
|
129
|
+
seen_ports.add(port)
|
|
130
|
+
src_label = "Dockerfile" if d == project_dir else f"{d.name}/Dockerfile"
|
|
131
|
+
results.append(("Exposed Port", port, src_label))
|
|
132
|
+
except Exception:
|
|
133
|
+
pass
|
|
134
|
+
|
|
135
|
+
return results
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
# ── Runbook Command Extractor ─────────────────────────────────────────────────
|
|
139
|
+
|
|
140
|
+
def discover_runbook_commands(project_dir: Path) -> list[tuple[str, str]]:
|
|
141
|
+
"""
|
|
142
|
+
Extract executable development, build, and test commands from project manifests.
|
|
143
|
+
Guarantees 100% executable commands without hallucinated placeholders.
|
|
144
|
+
Returns list of (Action, Command).
|
|
145
|
+
"""
|
|
146
|
+
commands: list[tuple[str, str]] = []
|
|
147
|
+
seen_commands: set[str] = set()
|
|
148
|
+
|
|
149
|
+
for d in _get_project_search_roots(project_dir):
|
|
150
|
+
dir_label = "" if d == project_dir else f" ({d.name})"
|
|
151
|
+
|
|
152
|
+
# 1. package.json scripts
|
|
153
|
+
pkg_file = d / "package.json"
|
|
154
|
+
if pkg_file.exists():
|
|
155
|
+
try:
|
|
156
|
+
data = json.loads(pkg_file.read_text(encoding="utf-8", errors="ignore"))
|
|
157
|
+
scripts = data.get("scripts", {})
|
|
158
|
+
runner = "pnpm" if (d / "pnpm-lock.yaml").exists() else (
|
|
159
|
+
"yarn" if (d / "yarn.lock").exists() else (
|
|
160
|
+
"bun" if (d / "bun.lockb").exists() else "npm"
|
|
161
|
+
)
|
|
162
|
+
)
|
|
163
|
+
for key in ("dev", "start", "build", "test", "lint"):
|
|
164
|
+
if key in scripts:
|
|
165
|
+
if d == project_dir:
|
|
166
|
+
cmd = f"{runner} run {key}" if runner != "yarn" else f"yarn {key}"
|
|
167
|
+
else:
|
|
168
|
+
cmd = f"{runner} --prefix {d.name} run {key}" if runner == "npm" else f"{runner} --cwd {d.name} run {key}"
|
|
169
|
+
if cmd not in seen_commands:
|
|
170
|
+
seen_commands.add(cmd)
|
|
171
|
+
commands.append((f"Run {key}{dir_label}", cmd))
|
|
172
|
+
except Exception:
|
|
173
|
+
pass
|
|
174
|
+
|
|
175
|
+
# 2. pyproject.toml (scripts & test runner)
|
|
176
|
+
pyproj = d / "pyproject.toml"
|
|
177
|
+
if pyproj.exists():
|
|
178
|
+
try:
|
|
179
|
+
raw_text = pyproj.read_text(encoding="utf-8", errors="ignore")
|
|
180
|
+
data = tomllib.loads(raw_text) if tomllib else {}
|
|
181
|
+
scripts = data.get("project", {}).get("scripts", {})
|
|
182
|
+
for script_name in scripts:
|
|
183
|
+
cmd = f"uv run {script_name}" if d == project_dir else f"uv run --directory {d.name} {script_name}"
|
|
184
|
+
if cmd not in seen_commands:
|
|
185
|
+
seen_commands.add(cmd)
|
|
186
|
+
commands.append((f"Run CLI ({script_name}){dir_label}", cmd))
|
|
187
|
+
|
|
188
|
+
if "pytest" in raw_text or (d / "tests").is_dir() or (d / "test").is_dir():
|
|
189
|
+
cmd = "uv run pytest" if d == project_dir else f"uv run --directory {d.name} pytest"
|
|
190
|
+
if cmd not in seen_commands:
|
|
191
|
+
seen_commands.add(cmd)
|
|
192
|
+
commands.append((f"Run Tests{dir_label}", cmd))
|
|
193
|
+
except Exception:
|
|
194
|
+
pass
|
|
195
|
+
|
|
196
|
+
# 3. Python indicators (manage.py, main.py, app.py, server.py)
|
|
197
|
+
if (d / "manage.py").exists():
|
|
198
|
+
target = "manage.py" if d == project_dir else f"{d.name}/manage.py"
|
|
199
|
+
cmd_run = f"python {target} runserver"
|
|
200
|
+
cmd_mig = f"python {target} migrate"
|
|
201
|
+
if cmd_run not in seen_commands:
|
|
202
|
+
seen_commands.add(cmd_run)
|
|
203
|
+
commands.append((f"Django Runserver{dir_label}", cmd_run))
|
|
204
|
+
if cmd_mig not in seen_commands:
|
|
205
|
+
seen_commands.add(cmd_mig)
|
|
206
|
+
commands.append((f"Django Migrate{dir_label}", cmd_mig))
|
|
207
|
+
else:
|
|
208
|
+
for main_candidate in ("main.py", "app.py", "server.py", "wsgi.py"):
|
|
209
|
+
if (d / main_candidate).exists():
|
|
210
|
+
target = main_candidate if d == project_dir else f"{d.name}/{main_candidate}"
|
|
211
|
+
cmd = f"python {target}"
|
|
212
|
+
if cmd not in seen_commands:
|
|
213
|
+
seen_commands.add(cmd)
|
|
214
|
+
commands.append((f"Start Server{dir_label}", cmd))
|
|
215
|
+
break
|
|
216
|
+
|
|
217
|
+
# 4. Makefile targets
|
|
218
|
+
makefile = d / "Makefile"
|
|
219
|
+
if makefile.exists():
|
|
220
|
+
try:
|
|
221
|
+
targets = re.findall(r"^([a-zA-Z0-9_-]+):", makefile.read_text(encoding="utf-8", errors="ignore"), re.M)
|
|
222
|
+
preferred = [t for t in targets if not t.startswith(".") and t in ("dev", "start", "build", "test", "lint", "all", "run")]
|
|
223
|
+
if not preferred:
|
|
224
|
+
preferred = [t for t in targets if not t.startswith(".")][:3]
|
|
225
|
+
for t in preferred[:3]:
|
|
226
|
+
cmd = f"make {t}" if d == project_dir else f"make -C {d.name} {t}"
|
|
227
|
+
if cmd not in seen_commands:
|
|
228
|
+
seen_commands.add(cmd)
|
|
229
|
+
commands.append((f"Make {t}{dir_label}", cmd))
|
|
230
|
+
except Exception:
|
|
231
|
+
pass
|
|
232
|
+
|
|
233
|
+
# 5. Rust Cargo
|
|
234
|
+
cargo = d / "Cargo.toml"
|
|
235
|
+
if cargo.exists():
|
|
236
|
+
cmd_run = "cargo run" if d == project_dir else f"cargo run --manifest-path {d.name}/Cargo.toml"
|
|
237
|
+
cmd_test = "cargo test" if d == project_dir else f"cargo test --manifest-path {d.name}/Cargo.toml"
|
|
238
|
+
if cmd_run not in seen_commands:
|
|
239
|
+
seen_commands.add(cmd_run)
|
|
240
|
+
commands.append((f"Cargo Run{dir_label}", cmd_run))
|
|
241
|
+
if cmd_test not in seen_commands:
|
|
242
|
+
seen_commands.add(cmd_test)
|
|
243
|
+
commands.append((f"Cargo Test{dir_label}", cmd_test))
|
|
244
|
+
|
|
245
|
+
# 6. Go mod
|
|
246
|
+
go_mod = d / "go.mod"
|
|
247
|
+
if go_mod.exists():
|
|
248
|
+
cmd_run = "go run ." if d == project_dir else f"cd {d.name} && go run ."
|
|
249
|
+
cmd_test = "go test ./..." if d == project_dir else f"cd {d.name} && go test ./..."
|
|
250
|
+
if cmd_run not in seen_commands:
|
|
251
|
+
seen_commands.add(cmd_run)
|
|
252
|
+
commands.append((f"Go Run{dir_label}", cmd_run))
|
|
253
|
+
if cmd_test not in seen_commands:
|
|
254
|
+
seen_commands.add(cmd_test)
|
|
255
|
+
commands.append((f"Go Test{dir_label}", cmd_test))
|
|
256
|
+
|
|
257
|
+
# 7. Docker Compose
|
|
258
|
+
if (d / "docker-compose.yml").exists() or (d / "compose.yaml").exists():
|
|
259
|
+
cmd = "docker compose up -d" if d == project_dir else f"docker compose -f {d.name}/docker-compose.yml up -d"
|
|
260
|
+
if cmd not in seen_commands:
|
|
261
|
+
seen_commands.add(cmd)
|
|
262
|
+
commands.append((f"Start Docker Services{dir_label}", cmd))
|
|
263
|
+
|
|
264
|
+
return commands[:6]
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
# ── Routes & Endpoints Extractor ──────────────────────────────────────────────
|
|
268
|
+
|
|
269
|
+
def discover_routes(project_dir: Path) -> list[str]:
|
|
270
|
+
"""
|
|
271
|
+
Extract API and UI routes using Next.js, Express, FastAPI, and Django conventions.
|
|
272
|
+
Returns clean list of detected route paths.
|
|
273
|
+
"""
|
|
274
|
+
routes: list[str] = []
|
|
275
|
+
|
|
276
|
+
try:
|
|
277
|
+
# 1. Next.js App Router (app/ or src/app/)
|
|
278
|
+
app_dirs = [d for d in (project_dir / "app", project_dir / "src" / "app") if d.is_dir()]
|
|
279
|
+
for app_dir in app_dirs:
|
|
280
|
+
for file_path in app_dir.rglob("*"):
|
|
281
|
+
if file_path.is_file() and file_path.name in ("route.ts", "route.js", "page.tsx", "page.jsx"):
|
|
282
|
+
rel = file_path.parent.relative_to(app_dir).as_posix()
|
|
283
|
+
route_path = "/" if rel == "." else f"/{rel}"
|
|
284
|
+
kind = "API" if file_path.name.startswith("route") else "Page"
|
|
285
|
+
routes.append(f"[{kind}] {route_path} -> {file_path.relative_to(project_dir).as_posix()}")
|
|
286
|
+
|
|
287
|
+
# 2. Next.js Pages Router (pages/ or src/pages/)
|
|
288
|
+
page_dirs = [d for d in (project_dir / "pages", project_dir / "src" / "pages") if d.is_dir()]
|
|
289
|
+
for page_dir in page_dirs:
|
|
290
|
+
for file_path in page_dir.rglob("*.tsx"):
|
|
291
|
+
if not file_path.name.startswith("_"):
|
|
292
|
+
rel = file_path.relative_to(page_dir).with_suffix("").as_posix()
|
|
293
|
+
route_path = "/" if rel == "index" else f"/{rel.replace('/index', '')}"
|
|
294
|
+
kind = "API" if "api" in rel else "Page"
|
|
295
|
+
routes.append(f"[{kind}] {route_path} -> {file_path.relative_to(project_dir).as_posix()}")
|
|
296
|
+
|
|
297
|
+
# 3. Modular Route, Controller & CLI Command Folders (Deep Convention Search)
|
|
298
|
+
target_dirs = ("routes", "controllers", "routers", "endpoints", "commands", "handlers", "views", "api")
|
|
299
|
+
ignore_parts = {"node_modules", ".git", ".venv", "venv", "__pycache__", ".agents", ".gemini", "dist", "build", "site-packages"}
|
|
300
|
+
|
|
301
|
+
for folder in project_dir.rglob("*"):
|
|
302
|
+
if folder.is_dir() and folder.name in target_dirs:
|
|
303
|
+
if any(ignored in folder.parts for ignored in ignore_parts):
|
|
304
|
+
continue
|
|
305
|
+
|
|
306
|
+
label = "Command" if folder.name in ("commands", "cli") else "Route"
|
|
307
|
+
for ext in ("*.ts", "*.tsx", "*.js", "*.jsx", "*.py", "*.go", "*.rs"):
|
|
308
|
+
for f in folder.glob(ext):
|
|
309
|
+
if not f.name.startswith(("_", ".")):
|
|
310
|
+
rel_file = f.relative_to(project_dir).as_posix()
|
|
311
|
+
routes.append(f"[{label}] {f.stem} -> {rel_file}")
|
|
312
|
+
|
|
313
|
+
except Exception:
|
|
314
|
+
pass
|
|
315
|
+
|
|
316
|
+
return routes[:35] # Cap to prevent unbounded lists
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
# ── Database Models Extractor ─────────────────────────────────────────────────
|
|
320
|
+
|
|
321
|
+
def discover_database_models(project_dir: Path) -> list[str]:
|
|
322
|
+
"""
|
|
323
|
+
Extract data models from Prisma, Django, SQLAlchemy, Drizzle schemas.
|
|
324
|
+
Returns list of discovered model names and schema files.
|
|
325
|
+
"""
|
|
326
|
+
models: list[str] = []
|
|
327
|
+
|
|
328
|
+
try:
|
|
329
|
+
# 1. Prisma schema
|
|
330
|
+
for prisma_path in (project_dir / "prisma" / "schema.prisma", project_dir / "schema.prisma"):
|
|
331
|
+
if prisma_path.exists():
|
|
332
|
+
text = prisma_path.read_text(encoding="utf-8", errors="ignore")
|
|
333
|
+
found = re.findall(r"\bmodel\s+([A-Za-z0-9_]+)\s*\{", text)
|
|
334
|
+
for m in found:
|
|
335
|
+
models.append(f"[Prisma] {m} ({prisma_path.relative_to(project_dir).as_posix()})")
|
|
336
|
+
|
|
337
|
+
# 2. Django / SQLAlchemy models.py
|
|
338
|
+
for models_file in project_dir.rglob("models.py"):
|
|
339
|
+
if "venv" not in models_file.parts and ".git" not in models_file.parts:
|
|
340
|
+
text = models_file.read_text(encoding="utf-8", errors="ignore")
|
|
341
|
+
found = re.findall(r"\bclass\s+([A-Za-z0-9_]+)\s*\(", text)
|
|
342
|
+
for m in found:
|
|
343
|
+
if m not in ("Meta",):
|
|
344
|
+
models.append(f"[Model] {m} ({models_file.relative_to(project_dir).as_posix()})")
|
|
345
|
+
|
|
346
|
+
# 3. Drizzle schema
|
|
347
|
+
for drizzle_file in project_dir.rglob("schema.ts"):
|
|
348
|
+
if "node_modules" not in drizzle_file.parts:
|
|
349
|
+
text = drizzle_file.read_text(encoding="utf-8", errors="ignore")
|
|
350
|
+
found = re.findall(r"export\s+const\s+([A-Za-z0-9_]+)\s+=\s+(?:pgTable|mysqlTable|sqliteTable)", text)
|
|
351
|
+
for m in found:
|
|
352
|
+
models.append(f"[Drizzle] {m} ({drizzle_file.relative_to(project_dir).as_posix()})")
|
|
353
|
+
|
|
354
|
+
except Exception:
|
|
355
|
+
pass
|
|
356
|
+
|
|
357
|
+
return models[:30]
|
|
358
|
+
|
|
359
|
+
|
|
360
|
+
# ── Satellites Generator (.agents/context/) ───────────────────────────────────
|
|
361
|
+
|
|
362
|
+
def generate_satellites(project_dir: Path, force: bool = False) -> dict[str, Path]:
|
|
363
|
+
"""
|
|
364
|
+
Generate stack-adaptive satellite files in .agents/context/ on-demand.
|
|
365
|
+
Enforces strict idempotency and zero token bloat in the main hub.
|
|
366
|
+
"""
|
|
367
|
+
satellites_dir = project_dir / ".agents" / "context"
|
|
368
|
+
satellites_dir.mkdir(parents=True, exist_ok=True)
|
|
369
|
+
generated: dict[str, Path] = {}
|
|
370
|
+
|
|
371
|
+
routes = discover_routes(project_dir)
|
|
372
|
+
models = discover_database_models(project_dir)
|
|
373
|
+
|
|
374
|
+
# 1. routes.md (if web routes detected)
|
|
375
|
+
if routes:
|
|
376
|
+
routes_path = satellites_dir / "routes.md"
|
|
377
|
+
routes_content = (
|
|
378
|
+
"# System Route & Endpoint Map\n\n"
|
|
379
|
+
"> Discovered automatically via convention scanner. Used by AI agents for precision navigation.\n\n"
|
|
380
|
+
"| Kind | Route Path | Implementation File |\n"
|
|
381
|
+
"| :--- | :--- | :--- |\n"
|
|
382
|
+
)
|
|
383
|
+
for r in routes:
|
|
384
|
+
parts = r.split(" -> ")
|
|
385
|
+
kind_path = parts[0]
|
|
386
|
+
impl = parts[1] if len(parts) > 1 else ""
|
|
387
|
+
routes_content += f"| {kind_path} | `{impl}` |\n"
|
|
388
|
+
|
|
389
|
+
write_idempotent(routes_path, routes_content, "Routes Satellite")
|
|
390
|
+
generated["routes"] = routes_path
|
|
391
|
+
|
|
392
|
+
# 2. schema.md (if database models detected)
|
|
393
|
+
if models:
|
|
394
|
+
schema_path = satellites_dir / "schema.md"
|
|
395
|
+
schema_content = (
|
|
396
|
+
"# Database Schema & Entity Models\n\n"
|
|
397
|
+
"> Discovered data models and ORM schema definitions. Used by AI agents for database context.\n\n"
|
|
398
|
+
"| Model / Entity | Definition File |\n"
|
|
399
|
+
"| :--- | :--- |\n"
|
|
400
|
+
)
|
|
401
|
+
for m in models:
|
|
402
|
+
schema_content += f"| {m} |\n"
|
|
403
|
+
|
|
404
|
+
write_idempotent(schema_path, schema_content, "Schema Satellite")
|
|
405
|
+
generated["schema"] = schema_path
|
|
406
|
+
|
|
407
|
+
# 3. gotchas.md (Critical Traps & Anti-Patterns — Replaces useless bug diaries)
|
|
408
|
+
gotchas_path = satellites_dir / "gotchas.md"
|
|
409
|
+
if not gotchas_path.exists() or force:
|
|
410
|
+
gotchas_content = (
|
|
411
|
+
"# Critical Gotchas, Anti-Patterns & Architecture Invariants\n\n"
|
|
412
|
+
"> Institutional knowledge and non-obvious traps that cannot be guessed from reading code.\n\n"
|
|
413
|
+
"## ⚠️ Active Constraints & Traps\n\n"
|
|
414
|
+
"1. **Port Invariants:** Inspect discovered ports before binding local services.\n"
|
|
415
|
+
"2. **Secret Isolation:** Production secrets belong in `.env`; dummy dev accounts belong in `context.local.md`.\n"
|
|
416
|
+
"3. **State Boundaries:** Verify migrations before initiating build or release cycles.\n"
|
|
417
|
+
)
|
|
418
|
+
write_idempotent(gotchas_path, gotchas_content, "Gotchas Satellite")
|
|
419
|
+
generated["gotchas"] = gotchas_path
|
|
420
|
+
|
|
421
|
+
return generated
|
{auto_context_cli-1.0.4 → auto_context_cli-1.0.5}/src/auto_context/templates/context.template.md
RENAMED
|
@@ -28,8 +28,7 @@
|
|
|
28
28
|
|
|
29
29
|
| Action | Command |
|
|
30
30
|
| :--- | :--- |
|
|
31
|
-
| **
|
|
32
|
-
| **Codebase Rescan** | `init-context scan` |
|
|
31
|
+
| **Rescan & Sync Runbook** | `init-context scan` |
|
|
33
32
|
<!-- AUTO-CONTEXT:DISCOVERY:END -->
|
|
34
33
|
|
|
35
34
|
---
|
|
@@ -1,297 +0,0 @@
|
|
|
1
|
-
"""
|
|
2
|
-
scanner.py — Stack-Adaptive Codebase Introspection & Brownfield Discovery Engine.
|
|
3
|
-
|
|
4
|
-
Discovers ports, routes, database schemas, services, and runbooks using lightweight
|
|
5
|
-
filesystem convention heuristics (0 LLM tokens, 100% crash-proof, idempotency-guaranteed).
|
|
6
|
-
"""
|
|
7
|
-
import hashlib
|
|
8
|
-
import json
|
|
9
|
-
from pathlib import Path
|
|
10
|
-
import re
|
|
11
|
-
from typing import Optional
|
|
12
|
-
|
|
13
|
-
from rich.console import Console
|
|
14
|
-
|
|
15
|
-
console = Console()
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
# ── Idempotency & Hash Helpers ────────────────────────────────────────────────
|
|
19
|
-
|
|
20
|
-
def get_content_hash(text: str) -> str:
|
|
21
|
-
"""Compute SHA-256 hash of text content."""
|
|
22
|
-
return hashlib.sha256(text.encode("utf-8")).hexdigest()
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
def write_idempotent(path: Path, content: str, label: str = "File") -> bool:
|
|
26
|
-
"""
|
|
27
|
-
Write content to path only if content has changed (Content-Hash Guard).
|
|
28
|
-
Returns True if a write occurred, False if content was already identical.
|
|
29
|
-
"""
|
|
30
|
-
if path.exists():
|
|
31
|
-
try:
|
|
32
|
-
existing = path.read_text(encoding="utf-8")
|
|
33
|
-
if get_content_hash(existing) == get_content_hash(content):
|
|
34
|
-
console.print(f" [dim]— Up to date:[/] {path.name}")
|
|
35
|
-
return False
|
|
36
|
-
except Exception:
|
|
37
|
-
pass
|
|
38
|
-
|
|
39
|
-
path.parent.mkdir(parents=True, exist_ok=True)
|
|
40
|
-
path.write_text(content, encoding="utf-8")
|
|
41
|
-
console.print(f" [green][✓] Synchronized:[/] {path.name}")
|
|
42
|
-
return True
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
# ── Port & Service Extractor ──────────────────────────────────────────────────
|
|
46
|
-
|
|
47
|
-
def discover_ports_and_services(project_dir: Path) -> list[tuple[str, str, str]]:
|
|
48
|
-
"""
|
|
49
|
-
Extract network ports and service bindings from configs without leaking secrets.
|
|
50
|
-
Returns list of (Service, Port, SourceFile).
|
|
51
|
-
"""
|
|
52
|
-
results: list[tuple[str, str, str]] = []
|
|
53
|
-
seen_ports: set[str] = set()
|
|
54
|
-
|
|
55
|
-
# 1. Check docker-compose.yml / compose.yaml
|
|
56
|
-
for compose_name in ("docker-compose.yml", "docker-compose.yaml", "compose.yml", "compose.yaml"):
|
|
57
|
-
compose_file = project_dir / compose_name
|
|
58
|
-
if compose_file.exists():
|
|
59
|
-
try:
|
|
60
|
-
text = compose_file.read_text(encoding="utf-8", errors="ignore")
|
|
61
|
-
matches = re.findall(r'["\']?(\d{2,5}):(\d{2,5})["\']?', text)
|
|
62
|
-
for host_port, container_port in matches:
|
|
63
|
-
if host_port not in seen_ports and int(host_port) > 80:
|
|
64
|
-
seen_ports.add(host_port)
|
|
65
|
-
results.append(("Container Port", host_port, compose_name))
|
|
66
|
-
except Exception:
|
|
67
|
-
pass
|
|
68
|
-
|
|
69
|
-
# 2. Check .env.example / .env
|
|
70
|
-
for env_name in (".env.example", ".env", ".env.local"):
|
|
71
|
-
env_file = project_dir / env_name
|
|
72
|
-
if env_file.exists():
|
|
73
|
-
try:
|
|
74
|
-
for line in env_file.read_text(encoding="utf-8", errors="ignore").splitlines():
|
|
75
|
-
line = line.strip()
|
|
76
|
-
if line.startswith("#") or "=" not in line:
|
|
77
|
-
continue
|
|
78
|
-
key, val = line.split("=", 1)
|
|
79
|
-
key = key.strip()
|
|
80
|
-
val = val.strip().strip('"').strip("'")
|
|
81
|
-
if "PORT" in key.upper() and val.isdigit() and val not in seen_ports:
|
|
82
|
-
seen_ports.add(val)
|
|
83
|
-
results.append((key, val, env_name))
|
|
84
|
-
except Exception:
|
|
85
|
-
pass
|
|
86
|
-
|
|
87
|
-
# 3. Check Dockerfile
|
|
88
|
-
dockerfile = project_dir / "Dockerfile"
|
|
89
|
-
if dockerfile.exists():
|
|
90
|
-
try:
|
|
91
|
-
for line in dockerfile.read_text(encoding="utf-8", errors="ignore").splitlines():
|
|
92
|
-
if line.strip().upper().startswith("EXPOSE "):
|
|
93
|
-
port = line.strip().split()[1]
|
|
94
|
-
if port.isdigit() and port not in seen_ports:
|
|
95
|
-
seen_ports.add(port)
|
|
96
|
-
results.append(("Exposed Port", port, "Dockerfile"))
|
|
97
|
-
except Exception:
|
|
98
|
-
pass
|
|
99
|
-
|
|
100
|
-
return results
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
# ── Runbook Command Extractor ─────────────────────────────────────────────────
|
|
104
|
-
|
|
105
|
-
def discover_runbook_commands(project_dir: Path) -> list[tuple[str, str]]:
|
|
106
|
-
"""
|
|
107
|
-
Extract executable development, build, and test commands from project manifests.
|
|
108
|
-
Returns list of (Action, Command).
|
|
109
|
-
"""
|
|
110
|
-
commands: list[tuple[str, str]] = []
|
|
111
|
-
|
|
112
|
-
# 1. package.json scripts
|
|
113
|
-
pkg_file = project_dir / "package.json"
|
|
114
|
-
if pkg_file.exists():
|
|
115
|
-
try:
|
|
116
|
-
data = json.loads(pkg_file.read_text(encoding="utf-8", errors="ignore"))
|
|
117
|
-
scripts = data.get("scripts", {})
|
|
118
|
-
for key in ("dev", "start", "build", "test", "lint"):
|
|
119
|
-
if key in scripts:
|
|
120
|
-
commands.append((f"Run {key}", f"npm run {key}"))
|
|
121
|
-
except Exception:
|
|
122
|
-
pass
|
|
123
|
-
|
|
124
|
-
# 2. Makefile
|
|
125
|
-
makefile = project_dir / "Makefile"
|
|
126
|
-
if makefile.exists():
|
|
127
|
-
try:
|
|
128
|
-
targets = re.findall(r"^([a-zA-Z0-9_-]+):", makefile.read_text(encoding="utf-8", errors="ignore"), re.M)
|
|
129
|
-
for t in targets[:4]:
|
|
130
|
-
if not t.startswith("."):
|
|
131
|
-
commands.append((f"Make {t}", f"make {t}"))
|
|
132
|
-
except Exception:
|
|
133
|
-
pass
|
|
134
|
-
|
|
135
|
-
# 3. Python indicators
|
|
136
|
-
if (project_dir / "manage.py").exists():
|
|
137
|
-
commands.append(("Django Runserver", "python manage.py runserver"))
|
|
138
|
-
commands.append(("Django Migrate", "python manage.py migrate"))
|
|
139
|
-
elif (project_dir / "pyproject.toml").exists() or (project_dir / "requirements.txt").exists():
|
|
140
|
-
for main_candidate in ("main.py", "app.py", "server.py", "wsgi.py"):
|
|
141
|
-
if (project_dir / main_candidate).exists():
|
|
142
|
-
commands.append(("Start Python Server", f"python {main_candidate}"))
|
|
143
|
-
break
|
|
144
|
-
|
|
145
|
-
# 4. Docker Compose
|
|
146
|
-
if (project_dir / "docker-compose.yml").exists() or (project_dir / "compose.yaml").exists():
|
|
147
|
-
commands.append(("Start Docker Services", "docker compose up -d"))
|
|
148
|
-
|
|
149
|
-
return commands
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
# ── Routes & Endpoints Extractor ──────────────────────────────────────────────
|
|
153
|
-
|
|
154
|
-
def discover_routes(project_dir: Path) -> list[str]:
|
|
155
|
-
"""
|
|
156
|
-
Extract API and UI routes using Next.js, Express, FastAPI, and Django conventions.
|
|
157
|
-
Returns clean list of detected route paths.
|
|
158
|
-
"""
|
|
159
|
-
routes: list[str] = []
|
|
160
|
-
|
|
161
|
-
try:
|
|
162
|
-
# 1. Next.js App Router (app/ or src/app/)
|
|
163
|
-
app_dirs = [d for d in (project_dir / "app", project_dir / "src" / "app") if d.is_dir()]
|
|
164
|
-
for app_dir in app_dirs:
|
|
165
|
-
for file_path in app_dir.rglob("*"):
|
|
166
|
-
if file_path.is_file() and file_path.name in ("route.ts", "route.js", "page.tsx", "page.jsx"):
|
|
167
|
-
rel = file_path.parent.relative_to(app_dir).as_posix()
|
|
168
|
-
route_path = "/" if rel == "." else f"/{rel}"
|
|
169
|
-
kind = "API" if file_path.name.startswith("route") else "Page"
|
|
170
|
-
routes.append(f"[{kind}] {route_path} -> {file_path.relative_to(project_dir).as_posix()}")
|
|
171
|
-
|
|
172
|
-
# 2. Next.js Pages Router (pages/ or src/pages/)
|
|
173
|
-
page_dirs = [d for d in (project_dir / "pages", project_dir / "src" / "pages") if d.is_dir()]
|
|
174
|
-
for page_dir in page_dirs:
|
|
175
|
-
for file_path in page_dir.rglob("*.tsx"):
|
|
176
|
-
if not file_path.name.startswith("_"):
|
|
177
|
-
rel = file_path.relative_to(page_dir).with_suffix("").as_posix()
|
|
178
|
-
route_path = "/" if rel == "index" else f"/{rel.replace('/index', '')}"
|
|
179
|
-
kind = "API" if "api" in rel else "Page"
|
|
180
|
-
routes.append(f"[{kind}] {route_path} -> {file_path.relative_to(project_dir).as_posix()}")
|
|
181
|
-
|
|
182
|
-
# 3. Express / FastAPI / Django Route folders
|
|
183
|
-
for folder_name in ("routes", "controllers", "routers", "endpoints"):
|
|
184
|
-
for candidate in (project_dir / folder_name, project_dir / "src" / folder_name):
|
|
185
|
-
if candidate.is_dir():
|
|
186
|
-
for f in candidate.glob("*.py") if candidate.glob("*.py") else candidate.glob("*.ts"):
|
|
187
|
-
routes.append(f"[Module] {f.stem} -> {f.relative_to(project_dir).as_posix()}")
|
|
188
|
-
|
|
189
|
-
except Exception:
|
|
190
|
-
pass
|
|
191
|
-
|
|
192
|
-
return routes[:35] # Cap to prevent unbounded lists
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
# ── Database Models Extractor ─────────────────────────────────────────────────
|
|
196
|
-
|
|
197
|
-
def discover_database_models(project_dir: Path) -> list[str]:
|
|
198
|
-
"""
|
|
199
|
-
Extract data models from Prisma, Django, SQLAlchemy, Drizzle schemas.
|
|
200
|
-
Returns list of discovered model names and schema files.
|
|
201
|
-
"""
|
|
202
|
-
models: list[str] = []
|
|
203
|
-
|
|
204
|
-
try:
|
|
205
|
-
# 1. Prisma schema
|
|
206
|
-
for prisma_path in (project_dir / "prisma" / "schema.prisma", project_dir / "schema.prisma"):
|
|
207
|
-
if prisma_path.exists():
|
|
208
|
-
text = prisma_path.read_text(encoding="utf-8", errors="ignore")
|
|
209
|
-
found = re.findall(r"\bmodel\s+([A-Za-z0-9_]+)\s*\{", text)
|
|
210
|
-
for m in found:
|
|
211
|
-
models.append(f"[Prisma] {m} ({prisma_path.relative_to(project_dir).as_posix()})")
|
|
212
|
-
|
|
213
|
-
# 2. Django / SQLAlchemy models.py
|
|
214
|
-
for models_file in project_dir.rglob("models.py"):
|
|
215
|
-
if "venv" not in models_file.parts and ".git" not in models_file.parts:
|
|
216
|
-
text = models_file.read_text(encoding="utf-8", errors="ignore")
|
|
217
|
-
found = re.findall(r"\bclass\s+([A-Za-z0-9_]+)\s*\(", text)
|
|
218
|
-
for m in found:
|
|
219
|
-
if m not in ("Meta",):
|
|
220
|
-
models.append(f"[Model] {m} ({models_file.relative_to(project_dir).as_posix()})")
|
|
221
|
-
|
|
222
|
-
# 3. Drizzle schema
|
|
223
|
-
for drizzle_file in project_dir.rglob("schema.ts"):
|
|
224
|
-
if "node_modules" not in drizzle_file.parts:
|
|
225
|
-
text = drizzle_file.read_text(encoding="utf-8", errors="ignore")
|
|
226
|
-
found = re.findall(r"export\s+const\s+([A-Za-z0-9_]+)\s+=\s+(?:pgTable|mysqlTable|sqliteTable)", text)
|
|
227
|
-
for m in found:
|
|
228
|
-
models.append(f"[Drizzle] {m} ({drizzle_file.relative_to(project_dir).as_posix()})")
|
|
229
|
-
|
|
230
|
-
except Exception:
|
|
231
|
-
pass
|
|
232
|
-
|
|
233
|
-
return models[:30]
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
# ── Satellites Generator (.agents/context/) ───────────────────────────────────
|
|
237
|
-
|
|
238
|
-
def generate_satellites(project_dir: Path, force: bool = False) -> dict[str, Path]:
|
|
239
|
-
"""
|
|
240
|
-
Generate stack-adaptive satellite files in .agents/context/ on-demand.
|
|
241
|
-
Enforces strict idempotency and zero token bloat in the main hub.
|
|
242
|
-
"""
|
|
243
|
-
satellites_dir = project_dir / ".agents" / "context"
|
|
244
|
-
satellites_dir.mkdir(parents=True, exist_ok=True)
|
|
245
|
-
generated: dict[str, Path] = {}
|
|
246
|
-
|
|
247
|
-
routes = discover_routes(project_dir)
|
|
248
|
-
models = discover_database_models(project_dir)
|
|
249
|
-
|
|
250
|
-
# 1. routes.md (if web routes detected)
|
|
251
|
-
if routes:
|
|
252
|
-
routes_path = satellites_dir / "routes.md"
|
|
253
|
-
routes_content = (
|
|
254
|
-
"# System Route & Endpoint Map\n\n"
|
|
255
|
-
"> Discovered automatically via convention scanner. Used by AI agents for precision navigation.\n\n"
|
|
256
|
-
"| Kind | Route Path | Implementation File |\n"
|
|
257
|
-
"| :--- | :--- | :--- |\n"
|
|
258
|
-
)
|
|
259
|
-
for r in routes:
|
|
260
|
-
parts = r.split(" -> ")
|
|
261
|
-
kind_path = parts[0]
|
|
262
|
-
impl = parts[1] if len(parts) > 1 else ""
|
|
263
|
-
routes_content += f"| {kind_path} | `{impl}` |\n"
|
|
264
|
-
|
|
265
|
-
write_idempotent(routes_path, routes_content, "Routes Satellite")
|
|
266
|
-
generated["routes"] = routes_path
|
|
267
|
-
|
|
268
|
-
# 2. schema.md (if database models detected)
|
|
269
|
-
if models:
|
|
270
|
-
schema_path = satellites_dir / "schema.md"
|
|
271
|
-
schema_content = (
|
|
272
|
-
"# Database Schema & Entity Models\n\n"
|
|
273
|
-
"> Discovered data models and ORM schema definitions. Used by AI agents for database context.\n\n"
|
|
274
|
-
"| Model / Entity | Definition File |\n"
|
|
275
|
-
"| :--- | :--- |\n"
|
|
276
|
-
)
|
|
277
|
-
for m in models:
|
|
278
|
-
schema_content += f"| {m} |\n"
|
|
279
|
-
|
|
280
|
-
write_idempotent(schema_path, schema_content, "Schema Satellite")
|
|
281
|
-
generated["schema"] = schema_path
|
|
282
|
-
|
|
283
|
-
# 3. gotchas.md (Critical Traps & Anti-Patterns — Replaces useless bug diaries)
|
|
284
|
-
gotchas_path = satellites_dir / "gotchas.md"
|
|
285
|
-
if not gotchas_path.exists() or force:
|
|
286
|
-
gotchas_content = (
|
|
287
|
-
"# Critical Gotchas, Anti-Patterns & Architecture Invariants\n\n"
|
|
288
|
-
"> Institutional knowledge and non-obvious traps that cannot be guessed from reading code.\n\n"
|
|
289
|
-
"## ⚠️ Active Constraints & Traps\n\n"
|
|
290
|
-
"1. **Port Invariants:** Inspect discovered ports before binding local services.\n"
|
|
291
|
-
"2. **Secret Isolation:** Production secrets belong in `.env`; dummy dev accounts belong in `context.local.md`.\n"
|
|
292
|
-
"3. **State Boundaries:** Verify migrations before initiating build or release cycles.\n"
|
|
293
|
-
)
|
|
294
|
-
write_idempotent(gotchas_path, gotchas_content, "Gotchas Satellite")
|
|
295
|
-
generated["gotchas"] = gotchas_path
|
|
296
|
-
|
|
297
|
-
return generated
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{auto_context_cli-1.0.4 → auto_context_cli-1.0.5}/src/auto_context/templates/AGENTS.template.md
RENAMED
|
File without changes
|
{auto_context_cli-1.0.4 → auto_context_cli-1.0.5}/src/auto_context/templates/GEMINI.template.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{auto_context_cli-1.0.4 → auto_context_cli-1.0.5}/src/auto_context/templates/profile.template.md
RENAMED
|
File without changes
|
{auto_context_cli-1.0.4 → auto_context_cli-1.0.5}/src/auto_context/templates/rule.template.md
RENAMED
|
File without changes
|
{auto_context_cli-1.0.4 → auto_context_cli-1.0.5}/src/auto_context/templates/rules/code-standards.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{auto_context_cli-1.0.4 → auto_context_cli-1.0.5}/src/auto_context/templates/skill.template.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|