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.
Files changed (41) hide show
  1. claude_mpm/VERSION +1 -1
  2. claude_mpm/agents/INSTRUCTIONS.md +8 -0
  3. claude_mpm/cli/__init__.py +1 -1
  4. claude_mpm/cli/commands/monitor.py +88 -627
  5. claude_mpm/cli/commands/mpm_init.py +127 -107
  6. claude_mpm/cli/commands/mpm_init_handler.py +24 -23
  7. claude_mpm/cli/parsers/mpm_init_parser.py +34 -28
  8. claude_mpm/core/config.py +18 -0
  9. claude_mpm/core/instruction_reinforcement_hook.py +266 -0
  10. claude_mpm/core/pm_hook_interceptor.py +105 -8
  11. claude_mpm/dashboard/static/built/components/activity-tree.js +1 -1
  12. claude_mpm/dashboard/static/built/components/code-tree.js +1 -1
  13. claude_mpm/dashboard/static/built/dashboard.js +1 -1
  14. claude_mpm/dashboard/static/built/socket-client.js +1 -1
  15. claude_mpm/dashboard/static/css/activity.css +1239 -267
  16. claude_mpm/dashboard/static/css/dashboard.css +511 -0
  17. claude_mpm/dashboard/static/dist/components/activity-tree.js +1 -1
  18. claude_mpm/dashboard/static/dist/components/code-tree.js +1 -1
  19. claude_mpm/dashboard/static/dist/components/module-viewer.js +1 -1
  20. claude_mpm/dashboard/static/dist/dashboard.js +1 -1
  21. claude_mpm/dashboard/static/dist/socket-client.js +1 -1
  22. claude_mpm/dashboard/static/js/components/activity-tree.js +1193 -892
  23. claude_mpm/dashboard/static/js/components/build-tracker.js +15 -13
  24. claude_mpm/dashboard/static/js/components/code-tree.js +534 -143
  25. claude_mpm/dashboard/static/js/components/module-viewer.js +21 -7
  26. claude_mpm/dashboard/static/js/components/unified-data-viewer.js +1066 -0
  27. claude_mpm/dashboard/static/js/connection-manager.js +1 -1
  28. claude_mpm/dashboard/static/js/dashboard.js +227 -84
  29. claude_mpm/dashboard/static/js/socket-client.js +2 -2
  30. claude_mpm/dashboard/templates/index.html +100 -23
  31. claude_mpm/services/agents/deployment/agent_template_builder.py +11 -7
  32. claude_mpm/services/cli/socketio_manager.py +39 -8
  33. claude_mpm/services/infrastructure/monitoring.py +1 -1
  34. claude_mpm/services/socketio/handlers/code_analysis.py +83 -136
  35. claude_mpm/tools/code_tree_analyzer.py +290 -202
  36. {claude_mpm-4.1.11.dist-info → claude_mpm-4.1.13.dist-info}/METADATA +1 -1
  37. {claude_mpm-4.1.11.dist-info → claude_mpm-4.1.13.dist-info}/RECORD +41 -39
  38. {claude_mpm-4.1.11.dist-info → claude_mpm-4.1.13.dist-info}/WHEEL +0 -0
  39. {claude_mpm-4.1.11.dist-info → claude_mpm-4.1.13.dist-info}/entry_points.txt +0 -0
  40. {claude_mpm-4.1.11.dist-info → claude_mpm-4.1.13.dist-info}/licenses/LICENSE +0 -0
  41. {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, show_hidden_files: bool = False):
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 = {'.DS_Store', 'Thumbs.db', '.pyc', '.pyo', '.pyd'}
163
- if filename in ALWAYS_HIDE or filename.endswith(('.pyc', '.pyo', '.pyd')):
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 BEFORE exceptions
167
- if filename.startswith('.'):
168
- # If showing hidden files, show all dotfiles
169
- if self.show_hidden_files:
170
- return False # Show the dotfile
171
- else:
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(rel_path_str) or pathspec_obj.match_file(rel_path_str + '/')
188
- else:
189
- return pathspec_obj.match_file(rel_path_str)
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 = {'.DS_Store', 'Thumbs.db', '.pyc', '.pyo', '.pyd'}
298
- if path_name in ALWAYS_HIDE or path_name.endswith(('.pyc', '.pyo', '.pyd')):
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 BEFORE exceptions
302
- if path_name.startswith('.'):
303
- # If showing hidden files, check exceptions
304
- if self.show_hidden_files:
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
- '.py', '.js', '.ts', '.tsx', '.jsx', '.java', '.cpp', '.c', '.h', '.hpp',
805
- '.cs', '.go', '.rs', '.rb', '.php', '.swift', '.kt', '.scala', '.r',
806
- '.m', '.mm', '.sh', '.bash', '.zsh', '.fish', '.ps1', '.bat', '.cmd',
807
- '.sql', '.html', '.css', '.scss', '.sass', '.less', '.xml', '.json',
808
- '.yaml', '.yml', '.toml', '.ini', '.cfg', '.conf', '.md', '.rst', '.txt'
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 with hidden files setting (default False)
843
- self.gitignore_manager = GitignoreManager(show_hidden_files=show_hidden_files)
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(self, directory: Path, depth: int = 5, current_depth: int = 0) -> bool:
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 = {'node_modules', '__pycache__', '.git', '.venv', 'venv', 'dist', 'build',
1153
- '.tox', 'htmlcov', '.pytest_cache', '.mypy_cache', 'coverage',
1154
- '.idea', '.vscode', 'env', '.coverage', '__MACOSX', '.ipynb_checkpoints'}
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
- self.emitter.emit('info', {
1198
- 'type': 'discovery.start',
1199
- 'action': 'scanning_directory',
1200
- 'path': str(directory),
1201
- 'message': f'Starting discovery of {directory.name}',
1202
- 'timestamp': datetime.now().isoformat()
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
- self.emitter.emit('info', {
1229
- 'type': 'filter.gitignore',
1230
- 'path': str(item),
1231
- 'reason': 'gitignore pattern',
1232
- 'message': f'Ignored by gitignore: {item.name}',
1233
- 'timestamp': datetime.now().isoformat()
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
- self.emitter.emit('info', {
1243
- 'type': 'filter.pattern',
1244
- 'path': str(item),
1245
- 'reason': 'custom pattern',
1246
- 'message': f'Ignored by pattern: {item.name}',
1247
- 'timestamp': datetime.now().isoformat()
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
- self.emitter.emit('info', {
1258
- 'type': 'filter.no_code',
1259
- 'path': str(item.name),
1260
- 'reason': 'no code files',
1261
- 'message': f'Skipped directory without code: {item.name}',
1262
- 'timestamp': datetime.now().isoformat()
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 mark as unexplored
1268
- # CRITICAL FIX: Use relative path from working directory
1269
- # This prevents the frontend from showing parent directories
1270
- try:
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
- self.emitter.emit('info', {
1282
- 'type': 'discovery.directory',
1283
- 'path': str(item),
1284
- 'message': f'Found directory: {item.name}',
1285
- 'timestamp': datetime.now().isoformat()
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 ['.gitignore', '.env.example', '.env.sample']:
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
- # CRITICAL FIX: Use relative path from working directory
1308
- # This prevents the frontend from showing parent directories
1309
- try:
1310
- relative_path = item.relative_to(working_dir)
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
- self.emitter.emit('info', {
1321
- 'type': 'discovery.file',
1322
- 'path': str(item),
1323
- 'language': language,
1324
- 'size': item.stat().st_size,
1325
- 'message': f'Found file: {item.name} ({language})',
1326
- 'timestamp': datetime.now().isoformat()
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
- self.emitter.emit('info', {
1354
- 'type': 'discovery.complete',
1355
- 'path': str(directory),
1356
- 'stats': {
1357
- 'files': files_count,
1358
- 'directories': dirs_count,
1359
- 'ignored': ignored_count
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
- 'message': f'Discovery complete: {files_count} files, {dirs_count} directories, {ignored_count} ignored',
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
- return self.discover_top_level(directory, ignore_patterns)
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
- self.emitter.emit('info', {
1411
- 'type': 'analysis.start',
1412
- 'file': str(path),
1413
- 'language': language,
1414
- 'message': f'Analyzing: {path.name}',
1415
- 'timestamp': datetime.now().isoformat()
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
- self.emitter.emit('info', {
1427
- 'type': 'cache.hit',
1428
- 'file': str(path),
1429
- 'message': f'Using cached analysis for {path.name}',
1430
- 'timestamp': datetime.now().isoformat()
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
- self.emitter.emit('info', {
1437
- 'type': 'cache.miss',
1438
- 'file': str(path),
1439
- 'message': f'Cache miss, analyzing fresh: {path.name}',
1440
- 'timestamp': datetime.now().isoformat()
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
- self.emitter.emit('info', {
1456
- 'type': 'analysis.parse',
1457
- 'file': str(path),
1458
- 'message': f'Parsing file content: {path.name}',
1459
- 'timestamp': datetime.now().isoformat()
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
- self.emitter.emit('info', {
1481
- 'type': f'analysis.{node.node_type}',
1482
- 'name': node.name,
1483
- 'file': str(path),
1484
- 'line_start': node.line_start,
1485
- 'complexity': node.complexity,
1486
- 'message': f'Found {node.node_type}: {node.name}',
1487
- 'timestamp': datetime.now().isoformat()
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 == 'class':
1575
+ if node.node_type == "class":
1492
1576
  classes_count += 1
1493
- elif node.node_type == 'function':
1577
+ elif node.node_type == "function":
1494
1578
  functions_count += 1
1495
- elif node.node_type == 'method':
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
- self.emitter.emit('info', {
1514
- 'type': 'analysis.complete',
1515
- 'file': str(path),
1516
- 'stats': {
1517
- 'classes': classes_count,
1518
- 'functions': functions_count,
1519
- 'methods': methods_count,
1520
- 'total_nodes': len(filtered_nodes)
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
- 'duration': duration,
1523
- 'message': f'Analysis complete: {classes_count} classes, {functions_count} functions, {methods_count} methods',
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 {