grace-context-manager 4.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.
grace_app/__init__.py ADDED
@@ -0,0 +1,3 @@
1
+ "GRACE Context Manager - Smart context assembly for LLM workflows."
2
+
3
+ __version__ = "4.0.0"
Binary file
Binary file
grace_app/config.py ADDED
@@ -0,0 +1,40 @@
1
+ from pathlib import Path
2
+
3
+ CONFIG_DIR = Path.home() / ".grace_manager"
4
+ PROFILES_FILE = CONFIG_DIR / "profiles.json"
5
+ RECENT_FILE = CONFIG_DIR / "recent_projects.json"
6
+ SETTINGS_FILE = CONFIG_DIR / "settings.json"
7
+ EXTENSION_PRESETS_FILE = CONFIG_DIR / "extension_presets.json"
8
+
9
+ DEFAULT_EXTENSIONS = ".py, .md, .txt, .json, .yaml, .toml, .cfg, .ini"
10
+
11
+ BUILTIN_IGNORE_DIRS = {'__pycache__', 'venv', 'node_modules', 'egg-info'}
12
+ BUILTIN_IGNORE_FILES = {'Thumbs.db'}
13
+
14
+ SCALE_STEPS = [75, 90, 100, 110, 125, 150, 175, 200]
15
+
16
+ MAX_FILE_SIZE = 1024 * 1024
17
+
18
+ DEFAULT_EXTENSION_PRESETS = {
19
+ "python_ml": [".py", ".json", ".yaml", ".yml", ".toml", ".md", ".cfg"],
20
+ "web_dev": [".js", ".ts", ".jsx", ".tsx", ".json", ".html", ".css", ".scss", ".md"],
21
+ "data_science": [".py", ".ipynb", ".json", ".csv", ".sql", ".r", ".md"],
22
+ "rust": [".rs", ".toml", ".md", ".json"],
23
+ "golang": [".go", ".mod", ".sum", ".md", ".json", ".yaml"],
24
+ "python": [".py", ".md", ".txt", ".json", ".yaml", ".toml"],
25
+ "all": [],
26
+ }
27
+
28
+ EXT_ICONS = {
29
+ ".py": "\ue606", ".js": "JS", ".ts": "TS", ".jsx": "JS",
30
+ ".tsx": "TS", ".md": "MD", ".json": "{}", ".yaml": "Y",
31
+ ".yml": "Y", ".xml": "X", ".html": "H", ".css": "#",
32
+ ".toml": "T", ".cfg": "C", ".ini": "I", ".rs": "RS",
33
+ ".go": "Go", ".java": "Jv", ".c": "C", ".cpp": "C+",
34
+ ".h": "H", ".rb": "Rb", ".php": "P", ".sql": "SQ",
35
+ ".sh": "Sh", ".bash": "Sh", ".zsh": "Sh", ".fish": "Fi",
36
+ ".ps1": "PS", ".bat": "B", ".cmake": "CM", ".make": "Mk",
37
+ ".dockerfile": "D", ".kt": "Kt", ".swift": "Sw",
38
+ ".r": "R", ".m": "M", ".scala": "Sc", ".lua": "Lu",
39
+ ".vim": "Vi", ".tex": "Tx", ".gitignore": "Gi",
40
+ }
grace_app/engine.py ADDED
@@ -0,0 +1,127 @@
1
+ import os
2
+ from pathlib import Path
3
+ from typing import List, Set, Optional
4
+ from .models import ContextBlock
5
+ from .config import MAX_FILE_SIZE
6
+
7
+ class TokenEstimator:
8
+ @staticmethod
9
+ def estimate(text: str) -> int:
10
+ return max(1, len(text) // 4)
11
+
12
+ @staticmethod
13
+ def format_count(count: int) -> str:
14
+ if count < 1000:
15
+ return f"{count} tokens"
16
+ return f"{count/1000:.1f}k tokens"
17
+
18
+ class FileScanner:
19
+ @staticmethod
20
+ def scan_extensions(root_path: str, ignore_dirs: Set[str], max_exts: int = 60) -> List[str]:
21
+ extensions: Set[str] = set()
22
+ try:
23
+ for dirpath, dirnames, filenames in os.walk(root_path):
24
+ dirnames[:] = [d for d in dirnames if d not in ignore_dirs and not d.startswith('.')]
25
+ for fname in filenames:
26
+ if fname.startswith('.'):
27
+ continue
28
+ ext = os.path.splitext(fname)[1].lower()
29
+ if ext:
30
+ extensions.add(ext)
31
+ if len(extensions) > max_exts:
32
+ break
33
+ if len(extensions) > max_exts:
34
+ break
35
+ except (PermissionError, OSError):
36
+ pass
37
+ return sorted(extensions)
38
+
39
+ @staticmethod
40
+ def list_entries(base_path: str, extensions: List[str], ignore_dirs: Set[str],
41
+ ignore_files: Set[str], keyword_filter: str = "") -> List[str]:
42
+ exts = {e.strip().lower() for e in extensions if e.strip()}
43
+ kw = keyword_filter.strip().lower()
44
+ valid: List[str] = []
45
+ try:
46
+ for dirpath, dirnames, filenames in os.walk(base_path):
47
+ dirnames[:] = [d for d in dirnames if d not in ignore_dirs and not d.startswith('.')]
48
+ for fname in filenames:
49
+ if fname in ignore_files or fname.startswith('.'):
50
+ continue
51
+ ext = os.path.splitext(fname)[1].lower()
52
+ if exts and ext not in exts:
53
+ continue
54
+ if kw and kw not in fname.lower():
55
+ continue
56
+ valid.append(os.path.join(dirpath, fname))
57
+ except (PermissionError, OSError):
58
+ pass
59
+ return valid
60
+
61
+ class ContextBuilder:
62
+ @staticmethod
63
+ def build_tree_string(tree: dict, prefix: str = "") -> List[str]:
64
+ lines: List[str] = []
65
+ keys = sorted(tree.keys())
66
+ for i, key in enumerate(keys):
67
+ is_last = i == len(keys) - 1
68
+ connector = "\u2514\u2500\u2500 " if is_last else "\u251c\u2500\u2500 "
69
+ if tree[key]:
70
+ lines.append(f"{prefix}{connector}{key}/")
71
+ ext = " " if is_last else "\u2502 "
72
+ lines.extend(ContextBuilder.build_tree_string(tree[key], prefix + ext))
73
+ else:
74
+ lines.append(f"{prefix}{connector}{key}")
75
+ return lines
76
+
77
+ @staticmethod
78
+ def build(blocks: List[ContextBlock], selected_files: List[str],
79
+ base_dir: str, mode: str, instruction: str,
80
+ max_file_size: int = MAX_FILE_SIZE) -> str:
81
+ parts: List[str] = []
82
+
83
+ if blocks:
84
+ parts.append("=== CONTEXT BLOCKS ===\n")
85
+ for block in sorted(blocks, key=lambda b: b.order):
86
+ parts.append(f"--- {block.name} ({block.category}) ---")
87
+ parts.append(block.content)
88
+ parts.append("")
89
+
90
+ if selected_files and mode != "Files Only":
91
+ tree_dict = {}
92
+ for fp in sorted(selected_files):
93
+ rel = os.path.relpath(fp, base_dir)
94
+ curr = tree_dict
95
+ for part in rel.split(os.sep):
96
+ curr = curr.setdefault(part, {})
97
+ parts.append("=== PROJECT STRUCTURE ===")
98
+ parts.append(".")
99
+ parts.extend(ContextBuilder.build_tree_string(tree_dict))
100
+ parts.append("")
101
+
102
+ if mode != "Structure Only":
103
+ parts.append("=== FILE CONTENTS ===\n")
104
+ for fp in sorted(selected_files):
105
+ rel = os.path.relpath(fp, base_dir)
106
+ try:
107
+ size = os.path.getsize(fp)
108
+ if size > max_file_size:
109
+ parts.append(f"--- {rel} ---")
110
+ parts.append(f"[Omitted: {size/1024/1024:.1f}MB exceeds 1MB limit]\n")
111
+ continue
112
+ with open(fp, 'r', encoding='utf-8', errors='replace') as fh:
113
+ content = fh.read()
114
+ ext = os.path.splitext(rel)[1].lstrip('.') or 'text'
115
+ parts.append(f"--- {rel} ---")
116
+ parts.append(f"```{ext}")
117
+ parts.append(content)
118
+ parts.append("```\n")
119
+ except Exception as e:
120
+ parts.append(f"--- {rel} ---")
121
+ parts.append(f"[Error: {e}]\n")
122
+
123
+ if instruction:
124
+ parts.append("=== YOUR TASK ===")
125
+ parts.append(instruction)
126
+
127
+ return "\n".join(parts)