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 +3 -0
- grace_app/assets/icon.ico +0 -0
- grace_app/assets/icon.png +0 -0
- grace_app/config.py +40 -0
- grace_app/engine.py +127 -0
- grace_app/main.py +1118 -0
- grace_app/models.py +54 -0
- grace_app/tree_model.py +320 -0
- grace_context_manager-4.0.0.dist-info/METADATA +170 -0
- grace_context_manager-4.0.0.dist-info/RECORD +13 -0
- grace_context_manager-4.0.0.dist-info/WHEEL +5 -0
- grace_context_manager-4.0.0.dist-info/entry_points.txt +2 -0
- grace_context_manager-4.0.0.dist-info/top_level.txt +1 -0
grace_app/__init__.py
ADDED
|
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)
|