superqode 0.1.5__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 (288) hide show
  1. superqode/__init__.py +33 -0
  2. superqode/acp/__init__.py +23 -0
  3. superqode/acp/client.py +913 -0
  4. superqode/acp/permission_screen.py +457 -0
  5. superqode/acp/types.py +480 -0
  6. superqode/acp_discovery.py +856 -0
  7. superqode/agent/__init__.py +22 -0
  8. superqode/agent/edit_strategies.py +334 -0
  9. superqode/agent/loop.py +892 -0
  10. superqode/agent/qe_report_templates.py +39 -0
  11. superqode/agent/system_prompts.py +353 -0
  12. superqode/agent_output.py +721 -0
  13. superqode/agent_stream.py +953 -0
  14. superqode/agents/__init__.py +59 -0
  15. superqode/agents/acp_registry.py +305 -0
  16. superqode/agents/client.py +249 -0
  17. superqode/agents/data/augmentcode.com.toml +51 -0
  18. superqode/agents/data/cagent.dev.toml +51 -0
  19. superqode/agents/data/claude.com.toml +60 -0
  20. superqode/agents/data/codeassistant.dev.toml +51 -0
  21. superqode/agents/data/codex.openai.com.toml +57 -0
  22. superqode/agents/data/fastagent.ai.toml +66 -0
  23. superqode/agents/data/geminicli.com.toml +77 -0
  24. superqode/agents/data/goose.block.xyz.toml +54 -0
  25. superqode/agents/data/junie.jetbrains.com.toml +56 -0
  26. superqode/agents/data/kimi.moonshot.cn.toml +57 -0
  27. superqode/agents/data/llmlingagent.dev.toml +51 -0
  28. superqode/agents/data/molt.bot.toml +49 -0
  29. superqode/agents/data/opencode.ai.toml +60 -0
  30. superqode/agents/data/stakpak.dev.toml +51 -0
  31. superqode/agents/data/vtcode.dev.toml +51 -0
  32. superqode/agents/discovery.py +266 -0
  33. superqode/agents/messaging.py +160 -0
  34. superqode/agents/persona.py +166 -0
  35. superqode/agents/registry.py +421 -0
  36. superqode/agents/schema.py +72 -0
  37. superqode/agents/unified.py +367 -0
  38. superqode/app/__init__.py +111 -0
  39. superqode/app/constants.py +314 -0
  40. superqode/app/css.py +366 -0
  41. superqode/app/models.py +118 -0
  42. superqode/app/suggester.py +125 -0
  43. superqode/app/widgets.py +1591 -0
  44. superqode/app_enhanced.py +399 -0
  45. superqode/app_main.py +17187 -0
  46. superqode/approval.py +312 -0
  47. superqode/atomic.py +296 -0
  48. superqode/commands/__init__.py +1 -0
  49. superqode/commands/acp.py +965 -0
  50. superqode/commands/agents.py +180 -0
  51. superqode/commands/auth.py +278 -0
  52. superqode/commands/config.py +374 -0
  53. superqode/commands/init.py +826 -0
  54. superqode/commands/providers.py +819 -0
  55. superqode/commands/qe.py +1145 -0
  56. superqode/commands/roles.py +380 -0
  57. superqode/commands/serve.py +172 -0
  58. superqode/commands/suggestions.py +127 -0
  59. superqode/commands/superqe.py +460 -0
  60. superqode/config/__init__.py +51 -0
  61. superqode/config/loader.py +812 -0
  62. superqode/config/schema.py +498 -0
  63. superqode/core/__init__.py +111 -0
  64. superqode/core/roles.py +281 -0
  65. superqode/danger.py +386 -0
  66. superqode/data/superqode-template.yaml +1522 -0
  67. superqode/design_system.py +1080 -0
  68. superqode/dialogs/__init__.py +6 -0
  69. superqode/dialogs/base.py +39 -0
  70. superqode/dialogs/model.py +130 -0
  71. superqode/dialogs/provider.py +870 -0
  72. superqode/diff_view.py +919 -0
  73. superqode/enterprise.py +21 -0
  74. superqode/evaluation/__init__.py +25 -0
  75. superqode/evaluation/adapters.py +93 -0
  76. superqode/evaluation/behaviors.py +89 -0
  77. superqode/evaluation/engine.py +209 -0
  78. superqode/evaluation/scenarios.py +96 -0
  79. superqode/execution/__init__.py +36 -0
  80. superqode/execution/linter.py +538 -0
  81. superqode/execution/modes.py +347 -0
  82. superqode/execution/resolver.py +283 -0
  83. superqode/execution/runner.py +642 -0
  84. superqode/file_explorer.py +811 -0
  85. superqode/file_viewer.py +471 -0
  86. superqode/flash.py +183 -0
  87. superqode/guidance/__init__.py +58 -0
  88. superqode/guidance/config.py +203 -0
  89. superqode/guidance/prompts.py +71 -0
  90. superqode/harness/__init__.py +54 -0
  91. superqode/harness/accelerator.py +291 -0
  92. superqode/harness/config.py +319 -0
  93. superqode/harness/validator.py +147 -0
  94. superqode/history.py +279 -0
  95. superqode/integrations/superopt_runner.py +124 -0
  96. superqode/logging/__init__.py +49 -0
  97. superqode/logging/adapters.py +219 -0
  98. superqode/logging/formatter.py +923 -0
  99. superqode/logging/integration.py +341 -0
  100. superqode/logging/sinks.py +170 -0
  101. superqode/logging/unified_log.py +417 -0
  102. superqode/lsp/__init__.py +26 -0
  103. superqode/lsp/client.py +544 -0
  104. superqode/main.py +1069 -0
  105. superqode/mcp/__init__.py +89 -0
  106. superqode/mcp/auth_storage.py +380 -0
  107. superqode/mcp/client.py +1236 -0
  108. superqode/mcp/config.py +319 -0
  109. superqode/mcp/integration.py +337 -0
  110. superqode/mcp/oauth.py +436 -0
  111. superqode/mcp/oauth_callback.py +385 -0
  112. superqode/mcp/types.py +290 -0
  113. superqode/memory/__init__.py +31 -0
  114. superqode/memory/feedback.py +342 -0
  115. superqode/memory/store.py +522 -0
  116. superqode/notifications.py +369 -0
  117. superqode/optimization/__init__.py +5 -0
  118. superqode/optimization/config.py +33 -0
  119. superqode/permissions/__init__.py +25 -0
  120. superqode/permissions/rules.py +488 -0
  121. superqode/plan.py +323 -0
  122. superqode/providers/__init__.py +33 -0
  123. superqode/providers/gateway/__init__.py +165 -0
  124. superqode/providers/gateway/base.py +228 -0
  125. superqode/providers/gateway/litellm_gateway.py +1170 -0
  126. superqode/providers/gateway/openresponses_gateway.py +436 -0
  127. superqode/providers/health.py +297 -0
  128. superqode/providers/huggingface/__init__.py +74 -0
  129. superqode/providers/huggingface/downloader.py +472 -0
  130. superqode/providers/huggingface/endpoints.py +442 -0
  131. superqode/providers/huggingface/hub.py +531 -0
  132. superqode/providers/huggingface/inference.py +394 -0
  133. superqode/providers/huggingface/transformers_runner.py +516 -0
  134. superqode/providers/local/__init__.py +100 -0
  135. superqode/providers/local/base.py +438 -0
  136. superqode/providers/local/discovery.py +418 -0
  137. superqode/providers/local/lmstudio.py +256 -0
  138. superqode/providers/local/mlx.py +457 -0
  139. superqode/providers/local/ollama.py +486 -0
  140. superqode/providers/local/sglang.py +268 -0
  141. superqode/providers/local/tgi.py +260 -0
  142. superqode/providers/local/tool_support.py +477 -0
  143. superqode/providers/local/vllm.py +258 -0
  144. superqode/providers/manager.py +1338 -0
  145. superqode/providers/models.py +1016 -0
  146. superqode/providers/models_dev.py +578 -0
  147. superqode/providers/openresponses/__init__.py +87 -0
  148. superqode/providers/openresponses/converters/__init__.py +17 -0
  149. superqode/providers/openresponses/converters/messages.py +343 -0
  150. superqode/providers/openresponses/converters/tools.py +268 -0
  151. superqode/providers/openresponses/schema/__init__.py +56 -0
  152. superqode/providers/openresponses/schema/models.py +585 -0
  153. superqode/providers/openresponses/streaming/__init__.py +5 -0
  154. superqode/providers/openresponses/streaming/parser.py +338 -0
  155. superqode/providers/openresponses/tools/__init__.py +21 -0
  156. superqode/providers/openresponses/tools/apply_patch.py +352 -0
  157. superqode/providers/openresponses/tools/code_interpreter.py +290 -0
  158. superqode/providers/openresponses/tools/file_search.py +333 -0
  159. superqode/providers/openresponses/tools/mcp_adapter.py +252 -0
  160. superqode/providers/registry.py +716 -0
  161. superqode/providers/usage.py +332 -0
  162. superqode/pure_mode.py +384 -0
  163. superqode/qr/__init__.py +23 -0
  164. superqode/qr/dashboard.py +781 -0
  165. superqode/qr/generator.py +1018 -0
  166. superqode/qr/templates.py +135 -0
  167. superqode/safety/__init__.py +41 -0
  168. superqode/safety/sandbox.py +413 -0
  169. superqode/safety/warnings.py +256 -0
  170. superqode/server/__init__.py +33 -0
  171. superqode/server/lsp_server.py +775 -0
  172. superqode/server/web.py +250 -0
  173. superqode/session/__init__.py +25 -0
  174. superqode/session/persistence.py +580 -0
  175. superqode/session/sharing.py +477 -0
  176. superqode/session.py +475 -0
  177. superqode/sidebar.py +2991 -0
  178. superqode/stream_view.py +648 -0
  179. superqode/styles/__init__.py +3 -0
  180. superqode/superqe/__init__.py +184 -0
  181. superqode/superqe/acp_runner.py +1064 -0
  182. superqode/superqe/constitution/__init__.py +62 -0
  183. superqode/superqe/constitution/evaluator.py +308 -0
  184. superqode/superqe/constitution/loader.py +432 -0
  185. superqode/superqe/constitution/schema.py +250 -0
  186. superqode/superqe/events.py +591 -0
  187. superqode/superqe/frameworks/__init__.py +65 -0
  188. superqode/superqe/frameworks/base.py +234 -0
  189. superqode/superqe/frameworks/e2e.py +263 -0
  190. superqode/superqe/frameworks/executor.py +237 -0
  191. superqode/superqe/frameworks/javascript.py +409 -0
  192. superqode/superqe/frameworks/python.py +373 -0
  193. superqode/superqe/frameworks/registry.py +92 -0
  194. superqode/superqe/mcp_tools/__init__.py +47 -0
  195. superqode/superqe/mcp_tools/core_tools.py +418 -0
  196. superqode/superqe/mcp_tools/registry.py +230 -0
  197. superqode/superqe/mcp_tools/testing_tools.py +167 -0
  198. superqode/superqe/noise.py +89 -0
  199. superqode/superqe/orchestrator.py +778 -0
  200. superqode/superqe/roles.py +609 -0
  201. superqode/superqe/session.py +713 -0
  202. superqode/superqe/skills/__init__.py +57 -0
  203. superqode/superqe/skills/base.py +106 -0
  204. superqode/superqe/skills/core_skills.py +899 -0
  205. superqode/superqe/skills/registry.py +90 -0
  206. superqode/superqe/verifier.py +101 -0
  207. superqode/superqe_cli.py +76 -0
  208. superqode/tool_call.py +358 -0
  209. superqode/tools/__init__.py +93 -0
  210. superqode/tools/agent_tools.py +496 -0
  211. superqode/tools/base.py +324 -0
  212. superqode/tools/batch_tool.py +133 -0
  213. superqode/tools/diagnostics.py +311 -0
  214. superqode/tools/edit_tools.py +653 -0
  215. superqode/tools/enhanced_base.py +515 -0
  216. superqode/tools/file_tools.py +269 -0
  217. superqode/tools/file_tracking.py +45 -0
  218. superqode/tools/lsp_tools.py +610 -0
  219. superqode/tools/network_tools.py +350 -0
  220. superqode/tools/permissions.py +400 -0
  221. superqode/tools/question_tool.py +324 -0
  222. superqode/tools/search_tools.py +598 -0
  223. superqode/tools/shell_tools.py +259 -0
  224. superqode/tools/todo_tools.py +121 -0
  225. superqode/tools/validation.py +80 -0
  226. superqode/tools/web_tools.py +639 -0
  227. superqode/tui.py +1152 -0
  228. superqode/tui_integration.py +875 -0
  229. superqode/tui_widgets/__init__.py +27 -0
  230. superqode/tui_widgets/widgets/__init__.py +18 -0
  231. superqode/tui_widgets/widgets/progress.py +185 -0
  232. superqode/tui_widgets/widgets/tool_display.py +188 -0
  233. superqode/undo_manager.py +574 -0
  234. superqode/utils/__init__.py +5 -0
  235. superqode/utils/error_handling.py +323 -0
  236. superqode/utils/fuzzy.py +257 -0
  237. superqode/widgets/__init__.py +477 -0
  238. superqode/widgets/agent_collab.py +390 -0
  239. superqode/widgets/agent_store.py +936 -0
  240. superqode/widgets/agent_switcher.py +395 -0
  241. superqode/widgets/animation_manager.py +284 -0
  242. superqode/widgets/code_context.py +356 -0
  243. superqode/widgets/command_palette.py +412 -0
  244. superqode/widgets/connection_status.py +537 -0
  245. superqode/widgets/conversation_history.py +470 -0
  246. superqode/widgets/diff_indicator.py +155 -0
  247. superqode/widgets/enhanced_status_bar.py +385 -0
  248. superqode/widgets/enhanced_toast.py +476 -0
  249. superqode/widgets/file_browser.py +809 -0
  250. superqode/widgets/file_reference.py +585 -0
  251. superqode/widgets/issue_timeline.py +340 -0
  252. superqode/widgets/leader_key.py +264 -0
  253. superqode/widgets/mode_switcher.py +445 -0
  254. superqode/widgets/model_picker.py +234 -0
  255. superqode/widgets/permission_preview.py +1205 -0
  256. superqode/widgets/prompt.py +358 -0
  257. superqode/widgets/provider_connect.py +725 -0
  258. superqode/widgets/pty_shell.py +587 -0
  259. superqode/widgets/qe_dashboard.py +321 -0
  260. superqode/widgets/resizable_sidebar.py +377 -0
  261. superqode/widgets/response_changes.py +218 -0
  262. superqode/widgets/response_display.py +528 -0
  263. superqode/widgets/rich_tool_display.py +613 -0
  264. superqode/widgets/sidebar_panels.py +1180 -0
  265. superqode/widgets/slash_complete.py +356 -0
  266. superqode/widgets/split_view.py +612 -0
  267. superqode/widgets/status_bar.py +273 -0
  268. superqode/widgets/superqode_display.py +786 -0
  269. superqode/widgets/thinking_display.py +815 -0
  270. superqode/widgets/throbber.py +87 -0
  271. superqode/widgets/toast.py +206 -0
  272. superqode/widgets/unified_output.py +1073 -0
  273. superqode/workspace/__init__.py +75 -0
  274. superqode/workspace/artifacts.py +472 -0
  275. superqode/workspace/coordinator.py +353 -0
  276. superqode/workspace/diff_tracker.py +429 -0
  277. superqode/workspace/git_guard.py +373 -0
  278. superqode/workspace/git_snapshot.py +526 -0
  279. superqode/workspace/manager.py +750 -0
  280. superqode/workspace/snapshot.py +357 -0
  281. superqode/workspace/watcher.py +535 -0
  282. superqode/workspace/worktree.py +440 -0
  283. superqode-0.1.5.dist-info/METADATA +204 -0
  284. superqode-0.1.5.dist-info/RECORD +288 -0
  285. superqode-0.1.5.dist-info/WHEEL +5 -0
  286. superqode-0.1.5.dist-info/entry_points.txt +3 -0
  287. superqode-0.1.5.dist-info/licenses/LICENSE +648 -0
  288. superqode-0.1.5.dist-info/top_level.txt +1 -0
@@ -0,0 +1,580 @@
1
+ """
2
+ Session Persistence - Save and Restore Conversations.
3
+
4
+ Provides comprehensive session persistence including:
5
+ - Conversation history (messages, tool calls)
6
+ - Session state (files, tasks, quality issues)
7
+ - Session forking and sharing
8
+ - Export to various formats
9
+ - Tailored for SuperQode's multi-agent QE workflow
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import gzip
15
+ import hashlib
16
+ import json
17
+ import os
18
+ import shutil
19
+ from dataclasses import dataclass, field
20
+ from datetime import datetime
21
+ from enum import Enum
22
+ from pathlib import Path
23
+ from typing import Any, Dict, List, Optional
24
+ import time
25
+
26
+
27
+ class MessageRole(Enum):
28
+ """Role of message sender."""
29
+
30
+ USER = "user"
31
+ ASSISTANT = "assistant"
32
+ SYSTEM = "system"
33
+ TOOL = "tool"
34
+
35
+
36
+ @dataclass
37
+ class Message:
38
+ """A single message in the conversation."""
39
+
40
+ id: str
41
+ role: MessageRole
42
+ content: str
43
+ timestamp: datetime = field(default_factory=datetime.now)
44
+ agent_name: Optional[str] = None
45
+ tool_calls: Optional[List[Dict]] = None
46
+ tool_call_id: Optional[str] = None
47
+ metadata: Dict[str, Any] = field(default_factory=dict)
48
+
49
+ def to_dict(self) -> dict:
50
+ return {
51
+ "id": self.id,
52
+ "role": self.role.value,
53
+ "content": self.content,
54
+ "timestamp": self.timestamp.isoformat(),
55
+ "agent_name": self.agent_name,
56
+ "tool_calls": self.tool_calls,
57
+ "tool_call_id": self.tool_call_id,
58
+ "metadata": self.metadata,
59
+ }
60
+
61
+ @classmethod
62
+ def from_dict(cls, data: dict) -> "Message":
63
+ return cls(
64
+ id=data["id"],
65
+ role=MessageRole(data["role"]),
66
+ content=data["content"],
67
+ timestamp=datetime.fromisoformat(data["timestamp"]),
68
+ agent_name=data.get("agent_name"),
69
+ tool_calls=data.get("tool_calls"),
70
+ tool_call_id=data.get("tool_call_id"),
71
+ metadata=data.get("metadata", {}),
72
+ )
73
+
74
+
75
+ @dataclass
76
+ class ToolExecution:
77
+ """Record of a tool execution."""
78
+
79
+ id: str
80
+ tool_name: str
81
+ arguments: Dict[str, Any]
82
+ result: str
83
+ success: bool
84
+ timestamp: datetime = field(default_factory=datetime.now)
85
+ duration_ms: int = 0
86
+ agent_name: Optional[str] = None
87
+
88
+ def to_dict(self) -> dict:
89
+ return {
90
+ "id": self.id,
91
+ "tool_name": self.tool_name,
92
+ "arguments": self.arguments,
93
+ "result": self.result,
94
+ "success": self.success,
95
+ "timestamp": self.timestamp.isoformat(),
96
+ "duration_ms": self.duration_ms,
97
+ "agent_name": self.agent_name,
98
+ }
99
+
100
+ @classmethod
101
+ def from_dict(cls, data: dict) -> "ToolExecution":
102
+ return cls(
103
+ id=data["id"],
104
+ tool_name=data["tool_name"],
105
+ arguments=data["arguments"],
106
+ result=data["result"],
107
+ success=data["success"],
108
+ timestamp=datetime.fromisoformat(data["timestamp"]),
109
+ duration_ms=data.get("duration_ms", 0),
110
+ agent_name=data.get("agent_name"),
111
+ )
112
+
113
+
114
+ @dataclass
115
+ class SessionSnapshot:
116
+ """Snapshot of session state at a point in time."""
117
+
118
+ id: str
119
+ name: str
120
+ message_count: int
121
+ created_at: datetime = field(default_factory=datetime.now)
122
+ description: str = ""
123
+
124
+
125
+ @dataclass
126
+ class Session:
127
+ """A complete conversation session."""
128
+
129
+ id: str
130
+ title: str
131
+ created_at: datetime = field(default_factory=datetime.now)
132
+ updated_at: datetime = field(default_factory=datetime.now)
133
+ project_path: str = ""
134
+
135
+ # Conversation
136
+ messages: List[Message] = field(default_factory=list)
137
+ tool_executions: List[ToolExecution] = field(default_factory=list)
138
+
139
+ # State
140
+ files_modified: List[str] = field(default_factory=list)
141
+ files_created: List[str] = field(default_factory=list)
142
+
143
+ # Metadata
144
+ agents_used: List[str] = field(default_factory=list)
145
+ tags: List[str] = field(default_factory=list)
146
+ parent_session_id: Optional[str] = None # For forked sessions
147
+
148
+ # Snapshots for undo/redo
149
+ snapshots: List[SessionSnapshot] = field(default_factory=list)
150
+
151
+ # Additional data
152
+ metadata: Dict[str, Any] = field(default_factory=dict)
153
+
154
+ def add_message(
155
+ self,
156
+ role: MessageRole,
157
+ content: str,
158
+ agent_name: Optional[str] = None,
159
+ tool_calls: Optional[List[Dict]] = None,
160
+ tool_call_id: Optional[str] = None,
161
+ ) -> Message:
162
+ """Add a message to the session."""
163
+ msg_id = f"msg-{len(self.messages) + 1}-{int(time.time() * 1000) % 10000}"
164
+
165
+ message = Message(
166
+ id=msg_id,
167
+ role=role,
168
+ content=content,
169
+ agent_name=agent_name,
170
+ tool_calls=tool_calls,
171
+ tool_call_id=tool_call_id,
172
+ )
173
+
174
+ self.messages.append(message)
175
+ self.updated_at = datetime.now()
176
+
177
+ if agent_name and agent_name not in self.agents_used:
178
+ self.agents_used.append(agent_name)
179
+
180
+ return message
181
+
182
+ def add_tool_execution(
183
+ self,
184
+ tool_name: str,
185
+ arguments: Dict[str, Any],
186
+ result: str,
187
+ success: bool,
188
+ duration_ms: int = 0,
189
+ agent_name: Optional[str] = None,
190
+ ) -> ToolExecution:
191
+ """Record a tool execution."""
192
+ exec_id = f"tool-{len(self.tool_executions) + 1}-{int(time.time() * 1000) % 10000}"
193
+
194
+ execution = ToolExecution(
195
+ id=exec_id,
196
+ tool_name=tool_name,
197
+ arguments=arguments,
198
+ result=result,
199
+ success=success,
200
+ duration_ms=duration_ms,
201
+ agent_name=agent_name,
202
+ )
203
+
204
+ self.tool_executions.append(execution)
205
+ self.updated_at = datetime.now()
206
+
207
+ return execution
208
+
209
+ def create_snapshot(self, name: str, description: str = "") -> SessionSnapshot:
210
+ """Create a snapshot of current state."""
211
+ snap_id = f"snap-{len(self.snapshots) + 1}-{int(time.time())}"
212
+
213
+ snapshot = SessionSnapshot(
214
+ id=snap_id,
215
+ name=name,
216
+ message_count=len(self.messages),
217
+ description=description,
218
+ )
219
+
220
+ self.snapshots.append(snapshot)
221
+ return snapshot
222
+
223
+ def revert_to_snapshot(self, snapshot_id: str) -> bool:
224
+ """Revert session to a snapshot."""
225
+ for snapshot in self.snapshots:
226
+ if snapshot.id == snapshot_id:
227
+ # Truncate messages and tool executions
228
+ self.messages = self.messages[: snapshot.message_count]
229
+
230
+ # Find corresponding tool executions
231
+ if self.messages:
232
+ last_msg_time = self.messages[-1].timestamp
233
+ self.tool_executions = [
234
+ t for t in self.tool_executions if t.timestamp <= last_msg_time
235
+ ]
236
+ else:
237
+ self.tool_executions = []
238
+
239
+ self.updated_at = datetime.now()
240
+ return True
241
+
242
+ return False
243
+
244
+ def fork(self, new_title: str) -> "Session":
245
+ """Create a forked copy of this session."""
246
+ fork_id = f"session-{int(time.time())}-fork"
247
+
248
+ forked = Session(
249
+ id=fork_id,
250
+ title=new_title,
251
+ project_path=self.project_path,
252
+ messages=list(self.messages),
253
+ tool_executions=list(self.tool_executions),
254
+ files_modified=list(self.files_modified),
255
+ files_created=list(self.files_created),
256
+ agents_used=list(self.agents_used),
257
+ tags=list(self.tags),
258
+ parent_session_id=self.id,
259
+ metadata=dict(self.metadata),
260
+ )
261
+
262
+ return forked
263
+
264
+ def to_dict(self) -> dict:
265
+ return {
266
+ "id": self.id,
267
+ "title": self.title,
268
+ "created_at": self.created_at.isoformat(),
269
+ "updated_at": self.updated_at.isoformat(),
270
+ "project_path": self.project_path,
271
+ "messages": [m.to_dict() for m in self.messages],
272
+ "tool_executions": [t.to_dict() for t in self.tool_executions],
273
+ "files_modified": self.files_modified,
274
+ "files_created": self.files_created,
275
+ "agents_used": self.agents_used,
276
+ "tags": self.tags,
277
+ "parent_session_id": self.parent_session_id,
278
+ "snapshots": [
279
+ {
280
+ "id": s.id,
281
+ "name": s.name,
282
+ "message_count": s.message_count,
283
+ "created_at": s.created_at.isoformat(),
284
+ "description": s.description,
285
+ }
286
+ for s in self.snapshots
287
+ ],
288
+ "metadata": self.metadata,
289
+ }
290
+
291
+ @classmethod
292
+ def from_dict(cls, data: dict) -> "Session":
293
+ session = cls(
294
+ id=data["id"],
295
+ title=data["title"],
296
+ created_at=datetime.fromisoformat(data["created_at"]),
297
+ updated_at=datetime.fromisoformat(data["updated_at"]),
298
+ project_path=data.get("project_path", ""),
299
+ )
300
+
301
+ session.messages = [Message.from_dict(m) for m in data.get("messages", [])]
302
+ session.tool_executions = [
303
+ ToolExecution.from_dict(t) for t in data.get("tool_executions", [])
304
+ ]
305
+ session.files_modified = data.get("files_modified", [])
306
+ session.files_created = data.get("files_created", [])
307
+ session.agents_used = data.get("agents_used", [])
308
+ session.tags = data.get("tags", [])
309
+ session.parent_session_id = data.get("parent_session_id")
310
+ session.metadata = data.get("metadata", {})
311
+
312
+ for snap_data in data.get("snapshots", []):
313
+ session.snapshots.append(
314
+ SessionSnapshot(
315
+ id=snap_data["id"],
316
+ name=snap_data["name"],
317
+ message_count=snap_data["message_count"],
318
+ created_at=datetime.fromisoformat(snap_data["created_at"]),
319
+ description=snap_data.get("description", ""),
320
+ )
321
+ )
322
+
323
+ return session
324
+
325
+
326
+ class SessionStore:
327
+ """
328
+ Persistent storage for sessions.
329
+
330
+ Stores sessions as compressed JSON files with indexing for fast listing.
331
+
332
+ Usage:
333
+ store = SessionStore()
334
+
335
+ # Create and save a session
336
+ session = Session(id="session-1", title="Fix bug")
337
+ session.add_message(MessageRole.USER, "Fix the null reference bug")
338
+ store.save(session)
339
+
340
+ # List sessions
341
+ sessions = store.list_sessions()
342
+
343
+ # Load a session
344
+ session = store.load("session-1")
345
+
346
+ # Resume last session
347
+ session = store.load_latest()
348
+ """
349
+
350
+ def __init__(self, storage_dir: Optional[Path] = None):
351
+ self.storage_dir = storage_dir or (Path.home() / ".superqode" / "sessions")
352
+ self.storage_dir.mkdir(parents=True, exist_ok=True)
353
+
354
+ self._index_file = self.storage_dir / "index.json"
355
+ self._index: Dict[str, dict] = {}
356
+
357
+ self._load_index()
358
+
359
+ def _load_index(self) -> None:
360
+ """Load session index from file."""
361
+ if self._index_file.exists():
362
+ try:
363
+ self._index = json.loads(self._index_file.read_text())
364
+ except json.JSONDecodeError:
365
+ self._index = {}
366
+
367
+ def _save_index(self) -> None:
368
+ """Save session index to file."""
369
+ self._index_file.write_text(json.dumps(self._index, indent=2))
370
+
371
+ def _session_path(self, session_id: str) -> Path:
372
+ """Get path for a session file."""
373
+ return self.storage_dir / f"{session_id}.json.gz"
374
+
375
+ def save(self, session: Session, compress: bool = True) -> None:
376
+ """Save a session to storage."""
377
+ data = json.dumps(session.to_dict(), indent=2)
378
+
379
+ file_path = self._session_path(session.id)
380
+
381
+ if compress:
382
+ with gzip.open(file_path, "wt", encoding="utf-8") as f:
383
+ f.write(data)
384
+ else:
385
+ file_path = file_path.with_suffix("") # Remove .gz
386
+ file_path.write_text(data)
387
+
388
+ # Update index
389
+ self._index[session.id] = {
390
+ "id": session.id,
391
+ "title": session.title,
392
+ "created_at": session.created_at.isoformat(),
393
+ "updated_at": session.updated_at.isoformat(),
394
+ "project_path": session.project_path,
395
+ "message_count": len(session.messages),
396
+ "agents_used": session.agents_used,
397
+ "tags": session.tags,
398
+ }
399
+ self._save_index()
400
+
401
+ def load(self, session_id: str) -> Optional[Session]:
402
+ """Load a session from storage."""
403
+ file_path = self._session_path(session_id)
404
+
405
+ if not file_path.exists():
406
+ # Try uncompressed
407
+ file_path = file_path.with_suffix("")
408
+ if not file_path.exists():
409
+ return None
410
+
411
+ try:
412
+ if file_path.suffix == ".gz":
413
+ with gzip.open(file_path, "rt", encoding="utf-8") as f:
414
+ data = json.load(f)
415
+ else:
416
+ data = json.loads(file_path.read_text())
417
+
418
+ return Session.from_dict(data)
419
+ except (json.JSONDecodeError, IOError):
420
+ return None
421
+
422
+ def delete(self, session_id: str) -> bool:
423
+ """Delete a session from storage."""
424
+ file_path = self._session_path(session_id)
425
+
426
+ if file_path.exists():
427
+ file_path.unlink()
428
+ elif file_path.with_suffix("").exists():
429
+ file_path.with_suffix("").unlink()
430
+ else:
431
+ return False
432
+
433
+ self._index.pop(session_id, None)
434
+ self._save_index()
435
+ return True
436
+
437
+ def list_sessions(
438
+ self,
439
+ project_path: Optional[str] = None,
440
+ tags: Optional[List[str]] = None,
441
+ limit: int = 50,
442
+ ) -> List[dict]:
443
+ """List sessions with optional filtering."""
444
+ sessions = list(self._index.values())
445
+
446
+ # Filter by project
447
+ if project_path:
448
+ sessions = [s for s in sessions if s.get("project_path") == project_path]
449
+
450
+ # Filter by tags
451
+ if tags:
452
+ sessions = [s for s in sessions if set(tags).issubset(set(s.get("tags", [])))]
453
+
454
+ # Sort by updated_at descending
455
+ sessions.sort(key=lambda s: s.get("updated_at", ""), reverse=True)
456
+
457
+ return sessions[:limit]
458
+
459
+ def load_latest(self, project_path: Optional[str] = None) -> Optional[Session]:
460
+ """Load the most recently updated session."""
461
+ sessions = self.list_sessions(project_path=project_path, limit=1)
462
+
463
+ if sessions:
464
+ return self.load(sessions[0]["id"])
465
+
466
+ return None
467
+
468
+ def search(self, query: str, limit: int = 20) -> List[dict]:
469
+ """Search sessions by title or content."""
470
+ query_lower = query.lower()
471
+ results = []
472
+
473
+ for session_id, info in self._index.items():
474
+ # Search in title
475
+ if query_lower in info.get("title", "").lower():
476
+ results.append(info)
477
+ continue
478
+
479
+ # Search in tags
480
+ if any(query_lower in tag.lower() for tag in info.get("tags", [])):
481
+ results.append(info)
482
+
483
+ # Sort by relevance (title matches first)
484
+ results.sort(
485
+ key=lambda s: (
486
+ 0 if query_lower in s.get("title", "").lower() else 1,
487
+ s.get("updated_at", ""),
488
+ ),
489
+ reverse=True,
490
+ )
491
+
492
+ return results[:limit]
493
+
494
+ def export_session(
495
+ self,
496
+ session_id: str,
497
+ output_path: Path,
498
+ format: str = "json",
499
+ ) -> bool:
500
+ """Export a session to a file.
501
+
502
+ Formats: json, markdown, text
503
+ """
504
+ session = self.load(session_id)
505
+ if not session:
506
+ return False
507
+
508
+ if format == "json":
509
+ output_path.write_text(json.dumps(session.to_dict(), indent=2))
510
+
511
+ elif format == "markdown":
512
+ lines = [
513
+ f"# {session.title}",
514
+ "",
515
+ f"**Created:** {session.created_at.strftime('%Y-%m-%d %H:%M')}",
516
+ f"**Project:** {session.project_path}",
517
+ "",
518
+ "---",
519
+ "",
520
+ ]
521
+
522
+ for msg in session.messages:
523
+ role_emoji = {"user": "👤", "assistant": "🤖", "system": "⚙️", "tool": "🔧"}
524
+ emoji = role_emoji.get(msg.role.value, "💬")
525
+
526
+ lines.append(f"### {emoji} {msg.role.value.title()}")
527
+ if msg.agent_name:
528
+ lines.append(f"*Agent: {msg.agent_name}*")
529
+ lines.append("")
530
+ lines.append(msg.content)
531
+ lines.append("")
532
+
533
+ output_path.write_text("\n".join(lines))
534
+
535
+ elif format == "text":
536
+ lines = [f"Session: {session.title}", "=" * 50, ""]
537
+
538
+ for msg in session.messages:
539
+ lines.append(f"[{msg.role.value.upper()}] {msg.timestamp.strftime('%H:%M:%S')}")
540
+ lines.append(msg.content)
541
+ lines.append("-" * 40)
542
+
543
+ output_path.write_text("\n".join(lines))
544
+
545
+ else:
546
+ return False
547
+
548
+ return True
549
+
550
+ def cleanup_old_sessions(self, days: int = 30) -> int:
551
+ """Delete sessions older than specified days."""
552
+ from datetime import timedelta
553
+
554
+ cutoff = datetime.now() - timedelta(days=days)
555
+ deleted = 0
556
+
557
+ for session_id, info in list(self._index.items()):
558
+ updated = datetime.fromisoformat(info.get("updated_at", ""))
559
+ if updated < cutoff:
560
+ if self.delete(session_id):
561
+ deleted += 1
562
+
563
+ return deleted
564
+
565
+
566
+ def create_session(
567
+ title: str = "",
568
+ project_path: Optional[Path] = None,
569
+ ) -> Session:
570
+ """Create a new session."""
571
+ session_id = f"session-{int(time.time())}"
572
+
573
+ if not title:
574
+ title = f"Session {datetime.now().strftime('%Y-%m-%d %H:%M')}"
575
+
576
+ return Session(
577
+ id=session_id,
578
+ title=title,
579
+ project_path=str(project_path) if project_path else "",
580
+ )