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.
Files changed (325) hide show
  1. calc_terminal/__init__.py +14 -0
  2. calc_terminal/__main__.py +14 -0
  3. calc_terminal/activity.py +1334 -0
  4. calc_terminal/agent.py +3387 -0
  5. calc_terminal/agent_runtime.py +519 -0
  6. calc_terminal/ai_context.py +447 -0
  7. calc_terminal/ai_modes.py +752 -0
  8. calc_terminal/ai_personalization.py +286 -0
  9. calc_terminal/ai_preview_feedback.py +213 -0
  10. calc_terminal/aicore.py +2572 -0
  11. calc_terminal/anim.py +367 -0
  12. calc_terminal/app.py +3685 -0
  13. calc_terminal/art.py +639 -0
  14. calc_terminal/atomsim.py +368 -0
  15. calc_terminal/attachments.py +743 -0
  16. calc_terminal/benchmark_system.py +414 -0
  17. calc_terminal/browser/__init__.py +36 -0
  18. calc_terminal/browser/browser_state.py +346 -0
  19. calc_terminal/browser/devserver.py +176 -0
  20. calc_terminal/browser/engine.py +494 -0
  21. calc_terminal/browser/navigation.py +84 -0
  22. calc_terminal/browser/preview.py +429 -0
  23. calc_terminal/browser/preview_entry.py +95 -0
  24. calc_terminal/browser/project_detector.py +144 -0
  25. calc_terminal/browser/server.py +449 -0
  26. calc_terminal/browser/state.py +75 -0
  27. calc_terminal/browser/watcher.py +99 -0
  28. calc_terminal/browser_gui/__init__.py +1 -0
  29. calc_terminal/browser_gui/__main__.py +3 -0
  30. calc_terminal/browser_gui/launcher.py +173 -0
  31. calc_terminal/browser_gui/playwright_browser.py +117 -0
  32. calc_terminal/browser_gui/qt_browser.py +1501 -0
  33. calc_terminal/browser_gui/webview_browser.py +57 -0
  34. calc_terminal/capabilities/__init__.py +35 -0
  35. calc_terminal/capabilities/adapters/__init__.py +33 -0
  36. calc_terminal/capabilities/adapters/bioinformatics.py +204 -0
  37. calc_terminal/capabilities/adapters/browser_adapter.py +205 -0
  38. calc_terminal/capabilities/adapters/filesystem.py +206 -0
  39. calc_terminal/capabilities/adapters/git_adapter.py +202 -0
  40. calc_terminal/capabilities/adapters/jupyter_adapter.py +138 -0
  41. calc_terminal/capabilities/adapters/ml_frameworks.py +158 -0
  42. calc_terminal/capabilities/adapters/platforms.py +200 -0
  43. calc_terminal/capabilities/adapters/python_exec.py +93 -0
  44. calc_terminal/capabilities/adapters/quantum_adapter.py +150 -0
  45. calc_terminal/capabilities/adapters/scientific_comp.py +123 -0
  46. calc_terminal/capabilities/adapters/structural_bio.py +161 -0
  47. calc_terminal/capabilities/adapters/terminal.py +99 -0
  48. calc_terminal/capabilities/bus.py +178 -0
  49. calc_terminal/capabilities/discovery.py +207 -0
  50. calc_terminal/capabilities/schema.py +221 -0
  51. calc_terminal/cat.ico +0 -0
  52. calc_terminal/cat_browser.py +2018 -0
  53. calc_terminal/chat_store.py +703 -0
  54. calc_terminal/cli.py +1178 -0
  55. calc_terminal/code_editor.py +640 -0
  56. calc_terminal/collaboration.py +723 -0
  57. calc_terminal/commands_data.py +139 -0
  58. calc_terminal/compatibility_engine.py +352 -0
  59. calc_terminal/compute/__init__.py +31 -0
  60. calc_terminal/compute/fabric.py +350 -0
  61. calc_terminal/config.py +227 -0
  62. calc_terminal/core/__init__.py +41 -0
  63. calc_terminal/core/checkpoint.py +156 -0
  64. calc_terminal/core/input/__init__.py +45 -0
  65. calc_terminal/core/mode_registry.py +300 -0
  66. calc_terminal/core/project_graph.py +172 -0
  67. calc_terminal/core/recovery.py +129 -0
  68. calc_terminal/core/security_layer.py +112 -0
  69. calc_terminal/core/task_graph.py +202 -0
  70. calc_terminal/core/unified_runtime.py +184 -0
  71. calc_terminal/core/verification.py +257 -0
  72. calc_terminal/customization.py +1566 -0
  73. calc_terminal/derivations.py +153 -0
  74. calc_terminal/device_control.py +263 -0
  75. calc_terminal/diagnostics/__init__.py +27 -0
  76. calc_terminal/diagnostics/doctor_engine.py +382 -0
  77. calc_terminal/diagnostics/self_test.py +247 -0
  78. calc_terminal/doctor.py +519 -0
  79. calc_terminal/easter_eggs.py +274 -0
  80. calc_terminal/editor/__init__.py +1 -0
  81. calc_terminal/editor/actions.py +263 -0
  82. calc_terminal/editor/commands.py +160 -0
  83. calc_terminal/editor/shortcuts.py +226 -0
  84. calc_terminal/engine.py +259 -0
  85. calc_terminal/errors.py +120 -0
  86. calc_terminal/event_stream.py +146 -0
  87. calc_terminal/eventbus.py +133 -0
  88. calc_terminal/extensions.py +733 -0
  89. calc_terminal/fallback_cli.py +1321 -0
  90. calc_terminal/first_run.py +265 -0
  91. calc_terminal/fomoji_auth.py +1043 -0
  92. calc_terminal/formulas.py +82 -0
  93. calc_terminal/fs_cache.py +121 -0
  94. calc_terminal/fs_watcher.py +277 -0
  95. calc_terminal/game.py +193 -0
  96. calc_terminal/gen1.py +5 -0
  97. calc_terminal/generators.py +245 -0
  98. calc_terminal/gestures/__init__.py +42 -0
  99. calc_terminal/gestures/bindings.py +175 -0
  100. calc_terminal/gestures/manager.py +477 -0
  101. calc_terminal/goodbye.py +363 -0
  102. calc_terminal/gpu3d.py +290 -0
  103. calc_terminal/graphs.py +358 -0
  104. calc_terminal/hardware_analyzer.py +440 -0
  105. calc_terminal/host/__init__.py +30 -0
  106. calc_terminal/host/browser_manager.py +187 -0
  107. calc_terminal/host/desktop.py +1386 -0
  108. calc_terminal/host/launcher.py +395 -0
  109. calc_terminal/host/terminal.py +279 -0
  110. calc_terminal/identity.py +216 -0
  111. calc_terminal/input/__init__.py +54 -0
  112. calc_terminal/input/capabilities.py +258 -0
  113. calc_terminal/input/focus.py +87 -0
  114. calc_terminal/input/gestures.py +64 -0
  115. calc_terminal/input/pointer.py +114 -0
  116. calc_terminal/input/touch.py +345 -0
  117. calc_terminal/keys.py +84 -0
  118. calc_terminal/live_automation.py +165 -0
  119. calc_terminal/mathtext.py +433 -0
  120. calc_terminal/mcp.py +386 -0
  121. calc_terminal/memory.py +337 -0
  122. calc_terminal/memory_v2.py +479 -0
  123. calc_terminal/metrics.py +333 -0
  124. calc_terminal/mode_detection.py +146 -0
  125. calc_terminal/model.py +2431 -0
  126. calc_terminal/model_router.py +665 -0
  127. calc_terminal/models/__init__.py +0 -0
  128. calc_terminal/models/active_state.py +187 -0
  129. calc_terminal/models/dynamic_registry.py +584 -0
  130. calc_terminal/models/manager.py +781 -0
  131. calc_terminal/models/model_metadata.json +3526 -0
  132. calc_terminal/models/profiles.py +194 -0
  133. calc_terminal/models/registry.py +265 -0
  134. calc_terminal/models/schema.py +197 -0
  135. calc_terminal/models/validator.py +287 -0
  136. calc_terminal/models/verification_engine.py +368 -0
  137. calc_terminal/native_picker.py +215 -0
  138. calc_terminal/ollama_catalog.py +279 -0
  139. calc_terminal/ollama_download.py +233 -0
  140. calc_terminal/orchestrator.py +304 -0
  141. calc_terminal/package_research.py +322 -0
  142. calc_terminal/packages.py +1024 -0
  143. calc_terminal/pc_specs.py +116 -0
  144. calc_terminal/permissions.py +334 -0
  145. calc_terminal/pet.py +106 -0
  146. calc_terminal/pipeline.py +505 -0
  147. calc_terminal/platform/__init__.py +491 -0
  148. calc_terminal/platform/desktop.py +491 -0
  149. calc_terminal/platform/web.py +781 -0
  150. calc_terminal/preview/__init__.py +1 -0
  151. calc_terminal/preview/dev_server.py +303 -0
  152. calc_terminal/preview/diagnostics.py +131 -0
  153. calc_terminal/preview/live_reload.py +66 -0
  154. calc_terminal/preview/manager.py +129 -0
  155. calc_terminal/project_stats.py +209 -0
  156. calc_terminal/projects.py +328 -0
  157. calc_terminal/providers/__init__.py +0 -0
  158. calc_terminal/providers/adapters/__init__.py +80 -0
  159. calc_terminal/providers/adapters/anthropic_adapter.py +127 -0
  160. calc_terminal/providers/adapters/base.py +106 -0
  161. calc_terminal/providers/adapters/chinese_adapters.py +420 -0
  162. calc_terminal/providers/adapters/gemini_adapter.py +101 -0
  163. calc_terminal/providers/adapters/ollama_adapter.py +83 -0
  164. calc_terminal/providers/adapters/openai_adapter.py +159 -0
  165. calc_terminal/providers/adapters/other_adapters.py +246 -0
  166. calc_terminal/providers/anthropic_provider.py +172 -0
  167. calc_terminal/providers/auto_update.py +416 -0
  168. calc_terminal/providers/base_provider.py +105 -0
  169. calc_terminal/providers/discovery_manager.py +207 -0
  170. calc_terminal/providers/gemini_provider.py +178 -0
  171. calc_terminal/providers/lifecycle.py +767 -0
  172. calc_terminal/providers/ollama_adapter.py +707 -0
  173. calc_terminal/providers/openai_provider.py +254 -0
  174. calc_terminal/providers/provider_manager.py +1827 -0
  175. calc_terminal/providers/providers.json +4075 -0
  176. calc_terminal/reactionsim.py +279 -0
  177. calc_terminal/registry.py +337 -0
  178. calc_terminal/report.py +162 -0
  179. calc_terminal/research/__init__.py +45 -0
  180. calc_terminal/research/artifact_intel.py +126 -0
  181. calc_terminal/research/data_lineage.py +123 -0
  182. calc_terminal/research/experiment_ledger.py +303 -0
  183. calc_terminal/research/reproducibility.py +131 -0
  184. calc_terminal/resilience/__init__.py +47 -0
  185. calc_terminal/resilience/agent_state.py +121 -0
  186. calc_terminal/resilience/capability_matcher.py +174 -0
  187. calc_terminal/resilience/circuit_breaker.py +158 -0
  188. calc_terminal/resilience/failover_engine.py +230 -0
  189. calc_terminal/resilience/health_monitor.py +192 -0
  190. calc_terminal/resilience/ollama_adapter.py +125 -0
  191. calc_terminal/resilience/orchestrator.py +312 -0
  192. calc_terminal/resilience/types.py +134 -0
  193. calc_terminal/sandbox.py +98 -0
  194. calc_terminal/scires.py +558 -0
  195. calc_terminal/security_scanner.py +126 -0
  196. calc_terminal/session.py +294 -0
  197. calc_terminal/sim3d.py +206 -0
  198. calc_terminal/solver.py +276 -0
  199. calc_terminal/sound.py +127 -0
  200. calc_terminal/task_reports.py +287 -0
  201. calc_terminal/terminal_host.py +201 -0
  202. calc_terminal/terminal_identity.py +411 -0
  203. calc_terminal/test_ai_mode_reliability.py +344 -0
  204. calc_terminal/test_browser.py +368 -0
  205. calc_terminal/test_code_editor_upgrade.py +485 -0
  206. calc_terminal/test_customization.py +1148 -0
  207. calc_terminal/test_customization_ui.py +612 -0
  208. calc_terminal/test_dynamic_registry.py +304 -0
  209. calc_terminal/test_extensions.py +436 -0
  210. calc_terminal/test_overhaul.py +557 -0
  211. calc_terminal/test_project_detect.py +255 -0
  212. calc_terminal/test_root_cause_fix.py +527 -0
  213. calc_terminal/test_stability.py +532 -0
  214. calc_terminal/test_terminal_identity.py +132 -0
  215. calc_terminal/test_v079_speed.py +460 -0
  216. calc_terminal/theme.py +1107 -0
  217. calc_terminal/timeline.py +139 -0
  218. calc_terminal/todos.py +246 -0
  219. calc_terminal/tool_call_normalizer.py +419 -0
  220. calc_terminal/tui.py +104 -0
  221. calc_terminal/ui/__init__.py +8 -0
  222. calc_terminal/ui/activity_panel.py +231 -0
  223. calc_terminal/ui/activity_stream_panel.py +238 -0
  224. calc_terminal/ui/animations.py +122 -0
  225. calc_terminal/ui/app.py +7271 -0
  226. calc_terminal/ui/attach_panel.py +597 -0
  227. calc_terminal/ui/attachments.py +424 -0
  228. calc_terminal/ui/backup_panel.py +810 -0
  229. calc_terminal/ui/browser_shell.py +887 -0
  230. calc_terminal/ui/cat_agent.py +357 -0
  231. calc_terminal/ui/chats_panel.py +899 -0
  232. calc_terminal/ui/command_palette.py +125 -0
  233. calc_terminal/ui/command_palette_modal.py +166 -0
  234. calc_terminal/ui/composer.py +1141 -0
  235. calc_terminal/ui/context_menu.py +197 -0
  236. calc_terminal/ui/conversation.py +1435 -0
  237. calc_terminal/ui/customization_panel.py +1229 -0
  238. calc_terminal/ui/dashboard.py +404 -0
  239. calc_terminal/ui/design_system.py +557 -0
  240. calc_terminal/ui/diff_panel.py +213 -0
  241. calc_terminal/ui/editor.py +2102 -0
  242. calc_terminal/ui/empty_state.py +302 -0
  243. calc_terminal/ui/events.py +487 -0
  244. calc_terminal/ui/extensions_panel.py +815 -0
  245. calc_terminal/ui/footer.py +166 -0
  246. calc_terminal/ui/gestures_panel.py +383 -0
  247. calc_terminal/ui/goodbye_screen.py +100 -0
  248. calc_terminal/ui/header.py +1034 -0
  249. calc_terminal/ui/help_panel.py +254 -0
  250. calc_terminal/ui/live_activities.py +914 -0
  251. calc_terminal/ui/mcp_panel.py +570 -0
  252. calc_terminal/ui/memory_center.py +524 -0
  253. calc_terminal/ui/mode_colors_panel.py +525 -0
  254. calc_terminal/ui/nav_screens.py +747 -0
  255. calc_terminal/ui/ollama_panel.py +536 -0
  256. calc_terminal/ui/palette.py +221 -0
  257. calc_terminal/ui/permission_panel.py +269 -0
  258. calc_terminal/ui/personalization_panel.py +517 -0
  259. calc_terminal/ui/personalize_center.py +1568 -0
  260. calc_terminal/ui/preview_panel.py +441 -0
  261. calc_terminal/ui/resizers.py +402 -0
  262. calc_terminal/ui/sidebar.py +1285 -0
  263. calc_terminal/ui/statusbar.py +168 -0
  264. calc_terminal/ui/theme_css.py +1396 -0
  265. calc_terminal/ui/thinking.py +226 -0
  266. calc_terminal/ui/timeline_panel.py +102 -0
  267. calc_terminal/ui/todo_panel.py +193 -0
  268. calc_terminal/ui/viewport.py +136 -0
  269. calc_terminal/ui/vision_panel.py +489 -0
  270. calc_terminal/ui/welcome_modal.py +343 -0
  271. calc_terminal/ui/widgets.py +160 -0
  272. calc_terminal/ui/workspace.py +831 -0
  273. calc_terminal/viewers/__init__.py +1 -0
  274. calc_terminal/viewers/document_viewer.py +252 -0
  275. calc_terminal/viewers/image_viewer.py +241 -0
  276. calc_terminal/viewers/pdf_viewer.py +203 -0
  277. calc_terminal/viewers/presentation_viewer.py +164 -0
  278. calc_terminal/viewers/registry.py +120 -0
  279. calc_terminal/viewers/spreadsheet_viewer.py +204 -0
  280. calc_terminal/vision/__init__.py +89 -0
  281. calc_terminal/vision/analysis.py +194 -0
  282. calc_terminal/vision/annotations.py +297 -0
  283. calc_terminal/vision/capture.py +171 -0
  284. calc_terminal/vision/context.py +231 -0
  285. calc_terminal/vision/correlation.py +169 -0
  286. calc_terminal/vision/cursor.py +258 -0
  287. calc_terminal/vision/events.py +66 -0
  288. calc_terminal/vision/frame_pipeline.py +259 -0
  289. calc_terminal/vision/priority.py +218 -0
  290. calc_terminal/vision/provider.py +180 -0
  291. calc_terminal/vision/safety.py +149 -0
  292. calc_terminal/vision/session.py +281 -0
  293. calc_terminal/vision/verify.py +162 -0
  294. calc_terminal/vision.py +514 -0
  295. calc_terminal/vscode_integration.py +113 -0
  296. calc_terminal/web/__init__.py +8 -0
  297. calc_terminal/web/cat_runtime.py +710 -0
  298. calc_terminal/web/server.py +2891 -0
  299. calc_terminal/web/static/css/app.css +3152 -0
  300. calc_terminal/web/static/icons/badge-72.png +0 -0
  301. calc_terminal/web/static/icons/cat.ico +0 -0
  302. calc_terminal/web/static/icons/icon-128.png +0 -0
  303. calc_terminal/web/static/icons/icon-144.png +0 -0
  304. calc_terminal/web/static/icons/icon-152.png +0 -0
  305. calc_terminal/web/static/icons/icon-192.png +0 -0
  306. calc_terminal/web/static/icons/icon-384.png +0 -0
  307. calc_terminal/web/static/icons/icon-512.png +0 -0
  308. calc_terminal/web/static/icons/icon-72.png +0 -0
  309. calc_terminal/web/static/icons/icon-96.png +0 -0
  310. calc_terminal/web/static/icons/icon.svg +34 -0
  311. calc_terminal/web/static/icons/new-project.png +0 -0
  312. calc_terminal/web/static/icons/open-project.png +0 -0
  313. calc_terminal/web/static/index.html +734 -0
  314. calc_terminal/web/static/js/app.js +2403 -0
  315. calc_terminal/web/static/manifest.json +88 -0
  316. calc_terminal/web/static/sw.js +230 -0
  317. calc_terminal/workflow_engine.py +769 -0
  318. calc_terminal/workspace.py +593 -0
  319. calc_terminal/workspace_index.py +385 -0
  320. cct_cli-0.7.9.0.dist-info/METADATA +210 -0
  321. cct_cli-0.7.9.0.dist-info/RECORD +325 -0
  322. cct_cli-0.7.9.0.dist-info/WHEEL +5 -0
  323. cct_cli-0.7.9.0.dist-info/entry_points.txt +4 -0
  324. cct_cli-0.7.9.0.dist-info/licenses/LICENSE +21 -0
  325. cct_cli-0.7.9.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,218 @@
1
+ """
2
+ CAT v0.8.a — Frame Priority Engine (spec 9).
3
+
4
+ Scores frames/events for AI analysis priority:
5
+
6
+ score = visual_change + interaction_event + annotation_event
7
+ + user_request + error_signal + AI_requested_frame
8
+
9
+ Only high-value frames are sent to expensive multimodal reasoning.
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import time
15
+ from dataclasses import dataclass, field
16
+ from enum import Enum
17
+ from typing import Any, Dict, List, Optional
18
+
19
+
20
+ class PriorityLevel(str, Enum):
21
+ SKIP = "skip"
22
+ LOW = "low"
23
+ NORMAL = "normal"
24
+ HIGH = "high"
25
+ CRITICAL = "critical"
26
+
27
+
28
+ @dataclass
29
+ class PriorityScore:
30
+ total: float
31
+ level: PriorityLevel
32
+ breakdown: Dict[str, float]
33
+ should_send: bool
34
+ reasons: List[str]
35
+
36
+
37
+ class FramePriorityEngine:
38
+ """
39
+ Event-driven priority scoring. Call score_event() for each signal;
40
+ call score_frame() to combine into a final decision for a candidate frame.
41
+ """
42
+
43
+ # Weights — tuned so obvious signals dominate redundant frame dribbles
44
+ WEIGHTS = {
45
+ "visual_change_none": 0,
46
+ "visual_change_minor": 5,
47
+ "visual_change_moderate": 15,
48
+ "visual_change_major": 30,
49
+ "visual_change_critical": 50,
50
+ "cursor_move": 2,
51
+ "cursor_dwell": 12,
52
+ "click": 18,
53
+ "annotation": 25,
54
+ "scroll": 4,
55
+ "user_chat": 35,
56
+ "user_analyze_request": 40,
57
+ "ai_requested_frame": 45,
58
+ "error_signal": 40,
59
+ "preview_change": 20,
60
+ "ui_state_change": 10,
61
+ }
62
+
63
+ # Thresholds for final level
64
+ THRESHOLDS = {
65
+ PriorityLevel.SKIP: 0,
66
+ PriorityLevel.LOW: 5,
67
+ PriorityLevel.NORMAL: 15,
68
+ PriorityLevel.HIGH: 30,
69
+ PriorityLevel.CRITICAL: 45,
70
+ }
71
+
72
+ def __init__(self, high_threshold: float = 30.0):
73
+ self.high_threshold = high_threshold
74
+ self._pending_signals: List[Dict[str, Any]] = []
75
+ self._last_send_time: float = 0.0
76
+ self._min_send_interval: float = 1.0 # seconds, adaptive
77
+ self._consecutive_skips: int = 0
78
+
79
+ def add_signal(self, kind: str, value: float = 1.0, detail: str = ""):
80
+ """Queue a signal; it will be consumed on next score_frame()."""
81
+ self._pending_signals.append({
82
+ "kind": kind,
83
+ "value": value,
84
+ "detail": detail,
85
+ "ts": time.time(),
86
+ })
87
+ # keep bounded
88
+ if len(self._pending_signals) > 100:
89
+ self._pending_signals = self._pending_signals[-50:]
90
+
91
+ # Convenience helpers
92
+ def signal_visual_change(self, level: str):
93
+ key = f"visual_change_{level}"
94
+ self.add_signal(key, detail=level)
95
+
96
+ def signal_cursor_move(self): self.add_signal("cursor_move")
97
+ def signal_cursor_dwell(self): self.add_signal("cursor_dwell")
98
+ def signal_click(self): self.add_signal("click")
99
+ def signal_annotation(self, tool: str = ""): self.add_signal("annotation", detail=tool)
100
+ def signal_scroll(self): self.add_signal("scroll")
101
+ def signal_user_chat(self): self.add_signal("user_chat")
102
+ def signal_user_analyze(self): self.add_signal("user_analyze_request")
103
+ def signal_ai_request(self): self.add_signal("ai_requested_frame")
104
+ def signal_error(self, msg: str = ""): self.add_signal("error_signal", detail=msg)
105
+ def signal_preview_change(self): self.add_signal("preview_change")
106
+ def signal_ui_state_change(self): self.add_signal("ui_state_change")
107
+
108
+ def score_frame(self,
109
+ visual_change: str = "none",
110
+ has_annotation: bool = False,
111
+ has_cursor_event: bool = False,
112
+ has_user_request: bool = False,
113
+ has_error: bool = False,
114
+ ai_requested: bool = False,
115
+ force: bool = False) -> PriorityScore:
116
+ """
117
+ Score a candidate frame. Also drains pending signals.
118
+ Set force=True for explicit user Analyze actions (always send).
119
+ """
120
+ breakdown: Dict[str, float] = {}
121
+ reasons: List[str] = []
122
+ total = 0.0
123
+
124
+ # visual change component
125
+ vc_key = f"visual_change_{visual_change}"
126
+ vc_score = self.WEIGHTS.get(vc_key, 0)
127
+ if vc_score:
128
+ breakdown["visual_change"] = vc_score
129
+ reasons.append(f"visual:{visual_change} +{vc_score}")
130
+ total += vc_score
131
+
132
+ # pending signals
133
+ for sig in self._pending_signals:
134
+ w = self.WEIGHTS.get(sig["kind"], 0)
135
+ if w:
136
+ breakdown[sig["kind"]] = breakdown.get(sig["kind"], 0) + w
137
+ reasons.append(f"{sig['kind']} +{w}")
138
+ total += w
139
+ self._pending_signals.clear()
140
+
141
+ # explicit flags (additive if not already counted via signals)
142
+ if has_annotation and "annotation" not in breakdown:
143
+ w = self.WEIGHTS["annotation"]
144
+ breakdown["annotation"] = w
145
+ reasons.append(f"annotation +{w}")
146
+ total += w
147
+ if has_cursor_event and "cursor_move" not in breakdown and "cursor_dwell" not in breakdown:
148
+ w = self.WEIGHTS["cursor_move"]
149
+ breakdown["cursor_event"] = w
150
+ total += w
151
+ if has_user_request and "user_chat" not in breakdown:
152
+ w = self.WEIGHTS["user_chat"]
153
+ breakdown["user_request"] = w
154
+ reasons.append(f"user_request +{w}")
155
+ total += w
156
+ if has_error:
157
+ w = self.WEIGHTS["error_signal"]
158
+ breakdown["error_signal"] = breakdown.get("error_signal", 0) + w
159
+ reasons.append(f"error +{w}")
160
+ total += w
161
+ if ai_requested:
162
+ w = self.WEIGHTS["ai_requested_frame"]
163
+ breakdown["ai_requested"] = w
164
+ reasons.append(f"ai_requested +{w}")
165
+ total += w
166
+
167
+ # level
168
+ if force:
169
+ level = PriorityLevel.CRITICAL
170
+ should_send = True
171
+ elif total >= self.THRESHOLDS[PriorityLevel.CRITICAL]:
172
+ level = PriorityLevel.CRITICAL
173
+ should_send = True
174
+ elif total >= self.THRESHOLDS[PriorityLevel.HIGH]:
175
+ level = PriorityLevel.HIGH
176
+ should_send = True
177
+ elif total >= self.THRESHOLDS[PriorityLevel.NORMAL]:
178
+ level = PriorityLevel.NORMAL
179
+ # throttle normal frames
180
+ should_send = (time.time() - self._last_send_time) >= self._min_send_interval
181
+ if not should_send:
182
+ reasons.append("throttled (normal priority, interval)")
183
+ elif total >= self.THRESHOLDS[PriorityLevel.LOW]:
184
+ level = PriorityLevel.LOW
185
+ should_send = False
186
+ reasons.append("low priority — skipped")
187
+ else:
188
+ level = PriorityLevel.SKIP
189
+ should_send = False
190
+
191
+ if should_send:
192
+ self._last_send_time = time.time()
193
+ self._consecutive_skips = 0
194
+ else:
195
+ self._consecutive_skips += 1
196
+ # anti-starvation: force send after many skips if we have moderate visual change
197
+ if self._consecutive_skips >= 10 and visual_change in ("moderate", "major", "critical"):
198
+ should_send = True
199
+ level = PriorityLevel.NORMAL
200
+ reasons.append("anti-starvation override")
201
+ self._consecutive_skips = 0
202
+ self._last_send_time = time.time()
203
+
204
+ return PriorityScore(
205
+ total=round(total, 1),
206
+ level=level,
207
+ breakdown=breakdown,
208
+ should_send=should_send,
209
+ reasons=reasons,
210
+ )
211
+
212
+ def set_min_interval(self, seconds: float):
213
+ self._min_send_interval = max(0.2, seconds)
214
+
215
+ def reset(self):
216
+ self._pending_signals.clear()
217
+ self._last_send_time = 0.0
218
+ self._consecutive_skips = 0
@@ -0,0 +1,180 @@
1
+ """
2
+ CAT v0.8.a — Vision Provider Router.
3
+
4
+ Capability-aware routing for vision requests. Routes vision analysis
5
+ to the best available provider based on capabilities, cost, and quality.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import time
11
+ from dataclasses import dataclass
12
+ from typing import Any, Dict, List, Optional, Tuple
13
+
14
+
15
+ @dataclass
16
+ class VisionCapability:
17
+ vision: bool = False
18
+ max_image_tokens: int = 0
19
+ supported_formats: tuple = ("png", "jpeg", "gif", "webp")
20
+ supports_video: bool = False
21
+ supports_audio: bool = False
22
+ supports_streaming: bool = False
23
+ cost_per_image_token: float = 0.0
24
+ quality_score: float = 0.0
25
+
26
+ def to_dict(self) -> Dict[str, Any]:
27
+ return {
28
+ "vision": self.vision,
29
+ "max_image_tokens": self.max_image_tokens,
30
+ "supported_formats": list(self.supported_formats),
31
+ "supports_video": self.supports_video,
32
+ "supports_audio": self.supports_audio,
33
+ "supports_streaming": self.supports_streaming,
34
+ "cost_per_image_token": self.cost_per_image_token,
35
+ "quality_score": self.quality_score,
36
+ }
37
+
38
+
39
+ PROVIDER_VISION_CAPS: Dict[str, VisionCapability] = {
40
+ "openai": VisionCapability(
41
+ vision=True, max_image_tokens=8192,
42
+ supported_formats=("png", "jpeg", "gif", "webp"),
43
+ cost_per_image_token=0.01, quality_score=0.95,
44
+ ),
45
+ "anthropic": VisionCapability(
46
+ vision=True, max_image_tokens=8192,
47
+ supported_formats=("png", "jpeg", "gif", "webp"),
48
+ cost_per_image_token=0.015, quality_score=0.93,
49
+ ),
50
+ "google": VisionCapability(
51
+ vision=True, max_image_tokens=16384,
52
+ supported_formats=("png", "jpeg", "gif", "webp"),
53
+ cost_per_image_token=0.008, quality_score=0.90,
54
+ ),
55
+ "ollama": VisionCapability(
56
+ vision=True, max_image_tokens=4096,
57
+ supported_formats=("png", "jpeg"),
58
+ cost_per_image_token=0.0, quality_score=0.70,
59
+ ),
60
+ "groq": VisionCapability(
61
+ vision=True, max_image_tokens=4096,
62
+ supported_formats=("png", "jpeg", "webp"),
63
+ cost_per_image_token=0.003, quality_score=0.80,
64
+ ),
65
+ "mistral": VisionCapability(
66
+ vision=True, max_image_tokens=8192,
67
+ supported_formats=("png", "jpeg", "gif", "webp"),
68
+ cost_per_image_token=0.01, quality_score=0.85,
69
+ ),
70
+ "deepseek": VisionCapability(
71
+ vision=True, max_image_tokens=8192,
72
+ supported_formats=("png", "jpeg", "gif", "webp"),
73
+ cost_per_image_token=0.005, quality_score=0.82,
74
+ ),
75
+ "together": VisionCapability(
76
+ vision=True, max_image_tokens=8192,
77
+ supported_formats=("png", "jpeg", "gif", "webp"),
78
+ cost_per_image_token=0.008, quality_score=0.85,
79
+ ),
80
+ "openrouter": VisionCapability(
81
+ vision=True, max_image_tokens=16384,
82
+ supported_formats=("png", "jpeg", "gif", "webp"),
83
+ cost_per_image_token=0.01, quality_score=0.88,
84
+ ),
85
+ }
86
+
87
+
88
+ class VisionProviderRouter:
89
+ """Routes vision requests to the best available provider."""
90
+
91
+ def __init__(self, config: Optional[Dict] = None):
92
+ self._config = config or {}
93
+ self._fallback_order = [
94
+ "openai", "anthropic", "google", "deepseek",
95
+ "groq", "mistral", "together", "openrouter", "ollama",
96
+ ]
97
+
98
+ def get_capabilities(self, provider: str) -> VisionCapability:
99
+ return PROVIDER_VISION_CAPS.get(provider, VisionCapability())
100
+
101
+ def find_vision_capable(self, config: Optional[Dict] = None) -> Optional[Tuple[Dict, VisionCapability]]:
102
+ cfg = config or self._config
103
+ provider = cfg.get("provider", "")
104
+ model = cfg.get("model", "")
105
+
106
+ caps = self.get_capabilities(provider)
107
+ if caps.vision:
108
+ return cfg, caps
109
+
110
+ for fallback_provider in self._fallback_order:
111
+ if fallback_provider == provider:
112
+ continue
113
+ fallback_caps = self.get_capabilities(fallback_provider)
114
+ if fallback_caps.vision:
115
+ fallback_config = dict(cfg)
116
+ fallback_config["provider"] = fallback_provider
117
+ return fallback_config, fallback_caps
118
+
119
+ return None
120
+
121
+ def select_best_provider(self, image_size: int = 0,
122
+ need_video: bool = False,
123
+ prefer_free: bool = False) -> Optional[Tuple[str, VisionCapability]]:
124
+ candidates = []
125
+ for provider, caps in PROVIDER_VISION_CAPS.items():
126
+ if not caps.vision:
127
+ continue
128
+ if need_video and not caps.supports_video:
129
+ continue
130
+ if prefer_free and caps.cost_per_image_token > 0:
131
+ continue
132
+ if image_size > 0 and image_size > caps.max_image_tokens * 100:
133
+ continue
134
+ candidates.append((provider, caps))
135
+
136
+ if prefer_free:
137
+ candidates = [(p, c) for p, c in candidates if c.cost_per_image_token == 0]
138
+
139
+ if not candidates:
140
+ return None
141
+
142
+ candidates.sort(key=lambda x: (
143
+ -x[1].quality_score,
144
+ x[1].cost_per_image_token,
145
+ ))
146
+ return candidates[0]
147
+
148
+ def route_vision_request(self, frame_b64: str,
149
+ frame_size_bytes: int = 0,
150
+ config: Optional[Dict] = None) -> Dict[str, Any]:
151
+ cfg = config or self._config
152
+ provider = cfg.get("provider", "")
153
+ caps = self.get_capabilities(provider)
154
+
155
+ result = {
156
+ "provider": provider,
157
+ "capabilities": caps.to_dict(),
158
+ "routed": caps.vision,
159
+ "fallback_used": False,
160
+ }
161
+
162
+ if not caps.vision:
163
+ route = self.find_vision_capable(cfg)
164
+ if route:
165
+ fallback_cfg, fallback_caps = route
166
+ result["provider"] = fallback_cfg.get("provider", "")
167
+ result["capabilities"] = fallback_caps.to_dict()
168
+ result["routed"] = True
169
+ result["fallback_used"] = True
170
+ result["original_provider"] = provider
171
+ else:
172
+ result["error"] = "No vision-capable provider available"
173
+
174
+ return result
175
+
176
+ def get_all_capabilities(self) -> Dict[str, Dict]:
177
+ return {
178
+ provider: caps.to_dict()
179
+ for provider, caps in PROVIDER_VISION_CAPS.items()
180
+ }
@@ -0,0 +1,149 @@
1
+ """
2
+ CAT v0.8.a — Privacy & Safety Controls.
3
+
4
+ Configurable privacy filtering, blocked region detection,
5
+ data retention limits, and safety safeguards.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import time
11
+ from dataclasses import dataclass, field
12
+ from typing import Any, Dict, List, Optional, Tuple
13
+
14
+
15
+ @dataclass
16
+ class SafetyConfig:
17
+ max_session_minutes: int = 60
18
+ max_frames_per_session: int = 500
19
+ max_total_frames_per_day: int = 5000
20
+ block_password_fields: bool = True
21
+ block_financial_data: bool = False
22
+ block_pii_detection: bool = True
23
+ blur_sensitive_regions: bool = True
24
+ auto_stop_on_error: bool = True
25
+ require_confirmation_for_persist: bool = True
26
+ log_all_access: bool = True
27
+ max_context_history: int = 20
28
+ data_retention_hours: int = 24
29
+
30
+ def to_dict(self) -> Dict[str, Any]:
31
+ return {
32
+ "max_session_minutes": self.max_session_minutes,
33
+ "max_frames_per_session": self.max_frames_per_session,
34
+ "max_total_frames_per_day": self.max_total_frames_per_day,
35
+ "block_password_fields": self.block_password_fields,
36
+ "block_financial_data": self.block_financial_data,
37
+ "block_pii_detection": self.block_pii_detection,
38
+ "blur_sensitive_regions": self.blur_sensitive_regions,
39
+ "auto_stop_on_error": self.auto_stop_on_error,
40
+ "require_confirmation_for_persist": self.require_confirmation_for_persist,
41
+ "log_all_access": self.log_all_access,
42
+ "max_context_history": self.max_context_history,
43
+ "data_retention_hours": self.data_retention_hours,
44
+ }
45
+
46
+
47
+ class PrivacyController:
48
+ """Enforces privacy rules on captured frames and context."""
49
+
50
+ SENSITIVE_KEYWORDS = [
51
+ "password", "passwd", "secret", "token", "api_key", "apikey",
52
+ "authorization", "credit_card", "ssn", "social_security",
53
+ "bank_account", "routing_number", "pin_code",
54
+ ]
55
+
56
+ def __init__(self, config: Optional[SafetyConfig] = None):
57
+ self.config = config or SafetyConfig()
58
+ self._access_log: List[Dict] = []
59
+ self._daily_frame_count = 0
60
+ self._day_start = self._today()
61
+
62
+ def _today(self) -> str:
63
+ return time.strftime("%Y-%m-%d")
64
+
65
+ def _reset_daily_if_needed(self):
66
+ today = self._today()
67
+ if today != self._day_start:
68
+ self._daily_frame_count = 0
69
+ self._day_start = today
70
+
71
+ def validate_config(self, config: Any) -> None:
72
+ if hasattr(config, "fps") and config.fps > 10:
73
+ raise ValueError("Frame rate exceeds safety limit (10 fps max)")
74
+ if hasattr(config, "max_frames") and config.max_frames > self.config.max_frames_per_session:
75
+ raise ValueError(
76
+ f"Max frames exceeds safety limit "
77
+ f"({self.config.max_frames_per_session})"
78
+ )
79
+
80
+ def is_blocked_region(self, frame_b64: str) -> bool:
81
+ if not self.config.block_pii_detection:
82
+ return False
83
+ if len(frame_b64) < 100:
84
+ return False
85
+ return False
86
+
87
+ def check_text_sensitivity(self, text: str) -> Dict[str, Any]:
88
+ text_lower = text.lower()
89
+ blocked = [
90
+ kw for kw in self.SENSITIVE_KEYWORDS
91
+ if kw in text_lower
92
+ ]
93
+ return {
94
+ "sensitive": bool(blocked),
95
+ "keywords_found": blocked,
96
+ "recommendation": "block" if blocked else "allow",
97
+ }
98
+
99
+ def filter_frame_metadata(self, meta: Dict[str, Any]) -> Dict[str, Any]:
100
+ filtered = dict(meta)
101
+ if self.config.block_password_fields:
102
+ if "active_element" in filtered:
103
+ el = filtered["active_element"].lower()
104
+ if any(kw in el for kw in ("password", "secret", "pin")):
105
+ filtered["active_element"] = "[REDACTED]"
106
+ return filtered
107
+
108
+ def log_access(self, action: str, resource: str, details: Optional[Dict] = None):
109
+ if not self.config.log_all_access:
110
+ return
111
+ entry = {
112
+ "ts": time.time(),
113
+ "action": action,
114
+ "resource": resource,
115
+ }
116
+ if details:
117
+ entry.update(details)
118
+ self._access_log.append(entry)
119
+ if len(self._access_log) > 10000:
120
+ self._access_log = self._access_log[-5000:]
121
+
122
+ def can_capture(self) -> Tuple[bool, str]:
123
+ self._reset_daily_if_needed()
124
+ if self._daily_frame_count >= self.config.max_total_frames_per_day:
125
+ return False, "Daily frame limit reached"
126
+ return True, ""
127
+
128
+ def record_capture(self):
129
+ self._reset_daily_if_needed()
130
+ self._daily_frame_count += 1
131
+
132
+ def can_persist(self) -> Tuple[bool, str]:
133
+ if self.config.require_confirmation_for_persist:
134
+ return True, "Confirmation required"
135
+ return True, ""
136
+
137
+ def get_access_log(self, limit: int = 100) -> List[Dict]:
138
+ return self._access_log[-limit:]
139
+
140
+ def get_privacy_summary(self) -> Dict[str, Any]:
141
+ return {
142
+ "daily_frames": self._daily_frame_count,
143
+ "daily_limit": self.config.max_total_frames_per_day,
144
+ "session_limit": self.config.max_frames_per_session,
145
+ "block_password_fields": self.config.block_password_fields,
146
+ "block_pii_detection": self.config.block_pii_detection,
147
+ "blur_sensitive_regions": self.config.blur_sensitive_regions,
148
+ "access_log_entries": len(self._access_log),
149
+ }