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.
Files changed (36) hide show
  1. {auto_context_cli-1.0.4 → auto_context_cli-1.0.5}/PKG-INFO +2 -2
  2. {auto_context_cli-1.0.4 → auto_context_cli-1.0.5}/README.md +1 -1
  3. {auto_context_cli-1.0.4 → auto_context_cli-1.0.5}/pyproject.toml +1 -1
  4. {auto_context_cli-1.0.4 → auto_context_cli-1.0.5}/src/auto_context/__init__.py +1 -1
  5. {auto_context_cli-1.0.4 → auto_context_cli-1.0.5}/src/auto_context/commands/init_cmd.py +11 -0
  6. {auto_context_cli-1.0.4 → auto_context_cli-1.0.5}/src/auto_context/commands/scan_cmd.py +8 -3
  7. {auto_context_cli-1.0.4 → auto_context_cli-1.0.5}/src/auto_context/main.py +1 -1
  8. auto_context_cli-1.0.5/src/auto_context/scanner.py +421 -0
  9. {auto_context_cli-1.0.4 → auto_context_cli-1.0.5}/src/auto_context/templates/context.template.md +1 -2
  10. auto_context_cli-1.0.4/src/auto_context/scanner.py +0 -297
  11. {auto_context_cli-1.0.4 → auto_context_cli-1.0.5}/.gitignore +0 -0
  12. {auto_context_cli-1.0.4 → auto_context_cli-1.0.5}/LICENSE +0 -0
  13. {auto_context_cli-1.0.4 → auto_context_cli-1.0.5}/src/auto_context/commands/__init__.py +0 -0
  14. {auto_context_cli-1.0.4 → auto_context_cli-1.0.5}/src/auto_context/commands/import_cmd.py +0 -0
  15. {auto_context_cli-1.0.4 → auto_context_cli-1.0.5}/src/auto_context/commands/mcp.py +0 -0
  16. {auto_context_cli-1.0.4 → auto_context_cli-1.0.5}/src/auto_context/commands/memory.py +0 -0
  17. {auto_context_cli-1.0.4 → auto_context_cli-1.0.5}/src/auto_context/commands/share.py +0 -0
  18. {auto_context_cli-1.0.4 → auto_context_cli-1.0.5}/src/auto_context/commands/update_cmd.py +0 -0
  19. {auto_context_cli-1.0.4 → auto_context_cli-1.0.5}/src/auto_context/config.py +0 -0
  20. {auto_context_cli-1.0.4 → auto_context_cli-1.0.5}/src/auto_context/security.py +0 -0
  21. {auto_context_cli-1.0.4 → auto_context_cli-1.0.5}/src/auto_context/templates/AGENTS.template.md +0 -0
  22. {auto_context_cli-1.0.4 → auto_context_cli-1.0.5}/src/auto_context/templates/GEMINI.template.md +0 -0
  23. {auto_context_cli-1.0.4 → auto_context_cli-1.0.5}/src/auto_context/templates/context.local.template.md +0 -0
  24. {auto_context_cli-1.0.4 → auto_context_cli-1.0.5}/src/auto_context/templates/maintain_context.template.md +0 -0
  25. {auto_context_cli-1.0.4 → auto_context_cli-1.0.5}/src/auto_context/templates/mcp_config.template.json +0 -0
  26. {auto_context_cli-1.0.4 → auto_context_cli-1.0.5}/src/auto_context/templates/profile.template.md +0 -0
  27. {auto_context_cli-1.0.4 → auto_context_cli-1.0.5}/src/auto_context/templates/rule.template.md +0 -0
  28. {auto_context_cli-1.0.4 → auto_context_cli-1.0.5}/src/auto_context/templates/rules/code-standards.md +0 -0
  29. {auto_context_cli-1.0.4 → auto_context_cli-1.0.5}/src/auto_context/templates/rules/hub-satellite-architecture.md +0 -0
  30. {auto_context_cli-1.0.4 → auto_context_cli-1.0.5}/src/auto_context/templates/rules/readme-docs-sync.md +0 -0
  31. {auto_context_cli-1.0.4 → auto_context_cli-1.0.5}/src/auto_context/templates/rules/security-boundaries.md +0 -0
  32. {auto_context_cli-1.0.4 → auto_context_cli-1.0.5}/src/auto_context/templates/skill.template.md +0 -0
  33. {auto_context_cli-1.0.4 → auto_context_cli-1.0.5}/src/auto_context/templates/skills/browser-testing/SKILL.md +0 -0
  34. {auto_context_cli-1.0.4 → auto_context_cli-1.0.5}/src/auto_context/templates/skills/docker-ops/SKILL.md +0 -0
  35. {auto_context_cli-1.0.4 → auto_context_cli-1.0.5}/src/auto_context/templates/skills/git-workflow/SKILL.md +0 -0
  36. {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.4
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
  [![PyPI - Version](https://img.shields.io/pypi/v/auto-context-cli.svg?color=blue)](https://pypi.org/project/auto-context-cli/)
22
22
  [![PyPI - Python Version](https://img.shields.io/pypi/pyversions/auto-context-cli.svg)](https://pypi.org/project/auto-context-cli/)
23
- [![GitHub Release](https://img.shields.io/github/v/release/Cyber-arghya/auto-context?color=green)](https://github.com/Cyber-arghya/auto-context/releases/tag/v1.0.4)
23
+ [![GitHub Release](https://img.shields.io/github/v/release/Cyber-arghya/auto-context?color=green)](https://github.com/Cyber-arghya/auto-context/releases/tag/v1.0.5)
24
24
  [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT)
25
25
  [![Platform](https://img.shields.io/badge/Platform-Windows%20%7C%20macOS%20%7C%20Linux%20WSL2-success.svg)](#-quick-one-line-install)
26
26
  [![AI Supported](https://img.shields.io/badge/AI%20Supported-Antigravity%20%7C%20Cursor%20%7C%20Claude%20Code%20%7C%20Copilot%20%7C%20Windsurf-orange.svg)](#-supported-ai-assistants)
@@ -6,7 +6,7 @@
6
6
 
7
7
  [![PyPI - Version](https://img.shields.io/pypi/v/auto-context-cli.svg?color=blue)](https://pypi.org/project/auto-context-cli/)
8
8
  [![PyPI - Python Version](https://img.shields.io/pypi/pyversions/auto-context-cli.svg)](https://pypi.org/project/auto-context-cli/)
9
- [![GitHub Release](https://img.shields.io/github/v/release/Cyber-arghya/auto-context?color=green)](https://github.com/Cyber-arghya/auto-context/releases/tag/v1.0.4)
9
+ [![GitHub Release](https://img.shields.io/github/v/release/Cyber-arghya/auto-context?color=green)](https://github.com/Cyber-arghya/auto-context/releases/tag/v1.0.5)
10
10
  [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT)
11
11
  [![Platform](https://img.shields.io/badge/Platform-Windows%20%7C%20macOS%20%7C%20Linux%20WSL2-success.svg)](#-quick-one-line-install)
12
12
  [![AI Supported](https://img.shields.io/badge/AI%20Supported-Antigravity%20%7C%20Cursor%20%7C%20Claude%20Code%20%7C%20Copilot%20%7C%20Windsurf-orange.svg)](#-supported-ai-assistants)
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "auto-context-cli"
3
- version = "1.0.4"
3
+ version = "1.0.5"
4
4
  description = "Autonomous context & instruction manager for AI coding assistants"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.11"
@@ -1,3 +1,3 @@
1
1
  """auto-context: Autonomous context & instruction manager for AI coding assistants."""
2
2
 
3
- __version__ = "1.0.4"
3
+ __version__ = "1.0.5"
@@ -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
- ports_table += "| **App Port** | `3000` (Default) | Inferred |\n"
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 += "| Start Development | `npm run dev` / `python main.py` |\n"
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 = ""
@@ -69,7 +69,7 @@ def root(
69
69
  try:
70
70
  v = pkg_version("auto-context")
71
71
  except Exception:
72
- v = "1.0.4"
72
+ v = "1.0.5"
73
73
  console.print(f"init-context v{v}")
74
74
  raise typer.Exit()
75
75
 
@@ -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
@@ -28,8 +28,7 @@
28
28
 
29
29
  | Action | Command |
30
30
  | :--- | :--- |
31
- | **Start Development** | `npm run dev` / `python main.py` |
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