codebookx 3.0.0__py3-none-any.whl

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.
codebookx/__init__.py ADDED
@@ -0,0 +1 @@
1
+ __version__ = "3.0.0"
codebookx/cli.py ADDED
@@ -0,0 +1,85 @@
1
+ import argparse
2
+ import sys
3
+ from pathlib import Path
4
+ from .core import run_generate, run_analyze, run_decompose, run_enhance, run_ask, run_ask_chat
5
+ from .webapp.app import run_server
6
+
7
+ def main():
8
+ parser = argparse.ArgumentParser(
9
+ prog="codebookx",
10
+ description="Codebase-X: Evolution of Codebook for offline code comprehension."
11
+ )
12
+
13
+ subparsers = parser.add_subparsers(dest="command", help="Available commands")
14
+
15
+ # Generate (Classic Codebook)
16
+ gen_parser = subparsers.add_parser("generate", help="Generate classic CODEBASE_BOOK.md")
17
+ gen_parser.add_argument("target_dir", nargs="?", default=".", help="Directory to scan")
18
+ gen_parser.add_argument("--url", help="LM Studio base URL")
19
+ gen_parser.add_argument("--model", help="Model name")
20
+ gen_parser.add_argument("--output", help="Output file path")
21
+ gen_parser.add_argument("--prompt-lang", help="Language for explanations")
22
+ gen_parser.add_argument("--file", help="Annotate single file only")
23
+
24
+ # Analyze (PCTF)
25
+ analyze_parser = subparsers.add_parser("analyze", help="Run PCTF Codebase Teardown")
26
+ analyze_parser.add_argument("target_dir", nargs="?", default=".", help="Directory to scan")
27
+ analyze_parser.add_argument("--force", action="store_true", help="Force re-analysis")
28
+
29
+ # Decompose (AATD)
30
+ decompose_parser = subparsers.add_parser("decompose", help="Run AATD Feature Decomposition")
31
+ decompose_parser.add_argument("feature", help="Feature description to decompose")
32
+ decompose_parser.add_argument("target_dir", nargs="?", default=".", help="Directory to scan")
33
+ decompose_parser.add_argument("--deep", action="store_true", help="Use deep context (repomix)")
34
+
35
+ # Enhance (New: Prompt Enhancement)
36
+ enhance_parser = subparsers.add_parser("enhance", help="Enhance a prompt with codebase context (requires: codebook analyze first)")
37
+ enhance_parser.add_argument("feature", help="Feature description to enhance")
38
+ enhance_parser.add_argument("target_dir", nargs="?", default=".", help="Directory to scan")
39
+
40
+ # Ask
41
+ ask_parser = subparsers.add_parser("ask", help="Ask a question about the codebase (requires: codebook analyze first)")
42
+ ask_parser.add_argument("question", help="The question to ask about your codebase")
43
+ ask_parser.add_argument("target_dir", nargs="?", default=".", help="Directory to scan")
44
+ ask_parser.add_argument("--dir", default=None,
45
+ help="Directory to save Q&A log (default: $CODEBOOK_ASK_DIR or ./ask_history)")
46
+ ask_parser.add_argument("-c", "--chat", action="store_true",
47
+ help="Interactive chat mode (multi-turn, context carry-over) (requires: codebook analyze first)")
48
+
49
+ # View
50
+ view_parser = subparsers.add_parser("view", help="Launch local Knowledge Graph web UI")
51
+ view_parser.add_argument("target_dir", nargs="?", default=".", help="Directory to scan")
52
+ view_parser.add_argument("--port", type=int, default=8050, help="Port to run the UI on")
53
+
54
+ # Backward compatibility: Intercept sys.argv
55
+ if len(sys.argv) > 1 and not sys.argv[1].startswith("-") and sys.argv[1] not in ["generate", "analyze", "decompose", "ask", "enhance", "view", "-h", "--help"]:
56
+ sys.argv.insert(1, "generate")
57
+ elif len(sys.argv) == 1:
58
+ sys.argv.append("generate")
59
+
60
+ args = parser.parse_args()
61
+
62
+ if args.command == "generate":
63
+ run_generate(args)
64
+ elif args.command == "analyze":
65
+ run_analyze(args)
66
+ elif args.command == "decompose":
67
+ run_decompose(args)
68
+ elif args.command == "enhance":
69
+ run_enhance(args)
70
+ elif args.command == "ask":
71
+ if getattr(args, "chat", False):
72
+ run_ask_chat(args)
73
+ else:
74
+ run_ask(args)
75
+ elif args.command == "view":
76
+ root = Path(args.target_dir or ".").resolve()
77
+ db_path = root / ".codebook_cache.db"
78
+ print(f"Starting local UI on http://localhost:{args.port}...")
79
+ print(f"Using database: {db_path}")
80
+ run_server(port=args.port, db_path=str(db_path))
81
+ else:
82
+ parser.print_help()
83
+
84
+ if __name__ == "__main__":
85
+ main()
codebookx/core.py ADDED
@@ -0,0 +1,452 @@
1
+ import argparse
2
+ import os
3
+ import sys
4
+ from datetime import datetime
5
+ from pathlib import Path
6
+ from typing import Optional
7
+
8
+ import questionary
9
+ from tqdm import tqdm
10
+
11
+ from .llm import check_server, stream_chat_completion
12
+ from .engine.indexer import Indexer
13
+ from .engine.parser import extract_snippets
14
+ from .engine.vendor.claude_mem_lite import generate_code_skeleton
15
+ from .engine.vendor.repomix_lite import pack_repo
16
+ from .engine.graph import KnowledgeGraph
17
+ from .prompts import PCTF_SYSTEM_PROMPT, AATD_SYSTEM_PROMPT, ENHANCE_SYSTEM_PROMPT, QA_SYSTEM_PROMPT
18
+
19
+ # ──────────────────────────────────────────────────────────────────────────────
20
+ # CONFIG & PROMPTS
21
+ # ──────────────────────────────────────────────────────────────────────────────
22
+
23
+ SYSTEM_PROMPT = """You explain code like a friendly senior engineer talking to someone who just started learning to code. Think of it like explaining to a smart 7th grader — no jargon, no lectures, just clear and casual conversation.
24
+
25
+ YOUR RULES:
26
+ 1. Always start with one sentence: what does this code actually DO? Plain English, no tech words.
27
+ 2. Then walk through the important parts step by step. For each part, say what it does in everyday language first, then mention the technical term in parentheses if needed.
28
+ 3. Use a real-world analogy whenever you can. The best analogies are things anyone would recognize — kitchens, libraries, phone calls, to-do lists.
29
+ 4. Keep it short. 6 to 10 lines max. No padding, no filler.
30
+ 5. Never use a technical word without explaining it first. If you must use one, explain it right there in plain English.
31
+ 6. Write like you're texting a smart friend — casual, warm, and direct. Not a textbook. Not a lecture.
32
+
33
+ TONE: Friendly, clear, conversational. Like a helpful older sibling who happens to know how to code."""
34
+
35
+ FEW_SHOT_EXAMPLES = [
36
+ {
37
+ "role": "user",
38
+ "content": "File: math.py | Snippet: add_numbers\n\n```python\ndef add_numbers(a, b):\n result = a + b\n return result\n```",
39
+ },
40
+ {
41
+ "role": "assistant",
42
+ "content": "This takes two numbers, adds them together, and gives you back the answer.\n\nHere's what happens inside:\n- `add_numbers(a, b)` — the function takes two numbers as input. Think of `a` and `b` as two blank boxes you fill in when you use it.\n- `result = a + b` — it adds them and stores the answer in a variable called `result`. A variable is just a labeled box that holds a value.\n- `return result` — it hands the answer back to whoever called it. `return` is basically saying \"here's your answer, I'm done.\"\n\nReal-world version: it's a calculator. You punch in two numbers, it spits out the sum.",
43
+ },
44
+ ]
45
+
46
+ def load_config(args, root: Path) -> dict:
47
+ defaults = {
48
+ "url": "http://localhost:1234/v1",
49
+ "model": None,
50
+ "output": "CODEBASE_BOOK.md",
51
+ "skip_extensions": [],
52
+ "skip_dirs": ["node_modules", ".next", "__pycache__", ".git", "dist", "build", ".beads"],
53
+ "prompt_lang": "simple English",
54
+ "max_context": 30000,
55
+ }
56
+ config_file = root / "codebook.toml"
57
+ file_config = {}
58
+ if config_file.exists():
59
+ try:
60
+ import tomllib
61
+ except ImportError:
62
+ import tomli as tomllib
63
+ try:
64
+ with open(config_file, "rb") as f:
65
+ file_config = tomllib.load(f)
66
+ except Exception as e:
67
+ print(f"Warning: Could not read {config_file}: {e}")
68
+
69
+ config = {**defaults, **file_config}
70
+ config["url"] = os.environ.get("CODEBOOKX_URL") or config["url"]
71
+ if hasattr(args, 'url') and args.url: config["url"] = args.url
72
+ if hasattr(args, 'model') and args.model: config["model"] = args.model
73
+ if hasattr(args, 'output') and args.output: config["output"] = args.output
74
+ if hasattr(args, 'prompt_lang') and args.prompt_lang: config["prompt_lang"] = args.prompt_lang
75
+ return config
76
+
77
+ # ──────────────────────────────────────────────────────────────────────────────
78
+ # FILE DISCOVERY
79
+ # ──────────────────────────────────────────────────────────────────────────────
80
+
81
+ def get_all_extensions(root: Path, skip_dirs: set[str]) -> dict[str, list[str]]:
82
+ import os
83
+ code_extensions = {".py", ".ts", ".tsx", ".js", ".jsx", ".go", ".rs", ".java", ".kt", ".cpp", ".cc", ".c", ".h", ".cs", ".rb", ".php", ".swift"}
84
+ doc_extensions = {".md", ".markdown", ".rst", ".txt"}
85
+ config_extensions = {".json", ".yaml", ".yml", ".xml", ".toml"}
86
+ categories = {"CODE": set(), "DOCS": set(), "CONFIG": set(), "OTHER": set()}
87
+ binary_extensions = {".exe", ".dll", ".so", ".zip", ".tar", ".gz", ".pdf", ".png", ".jpg", ".jpeg", ".gif", ".svg"}
88
+
89
+ try:
90
+ for r, dirs, filenames in os.walk(root):
91
+ # Prune skip_dirs in-place
92
+ dirs[:] = [d for d in dirs if d not in skip_dirs]
93
+ for f in filenames:
94
+ ext = Path(f).suffix.lower()
95
+ if not ext or ext in binary_extensions: continue
96
+ if ext in code_extensions: categories["CODE"].add(ext)
97
+ elif ext in doc_extensions: categories["DOCS"].add(ext)
98
+ elif ext in config_extensions: categories["CONFIG"].add(ext)
99
+ else: categories["OTHER"].add(ext)
100
+ except Exception as e: print(f"Error: {e}")
101
+ return {k: sorted(list(v)) for k, v in categories.items() if v}
102
+
103
+ def run_setup_wizard(root: Path, config: dict) -> dict:
104
+ print("\n📚 Codebase-X — Setup\n")
105
+ ext_by_category = get_all_extensions(root, set(config["skip_dirs"]))
106
+ choices = []
107
+ for category, exts in ext_by_category.items():
108
+ for ext in exts: choices.append(f"{ext} ({category})")
109
+
110
+ if not choices:
111
+ return config
112
+
113
+ skip_choices = questionary.checkbox(
114
+ "Which file types should be SKIPPED?",
115
+ choices=choices
116
+ ).ask()
117
+
118
+ config["skip_extensions"] = [c.split()[0] for c in skip_choices] if skip_choices else []
119
+ config["prompt_lang"] = "simple English"
120
+ return config
121
+
122
+ def get_files(root: Path, skip_extensions: list[str], skip_dirs: set[str], single: Optional[str] = None) -> list[Path]:
123
+ if single:
124
+ p = Path(single)
125
+ return [p] if p.exists() else []
126
+
127
+ import os
128
+ skip_ext_lower = set(ext.lower() for ext in skip_extensions)
129
+ files = []
130
+ try:
131
+ for r, dirs, filenames in os.walk(root):
132
+ # Prune skip_dirs in-place
133
+ dirs[:] = [d for d in dirs if d not in skip_dirs]
134
+ for f in filenames:
135
+ file_path = Path(r) / f
136
+ if file_path.suffix.lower() in skip_ext_lower:
137
+ continue
138
+ files.append(file_path)
139
+ except Exception as e: print(f"Error: {e}")
140
+ return sorted(files)
141
+
142
+ # ──────────────────────────────────────────────────────────────────────────────
143
+ # OUTPUT (LEGACY)
144
+ # ──────────────────────────────────────────────────────────────────────────────
145
+
146
+ def already_done(name: str, path: str, output_file: Path) -> bool:
147
+ if not output_file.exists(): return False
148
+ content = output_file.read_text(errors="ignore")
149
+ return f"`{name}` — {path}" in content
150
+
151
+ def init_book(output_file: Path, root: Path, model: str) -> None:
152
+ if output_file.exists(): return
153
+ header = f"# Codebase Book\n\nAuto-generated by [Codebase-X].\nModel: {model} | Generated: {datetime.now()}\n\n---\n\n"
154
+ output_file.parent.mkdir(parents=True, exist_ok=True)
155
+ output_file.write_text(header)
156
+
157
+ def append_to_book(file_path: str, snippet: dict, explanation: str, output_file: Path) -> None:
158
+ entry = f"## `{snippet['name']}` — {file_path}\n\n**Lines {snippet['start']}–{snippet['end']}**\n\n{explanation}\n\n---\n\n"
159
+ with open(output_file, "a", encoding="utf-8") as f: f.write(entry)
160
+
161
+ # ──────────────────────────────────────────────────────────────────────────────
162
+ # COMMAND RUNNERS
163
+ # ──────────────────────────────────────────────────────────────────────────────
164
+
165
+ def run_ask(args):
166
+ """Run Q&A about the codebase (KG-backed)."""
167
+ root = Path(args.target_dir or ".").resolve()
168
+ config = load_config(args, root)
169
+
170
+ question = args.question
171
+ if not question.strip():
172
+ sys.exit("⚠️ Please provide a question. Usage: codebookx ask 'What does X do?'")
173
+
174
+ teardown_path = root / "CODEBASE_TEARDOWN.md"
175
+ if not teardown_path.exists():
176
+ sys.exit("⚠️ No teardown found at CODEBASE_TEARDOWN.md. Please run 'codebookx analyze' first.")
177
+
178
+ context = teardown_path.read_text(errors="ignore")
179
+ if not context.strip():
180
+ sys.exit("⚠️ Teardown file is empty. Run 'codebookx analyze' first.")
181
+
182
+ # NEW: KG-backed symbol context (graceful fallback if KG missing)
183
+ db_path = root / ".codebook_cache.db"
184
+ symbol_context = ""
185
+ if db_path.exists():
186
+ try:
187
+ kg = KnowledgeGraph(str(db_path))
188
+ symbol_context = kg.get_symbol_context_for_question(question)
189
+ except Exception:
190
+ pass # Graceful fallback — teardown-only is fine
191
+
192
+ if symbol_context:
193
+ full_context = f"# Codebase Teardown\n{context}\n\n# Symbols\n{symbol_context}"
194
+ else:
195
+ full_context = context # P0.6 fallback — teardown only
196
+
197
+ is_running, model = check_server(config["url"])
198
+ if not is_running:
199
+ print(f"⚠️ LLM server not running at {config['url']}. Start your server and try again.")
200
+ return
201
+
202
+ messages = [
203
+ {"role": "system", "content": QA_SYSTEM_PROMPT},
204
+ {"role": "user", "content": f"Codebase Context:\n{full_context}\n\nQuestion: {question}"}
205
+ ]
206
+ answer = stream_chat_completion(config["url"], model or "unknown", messages)
207
+ print(f"\n{answer}")
208
+
209
+ # S5.2: Save Q&A to log file
210
+ ask_dir = args.dir or os.environ.get("CODEBOOK_ASK_DIR") or str(root / "ask_history")
211
+ try:
212
+ Path(ask_dir).mkdir(parents=True, exist_ok=True)
213
+ except FileExistsError:
214
+ sys.exit(f"⚠️ {ask_dir} exists but is not a directory.")
215
+ timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
216
+ log_file = Path(ask_dir) / f"ask_{timestamp}.md"
217
+ log_file.write_text(f"# Question\n\n{question}\n\n# Answer\n\n{answer}")
218
+ print(f"💾 Saved to {log_file}")
219
+
220
+ def run_ask_chat(args):
221
+ """Interactive chat mode — multi-turn with context carry-over."""
222
+ root = Path(args.target_dir or ".").resolve()
223
+ config = load_config(args, root)
224
+
225
+ ask_dir = getattr(args, "dir", None) or os.environ.get("CODEBOOK_ASK_DIR") or str(root / "ask_history")
226
+ try:
227
+ Path(ask_dir).mkdir(parents=True, exist_ok=True)
228
+ except FileExistsError:
229
+ sys.exit(f"⚠️ {ask_dir} exists but is not a directory.")
230
+
231
+ timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
232
+ log_file = Path(ask_dir) / f"chat_{timestamp}.md"
233
+ with log_file.open("a", encoding="utf-8") as f:
234
+ f.write(f"# Chat Session — {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n")
235
+ f.write(f"Root: {root}\n\n")
236
+
237
+ question = args.question
238
+ if not question.strip():
239
+ sys.exit("⚠️ Please provide a question. Usage: codebookx ask -c 'What does X do?'")
240
+
241
+ teardown_path = root / "CODEBASE_TEARDOWN.md"
242
+ if not teardown_path.exists():
243
+ sys.exit("⚠️ No teardown found. Please run 'codebookx analyze' first.")
244
+
245
+ context = teardown_path.read_text(errors="ignore")
246
+ if not context.strip():
247
+ sys.exit("⚠️ Teardown is empty.")
248
+
249
+ # KG context (graceful fallback)
250
+ db_path = root / ".codebook_cache.db"
251
+ symbol_context = ""
252
+ if db_path.exists():
253
+ try:
254
+ kg = KnowledgeGraph(str(db_path))
255
+ symbol_context = kg.get_symbol_context_for_question(question)
256
+ except Exception:
257
+ pass
258
+
259
+ if symbol_context:
260
+ full_context = f"# Codebase Teardown\n{context}\n\n# Symbols\n{symbol_context}"
261
+ else:
262
+ full_context = context
263
+
264
+ is_running, model = check_server(config["url"])
265
+ if not is_running:
266
+ print(f"⚠️ LLM server not running at {config['url']}. Start your server and try again.")
267
+ return
268
+
269
+ # First turn: inject full context + question
270
+ messages = [
271
+ {"role": "system", "content": QA_SYSTEM_PROMPT},
272
+ {"role": "user", "content": f"Codebase Context:\n{full_context}\n\nQuestion: {question}"}
273
+ ]
274
+ print(f"\n🤖 {model or 'unknown'}")
275
+ answer = stream_chat_completion(config["url"], model or "unknown", messages)
276
+ print(f"\n{answer}")
277
+ messages.append({"role": "assistant", "content": answer})
278
+
279
+ with log_file.open("a", encoding="utf-8") as f:
280
+ f.write(f"## Question\n\n{question}\n\n## Answer\n\n{answer}\n\n")
281
+
282
+ # Follow-up turns: just the new question (full history in messages)
283
+ try:
284
+ while True:
285
+ user_input = input("\n💬 prompt> ").strip()
286
+ if user_input.lower() in ("exit", "quit", "/exit", "/quit"):
287
+ with log_file.open("a", encoding="utf-8") as f:
288
+ f.write("---\n*Session ended.*\n")
289
+ print("👋 Goodbye!")
290
+ break
291
+ if not user_input:
292
+ continue
293
+ messages.append({"role": "user", "content": user_input})
294
+ answer = stream_chat_completion(config["url"], model or "unknown", messages)
295
+ print(f"\n{answer}")
296
+ messages.append({"role": "assistant", "content": answer})
297
+
298
+ with log_file.open("a", encoding="utf-8") as f:
299
+ f.write(f"## Question\n\n{user_input}\n\n## Answer\n\n{answer}\n\n")
300
+
301
+ except (KeyboardInterrupt, EOFError):
302
+ with log_file.open("a", encoding="utf-8") as f:
303
+ f.write("---\n*Session ended (interrupted).*\n")
304
+ print("\n👋 Goodbye!")
305
+
306
+ def run_generate(args):
307
+ """The original Codebook documentation generation logic."""
308
+ root = Path(args.target_dir or ".").resolve()
309
+ config = load_config(args, root)
310
+ print("📚 Codebase-X — Checking LLM server...")
311
+ is_running, detected_model = check_server(config["url"])
312
+ if not is_running:
313
+ print(f"⚠️ LLM server not running at {config['url']}. Start your server and try again.")
314
+ return
315
+ if not config["model"]:
316
+ config["model"] = detected_model or "unknown"
317
+
318
+ print(f"✓ Server ready. Model: {config['model']}\n")
319
+ config = run_setup_wizard(root, config)
320
+
321
+ single_file = args.file if hasattr(args, 'file') else None
322
+ files = get_files(root, config["skip_extensions"], set(config["skip_dirs"]), single_file)
323
+
324
+ total_functions = 0
325
+ file_snippets = {}
326
+ for file_path in files:
327
+ try:
328
+ source = file_path.read_text(errors="ignore")
329
+ snippets = extract_snippets(file_path, source)
330
+ if snippets:
331
+ file_snippets[file_path] = snippets
332
+ total_functions += len(snippets)
333
+ except Exception as e: print(f"Error: {e}")
334
+
335
+ if total_functions == 0:
336
+ print("No functions found."); return
337
+
338
+ output_file = root / config["output"]
339
+ init_book(output_file, root, config["model"])
340
+ print(f"Found {len(files)} files with {total_functions} functions\n")
341
+
342
+ with tqdm(total=total_functions, file=sys.stderr, ncols=80, colour="green") as pbar:
343
+ for file_path, snippets in file_snippets.items():
344
+ rel_path = file_path.relative_to(root)
345
+ for snippet in snippets:
346
+ if already_done(snippet["name"], str(rel_path), output_file):
347
+ pbar.update(1); continue
348
+
349
+ print(f"\n {snippet['name']} ({rel_path}:{snippet['start']}-{snippet['end']})")
350
+ print(" " + "─" * 60)
351
+
352
+ messages = [
353
+ {"role": "system", "content": SYSTEM_PROMPT},
354
+ *FEW_SHOT_EXAMPLES,
355
+ {"role": "user", "content": f"File: {rel_path} | Snippet: {snippet['name']}\n\n```\n{snippet['code']}\n```"}
356
+ ]
357
+ explanation = stream_chat_completion(config["url"], config["model"], messages)
358
+ append_to_book(str(rel_path), snippet, explanation, output_file)
359
+ pbar.update(1)
360
+
361
+ print(f"\n✅ Done! Generated: {output_file}")
362
+
363
+ def run_analyze(args):
364
+ """Run PCTF Codebase Teardown."""
365
+ root = Path(args.target_dir or ".").resolve()
366
+ db_path = str(root / ".codebook_cache.db")
367
+ indexer = Indexer(str(root), db_path)
368
+ indexer.index(force=args.force)
369
+ config = load_config(args, root)
370
+ is_running, model = check_server(config["url"])
371
+ if not is_running: return
372
+ print("\n🔍 Generating Codebase Teardown...")
373
+ readme_content = ""
374
+ readme_path = root / "README.md"
375
+ if readme_path.exists():
376
+ readme_content = readme_path.read_text(errors="ignore")[:2000]
377
+ folder_tree = [f"- {d.name}/" for d in root.iterdir() if d.is_dir() and not d.name.startswith((".", "__"))]
378
+ context = f"README:\n{readme_content}\n\nFolder Structure:\n" + "\n".join(folder_tree)
379
+ messages = [{"role": "system", "content": PCTF_SYSTEM_PROMPT}, {"role": "user", "content": f"Analyze this codebase:\n\n{context}"}]
380
+ report = stream_chat_completion(config["url"], model or "unknown", messages)
381
+ output_path = root / "CODEBASE_TEARDOWN.md"
382
+ output_path.write_text(report)
383
+ print(f"\n✅ Teardown saved to {output_path}")
384
+
385
+ def run_decompose(args):
386
+ """Run AATD Feature Decomposition."""
387
+ root = Path(args.target_dir or ".").resolve()
388
+ config = load_config(args, root)
389
+ is_running, model = check_server(config["url"])
390
+ if not is_running:
391
+ print(f"⚠️ LLM server not running at {config['url']}. Start your server and try again.")
392
+ return
393
+
394
+ context_source = "Teardown"
395
+ teardown_path = root / "CODEBASE_TEARDOWN.md"
396
+ teardown_context = ""
397
+ if teardown_path.exists():
398
+ teardown_context = teardown_path.read_text(errors="ignore")
399
+
400
+ # Wire --deep flag
401
+ if args.deep:
402
+ print("📦 Packing repository for deep context...")
403
+ deep_context = pack_repo(root)
404
+ if len(deep_context) > 100000:
405
+ print("⚠️ Deep context too large (>100k chars). Falling back to teardown context.")
406
+ context_source = "Teardown (fallback: deep context too large)"
407
+ else:
408
+ teardown_context = deep_context
409
+ context_source = "Deep Repomix Pack"
410
+
411
+ if not teardown_context and not args.deep:
412
+ sys.exit("❌ No codebase context found. Please run 'codebook analyze' first to generate a teardown, or use the '--deep' flag.")
413
+
414
+ if args.deep and not teardown_context and context_source == "Teardown (fallback: deep context too large)":
415
+ sys.exit("❌ Context too large for deep pack and no teardown found. Please run 'codebook analyze' first.")
416
+
417
+ print(f"\n🔨 Decomposing feature: {args.feature} (Context: {context_source})")
418
+ messages = [{"role": "system", "content": AATD_SYSTEM_PROMPT}, {"role": "user", "content": f"Codebase Context:\n{teardown_context}\n\nFeature Request: {args.feature}"}]
419
+ tasks_json = stream_chat_completion(config["url"], model or "unknown", messages)
420
+ output_path = root / "FEATURE_TASKS.md"
421
+ output_path.write_text(f"# Feature Tasks: {args.feature}\n\n{tasks_json}")
422
+ print(f"\n✅ Tasks saved to {output_path}")
423
+
424
+ def run_enhance(args):
425
+ """Run Prompt Enhancement (KG-backed)."""
426
+ root = Path(args.target_dir or ".").resolve()
427
+ config = load_config(args, root)
428
+
429
+ db_path = root / ".codebook_cache.db"
430
+ if not db_path.exists():
431
+ sys.exit("⚠️ No Knowledge Graph found. Please run 'codebookx analyze' first to generate codebase context.")
432
+
433
+ is_running, model = check_server(config["url"])
434
+ if not is_running:
435
+ print(f"⚠️ LLM server not running at {config['url']}. Start your server and try again.")
436
+ return
437
+
438
+ print(f"\n✨ Enhancing prompt for: {args.feature}")
439
+ kg = KnowledgeGraph(str(db_path))
440
+ context = kg.get_all_symbol_context()
441
+
442
+ if not context.strip():
443
+ sys.exit("⚠️ Knowledge Graph is empty. Run 'codebookx analyze' first.")
444
+
445
+ messages = [
446
+ {"role": "system", "content": ENHANCE_SYSTEM_PROMPT},
447
+ {"role": "user", "content": f"Codebase Structure:\n{context}\n\nUser Request: {args.feature}\n\nGenerate an enhanced prompt."}
448
+ ]
449
+ enhanced_prompt = stream_chat_completion(config["url"], model or "unknown", messages)
450
+ output_path = root / "ENHANCED_PROMPT.txt"
451
+ output_path.write_text(enhanced_prompt)
452
+ print(f"\n✅ Enhanced prompt saved to {output_path}")