claude-mpm 4.1.11__py3-none-any.whl → 4.1.13__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.
- claude_mpm/VERSION +1 -1
- claude_mpm/agents/INSTRUCTIONS.md +8 -0
- claude_mpm/cli/__init__.py +1 -1
- claude_mpm/cli/commands/monitor.py +88 -627
- claude_mpm/cli/commands/mpm_init.py +127 -107
- claude_mpm/cli/commands/mpm_init_handler.py +24 -23
- claude_mpm/cli/parsers/mpm_init_parser.py +34 -28
- claude_mpm/core/config.py +18 -0
- claude_mpm/core/instruction_reinforcement_hook.py +266 -0
- claude_mpm/core/pm_hook_interceptor.py +105 -8
- claude_mpm/dashboard/static/built/components/activity-tree.js +1 -1
- claude_mpm/dashboard/static/built/components/code-tree.js +1 -1
- claude_mpm/dashboard/static/built/dashboard.js +1 -1
- claude_mpm/dashboard/static/built/socket-client.js +1 -1
- claude_mpm/dashboard/static/css/activity.css +1239 -267
- claude_mpm/dashboard/static/css/dashboard.css +511 -0
- claude_mpm/dashboard/static/dist/components/activity-tree.js +1 -1
- claude_mpm/dashboard/static/dist/components/code-tree.js +1 -1
- claude_mpm/dashboard/static/dist/components/module-viewer.js +1 -1
- claude_mpm/dashboard/static/dist/dashboard.js +1 -1
- claude_mpm/dashboard/static/dist/socket-client.js +1 -1
- claude_mpm/dashboard/static/js/components/activity-tree.js +1193 -892
- claude_mpm/dashboard/static/js/components/build-tracker.js +15 -13
- claude_mpm/dashboard/static/js/components/code-tree.js +534 -143
- claude_mpm/dashboard/static/js/components/module-viewer.js +21 -7
- claude_mpm/dashboard/static/js/components/unified-data-viewer.js +1066 -0
- claude_mpm/dashboard/static/js/connection-manager.js +1 -1
- claude_mpm/dashboard/static/js/dashboard.js +227 -84
- claude_mpm/dashboard/static/js/socket-client.js +2 -2
- claude_mpm/dashboard/templates/index.html +100 -23
- claude_mpm/services/agents/deployment/agent_template_builder.py +11 -7
- claude_mpm/services/cli/socketio_manager.py +39 -8
- claude_mpm/services/infrastructure/monitoring.py +1 -1
- claude_mpm/services/socketio/handlers/code_analysis.py +83 -136
- claude_mpm/tools/code_tree_analyzer.py +290 -202
- {claude_mpm-4.1.11.dist-info → claude_mpm-4.1.13.dist-info}/METADATA +1 -1
- {claude_mpm-4.1.11.dist-info → claude_mpm-4.1.13.dist-info}/RECORD +41 -39
- {claude_mpm-4.1.11.dist-info → claude_mpm-4.1.13.dist-info}/WHEEL +0 -0
- {claude_mpm-4.1.11.dist-info → claude_mpm-4.1.13.dist-info}/entry_points.txt +0 -0
- {claude_mpm-4.1.11.dist-info → claude_mpm-4.1.13.dist-info}/licenses/LICENSE +0 -0
- {claude_mpm-4.1.11.dist-info → claude_mpm-4.1.13.dist-info}/top_level.txt +0 -0
|
@@ -85,38 +85,33 @@ class GitignoreManager:
|
|
|
85
85
|
".Trashes/",
|
|
86
86
|
"desktop.ini",
|
|
87
87
|
]
|
|
88
|
-
|
|
88
|
+
|
|
89
89
|
# Additional patterns to hide dotfiles (when enabled)
|
|
90
90
|
DOTFILE_PATTERNS = [
|
|
91
91
|
".*", # All dotfiles
|
|
92
92
|
".*/", # All dot directories
|
|
93
93
|
]
|
|
94
|
-
|
|
94
|
+
|
|
95
95
|
# Important files/directories to always show
|
|
96
96
|
DOTFILE_EXCEPTIONS = {
|
|
97
97
|
# Removed .gitignore from exceptions - it should be hidden by default
|
|
98
|
-
".env.example",
|
|
98
|
+
".env.example",
|
|
99
99
|
".env.sample",
|
|
100
100
|
".gitlab-ci.yml",
|
|
101
101
|
".travis.yml",
|
|
102
102
|
".dockerignore",
|
|
103
103
|
".editorconfig",
|
|
104
104
|
".eslintrc",
|
|
105
|
-
".prettierrc"
|
|
105
|
+
".prettierrc",
|
|
106
106
|
# Removed .github from exceptions - it should be hidden by default
|
|
107
107
|
}
|
|
108
108
|
|
|
109
|
-
def __init__(self
|
|
110
|
-
"""Initialize the GitignoreManager.
|
|
111
|
-
|
|
112
|
-
Args:
|
|
113
|
-
show_hidden_files: Whether to show hidden files/directories
|
|
114
|
-
"""
|
|
109
|
+
def __init__(self):
|
|
110
|
+
"""Initialize the GitignoreManager."""
|
|
115
111
|
self.logger = get_logger(__name__)
|
|
116
112
|
self._pathspec_cache: Dict[str, Any] = {}
|
|
117
113
|
self._gitignore_cache: Dict[str, List[str]] = {}
|
|
118
114
|
self._use_pathspec = PATHSPEC_AVAILABLE
|
|
119
|
-
self.show_hidden_files = show_hidden_files
|
|
120
115
|
|
|
121
116
|
if not self._use_pathspec:
|
|
122
117
|
self.logger.warning(
|
|
@@ -134,7 +129,7 @@ class GitignoreManager:
|
|
|
134
129
|
"""
|
|
135
130
|
# Always include default patterns
|
|
136
131
|
patterns = self.DEFAULT_PATTERNS.copy()
|
|
137
|
-
|
|
132
|
+
|
|
138
133
|
# Don't add dotfile patterns here - handle them separately in should_ignore
|
|
139
134
|
# This prevents exceptions from being overridden by the .* pattern
|
|
140
135
|
|
|
@@ -157,22 +152,18 @@ class GitignoreManager:
|
|
|
157
152
|
"""
|
|
158
153
|
# Get the filename
|
|
159
154
|
filename = path.name
|
|
160
|
-
|
|
155
|
+
|
|
161
156
|
# 1. ALWAYS hide system files regardless of settings
|
|
162
|
-
ALWAYS_HIDE = {
|
|
163
|
-
if filename in ALWAYS_HIDE or filename.endswith((
|
|
157
|
+
ALWAYS_HIDE = {".DS_Store", "Thumbs.db", ".pyc", ".pyo", ".pyd"}
|
|
158
|
+
if filename in ALWAYS_HIDE or filename.endswith((".pyc", ".pyo", ".pyd")):
|
|
164
159
|
return True
|
|
165
|
-
|
|
166
|
-
# 2. Check dotfiles
|
|
167
|
-
if filename.startswith(
|
|
168
|
-
#
|
|
169
|
-
if
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
# Hide all dotfiles except those in the exceptions list
|
|
173
|
-
# This means: return True (ignore) if NOT in exceptions
|
|
174
|
-
return filename not in self.DOTFILE_EXCEPTIONS
|
|
175
|
-
|
|
160
|
+
|
|
161
|
+
# 2. Check dotfiles - ALWAYS filter them out (except exceptions)
|
|
162
|
+
if filename.startswith("."):
|
|
163
|
+
# Hide all dotfiles except those in the exceptions list
|
|
164
|
+
# This means: return True (ignore) if NOT in exceptions
|
|
165
|
+
return filename not in self.DOTFILE_EXCEPTIONS
|
|
166
|
+
|
|
176
167
|
# Get or create PathSpec for this working directory
|
|
177
168
|
pathspec_obj = self._get_pathspec(working_dir)
|
|
178
169
|
|
|
@@ -181,12 +172,13 @@ class GitignoreManager:
|
|
|
181
172
|
try:
|
|
182
173
|
rel_path = path.relative_to(working_dir)
|
|
183
174
|
rel_path_str = str(rel_path)
|
|
184
|
-
|
|
175
|
+
|
|
185
176
|
# For directories, also check with trailing slash
|
|
186
177
|
if path.is_dir():
|
|
187
|
-
return pathspec_obj.match_file(
|
|
188
|
-
|
|
189
|
-
|
|
178
|
+
return pathspec_obj.match_file(
|
|
179
|
+
rel_path_str
|
|
180
|
+
) or pathspec_obj.match_file(rel_path_str + "/")
|
|
181
|
+
return pathspec_obj.match_file(rel_path_str)
|
|
190
182
|
except ValueError:
|
|
191
183
|
# Path is outside working directory
|
|
192
184
|
return False
|
|
@@ -292,28 +284,24 @@ class GitignoreManager:
|
|
|
292
284
|
"""
|
|
293
285
|
path_str = str(path)
|
|
294
286
|
path_name = path.name
|
|
295
|
-
|
|
287
|
+
|
|
296
288
|
# 1. ALWAYS hide system files regardless of settings
|
|
297
|
-
ALWAYS_HIDE = {
|
|
298
|
-
if path_name in ALWAYS_HIDE or path_name.endswith((
|
|
289
|
+
ALWAYS_HIDE = {".DS_Store", "Thumbs.db", ".pyc", ".pyo", ".pyd"}
|
|
290
|
+
if path_name in ALWAYS_HIDE or path_name.endswith((".pyc", ".pyo", ".pyd")):
|
|
299
291
|
return True
|
|
300
|
-
|
|
301
|
-
# 2. Check dotfiles
|
|
302
|
-
if path_name.startswith(
|
|
303
|
-
#
|
|
304
|
-
|
|
305
|
-
return False # Show the dotfile
|
|
306
|
-
else:
|
|
307
|
-
# Only show if in exceptions list
|
|
308
|
-
return path_name not in self.DOTFILE_EXCEPTIONS
|
|
292
|
+
|
|
293
|
+
# 2. Check dotfiles - ALWAYS filter them out (except exceptions)
|
|
294
|
+
if path_name.startswith("."):
|
|
295
|
+
# Only show if in exceptions list
|
|
296
|
+
return path_name not in self.DOTFILE_EXCEPTIONS
|
|
309
297
|
|
|
310
298
|
patterns = self.get_ignore_patterns(working_dir)
|
|
311
|
-
|
|
299
|
+
|
|
312
300
|
for pattern in patterns:
|
|
313
301
|
# Skip dotfile patterns since we already handled them above
|
|
314
302
|
if pattern in [".*", ".*/"]:
|
|
315
303
|
continue
|
|
316
|
-
|
|
304
|
+
|
|
317
305
|
# Simple pattern matching
|
|
318
306
|
if pattern.endswith("/"):
|
|
319
307
|
# Directory pattern
|
|
@@ -801,11 +789,51 @@ class CodeTreeAnalyzer:
|
|
|
801
789
|
|
|
802
790
|
# Define code file extensions at class level for directory filtering
|
|
803
791
|
CODE_EXTENSIONS = {
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
792
|
+
".py",
|
|
793
|
+
".js",
|
|
794
|
+
".ts",
|
|
795
|
+
".tsx",
|
|
796
|
+
".jsx",
|
|
797
|
+
".java",
|
|
798
|
+
".cpp",
|
|
799
|
+
".c",
|
|
800
|
+
".h",
|
|
801
|
+
".hpp",
|
|
802
|
+
".cs",
|
|
803
|
+
".go",
|
|
804
|
+
".rs",
|
|
805
|
+
".rb",
|
|
806
|
+
".php",
|
|
807
|
+
".swift",
|
|
808
|
+
".kt",
|
|
809
|
+
".scala",
|
|
810
|
+
".r",
|
|
811
|
+
".m",
|
|
812
|
+
".mm",
|
|
813
|
+
".sh",
|
|
814
|
+
".bash",
|
|
815
|
+
".zsh",
|
|
816
|
+
".fish",
|
|
817
|
+
".ps1",
|
|
818
|
+
".bat",
|
|
819
|
+
".cmd",
|
|
820
|
+
".sql",
|
|
821
|
+
".html",
|
|
822
|
+
".css",
|
|
823
|
+
".scss",
|
|
824
|
+
".sass",
|
|
825
|
+
".less",
|
|
826
|
+
".xml",
|
|
827
|
+
".json",
|
|
828
|
+
".yaml",
|
|
829
|
+
".yml",
|
|
830
|
+
".toml",
|
|
831
|
+
".ini",
|
|
832
|
+
".cfg",
|
|
833
|
+
".conf",
|
|
834
|
+
".md",
|
|
835
|
+
".rst",
|
|
836
|
+
".txt",
|
|
809
837
|
}
|
|
810
838
|
|
|
811
839
|
# File extensions to language mapping
|
|
@@ -824,7 +852,6 @@ class CodeTreeAnalyzer:
|
|
|
824
852
|
emit_events: bool = True,
|
|
825
853
|
cache_dir: Optional[Path] = None,
|
|
826
854
|
emitter: Optional[CodeTreeEventEmitter] = None,
|
|
827
|
-
show_hidden_files: bool = False,
|
|
828
855
|
):
|
|
829
856
|
"""Initialize the code tree analyzer.
|
|
830
857
|
|
|
@@ -832,15 +859,13 @@ class CodeTreeAnalyzer:
|
|
|
832
859
|
emit_events: Whether to emit Socket.IO events
|
|
833
860
|
cache_dir: Directory for caching analysis results
|
|
834
861
|
emitter: Optional event emitter to use (creates one if not provided)
|
|
835
|
-
show_hidden_files: Whether to show hidden files/directories (default False - hide dotfiles)
|
|
836
862
|
"""
|
|
837
863
|
self.logger = get_logger(__name__)
|
|
838
864
|
self.emit_events = emit_events
|
|
839
865
|
self.cache_dir = cache_dir or Path.home() / ".claude-mpm" / "code-cache"
|
|
840
|
-
self.show_hidden_files = show_hidden_files
|
|
841
866
|
|
|
842
|
-
# Initialize gitignore manager
|
|
843
|
-
self.gitignore_manager = GitignoreManager(
|
|
867
|
+
# Initialize gitignore manager (always filters dotfiles)
|
|
868
|
+
self.gitignore_manager = GitignoreManager()
|
|
844
869
|
self._last_working_dir = None
|
|
845
870
|
|
|
846
871
|
# Use provided emitter or create one
|
|
@@ -1134,33 +1159,52 @@ class CodeTreeAnalyzer:
|
|
|
1134
1159
|
except Exception as e:
|
|
1135
1160
|
self.logger.warning(f"Failed to save cache: {e}")
|
|
1136
1161
|
|
|
1137
|
-
def has_code_files(
|
|
1162
|
+
def has_code_files(
|
|
1163
|
+
self, directory: Path, depth: int = 5, current_depth: int = 0
|
|
1164
|
+
) -> bool:
|
|
1138
1165
|
"""Check if directory contains code files up to 5 levels deep.
|
|
1139
|
-
|
|
1166
|
+
|
|
1140
1167
|
Args:
|
|
1141
1168
|
directory: Directory to check
|
|
1142
1169
|
depth: Maximum depth to search
|
|
1143
1170
|
current_depth: Current recursion depth
|
|
1144
|
-
|
|
1171
|
+
|
|
1145
1172
|
Returns:
|
|
1146
1173
|
True if directory contains code files within depth levels
|
|
1147
1174
|
"""
|
|
1148
1175
|
if current_depth >= depth:
|
|
1149
1176
|
return False
|
|
1150
|
-
|
|
1177
|
+
|
|
1151
1178
|
# Skip checking these directories entirely
|
|
1152
|
-
SKIP_DIRS = {
|
|
1153
|
-
|
|
1154
|
-
|
|
1179
|
+
SKIP_DIRS = {
|
|
1180
|
+
"node_modules",
|
|
1181
|
+
"__pycache__",
|
|
1182
|
+
".git",
|
|
1183
|
+
".venv",
|
|
1184
|
+
"venv",
|
|
1185
|
+
"dist",
|
|
1186
|
+
"build",
|
|
1187
|
+
".tox",
|
|
1188
|
+
"htmlcov",
|
|
1189
|
+
".pytest_cache",
|
|
1190
|
+
".mypy_cache",
|
|
1191
|
+
"coverage",
|
|
1192
|
+
".idea",
|
|
1193
|
+
".vscode",
|
|
1194
|
+
"env",
|
|
1195
|
+
".coverage",
|
|
1196
|
+
"__MACOSX",
|
|
1197
|
+
".ipynb_checkpoints",
|
|
1198
|
+
}
|
|
1155
1199
|
if directory.name in SKIP_DIRS:
|
|
1156
1200
|
return False
|
|
1157
|
-
|
|
1201
|
+
|
|
1158
1202
|
try:
|
|
1159
1203
|
for item in directory.iterdir():
|
|
1160
1204
|
# Skip hidden items in scan
|
|
1161
|
-
if item.name.startswith(
|
|
1205
|
+
if item.name.startswith("."):
|
|
1162
1206
|
continue
|
|
1163
|
-
|
|
1207
|
+
|
|
1164
1208
|
if item.is_file():
|
|
1165
1209
|
# Check if it's a code file
|
|
1166
1210
|
ext = item.suffix.lower()
|
|
@@ -1171,7 +1215,7 @@ class CodeTreeAnalyzer:
|
|
|
1171
1215
|
return True
|
|
1172
1216
|
except (PermissionError, OSError):
|
|
1173
1217
|
pass
|
|
1174
|
-
|
|
1218
|
+
|
|
1175
1219
|
return False
|
|
1176
1220
|
|
|
1177
1221
|
def discover_top_level(
|
|
@@ -1190,18 +1234,22 @@ class CodeTreeAnalyzer:
|
|
|
1190
1234
|
# NOT the current working directory. This ensures we only show items
|
|
1191
1235
|
# within the requested directory, not parent directories.
|
|
1192
1236
|
working_dir = Path(directory).absolute()
|
|
1193
|
-
|
|
1237
|
+
|
|
1194
1238
|
# Emit discovery start event
|
|
1195
1239
|
if self.emitter:
|
|
1196
1240
|
from datetime import datetime
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1241
|
+
|
|
1242
|
+
self.emitter.emit(
|
|
1243
|
+
"info",
|
|
1244
|
+
{
|
|
1245
|
+
"type": "discovery.start",
|
|
1246
|
+
"action": "scanning_directory",
|
|
1247
|
+
"path": str(directory),
|
|
1248
|
+
"message": f"Starting discovery of {directory.name}",
|
|
1249
|
+
"timestamp": datetime.now().isoformat(),
|
|
1250
|
+
},
|
|
1251
|
+
)
|
|
1252
|
+
|
|
1205
1253
|
result = {
|
|
1206
1254
|
"path": str(directory),
|
|
1207
1255
|
"name": directory.name,
|
|
@@ -1219,19 +1267,23 @@ class CodeTreeAnalyzer:
|
|
|
1219
1267
|
files_count = 0
|
|
1220
1268
|
dirs_count = 0
|
|
1221
1269
|
ignored_count = 0
|
|
1222
|
-
|
|
1270
|
+
|
|
1223
1271
|
for item in directory.iterdir():
|
|
1224
1272
|
# Use gitignore manager for filtering with the directory as working dir
|
|
1225
1273
|
if self.gitignore_manager.should_ignore(item, directory):
|
|
1226
1274
|
if self.emitter:
|
|
1227
1275
|
from datetime import datetime
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1276
|
+
|
|
1277
|
+
self.emitter.emit(
|
|
1278
|
+
"info",
|
|
1279
|
+
{
|
|
1280
|
+
"type": "filter.gitignore",
|
|
1281
|
+
"path": str(item),
|
|
1282
|
+
"reason": "gitignore pattern",
|
|
1283
|
+
"message": f"Ignored by gitignore: {item.name}",
|
|
1284
|
+
"timestamp": datetime.now().isoformat(),
|
|
1285
|
+
},
|
|
1286
|
+
)
|
|
1235
1287
|
ignored_count += 1
|
|
1236
1288
|
continue
|
|
1237
1289
|
|
|
@@ -1239,13 +1291,17 @@ class CodeTreeAnalyzer:
|
|
|
1239
1291
|
if ignore_patterns and any(p in str(item) for p in ignore_patterns):
|
|
1240
1292
|
if self.emitter:
|
|
1241
1293
|
from datetime import datetime
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1294
|
+
|
|
1295
|
+
self.emitter.emit(
|
|
1296
|
+
"info",
|
|
1297
|
+
{
|
|
1298
|
+
"type": "filter.pattern",
|
|
1299
|
+
"path": str(item),
|
|
1300
|
+
"reason": "custom pattern",
|
|
1301
|
+
"message": f"Ignored by pattern: {item.name}",
|
|
1302
|
+
"timestamp": datetime.now().isoformat(),
|
|
1303
|
+
},
|
|
1304
|
+
)
|
|
1249
1305
|
ignored_count += 1
|
|
1250
1306
|
continue
|
|
1251
1307
|
|
|
@@ -1254,38 +1310,39 @@ class CodeTreeAnalyzer:
|
|
|
1254
1310
|
if not self.has_code_files(item, depth=5):
|
|
1255
1311
|
if self.emitter:
|
|
1256
1312
|
from datetime import datetime
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1313
|
+
|
|
1314
|
+
self.emitter.emit(
|
|
1315
|
+
"info",
|
|
1316
|
+
{
|
|
1317
|
+
"type": "filter.no_code",
|
|
1318
|
+
"path": str(item.name),
|
|
1319
|
+
"reason": "no code files",
|
|
1320
|
+
"message": f"Skipped directory without code: {item.name}",
|
|
1321
|
+
"timestamp": datetime.now().isoformat(),
|
|
1322
|
+
},
|
|
1323
|
+
)
|
|
1264
1324
|
ignored_count += 1
|
|
1265
1325
|
continue
|
|
1266
|
-
|
|
1267
|
-
# Directory - just
|
|
1268
|
-
#
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
relative_path = item.relative_to(working_dir)
|
|
1272
|
-
path_str = str(relative_path)
|
|
1273
|
-
except ValueError:
|
|
1274
|
-
# If somehow the item is outside working_dir, skip it
|
|
1275
|
-
self.logger.warning(f"Directory outside working dir: {item}")
|
|
1276
|
-
continue
|
|
1277
|
-
|
|
1326
|
+
|
|
1327
|
+
# Directory - return just the item name
|
|
1328
|
+
# The frontend will construct the full path by combining parent path with child name
|
|
1329
|
+
path_str = item.name
|
|
1330
|
+
|
|
1278
1331
|
# Emit directory found event
|
|
1279
1332
|
if self.emitter:
|
|
1280
1333
|
from datetime import datetime
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1334
|
+
|
|
1335
|
+
self.emitter.emit(
|
|
1336
|
+
"info",
|
|
1337
|
+
{
|
|
1338
|
+
"type": "discovery.directory",
|
|
1339
|
+
"path": str(item),
|
|
1340
|
+
"message": f"Found directory: {item.name}",
|
|
1341
|
+
"timestamp": datetime.now().isoformat(),
|
|
1342
|
+
},
|
|
1343
|
+
)
|
|
1287
1344
|
dirs_count += 1
|
|
1288
|
-
|
|
1345
|
+
|
|
1289
1346
|
child = {
|
|
1290
1347
|
"path": path_str,
|
|
1291
1348
|
"name": item.name,
|
|
@@ -1300,33 +1357,35 @@ class CodeTreeAnalyzer:
|
|
|
1300
1357
|
|
|
1301
1358
|
elif item.is_file():
|
|
1302
1359
|
# Check if it's a supported code file or a special file we want to show
|
|
1303
|
-
if item.suffix in self.supported_extensions or item.name in [
|
|
1360
|
+
if item.suffix in self.supported_extensions or item.name in [
|
|
1361
|
+
".gitignore",
|
|
1362
|
+
".env.example",
|
|
1363
|
+
".env.sample",
|
|
1364
|
+
]:
|
|
1304
1365
|
# File - mark for lazy analysis
|
|
1305
1366
|
language = self._get_language(item)
|
|
1306
|
-
|
|
1307
|
-
#
|
|
1308
|
-
#
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
path_str = str(relative_path)
|
|
1312
|
-
except ValueError:
|
|
1313
|
-
# If somehow the item is outside working_dir, skip it
|
|
1314
|
-
self.logger.warning(f"File outside working dir: {item}")
|
|
1315
|
-
continue
|
|
1316
|
-
|
|
1367
|
+
|
|
1368
|
+
# File path should be just the item name
|
|
1369
|
+
# The frontend will construct the full path by combining parent path with child name
|
|
1370
|
+
path_str = item.name
|
|
1371
|
+
|
|
1317
1372
|
# Emit file found event
|
|
1318
1373
|
if self.emitter:
|
|
1319
1374
|
from datetime import datetime
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1375
|
+
|
|
1376
|
+
self.emitter.emit(
|
|
1377
|
+
"info",
|
|
1378
|
+
{
|
|
1379
|
+
"type": "discovery.file",
|
|
1380
|
+
"path": str(item),
|
|
1381
|
+
"language": language,
|
|
1382
|
+
"size": item.stat().st_size,
|
|
1383
|
+
"message": f"Found file: {item.name} ({language})",
|
|
1384
|
+
"timestamp": datetime.now().isoformat(),
|
|
1385
|
+
},
|
|
1386
|
+
)
|
|
1328
1387
|
files_count += 1
|
|
1329
|
-
|
|
1388
|
+
|
|
1330
1389
|
child = {
|
|
1331
1390
|
"path": path_str,
|
|
1332
1391
|
"name": item.name,
|
|
@@ -1350,17 +1409,21 @@ class CodeTreeAnalyzer:
|
|
|
1350
1409
|
# Emit discovery complete event with stats
|
|
1351
1410
|
if self.emitter:
|
|
1352
1411
|
from datetime import datetime
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1412
|
+
|
|
1413
|
+
self.emitter.emit(
|
|
1414
|
+
"info",
|
|
1415
|
+
{
|
|
1416
|
+
"type": "discovery.complete",
|
|
1417
|
+
"path": str(directory),
|
|
1418
|
+
"stats": {
|
|
1419
|
+
"files": files_count,
|
|
1420
|
+
"directories": dirs_count,
|
|
1421
|
+
"ignored": ignored_count,
|
|
1422
|
+
},
|
|
1423
|
+
"message": f"Discovery complete: {files_count} files, {dirs_count} directories, {ignored_count} ignored",
|
|
1424
|
+
"timestamp": datetime.now().isoformat(),
|
|
1360
1425
|
},
|
|
1361
|
-
|
|
1362
|
-
'timestamp': datetime.now().isoformat()
|
|
1363
|
-
})
|
|
1426
|
+
)
|
|
1364
1427
|
|
|
1365
1428
|
return result
|
|
1366
1429
|
|
|
@@ -1386,7 +1449,8 @@ class CodeTreeAnalyzer:
|
|
|
1386
1449
|
self._last_working_dir = directory.parent
|
|
1387
1450
|
|
|
1388
1451
|
# The discover_top_level method will emit all the INFO events
|
|
1389
|
-
|
|
1452
|
+
result = self.discover_top_level(directory, ignore_patterns)
|
|
1453
|
+
return result
|
|
1390
1454
|
|
|
1391
1455
|
def analyze_file(self, file_path: str) -> Dict[str, Any]:
|
|
1392
1456
|
"""Analyze a specific file and return its AST structure.
|
|
@@ -1407,13 +1471,17 @@ class CodeTreeAnalyzer:
|
|
|
1407
1471
|
# Emit analysis start event
|
|
1408
1472
|
if self.emitter:
|
|
1409
1473
|
from datetime import datetime
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1474
|
+
|
|
1475
|
+
self.emitter.emit(
|
|
1476
|
+
"info",
|
|
1477
|
+
{
|
|
1478
|
+
"type": "analysis.start",
|
|
1479
|
+
"file": str(path),
|
|
1480
|
+
"language": language,
|
|
1481
|
+
"message": f"Analyzing: {path.name}",
|
|
1482
|
+
"timestamp": datetime.now().isoformat(),
|
|
1483
|
+
},
|
|
1484
|
+
)
|
|
1417
1485
|
|
|
1418
1486
|
# Check cache
|
|
1419
1487
|
file_hash = self._get_file_hash(path)
|
|
@@ -1423,22 +1491,30 @@ class CodeTreeAnalyzer:
|
|
|
1423
1491
|
nodes = self.cache[cache_key]
|
|
1424
1492
|
if self.emitter:
|
|
1425
1493
|
from datetime import datetime
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1494
|
+
|
|
1495
|
+
self.emitter.emit(
|
|
1496
|
+
"info",
|
|
1497
|
+
{
|
|
1498
|
+
"type": "cache.hit",
|
|
1499
|
+
"file": str(path),
|
|
1500
|
+
"message": f"Using cached analysis for {path.name}",
|
|
1501
|
+
"timestamp": datetime.now().isoformat(),
|
|
1502
|
+
},
|
|
1503
|
+
)
|
|
1432
1504
|
else:
|
|
1433
1505
|
# Analyze file
|
|
1434
1506
|
if self.emitter:
|
|
1435
1507
|
from datetime import datetime
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1508
|
+
|
|
1509
|
+
self.emitter.emit(
|
|
1510
|
+
"info",
|
|
1511
|
+
{
|
|
1512
|
+
"type": "cache.miss",
|
|
1513
|
+
"file": str(path),
|
|
1514
|
+
"message": f"Cache miss, analyzing fresh: {path.name}",
|
|
1515
|
+
"timestamp": datetime.now().isoformat(),
|
|
1516
|
+
},
|
|
1517
|
+
)
|
|
1442
1518
|
|
|
1443
1519
|
if language == "python":
|
|
1444
1520
|
analyzer = self.python_analyzer
|
|
@@ -1448,17 +1524,21 @@ class CodeTreeAnalyzer:
|
|
|
1448
1524
|
analyzer = self.generic_analyzer
|
|
1449
1525
|
|
|
1450
1526
|
start_time = time.time()
|
|
1451
|
-
|
|
1527
|
+
|
|
1452
1528
|
# Emit parsing event
|
|
1453
1529
|
if self.emitter:
|
|
1454
1530
|
from datetime import datetime
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1531
|
+
|
|
1532
|
+
self.emitter.emit(
|
|
1533
|
+
"info",
|
|
1534
|
+
{
|
|
1535
|
+
"type": "analysis.parse",
|
|
1536
|
+
"file": str(path),
|
|
1537
|
+
"message": f"Parsing file content: {path.name}",
|
|
1538
|
+
"timestamp": datetime.now().isoformat(),
|
|
1539
|
+
},
|
|
1540
|
+
)
|
|
1541
|
+
|
|
1462
1542
|
nodes = analyzer.analyze_file(path) if analyzer else []
|
|
1463
1543
|
duration = time.time() - start_time
|
|
1464
1544
|
|
|
@@ -1470,31 +1550,35 @@ class CodeTreeAnalyzer:
|
|
|
1470
1550
|
classes_count = 0
|
|
1471
1551
|
functions_count = 0
|
|
1472
1552
|
methods_count = 0
|
|
1473
|
-
|
|
1553
|
+
|
|
1474
1554
|
for node in nodes:
|
|
1475
1555
|
# Only include main structural elements
|
|
1476
1556
|
if not self._is_internal_node(node):
|
|
1477
1557
|
# Emit found element event
|
|
1478
1558
|
if self.emitter:
|
|
1479
1559
|
from datetime import datetime
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1560
|
+
|
|
1561
|
+
self.emitter.emit(
|
|
1562
|
+
"info",
|
|
1563
|
+
{
|
|
1564
|
+
"type": f"analysis.{node.node_type}",
|
|
1565
|
+
"name": node.name,
|
|
1566
|
+
"file": str(path),
|
|
1567
|
+
"line_start": node.line_start,
|
|
1568
|
+
"complexity": node.complexity,
|
|
1569
|
+
"message": f"Found {node.node_type}: {node.name}",
|
|
1570
|
+
"timestamp": datetime.now().isoformat(),
|
|
1571
|
+
},
|
|
1572
|
+
)
|
|
1573
|
+
|
|
1490
1574
|
# Count node types
|
|
1491
|
-
if node.node_type ==
|
|
1575
|
+
if node.node_type == "class":
|
|
1492
1576
|
classes_count += 1
|
|
1493
|
-
elif node.node_type ==
|
|
1577
|
+
elif node.node_type == "function":
|
|
1494
1578
|
functions_count += 1
|
|
1495
|
-
elif node.node_type ==
|
|
1579
|
+
elif node.node_type == "method":
|
|
1496
1580
|
methods_count += 1
|
|
1497
|
-
|
|
1581
|
+
|
|
1498
1582
|
filtered_nodes.append(
|
|
1499
1583
|
{
|
|
1500
1584
|
"name": node.name,
|
|
@@ -1510,20 +1594,24 @@ class CodeTreeAnalyzer:
|
|
|
1510
1594
|
# Emit analysis complete event with stats
|
|
1511
1595
|
if self.emitter:
|
|
1512
1596
|
from datetime import datetime
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1597
|
+
|
|
1598
|
+
self.emitter.emit(
|
|
1599
|
+
"info",
|
|
1600
|
+
{
|
|
1601
|
+
"type": "analysis.complete",
|
|
1602
|
+
"file": str(path),
|
|
1603
|
+
"stats": {
|
|
1604
|
+
"classes": classes_count,
|
|
1605
|
+
"functions": functions_count,
|
|
1606
|
+
"methods": methods_count,
|
|
1607
|
+
"total_nodes": len(filtered_nodes),
|
|
1608
|
+
},
|
|
1609
|
+
"duration": duration,
|
|
1610
|
+
"message": f"Analysis complete: {classes_count} classes, {functions_count} functions, {methods_count} methods",
|
|
1611
|
+
"timestamp": datetime.now().isoformat(),
|
|
1521
1612
|
},
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
'timestamp': datetime.now().isoformat()
|
|
1525
|
-
})
|
|
1526
|
-
|
|
1613
|
+
)
|
|
1614
|
+
|
|
1527
1615
|
self.emitter.emit_file_analyzed(file_path, filtered_nodes, duration)
|
|
1528
1616
|
|
|
1529
1617
|
return {
|