Spaces:
Running
Running
| # -*- coding: utf-8 -*- | |
| """ | |
| π± [Imperial Coding Arsenal] μλμ° λΈλ μΈ μμ΄μ ν± μ½λ© λꡬ μΈνΈ | |
| Claude Code λ°©μμ μ½λ© μν¬νλ‘(νμ β μ½κΈ° β μμ β κ²μ¦ β λ°λ³΅)λ₯Ό | |
| μλμ° λΈλ μΈμ μ΄μνκΈ° μν λꡬ λͺ¨μμ λλ€. | |
| - Gemini AFC κ²½λ‘: CodingToolkitμ public λ©μλλ₯Ό callableλ‘ μ§μ λ±λ‘ | |
| - OpenAI νΈν κ²½λ‘(Copilot/Atlas/OpenRouter): OPENAI_SCHEMAS + execute() λμ€ν¨μ² | |
| """ | |
| import fnmatch | |
| import os | |
| import re | |
| import subprocess | |
| import logging | |
| logger = logging.getLogger("ShadowBrain.Coding") | |
| # μμ λ£¨νΈ β μ΄ λ°κΉ₯μ νμΌμ 건λλ¦¬μ§ μλλ€ (μμ κ°λ) | |
| DEFAULT_ROOT = r"D:\Git_Work\jarvis_taemin" | |
| # κ²μ/νμ μ 무μν λλ ν 리 | |
| _SKIP_DIRS = { | |
| ".git", "node_modules", "venv", ".venv", "__pycache__", "dist", "build", | |
| ".next", ".dart_tool", "site-packages", ".pub-cache", "docker_data", | |
| } | |
| # λͺ λ°±ν νκ΄΄μ μΈ λͺ λ Ή μ°¨λ¨ (λ§μλ PC 보νΈ) | |
| _BLOCKED_CMD_PATTERNS = [ | |
| r"\bformat\b", r"\brm\s+-rf\s+/", r"\bdel\s+/s\s+/q\s+[a-z]:\\\s*$", | |
| r"remove-item\s+-recurse\s+-force\s+[a-z]:\\\s*$", r"\bshutdown\b", r"\bmkfs\b", | |
| ] | |
| _TEXT_EXT_HINT = { | |
| ".py", ".dart", ".js", ".ts", ".tsx", ".jsx", ".json", ".yaml", ".yml", | |
| ".md", ".txt", ".html", ".css", ".sh", ".ps1", ".bat", ".rb", ".erb", | |
| ".sql", ".toml", ".ini", ".cfg", ".xml", ".csv", ".env", ".mjs", ".cjs", | |
| } | |
| class CodingToolkit: | |
| """νμΌ νμ/μ½κΈ°/μμ /κ²μ/λͺ λ Ή μ€ν λꡬ. λͺ¨λ κ²½λ‘λ μμ λ£¨νΈ κΈ°μ€ μλ/μ λ λ λ€ νμ©.""" | |
| def __init__(self, root: str = None): | |
| self.root = os.path.abspath(root or os.environ.get("SHADOW_CODING_ROOT", DEFAULT_ROOT)) | |
| # ββ λ΄λΆ ν¬νΌ ββββββββββββββββββββββββββββββββββββββββββββββ | |
| def _resolve(self, path: str) -> str: | |
| p = (path or "").strip().strip('"') | |
| if not p or p in (".", "./"): | |
| return self.root | |
| if not os.path.isabs(p): | |
| p = os.path.join(self.root, p) | |
| p = os.path.abspath(p) | |
| if os.path.commonpath([p, self.root]) != self.root: | |
| raise ValueError(f"μμ 루νΈ({self.root}) λ°κΉ₯ κ²½λ‘λ νμ©λμ§ μμ΅λλ€: {p}") | |
| return p | |
| def _is_text_file(path: str) -> bool: | |
| ext = os.path.splitext(path)[1].lower() | |
| if ext in _TEXT_EXT_HINT: | |
| return True | |
| try: | |
| with open(path, "rb") as f: | |
| chunk = f.read(2048) | |
| return b"\x00" not in chunk | |
| except Exception: | |
| return False | |
| def _walk(self): | |
| for dirpath, dirnames, filenames in os.walk(self.root): | |
| dirnames[:] = [d for d in dirnames if d not in _SKIP_DIRS and not d.startswith(".")] | |
| yield dirpath, filenames | |
| # ββ λꡬ: νμ βββββββββββββββββββββββββββββββββββββββββββββ | |
| def code_list_dir(self, path: str = ".") -> str: | |
| """Lists files and subdirectories at the given path (relative to the project root). Use this first to understand project structure. | |
| Args: | |
| path: Directory path relative to project root (default: root) | |
| """ | |
| try: | |
| target = self._resolve(path) | |
| if not os.path.isdir(target): | |
| return f"Error: λλ ν λ¦¬κ° μλλλ€: {path}" | |
| entries = sorted(os.listdir(target)) | |
| lines = [] | |
| for e in entries: | |
| full = os.path.join(target, e) | |
| if os.path.isdir(full): | |
| lines.append(f"[DIR] {e}/") | |
| else: | |
| try: | |
| size = os.path.getsize(full) | |
| except OSError: | |
| size = 0 | |
| lines.append(f"[FILE] {e} ({size:,} bytes)") | |
| rel = os.path.relpath(target, self.root) | |
| return f"# {rel} ({len(entries)} entries)\n" + "\n".join(lines[:300]) | |
| except Exception as e: | |
| return f"Error: {e}" | |
| def code_glob(self, pattern: str, max_results: int = 100) -> str: | |
| """Finds files matching a glob pattern (e.g. '**/*.py', 'portal-heimdall/lib/**/*.dart'). Returns matching paths relative to project root. | |
| Args: | |
| pattern: Glob pattern. '**' matches any directories. | |
| max_results: Maximum number of paths to return (default 100) | |
| """ | |
| try: | |
| pattern = (pattern or "").replace("\\", "/").lstrip("/") | |
| results = [] | |
| for dirpath, filenames in self._walk(): | |
| rel_dir = os.path.relpath(dirpath, self.root).replace("\\", "/") | |
| for fn in filenames: | |
| rel = fn if rel_dir == "." else f"{rel_dir}/{fn}" | |
| # '**/x' ν¨ν΄μ΄ λ£¨νΈ λ°λ‘ μλ νμΌλ μ‘λλ‘ λ³΄μ | |
| if fnmatch.fnmatch(rel, pattern) or ( | |
| pattern.startswith("**/") and fnmatch.fnmatch(rel, pattern[3:]) | |
| ): | |
| results.append(rel) | |
| if len(results) >= max_results: | |
| return "\n".join(results) + f"\n... (max {max_results} reached)" | |
| return "\n".join(results) if results else f"No files match: {pattern}" | |
| except Exception as e: | |
| return f"Error: {e}" | |
| def code_grep(self, pattern: str, file_glob: str = None, max_results: int = 50) -> str: | |
| """Searches file CONTENTS for a regex pattern across the project. Returns 'path:line: text' matches. Essential for finding where something is defined or used. | |
| Args: | |
| pattern: Regular expression to search for | |
| file_glob: Optional filename filter (e.g. '*.py', '*.dart') | |
| max_results: Maximum matching lines to return (default 50) | |
| """ | |
| try: | |
| rx = re.compile(pattern) | |
| except re.error as e: | |
| return f"Error: μλͺ»λ μ κ·μ: {e}" | |
| results = [] | |
| try: | |
| for dirpath, filenames in self._walk(): | |
| for fn in filenames: | |
| if file_glob and not fnmatch.fnmatch(fn, file_glob): | |
| continue | |
| full = os.path.join(dirpath, fn) | |
| if not self._is_text_file(full): | |
| continue | |
| rel = os.path.relpath(full, self.root) | |
| try: | |
| with open(full, "r", encoding="utf-8", errors="ignore") as f: | |
| for i, line in enumerate(f, 1): | |
| if rx.search(line): | |
| results.append(f"{rel}:{i}: {line.rstrip()[:200]}") | |
| if len(results) >= max_results: | |
| return "\n".join(results) + f"\n... (max {max_results} reached)" | |
| except OSError: | |
| continue | |
| return "\n".join(results) if results else f"No matches for: {pattern}" | |
| except Exception as e: | |
| return f"Error: {e}" | |
| # ββ λꡬ: μ½κΈ°/μ°κΈ° ββββββββββββββββββββββββββββββββββββββββ | |
| def code_read_file(self, path: str, offset: int = 1, limit: int = 400) -> str: | |
| """Reads a text file with line numbers. ALWAYS read a file before editing it. | |
| Args: | |
| path: File path (relative to project root or absolute) | |
| offset: 1-based line number to start from (default 1) | |
| limit: Max lines to return (default 400) | |
| """ | |
| try: | |
| full = self._resolve(path) | |
| if not os.path.isfile(full): | |
| return f"Error: νμΌμ΄ μμ΅λλ€: {path}" | |
| with open(full, "r", encoding="utf-8", errors="replace") as f: | |
| lines = f.readlines() | |
| total = len(lines) | |
| start = max(int(offset or 1), 1) | |
| end = min(start - 1 + int(limit or 400), total) | |
| body = "".join( | |
| f"{i:>5}\t{lines[i - 1]}" for i in range(start, end + 1) | |
| ) | |
| return f"# {path} (lines {start}-{end} of {total})\n{body}" | |
| except Exception as e: | |
| return f"Error: {e}" | |
| def code_write_file(self, path: str, content: str) -> str: | |
| """Creates or fully overwrites a file with the given content (UTF-8). For small changes to existing files, prefer code_edit_file. | |
| Args: | |
| path: File path (relative to project root or absolute) | |
| content: Full file content to write | |
| """ | |
| try: | |
| full = self._resolve(path) | |
| os.makedirs(os.path.dirname(full), exist_ok=True) | |
| with open(full, "w", encoding="utf-8", newline="") as f: | |
| f.write(content or "") | |
| return f"OK: wrote {len(content or '')} chars to {path}" | |
| except Exception as e: | |
| return f"Error: {e}" | |
| def code_edit_file(self, path: str, old_string: str, new_string: str, replace_all: bool = False) -> str: | |
| """Edits a file by exact string replacement. old_string must match the file content EXACTLY (including indentation) and be unique unless replace_all=true. Read the file first to copy the exact text. | |
| Args: | |
| path: File path (relative to project root or absolute) | |
| old_string: Exact text to find (must be unique in the file unless replace_all) | |
| new_string: Replacement text | |
| replace_all: Replace every occurrence (default false) | |
| """ | |
| try: | |
| full = self._resolve(path) | |
| if not os.path.isfile(full): | |
| return f"Error: νμΌμ΄ μμ΅λλ€: {path}" | |
| with open(full, "r", encoding="utf-8") as f: | |
| text = f.read() | |
| if old_string not in text: | |
| return "Error: old_stringμ νμΌμμ μ°Ύμ§ λͺ»νμ΅λλ€. code_read_fileλ‘ μ νν μλ¬Έ(λ€μ¬μ°κΈ° ν¬ν¨)μ 볡μ¬ν΄ λ€μ μλνμΈμ." | |
| count = text.count(old_string) | |
| if count > 1 and not replace_all: | |
| return f"Error: old_stringμ΄ {count}λ² λ±μ₯ν©λλ€. λ κΈΈκ³ κ³ μ ν λ¬Έμμ΄μ μ°κ±°λ replace_all=trueλ₯Ό μ§μ νμΈμ." | |
| new_text = text.replace(old_string, new_string) if replace_all else text.replace(old_string, new_string, 1) | |
| with open(full, "w", encoding="utf-8", newline="") as f: | |
| f.write(new_text) | |
| n = count if replace_all else 1 | |
| return f"OK: {path} β {n}κ³³ μμ μλ£" | |
| except Exception as e: | |
| return f"Error: {e}" | |
| # ββ λꡬ: λͺ λ Ή μ€ν(κ²μ¦) ββββββββββββββββββββββββββββββββββ | |
| def code_run_command(self, command: str, cwd: str = None, timeout: int = 120) -> str: | |
| """Runs a shell command (PowerShell on Windows) and returns stdout/stderr/exit code. Use to verify changes: run tests, compile checks (python -m py_compile, flutter analyze), git status, etc. | |
| Args: | |
| command: The command line to execute | |
| cwd: Working directory relative to project root (default: project root) | |
| timeout: Seconds before the command is killed (default 120, max 600) | |
| """ | |
| try: | |
| low = (command or "").lower() | |
| for pat in _BLOCKED_CMD_PATTERNS: | |
| if re.search(pat, low): | |
| return f"Error: νκ΄΄μ λͺ λ Ήμ΄ μ°¨λ¨λμμ΅λλ€: {command}" | |
| workdir = self._resolve(cwd) if cwd else self.root | |
| timeout = min(max(int(timeout or 120), 5), 600) | |
| if os.name == "nt": | |
| cmd = ["powershell", "-NoProfile", "-NonInteractive", "-Command", command] | |
| else: | |
| cmd = ["bash", "-lc", command] | |
| proc = subprocess.run( | |
| cmd, cwd=workdir, capture_output=True, timeout=timeout, | |
| ) | |
| out = proc.stdout.decode("utf-8", errors="replace") | |
| err = proc.stderr.decode("utf-8", errors="replace") | |
| res = f"[exit code: {proc.returncode}]\n" | |
| if out.strip(): | |
| res += f"--- stdout ---\n{out[-6000:]}\n" | |
| if err.strip(): | |
| res += f"--- stderr ---\n{err[-3000:]}\n" | |
| return res.strip() | |
| except subprocess.TimeoutExpired: | |
| return f"Error: λͺ λ Ήμ΄ {timeout}μ΄ μμ λλμ§ μμ μ€λ¨νμ΅λλ€: {command}" | |
| except Exception as e: | |
| return f"Error: {e}" | |
| # ββ OpenAI νΈν λμ€ν¨μ² ββββββββββββββββββββββββββββββββββ | |
| TOOL_NAMES = ( | |
| "code_list_dir", "code_glob", "code_grep", | |
| "code_read_file", "code_write_file", "code_edit_file", "code_run_command", | |
| ) | |
| def handles(self, name: str) -> bool: | |
| return name in self.TOOL_NAMES | |
| def execute(self, name: str, args: dict) -> str: | |
| if not self.handles(name): | |
| return f"Error: unknown coding tool '{name}'" | |
| try: | |
| return getattr(self, name)(**(args or {})) | |
| except TypeError as e: | |
| return f"Error: μλͺ»λ μΈμ: {e}" | |
| def callables(self): | |
| """Gemini AFC κ²½λ‘μ© β bound method 리μ€νΈ.""" | |
| return [getattr(self, n) for n in self.TOOL_NAMES] | |
| def _schema(name, desc, props, required): | |
| return { | |
| "type": "function", | |
| "function": { | |
| "name": name, | |
| "description": desc, | |
| "parameters": {"type": "object", "properties": props, "required": required}, | |
| }, | |
| } | |
| OPENAI_SCHEMAS = [ | |
| _schema( | |
| "code_list_dir", | |
| "Lists files and subdirectories at a path inside the project. Use first to learn the project layout.", | |
| {"path": {"type": "string", "description": "Directory path relative to project root (default '.')"}}, | |
| [], | |
| ), | |
| _schema( | |
| "code_glob", | |
| "Finds files by glob pattern, e.g. '**/*.py' or 'portal-heimdall/lib/**/*.dart'. Returns relative paths.", | |
| { | |
| "pattern": {"type": "string", "description": "Glob pattern ('**' = any directories)"}, | |
| "max_results": {"type": "integer", "description": "Max paths (default 100)"}, | |
| }, | |
| ["pattern"], | |
| ), | |
| _schema( | |
| "code_grep", | |
| "Searches file CONTENTS with a regex across the project. Returns 'path:line: text'. Use to find where code is defined or used.", | |
| { | |
| "pattern": {"type": "string", "description": "Regular expression"}, | |
| "file_glob": {"type": "string", "description": "Filename filter, e.g. '*.py'"}, | |
| "max_results": {"type": "integer", "description": "Max matching lines (default 50)"}, | |
| }, | |
| ["pattern"], | |
| ), | |
| _schema( | |
| "code_read_file", | |
| "Reads a text file with line numbers. ALWAYS read before editing.", | |
| { | |
| "path": {"type": "string", "description": "File path (relative or absolute)"}, | |
| "offset": {"type": "integer", "description": "1-based start line (default 1)"}, | |
| "limit": {"type": "integer", "description": "Max lines (default 400)"}, | |
| }, | |
| ["path"], | |
| ), | |
| _schema( | |
| "code_write_file", | |
| "Creates or fully overwrites a file (UTF-8). Prefer code_edit_file for small changes.", | |
| { | |
| "path": {"type": "string", "description": "File path"}, | |
| "content": {"type": "string", "description": "Full file content"}, | |
| }, | |
| ["path", "content"], | |
| ), | |
| _schema( | |
| "code_edit_file", | |
| "Edits a file by EXACT string replacement. old_string must match exactly (indentation included) and be unique unless replace_all.", | |
| { | |
| "path": {"type": "string", "description": "File path"}, | |
| "old_string": {"type": "string", "description": "Exact text to find"}, | |
| "new_string": {"type": "string", "description": "Replacement text"}, | |
| "replace_all": {"type": "boolean", "description": "Replace all occurrences (default false)"}, | |
| }, | |
| ["path", "old_string", "new_string"], | |
| ), | |
| _schema( | |
| "code_run_command", | |
| "Runs a shell command (PowerShell) in the project and returns output + exit code. Use to VERIFY changes: tests, py_compile, flutter analyze, git status.", | |
| { | |
| "command": {"type": "string", "description": "Command line to run"}, | |
| "cwd": {"type": "string", "description": "Working dir relative to project root"}, | |
| "timeout": {"type": "integer", "description": "Seconds (default 120, max 600)"}, | |
| }, | |
| ["command"], | |
| ), | |
| ] | |
| # μμ€ν μ§μμ λ§λΆμΌ μμ΄μ ν± μ½λ© μν¬νλ‘ κ°μ΄λ | |
| CODING_AGENT_GUIDE = """ | |
| ## π οΈ μμ΄μ ν± μ½λ© μμΉ (Imperial Coding Protocol) | |
| λλ μ½λ© λꡬ(code_*)λ₯Ό κ°μ§ μμ¨ μ½λ© μμ΄μ νΈλ€. μ½λ κ΄λ ¨ μμ²μ λ°μΌλ©΄ μΆμΈ‘μΌλ‘ λ΅νμ§ λ§κ³ λ°λμ λκ΅¬λ‘ νμΈνλΌ. | |
| μμ μμ (λ°λμ μ€μ): | |
| 1. **νμ**: code_grep / code_glob / code_list_dir λ‘ κ΄λ ¨ νμΌκ³Ό μ½λλ₯Ό λ¨Όμ μ°Ύλλ€. νμΌ μμΉλ₯Ό μΆμΈ‘νμ§ λ§λΌ. | |
| 2. **μ½κΈ°**: code_read_file λ‘ μμ ν λΆλΆμ μ€μ μ½λλ₯Ό μ½λλ€. μ½μ§ μμ νμΌμ μμ νλ κ²μ κΈμ§λ€. | |
| 3. **μμ **: code_edit_file λ‘ μ νν μλ¬Έ(λ€μ¬μ°κΈ° ν¬ν¨)μ 볡μ¬ν΄ κ΅μ²΄νλ€. μ νμΌλ§ code_write_file μ μ΄λ€. | |
| 4. **κ²μ¦**: code_run_command λ‘ κ²μ¦νλ€ (python -m py_compile, flutter analyze, ν μ€νΈ μ€ν λ±). κ²μ¦ μμ΄ "μλ£"λΌκ³ λ³΄κ³ νμ§ λ§λΌ. | |
| 5. **λ°λ³΅**: κ²μ¦μ΄ μ€ν¨νλ©΄ μ€λ₯ λ©μμ§λ₯Ό μ½κ³ 1λ²λΆν° λ€μ μννλ€. κ°μ μ€ν¨λ₯Ό λ°λ³΅νλ©΄ μ κ·Όμ λ°κΏλΌ. | |
| κ·μΉ: | |
| - λꡬ κ²°κ³Ό(νμΌ λ΄μ©, κ²μ κ²°κ³Ό)μ κ·Όκ±°ν΄μλ§ λ§νλΌ. μ‘΄μ¬λ₯Ό νμΈνμ§ μμ νμΌ/ν¨μλ₯Ό μΈκΈνμ§ λ§λΌ. | |
| - μμ μ μ΅μ λ²μλ‘ νκ³ , μ£Όλ³ μ½λμ μ€νμΌμ λ°λ₯Έλ€. | |
| - μ€ν¨νλ©΄ μμ§νκ² μ€ν¨νλ€κ³ λ³΄κ³ νκ³ μμΈκ³Ό λ€μ μλλ₯Ό μ€λͺ νλ€. | |
| """ | |