claude-self-reflect 3.3.0 โ 4.0.0
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.
- package/.claude/agents/claude-self-reflect-test.md +525 -11
- package/.claude/agents/quality-fixer.md +314 -0
- package/.claude/agents/reflection-specialist.md +40 -1
- package/installer/cli.js +16 -0
- package/installer/postinstall.js +14 -0
- package/installer/statusline-setup.js +289 -0
- package/mcp-server/run-mcp.sh +45 -7
- package/mcp-server/src/code_reload_tool.py +271 -0
- package/mcp-server/src/embedding_manager.py +60 -26
- package/mcp-server/src/enhanced_tool_registry.py +407 -0
- package/mcp-server/src/mode_switch_tool.py +181 -0
- package/mcp-server/src/parallel_search.py +24 -85
- package/mcp-server/src/project_resolver.py +20 -2
- package/mcp-server/src/reflection_tools.py +60 -13
- package/mcp-server/src/rich_formatting.py +103 -0
- package/mcp-server/src/search_tools.py +180 -79
- package/mcp-server/src/security_patches.py +555 -0
- package/mcp-server/src/server.py +318 -240
- package/mcp-server/src/status.py +13 -8
- package/mcp-server/src/temporal_tools.py +10 -3
- package/mcp-server/src/test_quality.py +153 -0
- package/package.json +6 -1
- package/scripts/ast_grep_final_analyzer.py +328 -0
- package/scripts/ast_grep_unified_registry.py +710 -0
- package/scripts/csr-status +511 -0
- package/scripts/import-conversations-unified.py +114 -28
- package/scripts/session_quality_tracker.py +661 -0
- package/scripts/streaming-watcher.py +140 -5
- package/scripts/update_patterns.py +334 -0
|
@@ -0,0 +1,511 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Claude Self-Reflect Status for CC Statusline
|
|
4
|
+
Standalone script that doesn't require venv activation.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import json
|
|
8
|
+
import time
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from datetime import datetime, timedelta
|
|
11
|
+
import sys
|
|
12
|
+
|
|
13
|
+
# Configuration
|
|
14
|
+
CYCLE_FILE = Path.home() / ".claude-self-reflect" / "statusline_cycle.json"
|
|
15
|
+
CYCLE_INTERVAL = 5 # seconds between cycles
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def get_import_status():
|
|
19
|
+
"""Get current import/indexing status."""
|
|
20
|
+
state_file = Path.home() / ".claude-self-reflect" / "config" / "imported-files.json"
|
|
21
|
+
|
|
22
|
+
if not state_file.exists():
|
|
23
|
+
return "๐ CSR: Not configured"
|
|
24
|
+
|
|
25
|
+
try:
|
|
26
|
+
with open(state_file, 'r') as f:
|
|
27
|
+
state = json.load(f)
|
|
28
|
+
|
|
29
|
+
imported = len(state.get("imported_files", {}))
|
|
30
|
+
|
|
31
|
+
# Count total JSONL files
|
|
32
|
+
claude_dir = Path.home() / ".claude" / "projects"
|
|
33
|
+
total = 0
|
|
34
|
+
if claude_dir.exists():
|
|
35
|
+
for project_dir in claude_dir.iterdir():
|
|
36
|
+
if project_dir.is_dir():
|
|
37
|
+
total += len(list(project_dir.glob("*.jsonl")))
|
|
38
|
+
|
|
39
|
+
if total == 0:
|
|
40
|
+
return "๐ CSR: No files"
|
|
41
|
+
|
|
42
|
+
percent = min(100, (imported / total * 100))
|
|
43
|
+
|
|
44
|
+
# Color coding
|
|
45
|
+
if percent >= 95:
|
|
46
|
+
emoji = "โ
"
|
|
47
|
+
elif percent >= 50:
|
|
48
|
+
emoji = "๐"
|
|
49
|
+
else:
|
|
50
|
+
emoji = "โณ"
|
|
51
|
+
|
|
52
|
+
return f"{emoji} CSR: {percent:.0f}% indexed"
|
|
53
|
+
|
|
54
|
+
except Exception:
|
|
55
|
+
return "๐ CSR: Error"
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def categorize_issues(file_reports):
|
|
59
|
+
"""
|
|
60
|
+
Categorize issues from AST analysis into critical/medium/low.
|
|
61
|
+
"""
|
|
62
|
+
critical = 0
|
|
63
|
+
medium = 0
|
|
64
|
+
low = 0
|
|
65
|
+
|
|
66
|
+
for file_path, report in file_reports.items():
|
|
67
|
+
# Only use top_issues for accurate counting (avoid double-counting from recommendations)
|
|
68
|
+
for issue in report.get('top_issues', []):
|
|
69
|
+
severity = issue.get('severity', 'medium')
|
|
70
|
+
count = issue.get('count', 0)
|
|
71
|
+
issue_id = issue.get('id', '').lower()
|
|
72
|
+
|
|
73
|
+
if severity == 'high' or severity == 'critical':
|
|
74
|
+
critical += count
|
|
75
|
+
elif severity == 'medium':
|
|
76
|
+
# Console.log and print statements are low severity
|
|
77
|
+
if 'print' in issue_id or 'console' in issue_id:
|
|
78
|
+
low += count
|
|
79
|
+
else:
|
|
80
|
+
medium += count
|
|
81
|
+
else:
|
|
82
|
+
low += count
|
|
83
|
+
|
|
84
|
+
return critical, medium, low
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def get_quality_icon(critical=0, medium=0, low=0):
|
|
88
|
+
"""
|
|
89
|
+
Determine quality icon based on issue severity counts.
|
|
90
|
+
"""
|
|
91
|
+
# Icon selection based on highest severity present
|
|
92
|
+
if critical > 0:
|
|
93
|
+
if critical >= 10:
|
|
94
|
+
return "๐ด" # Red circle - Critical issues need immediate attention
|
|
95
|
+
else:
|
|
96
|
+
return "๐ " # Orange circle - Some critical issues
|
|
97
|
+
elif medium > 0:
|
|
98
|
+
if medium >= 50:
|
|
99
|
+
return "๐ก" # Yellow circle - Many medium issues
|
|
100
|
+
else:
|
|
101
|
+
return "๐ข" # Green circle - Few medium issues
|
|
102
|
+
elif low > 0:
|
|
103
|
+
if low >= 100:
|
|
104
|
+
return "โช" # White circle - Many minor issues (prints)
|
|
105
|
+
else:
|
|
106
|
+
return "โ
" # Check mark - Only minor issues
|
|
107
|
+
else:
|
|
108
|
+
return "โจ" # Sparkles - Perfect, no issues
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def format_statusline_quality(critical=0, medium=0, low=0):
|
|
112
|
+
"""
|
|
113
|
+
Format statusline with colored dot and labeled numbers.
|
|
114
|
+
"""
|
|
115
|
+
import os
|
|
116
|
+
icon = get_quality_icon(critical, medium, low)
|
|
117
|
+
|
|
118
|
+
# Check if we should use colors (when in a TTY)
|
|
119
|
+
use_colors = os.isatty(sys.stdout.fileno()) if hasattr(sys.stdout, 'fileno') else False
|
|
120
|
+
|
|
121
|
+
# Build count display with colors if supported
|
|
122
|
+
counts = []
|
|
123
|
+
if critical > 0:
|
|
124
|
+
if use_colors:
|
|
125
|
+
# Use bright red for critical
|
|
126
|
+
counts.append(f"\033[1;31mC:{critical}\033[0m")
|
|
127
|
+
else:
|
|
128
|
+
counts.append(f"C:{critical}")
|
|
129
|
+
if medium > 0:
|
|
130
|
+
if use_colors:
|
|
131
|
+
# Use bright yellow for medium
|
|
132
|
+
counts.append(f"\033[1;33mM:{medium}\033[0m")
|
|
133
|
+
else:
|
|
134
|
+
counts.append(f"M:{medium}")
|
|
135
|
+
if low > 0:
|
|
136
|
+
if use_colors:
|
|
137
|
+
# Use bright white/gray for low
|
|
138
|
+
counts.append(f"\033[1;37mL:{low}\033[0m")
|
|
139
|
+
else:
|
|
140
|
+
counts.append(f"L:{low}")
|
|
141
|
+
|
|
142
|
+
if counts:
|
|
143
|
+
return f"{icon} {' '.join(counts)}"
|
|
144
|
+
else:
|
|
145
|
+
return f"{icon}" # Perfect - no counts needed
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def get_session_health():
|
|
149
|
+
"""Get cached session health with icon-based quality display."""
|
|
150
|
+
# Check for session edit tracker to show appropriate label
|
|
151
|
+
tracker_file = Path.home() / ".claude-self-reflect" / "current_session_edits.json"
|
|
152
|
+
|
|
153
|
+
# Get quality cache file for current project
|
|
154
|
+
project_name = Path.cwd().name
|
|
155
|
+
cache_file = Path.home() / ".claude-self-reflect" / "quality_cache" / f"{project_name}.json"
|
|
156
|
+
|
|
157
|
+
# Default label prefix
|
|
158
|
+
label_prefix = ""
|
|
159
|
+
|
|
160
|
+
# Check if we have a session tracker with edited files
|
|
161
|
+
if tracker_file.exists():
|
|
162
|
+
try:
|
|
163
|
+
with open(tracker_file, 'r') as f:
|
|
164
|
+
tracker_data = json.load(f)
|
|
165
|
+
edited_files = tracker_data.get('edited_files', [])
|
|
166
|
+
if edited_files:
|
|
167
|
+
# Show session label with file count
|
|
168
|
+
file_count = len(edited_files)
|
|
169
|
+
label_prefix = f"Session ({file_count} file{'s' if file_count > 1 else ''}): "
|
|
170
|
+
except:
|
|
171
|
+
pass
|
|
172
|
+
|
|
173
|
+
if not cache_file.exists():
|
|
174
|
+
# Fall back to import status if no health data
|
|
175
|
+
return get_import_status()
|
|
176
|
+
|
|
177
|
+
try:
|
|
178
|
+
# Check cache age
|
|
179
|
+
mtime = datetime.fromtimestamp(cache_file.stat().st_mtime)
|
|
180
|
+
age = datetime.now() - mtime
|
|
181
|
+
|
|
182
|
+
if age > timedelta(minutes=5):
|
|
183
|
+
# Fall back to import status if stale
|
|
184
|
+
return get_import_status()
|
|
185
|
+
|
|
186
|
+
with open(cache_file, 'r') as f:
|
|
187
|
+
data = json.load(f)
|
|
188
|
+
|
|
189
|
+
if data.get('status') != 'success':
|
|
190
|
+
# Fall back to import status if no session
|
|
191
|
+
return get_import_status()
|
|
192
|
+
|
|
193
|
+
# Extract issue counts by severity
|
|
194
|
+
file_reports = data.get('file_reports', {})
|
|
195
|
+
critical, medium, low = categorize_issues(file_reports)
|
|
196
|
+
|
|
197
|
+
# Use the icon-based display with optional label
|
|
198
|
+
quality_display = format_statusline_quality(critical, medium, low)
|
|
199
|
+
|
|
200
|
+
# Add session label if we have one
|
|
201
|
+
if data.get('scope_label') == 'Session':
|
|
202
|
+
# For session scope, always show the label with counts
|
|
203
|
+
if label_prefix:
|
|
204
|
+
if critical == 0 and medium == 0 and low == 0:
|
|
205
|
+
return f"{label_prefix}0 0 0 {quality_display}"
|
|
206
|
+
else:
|
|
207
|
+
return f"{label_prefix}{critical} {medium} {low} {quality_display}"
|
|
208
|
+
else:
|
|
209
|
+
# Fallback if no tracker file
|
|
210
|
+
return f"Session: {critical} {medium} {low} {quality_display}"
|
|
211
|
+
|
|
212
|
+
return quality_display
|
|
213
|
+
|
|
214
|
+
except Exception:
|
|
215
|
+
return get_import_status()
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def get_current_cycle():
|
|
219
|
+
"""Determine which metric to show based on cycle."""
|
|
220
|
+
# Read or create cycle state
|
|
221
|
+
cycle_state = {"last_update": 0, "current": "import"}
|
|
222
|
+
|
|
223
|
+
if CYCLE_FILE.exists():
|
|
224
|
+
try:
|
|
225
|
+
with open(CYCLE_FILE, 'r') as f:
|
|
226
|
+
cycle_state = json.load(f)
|
|
227
|
+
except:
|
|
228
|
+
pass
|
|
229
|
+
|
|
230
|
+
# Check if it's time to cycle
|
|
231
|
+
now = time.time()
|
|
232
|
+
if now - cycle_state["last_update"] >= CYCLE_INTERVAL:
|
|
233
|
+
# Toggle between import and health
|
|
234
|
+
cycle_state["current"] = "health" if cycle_state["current"] == "import" else "import"
|
|
235
|
+
cycle_state["last_update"] = now
|
|
236
|
+
|
|
237
|
+
# Save state
|
|
238
|
+
CYCLE_FILE.parent.mkdir(exist_ok=True)
|
|
239
|
+
with open(CYCLE_FILE, 'w') as f:
|
|
240
|
+
json.dump(cycle_state, f)
|
|
241
|
+
|
|
242
|
+
return cycle_state["current"]
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
def get_compact_status():
|
|
246
|
+
"""Get both import and quality in compact format: [100%][๐ข:A+]"""
|
|
247
|
+
import subprocess
|
|
248
|
+
import os
|
|
249
|
+
import re
|
|
250
|
+
import shutil
|
|
251
|
+
|
|
252
|
+
# Get project-specific status using claude-self-reflect status command
|
|
253
|
+
import_pct = "?"
|
|
254
|
+
time_behind = ""
|
|
255
|
+
|
|
256
|
+
try:
|
|
257
|
+
# Get current working directory to determine project
|
|
258
|
+
cwd = os.getcwd()
|
|
259
|
+
project_name = os.path.basename(cwd)
|
|
260
|
+
|
|
261
|
+
# Get status from claude-self-reflect with secure path
|
|
262
|
+
import shutil
|
|
263
|
+
csr_binary = shutil.which("claude-self-reflect")
|
|
264
|
+
if not csr_binary or not os.path.isfile(csr_binary):
|
|
265
|
+
# Fallback if binary not found
|
|
266
|
+
import_pct = "?"
|
|
267
|
+
return f"[{import_pct}]"
|
|
268
|
+
|
|
269
|
+
result = subprocess.run(
|
|
270
|
+
[csr_binary, "status"],
|
|
271
|
+
capture_output=True,
|
|
272
|
+
text=True,
|
|
273
|
+
timeout=2
|
|
274
|
+
)
|
|
275
|
+
|
|
276
|
+
if result.returncode == 0:
|
|
277
|
+
status_data = json.loads(result.stdout)
|
|
278
|
+
|
|
279
|
+
# Try to find project-specific percentage
|
|
280
|
+
project_pct = None
|
|
281
|
+
encoded_path = None
|
|
282
|
+
|
|
283
|
+
# Try exact project name FIRST
|
|
284
|
+
if project_name in status_data.get("projects", {}):
|
|
285
|
+
project_pct = status_data["projects"][project_name].get("percentage")
|
|
286
|
+
encoded_path = project_name # Use project name for file lookup
|
|
287
|
+
|
|
288
|
+
# Only try encoded path if exact match not found
|
|
289
|
+
elif project_pct is None:
|
|
290
|
+
# Convert path to encoded format
|
|
291
|
+
encoded_path = cwd.replace("/", "-")
|
|
292
|
+
if encoded_path.startswith("-"):
|
|
293
|
+
encoded_path = encoded_path[1:] # Remove leading dash
|
|
294
|
+
|
|
295
|
+
if encoded_path in status_data.get("projects", {}):
|
|
296
|
+
project_pct = status_data["projects"][encoded_path].get("percentage")
|
|
297
|
+
|
|
298
|
+
# Use project percentage if found, otherwise use overall
|
|
299
|
+
if project_pct is not None:
|
|
300
|
+
pct = int(project_pct)
|
|
301
|
+
else:
|
|
302
|
+
pct = int(status_data.get("overall", {}).get("percentage", 0))
|
|
303
|
+
|
|
304
|
+
import_pct = f"{pct}%"
|
|
305
|
+
|
|
306
|
+
# Only show time behind if NOT at 100%
|
|
307
|
+
# This indicates how old the unindexed files are
|
|
308
|
+
if pct < 100:
|
|
309
|
+
# Check for newest unindexed file
|
|
310
|
+
state_file = Path.home() / ".claude-self-reflect" / "config" / "imported-files.json"
|
|
311
|
+
if state_file.exists():
|
|
312
|
+
with open(state_file, 'r') as f:
|
|
313
|
+
state = json.load(f)
|
|
314
|
+
|
|
315
|
+
# Find project directory
|
|
316
|
+
claude_dir = Path.home() / ".claude" / "projects"
|
|
317
|
+
if encoded_path:
|
|
318
|
+
project_dir = claude_dir / encoded_path
|
|
319
|
+
if not project_dir.exists() and not encoded_path.startswith("-"):
|
|
320
|
+
project_dir = claude_dir / f"-{encoded_path}"
|
|
321
|
+
|
|
322
|
+
if project_dir.exists():
|
|
323
|
+
# Find the newest UNINDEXED file
|
|
324
|
+
newest_unindexed_time = None
|
|
325
|
+
for jsonl_file in project_dir.glob("*.jsonl"):
|
|
326
|
+
file_key = str(jsonl_file)
|
|
327
|
+
# Only check unindexed files
|
|
328
|
+
if file_key not in state.get("imported_files", {}):
|
|
329
|
+
file_time = datetime.fromtimestamp(jsonl_file.stat().st_mtime)
|
|
330
|
+
if newest_unindexed_time is None or file_time > newest_unindexed_time:
|
|
331
|
+
newest_unindexed_time = file_time
|
|
332
|
+
|
|
333
|
+
# Calculate how behind we are
|
|
334
|
+
if newest_unindexed_time:
|
|
335
|
+
age = datetime.now() - newest_unindexed_time
|
|
336
|
+
if age < timedelta(minutes=5):
|
|
337
|
+
time_behind = " <5m"
|
|
338
|
+
elif age < timedelta(hours=1):
|
|
339
|
+
time_behind = f" {int(age.total_seconds() / 60)}m"
|
|
340
|
+
elif age < timedelta(days=1):
|
|
341
|
+
time_behind = f" {int(age.total_seconds() / 3600)}h"
|
|
342
|
+
else:
|
|
343
|
+
time_behind = f" {int(age.days)}d"
|
|
344
|
+
except:
|
|
345
|
+
# Fallback to simple file counting
|
|
346
|
+
state_file = Path.home() / ".claude-self-reflect" / "config" / "imported-files.json"
|
|
347
|
+
if state_file.exists():
|
|
348
|
+
try:
|
|
349
|
+
with open(state_file, 'r') as f:
|
|
350
|
+
state = json.load(f)
|
|
351
|
+
imported = len(state.get("imported_files", {}))
|
|
352
|
+
|
|
353
|
+
claude_dir = Path.home() / ".claude" / "projects"
|
|
354
|
+
total = 0
|
|
355
|
+
if claude_dir.exists():
|
|
356
|
+
for project_dir in claude_dir.iterdir():
|
|
357
|
+
if project_dir.is_dir():
|
|
358
|
+
total += len(list(project_dir.glob("*.jsonl")))
|
|
359
|
+
|
|
360
|
+
if total > 0:
|
|
361
|
+
pct = min(100, int(imported / total * 100))
|
|
362
|
+
import_pct = f"{pct}%"
|
|
363
|
+
except:
|
|
364
|
+
pass
|
|
365
|
+
|
|
366
|
+
# Get quality grade - PER PROJECT cache
|
|
367
|
+
# BUG FIX: Cache must be per-project, not global!
|
|
368
|
+
project_name = os.path.basename(os.getcwd())
|
|
369
|
+
# Secure sanitization with whitelist approach
|
|
370
|
+
import re
|
|
371
|
+
safe_project_name = re.sub(r'[^a-zA-Z0-9_-]', '_', project_name)[:100]
|
|
372
|
+
cache_dir = Path.home() / ".claude-self-reflect" / "quality_cache"
|
|
373
|
+
cache_file = cache_dir / f"{safe_project_name}.json"
|
|
374
|
+
|
|
375
|
+
# If the exact cache file doesn't exist, try to find one that ends with this project name
|
|
376
|
+
# This handles cases like "metafora-Atlas-gold.json" for project "Atlas-gold"
|
|
377
|
+
if not cache_file.exists():
|
|
378
|
+
# Look for files ending with the project name
|
|
379
|
+
possible_files = list(cache_dir.glob(f"*-{safe_project_name}.json"))
|
|
380
|
+
if possible_files:
|
|
381
|
+
cache_file = possible_files[0] # Use the first match
|
|
382
|
+
|
|
383
|
+
# Validate cache file path stays within cache directory
|
|
384
|
+
if cache_file.exists() and not str(cache_file.resolve()).startswith(str(cache_dir.resolve())):
|
|
385
|
+
# Security issue - return placeholder
|
|
386
|
+
grade_str = "[...]"
|
|
387
|
+
else:
|
|
388
|
+
cache_file.parent.mkdir(exist_ok=True, parents=True)
|
|
389
|
+
grade_str = ""
|
|
390
|
+
|
|
391
|
+
# Try to get quality data (regenerate if too old or missing)
|
|
392
|
+
quality_valid = False
|
|
393
|
+
|
|
394
|
+
if cache_file.exists():
|
|
395
|
+
try:
|
|
396
|
+
mtime = datetime.fromtimestamp(cache_file.stat().st_mtime)
|
|
397
|
+
age = datetime.now() - mtime
|
|
398
|
+
|
|
399
|
+
# Use quality data up to 30 minutes old for fresher results
|
|
400
|
+
if age < timedelta(minutes=30):
|
|
401
|
+
with open(cache_file, 'r') as f:
|
|
402
|
+
data = json.load(f)
|
|
403
|
+
|
|
404
|
+
if data.get('status') == 'non-code':
|
|
405
|
+
# Non-code project - show documentation indicator
|
|
406
|
+
grade_str = "[๐:Docs]"
|
|
407
|
+
quality_valid = True
|
|
408
|
+
elif data.get('status') == 'success':
|
|
409
|
+
# Extract issue counts by severity for icon display
|
|
410
|
+
file_reports = data.get('file_reports', {})
|
|
411
|
+
critical, medium, low = categorize_issues(file_reports)
|
|
412
|
+
|
|
413
|
+
# Get icon based on severity
|
|
414
|
+
icon = get_quality_icon(critical, medium, low)
|
|
415
|
+
|
|
416
|
+
# Build compact display with ANSI colors for each severity level
|
|
417
|
+
colored_parts = []
|
|
418
|
+
if critical > 0:
|
|
419
|
+
colored_parts.append(f"\033[31m{critical}\033[0m") # Standard red for critical
|
|
420
|
+
if medium > 0:
|
|
421
|
+
colored_parts.append(f"\033[33m{medium}\033[0m") # Standard yellow for medium
|
|
422
|
+
if low > 0:
|
|
423
|
+
colored_parts.append(f"\033[37m{low}\033[0m") # White/light gray for low
|
|
424
|
+
|
|
425
|
+
# Join with middle dot separator
|
|
426
|
+
if colored_parts:
|
|
427
|
+
grade_str = f"[{icon}:{'ยท'.join(colored_parts)}]"
|
|
428
|
+
else:
|
|
429
|
+
grade_str = f"[{icon}]"
|
|
430
|
+
|
|
431
|
+
quality_valid = True
|
|
432
|
+
except:
|
|
433
|
+
pass
|
|
434
|
+
|
|
435
|
+
# If no valid quality data, show last known value or placeholder
|
|
436
|
+
if not quality_valid and not grade_str:
|
|
437
|
+
# Try to use last known value from cache even if expired
|
|
438
|
+
try:
|
|
439
|
+
if cache_file.exists():
|
|
440
|
+
with open(cache_file, 'r') as f:
|
|
441
|
+
old_data = json.load(f)
|
|
442
|
+
if old_data.get('status') == 'non-code':
|
|
443
|
+
# Non-code project - show documentation indicator
|
|
444
|
+
grade_str = "[๐:Docs]"
|
|
445
|
+
elif old_data.get('status') == 'success':
|
|
446
|
+
# Extract issue counts by severity for icon display
|
|
447
|
+
file_reports = old_data.get('file_reports', {})
|
|
448
|
+
critical, medium, low = categorize_issues(file_reports)
|
|
449
|
+
|
|
450
|
+
# Get icon based on severity
|
|
451
|
+
icon = get_quality_icon(critical, medium, low)
|
|
452
|
+
|
|
453
|
+
# Build compact display with ANSI colors for each severity level
|
|
454
|
+
colored_parts = []
|
|
455
|
+
if critical > 0:
|
|
456
|
+
colored_parts.append(f"\033[31m{critical}\033[0m") # Standard red for critical
|
|
457
|
+
if medium > 0:
|
|
458
|
+
colored_parts.append(f"\033[33m{medium}\033[0m") # Standard yellow for medium
|
|
459
|
+
if low > 0:
|
|
460
|
+
colored_parts.append(f"\033[37m{low}\033[0m") # White/light gray for low
|
|
461
|
+
|
|
462
|
+
# Join with middle dot separator
|
|
463
|
+
if colored_parts:
|
|
464
|
+
grade_str = f"[{icon}:{'ยท'.join(colored_parts)}]"
|
|
465
|
+
else:
|
|
466
|
+
grade_str = f"[{icon}]"
|
|
467
|
+
else:
|
|
468
|
+
grade_str = "[...]"
|
|
469
|
+
else:
|
|
470
|
+
grade_str = "[...]"
|
|
471
|
+
except:
|
|
472
|
+
grade_str = "[...]"
|
|
473
|
+
|
|
474
|
+
# Add mini progress bar if not 100%
|
|
475
|
+
bar_str = ""
|
|
476
|
+
if import_pct != "?" and import_pct != "100%":
|
|
477
|
+
pct_num = int(import_pct.rstrip('%'))
|
|
478
|
+
filled = int(pct_num * 4 / 100) # 4-char mini bar
|
|
479
|
+
empty = 4 - filled
|
|
480
|
+
bar_str = "โ" * filled + "โ" * empty + " "
|
|
481
|
+
|
|
482
|
+
# Return compact format with bar, percentage, time behind, and grade
|
|
483
|
+
return f"[{bar_str}{import_pct}{time_behind}]{grade_str}"
|
|
484
|
+
|
|
485
|
+
def main():
|
|
486
|
+
"""Main entry point for CC statusline."""
|
|
487
|
+
# Check for forced mode
|
|
488
|
+
if len(sys.argv) > 1:
|
|
489
|
+
if sys.argv[1] == "--import":
|
|
490
|
+
print(get_import_status())
|
|
491
|
+
elif sys.argv[1] == "--health":
|
|
492
|
+
print(get_session_health())
|
|
493
|
+
elif sys.argv[1] == "--quality-only":
|
|
494
|
+
# Only show quality, not import (to avoid duplication with MCP status)
|
|
495
|
+
health = get_session_health()
|
|
496
|
+
# Only show if it's actual quality data, not fallback to import
|
|
497
|
+
if "Code:" in health:
|
|
498
|
+
print(health)
|
|
499
|
+
elif sys.argv[1] == "--compact":
|
|
500
|
+
print(get_compact_status())
|
|
501
|
+
else:
|
|
502
|
+
# Default to compact mode
|
|
503
|
+
print(get_compact_status())
|
|
504
|
+
return
|
|
505
|
+
|
|
506
|
+
# Default to compact format (no cycling)
|
|
507
|
+
print(get_compact_status())
|
|
508
|
+
|
|
509
|
+
|
|
510
|
+
if __name__ == "__main__":
|
|
511
|
+
main()
|