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 +1 -0
- codebookx/cli.py +85 -0
- codebookx/core.py +452 -0
- codebookx/engine/graph.py +144 -0
- codebookx/engine/indexer.py +127 -0
- codebookx/engine/parser.py +337 -0
- codebookx/engine/vendor/claude_mem_lite.py +39 -0
- codebookx/engine/vendor/repomix_lite.py +30 -0
- codebookx/llm.py +73 -0
- codebookx/prompts.py +43 -0
- codebookx/webapp/app.py +153 -0
- codebookx-3.0.0.dist-info/METADATA +115 -0
- codebookx-3.0.0.dist-info/RECORD +17 -0
- codebookx-3.0.0.dist-info/WHEEL +5 -0
- codebookx-3.0.0.dist-info/entry_points.txt +3 -0
- codebookx-3.0.0.dist-info/licenses/NOTICE.md +20 -0
- codebookx-3.0.0.dist-info/top_level.txt +1 -0
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}")
|