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,479 @@
1
+ """Layered memory system for CCT: session, long-term (SQLite), and per-project memory."""
2
+
3
+ import json
4
+ import os
5
+ import re
6
+ import sqlite3
7
+ import threading
8
+ import time
9
+ import hashlib
10
+ from dataclasses import dataclass, field, asdict
11
+ from typing import List, Optional, Dict, Any
12
+
13
+
14
+ VALID_CATEGORIES = {
15
+ "user_preferences",
16
+ "programming_preferences",
17
+ "project_preferences",
18
+ "important_instructions",
19
+ "workflow_preferences",
20
+ "conversation_facts",
21
+ "project_facts",
22
+ "custom",
23
+ }
24
+
25
+ API_KEY_PATTERNS = [
26
+ re.compile(r'(?i)(api[_-]?key|apikey)\s*[:=]\s*["\']?([A-Za-z0-9_\-]{16,})["\']?'),
27
+ re.compile(r'(?i)(secret|token|password|passwd|pwd)\s*[:=]\s*["\']?([^\s"\']{8,})["\']?'),
28
+ re.compile(r'(?i)(bearer)\s+([A-Za-z0-9_\-\.]{20,})'),
29
+ re.compile(r'["\']([A-Za-z0-9_\-]{32,})["\']'),
30
+ ]
31
+
32
+ REDACTED_PLACEHOLDER = "[REDACTED]"
33
+
34
+
35
+ def _sanitize_content(text: str) -> str:
36
+ """Detect and redact API keys, passwords, tokens before storage."""
37
+ sanitized = text
38
+ for pattern in API_KEY_PATTERNS:
39
+ sanitized = pattern.sub(lambda m: f"{m.group(1)}={REDACTED_PLACEHOLDER}", sanitized)
40
+ return sanitized
41
+
42
+
43
+ def _similarity(a: str, b: str) -> float:
44
+ """Simple lowercase token-based similarity (Jaccard)."""
45
+ set_a = set(a.lower().split())
46
+ set_b = set(b.lower().split())
47
+ if not set_a or not set_b:
48
+ return 1.0 if a.lower() == b.lower() else 0.0
49
+ intersection = set_a & set_b
50
+ union = set_a | set_b
51
+ return len(intersection) / len(union)
52
+
53
+
54
+ @dataclass
55
+ class Memory:
56
+ id: str = ""
57
+ category: str = "conversation_facts"
58
+ content: str = ""
59
+ source: str = "user"
60
+ created_at: float = 0.0
61
+ updated_at: float = 0.0
62
+ importance: float = 0.5
63
+ confidence: float = 1.0
64
+ last_used: float = 0.0
65
+ project_path: str = ""
66
+
67
+ def __post_init__(self):
68
+ now = time.time()
69
+ if not self.created_at:
70
+ self.created_at = now
71
+ if not self.updated_at:
72
+ self.updated_at = now
73
+ if not self.id:
74
+ raw = f"{self.content}{self.created_at}{os.urandom(8).hex()}"
75
+ self.id = hashlib.sha256(raw.encode()).hexdigest()[:16]
76
+
77
+ def to_dict(self) -> Dict[str, Any]:
78
+ return asdict(self)
79
+
80
+ @classmethod
81
+ def from_dict(cls, d: Dict[str, Any]) -> "Memory":
82
+ return cls(**{k: v for k, v in d.items() if k in cls.__dataclass_fields__})
83
+
84
+
85
+ class MemoryManager:
86
+ """Manages session, long-term, and per-project memory."""
87
+
88
+ def __init__(self, memory_dir: Optional[str] = None):
89
+ self.memory_dir = memory_dir or os.path.join(os.path.expanduser("~"), ".cct_memory_v2")
90
+ os.makedirs(self.memory_dir, exist_ok=True)
91
+
92
+ self._session: List[Dict[str, str]] = []
93
+
94
+ self._db_path = os.path.join(self.memory_dir, "cct_memory.db")
95
+ self._init_db()
96
+
97
+ self._project_memories: Dict[str, List[Dict[str, Any]]] = {}
98
+ self._load_all_project_memories()
99
+
100
+ # ── SQLite setup ──────────────────────────────────────────────────────
101
+
102
+ def _get_db(self) -> sqlite3.Connection:
103
+ conn = sqlite3.connect(self._db_path)
104
+ conn.row_factory = sqlite3.Row
105
+ return conn
106
+
107
+ def _init_db(self) -> None:
108
+ conn = self._get_db()
109
+ try:
110
+ conn.execute(
111
+ """
112
+ CREATE TABLE IF NOT EXISTS memories (
113
+ id TEXT PRIMARY KEY,
114
+ category TEXT NOT NULL,
115
+ content TEXT NOT NULL,
116
+ source TEXT DEFAULT 'user',
117
+ created_at REAL NOT NULL,
118
+ updated_at REAL NOT NULL,
119
+ importance REAL DEFAULT 0.5,
120
+ confidence REAL DEFAULT 1.0,
121
+ last_used REAL DEFAULT 0.0,
122
+ project_path TEXT DEFAULT ''
123
+ )
124
+ """
125
+ )
126
+ conn.execute(
127
+ "CREATE INDEX IF NOT EXISTS idx_memories_category ON memories(category)"
128
+ )
129
+ conn.execute(
130
+ "CREATE INDEX IF NOT EXISTS idx_memories_importance ON memories(importance DESC)"
131
+ )
132
+ conn.commit()
133
+ finally:
134
+ conn.close()
135
+
136
+ # ── Session memory ────────────────────────────────────────────────────
137
+
138
+ def add_session_turn(self, role: str, text: str, mode: str = "chat") -> None:
139
+ """Append a turn to the in-memory session log."""
140
+ self._session.append({
141
+ "role": role,
142
+ "text": text,
143
+ "mode": mode,
144
+ "timestamp": str(time.time()),
145
+ })
146
+
147
+ def get_session_context(self, max_turns: int = 20) -> List[Dict[str, str]]:
148
+ """Return the last *max_turns* session turns."""
149
+ return self._session[-max_turns:]
150
+
151
+ def clear_session(self) -> None:
152
+ """Clear all session turns."""
153
+ self._session.clear()
154
+
155
+ # ── Long-term memory (SQLite) ─────────────────────────────────────────
156
+
157
+ def store_memory(
158
+ self,
159
+ category: str,
160
+ content: str,
161
+ importance: float = 0.5,
162
+ confidence: float = 1.0,
163
+ source: str = "user",
164
+ ) -> Memory:
165
+ """Store a new memory record. Returns the created Memory."""
166
+ if category not in VALID_CATEGORIES:
167
+ raise ValueError(f"Invalid category '{category}'. Must be one of {VALID_CATEGORIES}")
168
+
169
+ sanitized = _sanitize_content(content)
170
+ mem = Memory(
171
+ category=category,
172
+ content=sanitized,
173
+ source=source,
174
+ importance=importance,
175
+ confidence=confidence,
176
+ )
177
+
178
+ conn = self._get_db()
179
+ try:
180
+ conn.execute(
181
+ """
182
+ INSERT INTO memories (id, category, content, source, created_at,
183
+ updated_at, importance, confidence, last_used, project_path)
184
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
185
+ """,
186
+ (
187
+ mem.id, mem.category, mem.content, mem.source,
188
+ mem.created_at, mem.updated_at, mem.importance,
189
+ mem.confidence, mem.last_used, mem.project_path,
190
+ ),
191
+ )
192
+ conn.commit()
193
+ finally:
194
+ conn.close()
195
+ return mem
196
+
197
+ def retrieve_memories(self, query: str = "", limit: int = 10) -> List[Memory]:
198
+ """Retrieve memories, optionally filtered by a query string."""
199
+ conn = self._get_db()
200
+ try:
201
+ if query:
202
+ rows = conn.execute(
203
+ "SELECT * FROM memories WHERE content LIKE ? ORDER BY importance DESC, created_at DESC LIMIT ?",
204
+ (f"%{query}%", limit),
205
+ ).fetchall()
206
+ else:
207
+ rows = conn.execute(
208
+ "SELECT * FROM memories ORDER BY importance DESC, created_at DESC LIMIT ?",
209
+ (limit,),
210
+ ).fetchall()
211
+
212
+ results = []
213
+ for row in rows:
214
+ mem = Memory(**dict(row))
215
+ mem.last_used = time.time()
216
+ results.append(mem)
217
+
218
+ if results:
219
+ ids = [r.id for r in results]
220
+ placeholders = ",".join("?" for _ in ids)
221
+ conn.execute(
222
+ f"UPDATE memories SET last_used = ? WHERE id IN ({placeholders})",
223
+ [time.time()] + ids,
224
+ )
225
+ conn.commit()
226
+
227
+ return results
228
+ finally:
229
+ conn.close()
230
+
231
+ def delete_memory(self, memory_id: str) -> bool:
232
+ """Delete a memory by id. Returns True if a row was removed."""
233
+ conn = self._get_db()
234
+ try:
235
+ cur = conn.execute("DELETE FROM memories WHERE id = ?", (memory_id,))
236
+ conn.commit()
237
+ return cur.rowcount > 0
238
+ finally:
239
+ conn.close()
240
+
241
+ def deduplicate(self) -> int:
242
+ """Remove duplicate memories by text similarity. Returns count removed."""
243
+ conn = self._get_db()
244
+ try:
245
+ rows = conn.execute(
246
+ "SELECT id, content FROM memories ORDER BY created_at"
247
+ ).fetchall()
248
+
249
+ to_remove: List[str] = []
250
+ seen: List[str] = []
251
+
252
+ for row in rows:
253
+ rid = row["id"]
254
+ content = row["content"]
255
+ is_dup = False
256
+ for existing in seen:
257
+ if _similarity(content, existing) > 0.85:
258
+ is_dup = True
259
+ break
260
+ if is_dup:
261
+ to_remove.append(rid)
262
+ else:
263
+ seen.append(content)
264
+
265
+ if to_remove:
266
+ placeholders = ",".join("?" for _ in to_remove)
267
+ conn.execute(
268
+ f"DELETE FROM memories WHERE id IN ({placeholders})", to_remove
269
+ )
270
+ conn.commit()
271
+
272
+ return len(to_remove)
273
+ finally:
274
+ conn.close()
275
+
276
+ def search_memories(self, query: str) -> List[Memory]:
277
+ """Search memories whose content contains the query."""
278
+ conn = self._get_db()
279
+ try:
280
+ rows = conn.execute(
281
+ "SELECT * FROM memories WHERE content LIKE ? ORDER BY importance DESC",
282
+ (f"%{query}%",),
283
+ ).fetchall()
284
+ return [Memory(**dict(r)) for r in rows]
285
+ finally:
286
+ conn.close()
287
+
288
+ def get_all_memories(self) -> List[Memory]:
289
+ """Return every stored memory."""
290
+ conn = self._get_db()
291
+ try:
292
+ rows = conn.execute(
293
+ "SELECT * FROM memories ORDER BY importance DESC"
294
+ ).fetchall()
295
+ return [Memory(**dict(r)) for r in rows]
296
+ finally:
297
+ conn.close()
298
+
299
+ def get_memories_by_category(self, category: str) -> List[Memory]:
300
+ """Return all memories in *category*."""
301
+ conn = self._get_db()
302
+ try:
303
+ rows = conn.execute(
304
+ "SELECT * FROM memories WHERE category = ? ORDER BY importance DESC",
305
+ (category,),
306
+ ).fetchall()
307
+ return [Memory(**dict(r)) for r in rows]
308
+ finally:
309
+ conn.close()
310
+
311
+ def update_memory(self, memory_id: str, **kwargs) -> Optional[Memory]:
312
+ """Update fields on an existing memory. Returns updated Memory or None."""
313
+ allowed = {
314
+ "category", "content", "source", "importance",
315
+ "confidence", "project_path", "last_used",
316
+ }
317
+ updates = {k: v for k, v in kwargs.items() if k in allowed}
318
+ if not updates:
319
+ return None
320
+
321
+ if "category" in updates and updates["category"] not in VALID_CATEGORIES:
322
+ raise ValueError(f"Invalid category '{updates['category']}'")
323
+
324
+ if "content" in updates:
325
+ updates["content"] = _sanitize_content(updates["content"])
326
+
327
+ updates["updated_at"] = time.time()
328
+
329
+ conn = self._get_db()
330
+ try:
331
+ set_clause = ", ".join(f"{k} = ?" for k in updates)
332
+ values = list(updates.values()) + [memory_id]
333
+ cur = conn.execute(
334
+ f"UPDATE memories SET {set_clause} WHERE id = ?", values
335
+ )
336
+ conn.commit()
337
+
338
+ if cur.rowcount == 0:
339
+ return None
340
+
341
+ row = conn.execute("SELECT * FROM memories WHERE id = ?", (memory_id,)).fetchone()
342
+ return Memory(**dict(row)) if row else None
343
+ finally:
344
+ conn.close()
345
+
346
+ # ── Project memory (per-project JSON) ─────────────────────────────────
347
+
348
+ def _project_file(self, project_path: str) -> str:
349
+ safe = hashlib.sha256(project_path.encode()).hexdigest()[:12]
350
+ return os.path.join(self.memory_dir, f"project_{safe}.json")
351
+
352
+ def _load_all_project_memories(self) -> None:
353
+ """Pre-load all project JSON files into memory."""
354
+ for fname in os.listdir(self.memory_dir):
355
+ if fname.startswith("project_") and fname.endswith(".json"):
356
+ fpath = os.path.join(self.memory_dir, fname)
357
+ try:
358
+ with open(fpath, "r", encoding="utf-8") as f:
359
+ data = json.load(f)
360
+ path_key = data.get("_project_path", fname)
361
+ self._project_memories[path_key] = data.get("facts", [])
362
+ except (json.JSONDecodeError, OSError):
363
+ continue
364
+
365
+ def _save_project(self, project_path: str) -> None:
366
+ fpath = self._project_file(project_path)
367
+ data = {
368
+ "_project_path": project_path,
369
+ "facts": self._project_memories.get(project_path, []),
370
+ }
371
+ with open(fpath, "w", encoding="utf-8") as f:
372
+ json.dump(data, f, indent=2, ensure_ascii=False)
373
+
374
+ def store_project_fact(
375
+ self, project_path: str, fact: str, category: str = "general"
376
+ ) -> None:
377
+ """Store a fact associated with a specific project."""
378
+ sanitized = _sanitize_content(fact)
379
+ entry = {
380
+ "fact": sanitized,
381
+ "category": category,
382
+ "timestamp": time.time(),
383
+ }
384
+ self._project_memories.setdefault(project_path, []).append(entry)
385
+ self._save_project(project_path)
386
+
387
+ def get_project_context(self, project_path: str) -> List[Dict[str, Any]]:
388
+ """Return all stored facts for *project_path*."""
389
+ return list(self._project_memories.get(project_path, []))
390
+
391
+ def clear_project_memory(self, project_path: str) -> None:
392
+ """Remove all facts for *project_path* and delete the backing file."""
393
+ self._project_memories.pop(project_path, None)
394
+ fpath = self._project_file(project_path)
395
+ if os.path.exists(fpath):
396
+ os.remove(fpath)
397
+
398
+ # ── Prompt context helper ─────────────────────────────────────────────
399
+
400
+ def context_block(self, project_path: Optional[str] = None,
401
+ query: str = "", limit: int = 6) -> str:
402
+ """Generate a formatted context block suitable for LLM prompts.
403
+
404
+ v0.7.9.0 — memory must not make CAT slow: retrieval is
405
+ RELEVANCE-BASED and SMALL (requirement #13). When `query` is
406
+ given, long-term memories are keyword-scored against it (top
407
+ `limit`) instead of dumping the top-10 by importance; session
408
+ turns shrink to the last few; project facts stay capped.
409
+ """
410
+ parts: List[str] = []
411
+
412
+ # Session
413
+ session = self.get_session_context(max_turns=4)
414
+ if session:
415
+ lines = [f" [{t['role']}] {t['text'][:200]}" for t in session]
416
+ parts.append("Recent Session:\n" + "\n".join(lines))
417
+
418
+ # Long-term — relevance-filtered when a query is available.
419
+ ltm = self.retrieve_relevant(query or "", limit=limit)
420
+ if ltm:
421
+ lines = [f" [{m.category}] {m.content}" for m in ltm]
422
+ parts.append("Long-term Memories:\n" + "\n".join(lines))
423
+
424
+ # Project
425
+ if project_path:
426
+ pfacts = self.get_project_context(project_path)[-8:]
427
+ if pfacts:
428
+ lines = [f" [{f.get('category','general')}] {f['fact']}" for f in pfacts]
429
+ parts.append("Project Facts:\n" + "\n".join(lines))
430
+
431
+ return "\n\n".join(parts) if parts else ""
432
+
433
+ def retrieve_relevant(self, query: str, limit: int = 6) -> List[Memory]:
434
+ """Keyword-overlap relevance retrieval over long-term memories.
435
+ Falls back to the plain importance-ordered list when there is no
436
+ usable query. Cheap: pulls a bounded window and scores locally."""
437
+ try:
438
+ pool = self.get_all_memories()[-120:]
439
+ except Exception:
440
+ return []
441
+ if not pool:
442
+ return []
443
+ words = {w for w in str(query or "").lower().split() if len(w) >= 3}
444
+ if not words:
445
+ return sorted(pool,
446
+ key=lambda m: m.importance, reverse=True)[:limit]
447
+ scored = []
448
+ for m in pool:
449
+ content_words = set(str(m.content).lower().split())
450
+ overlap = len(words & content_words)
451
+ if overlap > 0:
452
+ scored.append((overlap + m.importance, m))
453
+ scored.sort(key=lambda t: t[0], reverse=True)
454
+ return [m for _s, m in scored[:limit]]
455
+
456
+
457
+ # ---------------------------------------------------------------------------
458
+ # v0.7.9.0 — process-wide singleton. run_agent used to construct TWO fresh
459
+ # MemoryManagers per user turn; each __init__ re-created the schema and
460
+ # re-parsed every project_*.json file. One shared instance per memory_dir
461
+ # keeps that cost at exactly once per process (thread-safe).
462
+ # ---------------------------------------------------------------------------
463
+
464
+ _manager_lock = threading.Lock()
465
+ _manager_cache: Dict[str, "MemoryManager"] = {}
466
+
467
+
468
+ def get_manager(memory_dir: Optional[str] = None) -> MemoryManager:
469
+ key = os.path.normpath(memory_dir or os.path.join(
470
+ os.path.expanduser("~"), ".cct_memory_v2"))
471
+ mgr = _manager_cache.get(key)
472
+ if mgr is not None:
473
+ return mgr
474
+ with _manager_lock:
475
+ mgr = _manager_cache.get(key)
476
+ if mgr is None:
477
+ mgr = MemoryManager(memory_dir=memory_dir)
478
+ _manager_cache[key] = mgr
479
+ return mgr