cct-cli 0.7.9.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.
- calc_terminal/__init__.py +14 -0
- calc_terminal/__main__.py +14 -0
- calc_terminal/activity.py +1334 -0
- calc_terminal/agent.py +3387 -0
- calc_terminal/agent_runtime.py +519 -0
- calc_terminal/ai_context.py +447 -0
- calc_terminal/ai_modes.py +752 -0
- calc_terminal/ai_personalization.py +286 -0
- calc_terminal/ai_preview_feedback.py +213 -0
- calc_terminal/aicore.py +2572 -0
- calc_terminal/anim.py +367 -0
- calc_terminal/app.py +3685 -0
- calc_terminal/art.py +639 -0
- calc_terminal/atomsim.py +368 -0
- calc_terminal/attachments.py +743 -0
- calc_terminal/benchmark_system.py +414 -0
- calc_terminal/browser/__init__.py +36 -0
- calc_terminal/browser/browser_state.py +346 -0
- calc_terminal/browser/devserver.py +176 -0
- calc_terminal/browser/engine.py +494 -0
- calc_terminal/browser/navigation.py +84 -0
- calc_terminal/browser/preview.py +429 -0
- calc_terminal/browser/preview_entry.py +95 -0
- calc_terminal/browser/project_detector.py +144 -0
- calc_terminal/browser/server.py +449 -0
- calc_terminal/browser/state.py +75 -0
- calc_terminal/browser/watcher.py +99 -0
- calc_terminal/browser_gui/__init__.py +1 -0
- calc_terminal/browser_gui/__main__.py +3 -0
- calc_terminal/browser_gui/launcher.py +173 -0
- calc_terminal/browser_gui/playwright_browser.py +117 -0
- calc_terminal/browser_gui/qt_browser.py +1501 -0
- calc_terminal/browser_gui/webview_browser.py +57 -0
- calc_terminal/capabilities/__init__.py +35 -0
- calc_terminal/capabilities/adapters/__init__.py +33 -0
- calc_terminal/capabilities/adapters/bioinformatics.py +204 -0
- calc_terminal/capabilities/adapters/browser_adapter.py +205 -0
- calc_terminal/capabilities/adapters/filesystem.py +206 -0
- calc_terminal/capabilities/adapters/git_adapter.py +202 -0
- calc_terminal/capabilities/adapters/jupyter_adapter.py +138 -0
- calc_terminal/capabilities/adapters/ml_frameworks.py +158 -0
- calc_terminal/capabilities/adapters/platforms.py +200 -0
- calc_terminal/capabilities/adapters/python_exec.py +93 -0
- calc_terminal/capabilities/adapters/quantum_adapter.py +150 -0
- calc_terminal/capabilities/adapters/scientific_comp.py +123 -0
- calc_terminal/capabilities/adapters/structural_bio.py +161 -0
- calc_terminal/capabilities/adapters/terminal.py +99 -0
- calc_terminal/capabilities/bus.py +178 -0
- calc_terminal/capabilities/discovery.py +207 -0
- calc_terminal/capabilities/schema.py +221 -0
- calc_terminal/cat.ico +0 -0
- calc_terminal/cat_browser.py +2018 -0
- calc_terminal/chat_store.py +703 -0
- calc_terminal/cli.py +1178 -0
- calc_terminal/code_editor.py +640 -0
- calc_terminal/collaboration.py +723 -0
- calc_terminal/commands_data.py +139 -0
- calc_terminal/compatibility_engine.py +352 -0
- calc_terminal/compute/__init__.py +31 -0
- calc_terminal/compute/fabric.py +350 -0
- calc_terminal/config.py +227 -0
- calc_terminal/core/__init__.py +41 -0
- calc_terminal/core/checkpoint.py +156 -0
- calc_terminal/core/input/__init__.py +45 -0
- calc_terminal/core/mode_registry.py +300 -0
- calc_terminal/core/project_graph.py +172 -0
- calc_terminal/core/recovery.py +129 -0
- calc_terminal/core/security_layer.py +112 -0
- calc_terminal/core/task_graph.py +202 -0
- calc_terminal/core/unified_runtime.py +184 -0
- calc_terminal/core/verification.py +257 -0
- calc_terminal/customization.py +1566 -0
- calc_terminal/derivations.py +153 -0
- calc_terminal/device_control.py +263 -0
- calc_terminal/diagnostics/__init__.py +27 -0
- calc_terminal/diagnostics/doctor_engine.py +382 -0
- calc_terminal/diagnostics/self_test.py +247 -0
- calc_terminal/doctor.py +519 -0
- calc_terminal/easter_eggs.py +274 -0
- calc_terminal/editor/__init__.py +1 -0
- calc_terminal/editor/actions.py +263 -0
- calc_terminal/editor/commands.py +160 -0
- calc_terminal/editor/shortcuts.py +226 -0
- calc_terminal/engine.py +259 -0
- calc_terminal/errors.py +120 -0
- calc_terminal/event_stream.py +146 -0
- calc_terminal/eventbus.py +133 -0
- calc_terminal/extensions.py +733 -0
- calc_terminal/fallback_cli.py +1321 -0
- calc_terminal/first_run.py +265 -0
- calc_terminal/fomoji_auth.py +1043 -0
- calc_terminal/formulas.py +82 -0
- calc_terminal/fs_cache.py +121 -0
- calc_terminal/fs_watcher.py +277 -0
- calc_terminal/game.py +193 -0
- calc_terminal/gen1.py +5 -0
- calc_terminal/generators.py +245 -0
- calc_terminal/gestures/__init__.py +42 -0
- calc_terminal/gestures/bindings.py +175 -0
- calc_terminal/gestures/manager.py +477 -0
- calc_terminal/goodbye.py +363 -0
- calc_terminal/gpu3d.py +290 -0
- calc_terminal/graphs.py +358 -0
- calc_terminal/hardware_analyzer.py +440 -0
- calc_terminal/host/__init__.py +30 -0
- calc_terminal/host/browser_manager.py +187 -0
- calc_terminal/host/desktop.py +1386 -0
- calc_terminal/host/launcher.py +395 -0
- calc_terminal/host/terminal.py +279 -0
- calc_terminal/identity.py +216 -0
- calc_terminal/input/__init__.py +54 -0
- calc_terminal/input/capabilities.py +258 -0
- calc_terminal/input/focus.py +87 -0
- calc_terminal/input/gestures.py +64 -0
- calc_terminal/input/pointer.py +114 -0
- calc_terminal/input/touch.py +345 -0
- calc_terminal/keys.py +84 -0
- calc_terminal/live_automation.py +165 -0
- calc_terminal/mathtext.py +433 -0
- calc_terminal/mcp.py +386 -0
- calc_terminal/memory.py +337 -0
- calc_terminal/memory_v2.py +479 -0
- calc_terminal/metrics.py +333 -0
- calc_terminal/mode_detection.py +146 -0
- calc_terminal/model.py +2431 -0
- calc_terminal/model_router.py +665 -0
- calc_terminal/models/__init__.py +0 -0
- calc_terminal/models/active_state.py +187 -0
- calc_terminal/models/dynamic_registry.py +584 -0
- calc_terminal/models/manager.py +781 -0
- calc_terminal/models/model_metadata.json +3526 -0
- calc_terminal/models/profiles.py +194 -0
- calc_terminal/models/registry.py +265 -0
- calc_terminal/models/schema.py +197 -0
- calc_terminal/models/validator.py +287 -0
- calc_terminal/models/verification_engine.py +368 -0
- calc_terminal/native_picker.py +215 -0
- calc_terminal/ollama_catalog.py +279 -0
- calc_terminal/ollama_download.py +233 -0
- calc_terminal/orchestrator.py +304 -0
- calc_terminal/package_research.py +322 -0
- calc_terminal/packages.py +1024 -0
- calc_terminal/pc_specs.py +116 -0
- calc_terminal/permissions.py +334 -0
- calc_terminal/pet.py +106 -0
- calc_terminal/pipeline.py +505 -0
- calc_terminal/platform/__init__.py +491 -0
- calc_terminal/platform/desktop.py +491 -0
- calc_terminal/platform/web.py +781 -0
- calc_terminal/preview/__init__.py +1 -0
- calc_terminal/preview/dev_server.py +303 -0
- calc_terminal/preview/diagnostics.py +131 -0
- calc_terminal/preview/live_reload.py +66 -0
- calc_terminal/preview/manager.py +129 -0
- calc_terminal/project_stats.py +209 -0
- calc_terminal/projects.py +328 -0
- calc_terminal/providers/__init__.py +0 -0
- calc_terminal/providers/adapters/__init__.py +80 -0
- calc_terminal/providers/adapters/anthropic_adapter.py +127 -0
- calc_terminal/providers/adapters/base.py +106 -0
- calc_terminal/providers/adapters/chinese_adapters.py +420 -0
- calc_terminal/providers/adapters/gemini_adapter.py +101 -0
- calc_terminal/providers/adapters/ollama_adapter.py +83 -0
- calc_terminal/providers/adapters/openai_adapter.py +159 -0
- calc_terminal/providers/adapters/other_adapters.py +246 -0
- calc_terminal/providers/anthropic_provider.py +172 -0
- calc_terminal/providers/auto_update.py +416 -0
- calc_terminal/providers/base_provider.py +105 -0
- calc_terminal/providers/discovery_manager.py +207 -0
- calc_terminal/providers/gemini_provider.py +178 -0
- calc_terminal/providers/lifecycle.py +767 -0
- calc_terminal/providers/ollama_adapter.py +707 -0
- calc_terminal/providers/openai_provider.py +254 -0
- calc_terminal/providers/provider_manager.py +1827 -0
- calc_terminal/providers/providers.json +4075 -0
- calc_terminal/reactionsim.py +279 -0
- calc_terminal/registry.py +337 -0
- calc_terminal/report.py +162 -0
- calc_terminal/research/__init__.py +45 -0
- calc_terminal/research/artifact_intel.py +126 -0
- calc_terminal/research/data_lineage.py +123 -0
- calc_terminal/research/experiment_ledger.py +303 -0
- calc_terminal/research/reproducibility.py +131 -0
- calc_terminal/resilience/__init__.py +47 -0
- calc_terminal/resilience/agent_state.py +121 -0
- calc_terminal/resilience/capability_matcher.py +174 -0
- calc_terminal/resilience/circuit_breaker.py +158 -0
- calc_terminal/resilience/failover_engine.py +230 -0
- calc_terminal/resilience/health_monitor.py +192 -0
- calc_terminal/resilience/ollama_adapter.py +125 -0
- calc_terminal/resilience/orchestrator.py +312 -0
- calc_terminal/resilience/types.py +134 -0
- calc_terminal/sandbox.py +98 -0
- calc_terminal/scires.py +558 -0
- calc_terminal/security_scanner.py +126 -0
- calc_terminal/session.py +294 -0
- calc_terminal/sim3d.py +206 -0
- calc_terminal/solver.py +276 -0
- calc_terminal/sound.py +127 -0
- calc_terminal/task_reports.py +287 -0
- calc_terminal/terminal_host.py +201 -0
- calc_terminal/terminal_identity.py +411 -0
- calc_terminal/test_ai_mode_reliability.py +344 -0
- calc_terminal/test_browser.py +368 -0
- calc_terminal/test_code_editor_upgrade.py +485 -0
- calc_terminal/test_customization.py +1148 -0
- calc_terminal/test_customization_ui.py +612 -0
- calc_terminal/test_dynamic_registry.py +304 -0
- calc_terminal/test_extensions.py +436 -0
- calc_terminal/test_overhaul.py +557 -0
- calc_terminal/test_project_detect.py +255 -0
- calc_terminal/test_root_cause_fix.py +527 -0
- calc_terminal/test_stability.py +532 -0
- calc_terminal/test_terminal_identity.py +132 -0
- calc_terminal/test_v079_speed.py +460 -0
- calc_terminal/theme.py +1107 -0
- calc_terminal/timeline.py +139 -0
- calc_terminal/todos.py +246 -0
- calc_terminal/tool_call_normalizer.py +419 -0
- calc_terminal/tui.py +104 -0
- calc_terminal/ui/__init__.py +8 -0
- calc_terminal/ui/activity_panel.py +231 -0
- calc_terminal/ui/activity_stream_panel.py +238 -0
- calc_terminal/ui/animations.py +122 -0
- calc_terminal/ui/app.py +7271 -0
- calc_terminal/ui/attach_panel.py +597 -0
- calc_terminal/ui/attachments.py +424 -0
- calc_terminal/ui/backup_panel.py +810 -0
- calc_terminal/ui/browser_shell.py +887 -0
- calc_terminal/ui/cat_agent.py +357 -0
- calc_terminal/ui/chats_panel.py +899 -0
- calc_terminal/ui/command_palette.py +125 -0
- calc_terminal/ui/command_palette_modal.py +166 -0
- calc_terminal/ui/composer.py +1141 -0
- calc_terminal/ui/context_menu.py +197 -0
- calc_terminal/ui/conversation.py +1435 -0
- calc_terminal/ui/customization_panel.py +1229 -0
- calc_terminal/ui/dashboard.py +404 -0
- calc_terminal/ui/design_system.py +557 -0
- calc_terminal/ui/diff_panel.py +213 -0
- calc_terminal/ui/editor.py +2102 -0
- calc_terminal/ui/empty_state.py +302 -0
- calc_terminal/ui/events.py +487 -0
- calc_terminal/ui/extensions_panel.py +815 -0
- calc_terminal/ui/footer.py +166 -0
- calc_terminal/ui/gestures_panel.py +383 -0
- calc_terminal/ui/goodbye_screen.py +100 -0
- calc_terminal/ui/header.py +1034 -0
- calc_terminal/ui/help_panel.py +254 -0
- calc_terminal/ui/live_activities.py +914 -0
- calc_terminal/ui/mcp_panel.py +570 -0
- calc_terminal/ui/memory_center.py +524 -0
- calc_terminal/ui/mode_colors_panel.py +525 -0
- calc_terminal/ui/nav_screens.py +747 -0
- calc_terminal/ui/ollama_panel.py +536 -0
- calc_terminal/ui/palette.py +221 -0
- calc_terminal/ui/permission_panel.py +269 -0
- calc_terminal/ui/personalization_panel.py +517 -0
- calc_terminal/ui/personalize_center.py +1568 -0
- calc_terminal/ui/preview_panel.py +441 -0
- calc_terminal/ui/resizers.py +402 -0
- calc_terminal/ui/sidebar.py +1285 -0
- calc_terminal/ui/statusbar.py +168 -0
- calc_terminal/ui/theme_css.py +1396 -0
- calc_terminal/ui/thinking.py +226 -0
- calc_terminal/ui/timeline_panel.py +102 -0
- calc_terminal/ui/todo_panel.py +193 -0
- calc_terminal/ui/viewport.py +136 -0
- calc_terminal/ui/vision_panel.py +489 -0
- calc_terminal/ui/welcome_modal.py +343 -0
- calc_terminal/ui/widgets.py +160 -0
- calc_terminal/ui/workspace.py +831 -0
- calc_terminal/viewers/__init__.py +1 -0
- calc_terminal/viewers/document_viewer.py +252 -0
- calc_terminal/viewers/image_viewer.py +241 -0
- calc_terminal/viewers/pdf_viewer.py +203 -0
- calc_terminal/viewers/presentation_viewer.py +164 -0
- calc_terminal/viewers/registry.py +120 -0
- calc_terminal/viewers/spreadsheet_viewer.py +204 -0
- calc_terminal/vision/__init__.py +89 -0
- calc_terminal/vision/analysis.py +194 -0
- calc_terminal/vision/annotations.py +297 -0
- calc_terminal/vision/capture.py +171 -0
- calc_terminal/vision/context.py +231 -0
- calc_terminal/vision/correlation.py +169 -0
- calc_terminal/vision/cursor.py +258 -0
- calc_terminal/vision/events.py +66 -0
- calc_terminal/vision/frame_pipeline.py +259 -0
- calc_terminal/vision/priority.py +218 -0
- calc_terminal/vision/provider.py +180 -0
- calc_terminal/vision/safety.py +149 -0
- calc_terminal/vision/session.py +281 -0
- calc_terminal/vision/verify.py +162 -0
- calc_terminal/vision.py +514 -0
- calc_terminal/vscode_integration.py +113 -0
- calc_terminal/web/__init__.py +8 -0
- calc_terminal/web/cat_runtime.py +710 -0
- calc_terminal/web/server.py +2891 -0
- calc_terminal/web/static/css/app.css +3152 -0
- calc_terminal/web/static/icons/badge-72.png +0 -0
- calc_terminal/web/static/icons/cat.ico +0 -0
- calc_terminal/web/static/icons/icon-128.png +0 -0
- calc_terminal/web/static/icons/icon-144.png +0 -0
- calc_terminal/web/static/icons/icon-152.png +0 -0
- calc_terminal/web/static/icons/icon-192.png +0 -0
- calc_terminal/web/static/icons/icon-384.png +0 -0
- calc_terminal/web/static/icons/icon-512.png +0 -0
- calc_terminal/web/static/icons/icon-72.png +0 -0
- calc_terminal/web/static/icons/icon-96.png +0 -0
- calc_terminal/web/static/icons/icon.svg +34 -0
- calc_terminal/web/static/icons/new-project.png +0 -0
- calc_terminal/web/static/icons/open-project.png +0 -0
- calc_terminal/web/static/index.html +734 -0
- calc_terminal/web/static/js/app.js +2403 -0
- calc_terminal/web/static/manifest.json +88 -0
- calc_terminal/web/static/sw.js +230 -0
- calc_terminal/workflow_engine.py +769 -0
- calc_terminal/workspace.py +593 -0
- calc_terminal/workspace_index.py +385 -0
- cct_cli-0.7.9.0.dist-info/METADATA +210 -0
- cct_cli-0.7.9.0.dist-info/RECORD +325 -0
- cct_cli-0.7.9.0.dist-info/WHEEL +5 -0
- cct_cli-0.7.9.0.dist-info/entry_points.txt +4 -0
- cct_cli-0.7.9.0.dist-info/licenses/LICENSE +21 -0
- cct_cli-0.7.9.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
"""
|
|
2
|
+
CAT Checkpoint & Rollback System per §14:
|
|
3
|
+
- Automated snapshot of targeted files before agent modifications
|
|
4
|
+
- Precise git-style unified diff computation
|
|
5
|
+
- One-click safe rollback restoring previous workspace states
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import difflib
|
|
11
|
+
import json
|
|
12
|
+
import os
|
|
13
|
+
import shutil
|
|
14
|
+
import time
|
|
15
|
+
import uuid
|
|
16
|
+
from dataclasses import dataclass, field
|
|
17
|
+
from typing import Any, Dict, List, Optional, Tuple
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass
|
|
21
|
+
class FileSnapshot:
|
|
22
|
+
path: str
|
|
23
|
+
content: str
|
|
24
|
+
existed: bool
|
|
25
|
+
|
|
26
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
27
|
+
return {
|
|
28
|
+
"path": self.path,
|
|
29
|
+
"content": self.content,
|
|
30
|
+
"existed": self.existed,
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@dataclass
|
|
35
|
+
class Checkpoint:
|
|
36
|
+
id: str
|
|
37
|
+
description: str
|
|
38
|
+
timestamp: float = field(default_factory=time.time)
|
|
39
|
+
snapshots: Dict[str, FileSnapshot] = field(default_factory=dict)
|
|
40
|
+
applied: bool = True
|
|
41
|
+
|
|
42
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
43
|
+
return {
|
|
44
|
+
"id": self.id,
|
|
45
|
+
"description": self.description,
|
|
46
|
+
"timestamp": self.timestamp,
|
|
47
|
+
"files": list(self.snapshots.keys()),
|
|
48
|
+
"applied": self.applied,
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class CheckpointManager:
|
|
53
|
+
"""Manages workspace snapshots, diffs, and rollbacks."""
|
|
54
|
+
|
|
55
|
+
def __init__(self, storage_dir: Optional[str] = None):
|
|
56
|
+
self.storage_dir = storage_dir or os.path.join(os.path.expanduser("~"), ".cct_checkpoints")
|
|
57
|
+
self.checkpoints: Dict[str, Checkpoint] = {}
|
|
58
|
+
os.makedirs(self.storage_dir, exist_ok=True)
|
|
59
|
+
|
|
60
|
+
def create_checkpoint(self, description: str, files: List[str]) -> Checkpoint:
|
|
61
|
+
"""Capture pre-modification state of specified files."""
|
|
62
|
+
cid = f"chk_{int(time.time())}_{uuid.uuid4().hex[:6]}"
|
|
63
|
+
snapshots = {}
|
|
64
|
+
for f in files:
|
|
65
|
+
norm = os.path.abspath(os.path.expanduser(f))
|
|
66
|
+
if os.path.exists(norm) and os.path.isfile(norm):
|
|
67
|
+
try:
|
|
68
|
+
with open(norm, "r", encoding="utf-8", errors="ignore") as fp:
|
|
69
|
+
content = fp.read()
|
|
70
|
+
snapshots[norm] = FileSnapshot(path=norm, content=content, existed=True)
|
|
71
|
+
except Exception:
|
|
72
|
+
pass
|
|
73
|
+
else:
|
|
74
|
+
snapshots[norm] = FileSnapshot(path=norm, content="", existed=False)
|
|
75
|
+
|
|
76
|
+
chk = Checkpoint(id=cid, description=description, snapshots=snapshots)
|
|
77
|
+
self.checkpoints[cid] = chk
|
|
78
|
+
self._persist_checkpoint(chk)
|
|
79
|
+
return chk
|
|
80
|
+
|
|
81
|
+
def get_diff(self, checkpoint_id: str) -> Dict[str, str]:
|
|
82
|
+
"""Compute unified diff for each file in checkpoint against current workspace."""
|
|
83
|
+
chk = self.checkpoints.get(checkpoint_id)
|
|
84
|
+
if not chk:
|
|
85
|
+
return {}
|
|
86
|
+
|
|
87
|
+
diffs = {}
|
|
88
|
+
for path, snap in chk.snapshots.items():
|
|
89
|
+
orig_lines = snap.content.splitlines(keepends=True) if snap.existed else []
|
|
90
|
+
curr_lines = []
|
|
91
|
+
if os.path.exists(path) and os.path.isfile(path):
|
|
92
|
+
try:
|
|
93
|
+
with open(path, "r", encoding="utf-8", errors="ignore") as f:
|
|
94
|
+
curr_lines = f.readlines()
|
|
95
|
+
except Exception:
|
|
96
|
+
pass
|
|
97
|
+
|
|
98
|
+
diff = list(
|
|
99
|
+
difflib.unified_diff(
|
|
100
|
+
orig_lines,
|
|
101
|
+
curr_lines,
|
|
102
|
+
fromfile=f"a/{os.path.basename(path)} (checkpoint)",
|
|
103
|
+
tofile=f"b/{os.path.basename(path)} (current)",
|
|
104
|
+
)
|
|
105
|
+
)
|
|
106
|
+
if diff:
|
|
107
|
+
diffs[path] = "".join(diff)
|
|
108
|
+
|
|
109
|
+
return diffs
|
|
110
|
+
|
|
111
|
+
def rollback(self, checkpoint_id: str) -> Tuple[bool, List[str]]:
|
|
112
|
+
"""Restore workspace files to their exact checkpoint state."""
|
|
113
|
+
chk = self.checkpoints.get(checkpoint_id)
|
|
114
|
+
if not chk:
|
|
115
|
+
return False, ["Checkpoint not found"]
|
|
116
|
+
|
|
117
|
+
restored_files = []
|
|
118
|
+
errors = []
|
|
119
|
+
for path, snap in chk.snapshots.items():
|
|
120
|
+
try:
|
|
121
|
+
if snap.existed:
|
|
122
|
+
os.makedirs(os.path.dirname(path), exist_ok=True)
|
|
123
|
+
with open(path, "w", encoding="utf-8") as f:
|
|
124
|
+
f.write(snap.content)
|
|
125
|
+
restored_files.append(path)
|
|
126
|
+
else:
|
|
127
|
+
# File was newly created after checkpoint, so remove it
|
|
128
|
+
if os.path.exists(path):
|
|
129
|
+
os.remove(path)
|
|
130
|
+
restored_files.append(f"Deleted {path}")
|
|
131
|
+
except Exception as ex:
|
|
132
|
+
errors.append(f"Failed to restore {path}: {ex}")
|
|
133
|
+
|
|
134
|
+
chk.applied = False
|
|
135
|
+
return len(errors) == 0, restored_files
|
|
136
|
+
|
|
137
|
+
def list_checkpoints(self) -> List[Checkpoint]:
|
|
138
|
+
return sorted(self.checkpoints.values(), key=lambda c: c.timestamp, reverse=True)
|
|
139
|
+
|
|
140
|
+
def _persist_checkpoint(self, chk: Checkpoint):
|
|
141
|
+
try:
|
|
142
|
+
path = os.path.join(self.storage_dir, f"{chk.id}.json")
|
|
143
|
+
data = {
|
|
144
|
+
"id": chk.id,
|
|
145
|
+
"description": chk.description,
|
|
146
|
+
"timestamp": chk.timestamp,
|
|
147
|
+
"applied": chk.applied,
|
|
148
|
+
"snapshots": {p: s.to_dict() for p, s in chk.snapshots.items()},
|
|
149
|
+
}
|
|
150
|
+
with open(path, "w", encoding="utf-8") as f:
|
|
151
|
+
json.dump(data, f, indent=2)
|
|
152
|
+
except Exception:
|
|
153
|
+
pass
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
checkpoint_manager = CheckpointManager()
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"""
|
|
2
|
+
CAT Core Input Package — proxy re-export of calc_terminal.input.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from ...input import (
|
|
6
|
+
DeviceType,
|
|
7
|
+
InputCapabilities,
|
|
8
|
+
detect_capabilities,
|
|
9
|
+
get_input_capabilities,
|
|
10
|
+
set_touch_mode,
|
|
11
|
+
toggle_touch_mode,
|
|
12
|
+
register_listener,
|
|
13
|
+
unregister_listener,
|
|
14
|
+
TouchTapRecognizer,
|
|
15
|
+
TouchScrollHandler,
|
|
16
|
+
TAP_MAX_DISTANCE,
|
|
17
|
+
TAP_MAX_DURATION,
|
|
18
|
+
DOUBLE_TAP_MAX_DELAY,
|
|
19
|
+
LONG_PRESS_DURATION,
|
|
20
|
+
TouchHitZone,
|
|
21
|
+
RESIZER_TOUCH_PADDING,
|
|
22
|
+
TouchFocusManager,
|
|
23
|
+
GestureBridge,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
__all__ = [
|
|
27
|
+
"DeviceType",
|
|
28
|
+
"InputCapabilities",
|
|
29
|
+
"detect_capabilities",
|
|
30
|
+
"get_input_capabilities",
|
|
31
|
+
"set_touch_mode",
|
|
32
|
+
"toggle_touch_mode",
|
|
33
|
+
"register_listener",
|
|
34
|
+
"unregister_listener",
|
|
35
|
+
"TouchTapRecognizer",
|
|
36
|
+
"TouchScrollHandler",
|
|
37
|
+
"TAP_MAX_DISTANCE",
|
|
38
|
+
"TAP_MAX_DURATION",
|
|
39
|
+
"DOUBLE_TAP_MAX_DELAY",
|
|
40
|
+
"LONG_PRESS_DURATION",
|
|
41
|
+
"TouchHitZone",
|
|
42
|
+
"RESIZER_TOUCH_PADDING",
|
|
43
|
+
"TouchFocusManager",
|
|
44
|
+
"GestureBridge",
|
|
45
|
+
]
|
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
"""
|
|
2
|
+
CAT Custom Mode Engine & Registry per §2, §3, §55:
|
|
3
|
+
- Preserves the default AI modes (Notebook, Research, Plan, Build, Debugger, Agent)
|
|
4
|
+
- Supports user-created custom modes (e.g. BioLab, QuantumResearch, MLTrainer)
|
|
5
|
+
- Modes request capabilities from Capability Bus, never duplicating tool logic.
|
|
6
|
+
- Modes are installable, removable, editable, exportable, importable, validated, and permission-controlled.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import json
|
|
12
|
+
import os
|
|
13
|
+
import re
|
|
14
|
+
from dataclasses import dataclass, field, asdict
|
|
15
|
+
from typing import Any, Dict, List, Optional, Tuple, Union
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
CUSTOM_MODES_PATH = os.path.join(os.path.expanduser("~"), ".cct_custom_modes.json")
|
|
19
|
+
|
|
20
|
+
# Default immutable modes per §2
|
|
21
|
+
DEFAULT_MODE_KEYS = ("notebook", "research", "plan", "build", "debugger", "agent")
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@dataclass
|
|
25
|
+
class ModeBehavior:
|
|
26
|
+
planning: str = "standard" # standard, high, adaptive
|
|
27
|
+
verification: str = "standard" # standard, strict, relaxed
|
|
28
|
+
citations: str = "optional" # required, optional, none
|
|
29
|
+
experiment_tracking: bool = False
|
|
30
|
+
coding: str = "standard"
|
|
31
|
+
|
|
32
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
33
|
+
return asdict(self)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@dataclass
|
|
37
|
+
class ModeSpec:
|
|
38
|
+
name: str
|
|
39
|
+
description: str = ""
|
|
40
|
+
inherits: List[str] = field(default_factory=lambda: ["notebook"])
|
|
41
|
+
capabilities: List[str] = field(default_factory=list)
|
|
42
|
+
behavior: ModeBehavior = field(default_factory=ModeBehavior)
|
|
43
|
+
permissions: List[str] = field(default_factory=list)
|
|
44
|
+
preferred_models: List[str] = field(default_factory=list)
|
|
45
|
+
backup_models: List[str] = field(default_factory=list)
|
|
46
|
+
accent_hex: str = "#38bdf8"
|
|
47
|
+
icon: str = "●"
|
|
48
|
+
system_prompt: str = ""
|
|
49
|
+
enabled: bool = True
|
|
50
|
+
custom: bool = True
|
|
51
|
+
|
|
52
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
53
|
+
return {
|
|
54
|
+
"name": self.name,
|
|
55
|
+
"description": self.description,
|
|
56
|
+
"inherits": self.inherits,
|
|
57
|
+
"capabilities": self.capabilities,
|
|
58
|
+
"behavior": self.behavior.to_dict() if isinstance(self.behavior, ModeBehavior) else self.behavior,
|
|
59
|
+
"permissions": self.permissions,
|
|
60
|
+
"preferred_models": self.preferred_models,
|
|
61
|
+
"backup_models": self.backup_models,
|
|
62
|
+
"accent_hex": self.accent_hex,
|
|
63
|
+
"icon": self.icon,
|
|
64
|
+
"system_prompt": self.system_prompt,
|
|
65
|
+
"enabled": self.enabled,
|
|
66
|
+
"custom": self.custom,
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
@classmethod
|
|
70
|
+
def from_dict(cls, data: Dict[str, Any]) -> ModeSpec:
|
|
71
|
+
beh_data = data.get("behavior", {})
|
|
72
|
+
if isinstance(beh_data, dict):
|
|
73
|
+
behavior = ModeBehavior(
|
|
74
|
+
planning=beh_data.get("planning", "standard"),
|
|
75
|
+
verification=beh_data.get("verification", "standard"),
|
|
76
|
+
citations=beh_data.get("citations", "optional"),
|
|
77
|
+
experiment_tracking=bool(beh_data.get("experiment_tracking", False)),
|
|
78
|
+
coding=beh_data.get("coding", "standard"),
|
|
79
|
+
)
|
|
80
|
+
else:
|
|
81
|
+
behavior = ModeBehavior()
|
|
82
|
+
|
|
83
|
+
return cls(
|
|
84
|
+
name=data["name"],
|
|
85
|
+
description=data.get("description", ""),
|
|
86
|
+
inherits=data.get("inherits", ["notebook"]),
|
|
87
|
+
capabilities=data.get("capabilities", []),
|
|
88
|
+
behavior=behavior,
|
|
89
|
+
permissions=data.get("permissions", []),
|
|
90
|
+
preferred_models=data.get("preferred_models", []),
|
|
91
|
+
backup_models=data.get("backup_models", []),
|
|
92
|
+
accent_hex=data.get("accent_hex", "#38bdf8"),
|
|
93
|
+
icon=data.get("icon", "●"),
|
|
94
|
+
system_prompt=data.get("system_prompt", ""),
|
|
95
|
+
enabled=bool(data.get("enabled", True)),
|
|
96
|
+
custom=bool(data.get("custom", True)),
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
class ModeRegistry:
|
|
101
|
+
"""Registry managing custom user modes alongside project default modes."""
|
|
102
|
+
|
|
103
|
+
def __init__(self, storage_path: str = CUSTOM_MODES_PATH):
|
|
104
|
+
self.storage_path = storage_path
|
|
105
|
+
self._custom_modes: Dict[str, ModeSpec] = {}
|
|
106
|
+
self.load()
|
|
107
|
+
|
|
108
|
+
def is_default_mode(self, name: str) -> bool:
|
|
109
|
+
return name.strip().lower() in DEFAULT_MODE_KEYS
|
|
110
|
+
|
|
111
|
+
def validate_mode(self, data: Dict[str, Any]) -> Tuple[bool, List[str]]:
|
|
112
|
+
errors = []
|
|
113
|
+
name = data.get("name", "").strip()
|
|
114
|
+
if not name:
|
|
115
|
+
errors.append("Mode name cannot be empty")
|
|
116
|
+
elif not re.match(r"^[A-Za-z0-9_\-]+$", name):
|
|
117
|
+
errors.append("Mode name must be alphanumeric with dashes or underscores only")
|
|
118
|
+
elif self.is_default_mode(name):
|
|
119
|
+
errors.append(f"Cannot overwrite default mode '{name}'")
|
|
120
|
+
return (len(errors) == 0), errors
|
|
121
|
+
|
|
122
|
+
def register_mode(
|
|
123
|
+
self,
|
|
124
|
+
spec: Optional[ModeSpec] = None,
|
|
125
|
+
persist: bool = True,
|
|
126
|
+
name: str = "",
|
|
127
|
+
description: str = "",
|
|
128
|
+
system_prompt: str = "",
|
|
129
|
+
icon: str = "●",
|
|
130
|
+
capabilities: Optional[List[str]] = None,
|
|
131
|
+
inherits: Optional[str] = "agent",
|
|
132
|
+
) -> Tuple[bool, str]:
|
|
133
|
+
if spec is None:
|
|
134
|
+
spec = ModeSpec(
|
|
135
|
+
name=name,
|
|
136
|
+
description=description,
|
|
137
|
+
system_prompt=system_prompt,
|
|
138
|
+
icon=icon,
|
|
139
|
+
capabilities=capabilities or [],
|
|
140
|
+
inherits=inherits,
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
canonical = spec.name.strip().lower()
|
|
144
|
+
if self.is_default_mode(canonical):
|
|
145
|
+
return False, f"Cannot overwrite default mode '{spec.name}'"
|
|
146
|
+
|
|
147
|
+
ok, errs = self.validate_mode(spec.to_dict())
|
|
148
|
+
if not ok:
|
|
149
|
+
return False, "; ".join(errs)
|
|
150
|
+
|
|
151
|
+
self._custom_modes[canonical] = spec
|
|
152
|
+
if persist:
|
|
153
|
+
self.save()
|
|
154
|
+
self._sync_with_ai_modes()
|
|
155
|
+
return True, f"Mode '{spec.name}' registered successfully"
|
|
156
|
+
|
|
157
|
+
def unregister_mode(self, name: str) -> Tuple[bool, str]:
|
|
158
|
+
canonical = name.strip().lower()
|
|
159
|
+
if self.is_default_mode(canonical):
|
|
160
|
+
return False, f"Cannot delete default mode '{name}'"
|
|
161
|
+
if canonical not in self._custom_modes:
|
|
162
|
+
return False, f"Custom mode '{name}' not found"
|
|
163
|
+
|
|
164
|
+
del self._custom_modes[canonical]
|
|
165
|
+
self.save()
|
|
166
|
+
self._sync_with_ai_modes()
|
|
167
|
+
return True, f"Mode '{name}' removed successfully"
|
|
168
|
+
|
|
169
|
+
def get_mode(self, name: str) -> Optional[ModeSpec]:
|
|
170
|
+
"""Lookup mode by exact or case-insensitive name."""
|
|
171
|
+
canonical = (name or "").strip().lower()
|
|
172
|
+
return self._custom_modes.get(canonical)
|
|
173
|
+
|
|
174
|
+
def resolve_mode_name(self, query: str) -> Optional[str]:
|
|
175
|
+
"""Resolve a mode name from typed command or natural language (e.g., 'Use BioLab mode')."""
|
|
176
|
+
if not query:
|
|
177
|
+
return None
|
|
178
|
+
clean = query.strip()
|
|
179
|
+
# Direct check
|
|
180
|
+
low = clean.lower().lstrip("/")
|
|
181
|
+
if low in DEFAULT_MODE_KEYS:
|
|
182
|
+
return low
|
|
183
|
+
if low in self._custom_modes:
|
|
184
|
+
return self._custom_modes[low].name
|
|
185
|
+
|
|
186
|
+
# Natural language pattern check: e.g. "Use BioLab mode", "switch to Quantum mode"
|
|
187
|
+
m = re.search(r"(?:use|switch to|enter|mode)\s+([A-Za-z0-9_\-]+)", clean, re.IGNORECASE)
|
|
188
|
+
if m:
|
|
189
|
+
cand = m.group(1).lower()
|
|
190
|
+
if cand in DEFAULT_MODE_KEYS:
|
|
191
|
+
return cand
|
|
192
|
+
if cand in self._custom_modes:
|
|
193
|
+
return self._custom_modes[cand].name
|
|
194
|
+
|
|
195
|
+
return None
|
|
196
|
+
|
|
197
|
+
def list_modes(self, include_defaults: bool = True) -> List[Dict[str, Any]]:
|
|
198
|
+
modes = []
|
|
199
|
+
if include_defaults:
|
|
200
|
+
try:
|
|
201
|
+
from .. import ai_modes
|
|
202
|
+
for k in ai_modes.MODE_ORDER:
|
|
203
|
+
m = ai_modes.meta(k)
|
|
204
|
+
modes.append({
|
|
205
|
+
"name": m.get("label", k.title()),
|
|
206
|
+
"key": k,
|
|
207
|
+
"description": m.get("purpose", ""),
|
|
208
|
+
"icon": m.get("icon", "●"),
|
|
209
|
+
"default": True,
|
|
210
|
+
"enabled": True,
|
|
211
|
+
})
|
|
212
|
+
except Exception:
|
|
213
|
+
for k in DEFAULT_MODE_KEYS:
|
|
214
|
+
modes.append({"name": k.title(), "key": k, "default": True, "enabled": True})
|
|
215
|
+
|
|
216
|
+
for spec in self._custom_modes.values():
|
|
217
|
+
modes.append({
|
|
218
|
+
"name": spec.name,
|
|
219
|
+
"key": spec.name.lower(),
|
|
220
|
+
"description": spec.description,
|
|
221
|
+
"icon": spec.icon,
|
|
222
|
+
"default": False,
|
|
223
|
+
"enabled": spec.enabled,
|
|
224
|
+
"capabilities": spec.capabilities,
|
|
225
|
+
"inherits": spec.inherits,
|
|
226
|
+
})
|
|
227
|
+
return modes
|
|
228
|
+
|
|
229
|
+
def export_mode(self, name: str, output_path: str) -> Tuple[bool, str]:
|
|
230
|
+
spec = self.get_mode(name)
|
|
231
|
+
if not spec:
|
|
232
|
+
return False, f"Mode '{name}' not found"
|
|
233
|
+
try:
|
|
234
|
+
with open(output_path, "w", encoding="utf-8") as f:
|
|
235
|
+
json.dump(spec.to_dict(), f, indent=2)
|
|
236
|
+
return True, f"Exported mode '{name}' to {output_path}"
|
|
237
|
+
except Exception as e:
|
|
238
|
+
return False, f"Export failed: {e}"
|
|
239
|
+
|
|
240
|
+
def import_mode(self, source: Union[str, Dict[str, Any]]) -> Tuple[bool, str]:
|
|
241
|
+
try:
|
|
242
|
+
if isinstance(source, str):
|
|
243
|
+
if os.path.exists(source):
|
|
244
|
+
with open(source, "r", encoding="utf-8") as f:
|
|
245
|
+
data = json.load(f)
|
|
246
|
+
else:
|
|
247
|
+
data = json.loads(source)
|
|
248
|
+
else:
|
|
249
|
+
data = source
|
|
250
|
+
spec = ModeSpec.from_dict(data)
|
|
251
|
+
return self.register_mode(spec)
|
|
252
|
+
except Exception as e:
|
|
253
|
+
return False, f"Import failed: {e}"
|
|
254
|
+
|
|
255
|
+
def save(self) -> None:
|
|
256
|
+
try:
|
|
257
|
+
data = {k: spec.to_dict() for k, spec in self._custom_modes.items()}
|
|
258
|
+
with open(self.storage_path, "w", encoding="utf-8") as f:
|
|
259
|
+
json.dump(data, f, indent=2)
|
|
260
|
+
except Exception:
|
|
261
|
+
pass
|
|
262
|
+
|
|
263
|
+
def load(self) -> None:
|
|
264
|
+
if not os.path.exists(self.storage_path):
|
|
265
|
+
return
|
|
266
|
+
try:
|
|
267
|
+
with open(self.storage_path, "r", encoding="utf-8") as f:
|
|
268
|
+
data = json.load(f)
|
|
269
|
+
if isinstance(data, dict):
|
|
270
|
+
for k, v in data.items():
|
|
271
|
+
if not self.is_default_mode(k):
|
|
272
|
+
self._custom_modes[k.lower()] = ModeSpec.from_dict(v)
|
|
273
|
+
self._sync_with_ai_modes()
|
|
274
|
+
except Exception:
|
|
275
|
+
pass
|
|
276
|
+
|
|
277
|
+
def _sync_with_ai_modes(self) -> None:
|
|
278
|
+
"""Dynamically synchronize registered custom modes into ai_modes.MODE_META."""
|
|
279
|
+
try:
|
|
280
|
+
from .. import ai_modes
|
|
281
|
+
for spec in self._custom_modes.values():
|
|
282
|
+
if not spec.enabled:
|
|
283
|
+
continue
|
|
284
|
+
k = spec.name.lower()
|
|
285
|
+
rgb = ai_modes._hex_to_rgb(spec.accent_hex) or (140, 140, 240)
|
|
286
|
+
ai_modes.MODE_META[k] = {
|
|
287
|
+
"label": spec.name,
|
|
288
|
+
"icon": spec.icon,
|
|
289
|
+
"accent": rgb,
|
|
290
|
+
"purpose": spec.description or f"Custom mode {spec.name}",
|
|
291
|
+
"gradient": ai_modes._auto_gradient(rgb),
|
|
292
|
+
"custom_spec": spec,
|
|
293
|
+
}
|
|
294
|
+
if k not in ai_modes.MODE_ORDER:
|
|
295
|
+
ai_modes.MODE_ORDER.append(k)
|
|
296
|
+
except Exception:
|
|
297
|
+
pass
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
mode_registry = ModeRegistry()
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
"""
|
|
2
|
+
CAT Project Graph & Workspace Intelligence per §13:
|
|
3
|
+
- Module dependency graph, symbol definitions, call hierarchies, external imports, test mappings
|
|
4
|
+
- AST-based parsing for Python source files
|
|
5
|
+
- Incrementally updated symbol and dependency index
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import ast
|
|
11
|
+
import os
|
|
12
|
+
from dataclasses import dataclass, field
|
|
13
|
+
from typing import Any, Dict, List, Optional, Set
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@dataclass
|
|
17
|
+
class SymbolInfo:
|
|
18
|
+
name: str
|
|
19
|
+
kind: str # function, class, method, variable, import
|
|
20
|
+
file_path: str
|
|
21
|
+
line_number: int
|
|
22
|
+
docstring: Optional[str] = None
|
|
23
|
+
parameters: List[str] = field(default_factory=list)
|
|
24
|
+
|
|
25
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
26
|
+
return {
|
|
27
|
+
"name": self.name,
|
|
28
|
+
"kind": self.kind,
|
|
29
|
+
"file_path": self.file_path,
|
|
30
|
+
"line_number": self.line_number,
|
|
31
|
+
"docstring": self.docstring,
|
|
32
|
+
"parameters": self.parameters,
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@dataclass
|
|
37
|
+
class FileNode:
|
|
38
|
+
file_path: str
|
|
39
|
+
symbols: List[SymbolInfo] = field(default_factory=list)
|
|
40
|
+
imports: List[str] = field(default_factory=list)
|
|
41
|
+
imported_by: List[str] = field(default_factory=list)
|
|
42
|
+
is_test_file: bool = False
|
|
43
|
+
associated_sources: List[str] = field(default_factory=list)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class ProjectGraph:
|
|
47
|
+
"""Incrementally updated semantic graph of project codebase."""
|
|
48
|
+
|
|
49
|
+
def __init__(self, workspace_root: Optional[str] = None):
|
|
50
|
+
self.workspace_root = os.path.abspath(workspace_root or os.getcwd())
|
|
51
|
+
self.files: Dict[str, FileNode] = {}
|
|
52
|
+
self.symbol_index: Dict[str, List[SymbolInfo]] = {}
|
|
53
|
+
|
|
54
|
+
def scan_directory(self, root_dir: Optional[str] = None, max_files: int = 500):
|
|
55
|
+
"""Scan workspace and construct symbol and dependency graph."""
|
|
56
|
+
scan_root = os.path.abspath(root_dir or self.workspace_root)
|
|
57
|
+
count = 0
|
|
58
|
+
for dirpath, dirnames, filenames in os.walk(scan_root):
|
|
59
|
+
# Skip hidden and cache dirs
|
|
60
|
+
dirnames[:] = [d for d in dirnames if not d.startswith((".", "__pycache__", "node_modules", "venv", ".venv"))]
|
|
61
|
+
for fname in filenames:
|
|
62
|
+
if fname.endswith(".py"):
|
|
63
|
+
full_path = os.path.join(dirpath, fname)
|
|
64
|
+
self.index_file(full_path)
|
|
65
|
+
count += 1
|
|
66
|
+
if count >= max_files:
|
|
67
|
+
break
|
|
68
|
+
if count >= max_files:
|
|
69
|
+
break
|
|
70
|
+
self._resolve_dependencies()
|
|
71
|
+
|
|
72
|
+
def index_file(self, file_path: str):
|
|
73
|
+
"""Parse file via AST and extract symbols and imports."""
|
|
74
|
+
norm_path = os.path.normpath(file_path)
|
|
75
|
+
rel_path = os.path.relpath(norm_path, self.workspace_root) if norm_path.startswith(self.workspace_root) else norm_path
|
|
76
|
+
is_test = "test" in os.path.basename(norm_path).lower()
|
|
77
|
+
|
|
78
|
+
node = FileNode(file_path=norm_path, is_test_file=is_test)
|
|
79
|
+
try:
|
|
80
|
+
with open(norm_path, "r", encoding="utf-8", errors="ignore") as f:
|
|
81
|
+
content = f.read()
|
|
82
|
+
tree = ast.parse(content, filename=norm_path)
|
|
83
|
+
except Exception:
|
|
84
|
+
self.files[norm_path] = node
|
|
85
|
+
return
|
|
86
|
+
|
|
87
|
+
for stmt in ast.walk(tree):
|
|
88
|
+
if isinstance(stmt, ast.FunctionDef) or isinstance(stmt, ast.AsyncFunctionDef):
|
|
89
|
+
params = [a.arg for a in stmt.args.args]
|
|
90
|
+
doc = ast.get_docstring(stmt)
|
|
91
|
+
sym = SymbolInfo(
|
|
92
|
+
name=stmt.name,
|
|
93
|
+
kind="function",
|
|
94
|
+
file_path=norm_path,
|
|
95
|
+
line_number=stmt.lineno,
|
|
96
|
+
docstring=doc,
|
|
97
|
+
parameters=params,
|
|
98
|
+
)
|
|
99
|
+
node.symbols.append(sym)
|
|
100
|
+
self.symbol_index.setdefault(sym.name, []).append(sym)
|
|
101
|
+
elif isinstance(stmt, ast.ClassDef):
|
|
102
|
+
doc = ast.get_docstring(stmt)
|
|
103
|
+
sym = SymbolInfo(
|
|
104
|
+
name=stmt.name,
|
|
105
|
+
kind="class",
|
|
106
|
+
file_path=norm_path,
|
|
107
|
+
line_number=stmt.lineno,
|
|
108
|
+
docstring=doc,
|
|
109
|
+
)
|
|
110
|
+
node.symbols.append(sym)
|
|
111
|
+
self.symbol_index.setdefault(sym.name, []).append(sym)
|
|
112
|
+
elif isinstance(stmt, ast.Import):
|
|
113
|
+
for alias in stmt.names:
|
|
114
|
+
node.imports.append(alias.name)
|
|
115
|
+
elif isinstance(stmt, ast.ImportFrom):
|
|
116
|
+
if stmt.module:
|
|
117
|
+
node.imports.append(stmt.module)
|
|
118
|
+
|
|
119
|
+
self.files[norm_path] = node
|
|
120
|
+
|
|
121
|
+
def _resolve_dependencies(self):
|
|
122
|
+
"""Cross-reference imports to determine internal file dependencies."""
|
|
123
|
+
for path, node in self.files.items():
|
|
124
|
+
for imp in node.imports:
|
|
125
|
+
for target_path in self.files.keys():
|
|
126
|
+
# Check if imported module matches target file path
|
|
127
|
+
mod_name = os.path.splitext(os.path.basename(target_path))[0]
|
|
128
|
+
if imp.endswith(mod_name) and target_path != path:
|
|
129
|
+
if path not in self.files[target_path].imported_by:
|
|
130
|
+
self.files[target_path].imported_by.append(path)
|
|
131
|
+
if node.is_test_file and target_path not in node.associated_sources:
|
|
132
|
+
node.associated_sources.append(target_path)
|
|
133
|
+
|
|
134
|
+
def find_symbols(self, name_query: str) -> List[SymbolInfo]:
|
|
135
|
+
"""Find symbols matching name query."""
|
|
136
|
+
results = []
|
|
137
|
+
q = name_query.lower()
|
|
138
|
+
for sym_name, syms in self.symbol_index.items():
|
|
139
|
+
if q in sym_name.lower():
|
|
140
|
+
results.extend(syms)
|
|
141
|
+
return results
|
|
142
|
+
|
|
143
|
+
def get_file_dependencies(self, file_path: str) -> List[str]:
|
|
144
|
+
norm = os.path.normpath(file_path)
|
|
145
|
+
node = self.files.get(norm)
|
|
146
|
+
return node.imports if node else []
|
|
147
|
+
|
|
148
|
+
def get_file_dependents(self, file_path: str) -> List[str]:
|
|
149
|
+
norm = os.path.normpath(file_path)
|
|
150
|
+
node = self.files.get(norm)
|
|
151
|
+
return node.imported_by if node else []
|
|
152
|
+
|
|
153
|
+
def get_test_files_for_source(self, source_path: str) -> List[str]:
|
|
154
|
+
norm = os.path.normpath(source_path)
|
|
155
|
+
base = os.path.splitext(os.path.basename(norm))[0]
|
|
156
|
+
matching_tests = []
|
|
157
|
+
for path, node in self.files.items():
|
|
158
|
+
if node.is_test_file:
|
|
159
|
+
test_base = os.path.basename(path).lower()
|
|
160
|
+
if base.lower() in test_base or norm in node.associated_sources:
|
|
161
|
+
matching_tests.append(path)
|
|
162
|
+
return matching_tests
|
|
163
|
+
|
|
164
|
+
def summarize(self) -> Dict[str, Any]:
|
|
165
|
+
return {
|
|
166
|
+
"total_files_indexed": len(self.files),
|
|
167
|
+
"total_symbols": sum(len(s) for s in self.symbol_index.values()),
|
|
168
|
+
"test_files_count": sum(1 for n in self.files.values() if n.is_test_file),
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
project_graph = ProjectGraph()
|