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.
- superqode/__init__.py +33 -0
- superqode/acp/__init__.py +23 -0
- superqode/acp/client.py +913 -0
- superqode/acp/permission_screen.py +457 -0
- superqode/acp/types.py +480 -0
- superqode/acp_discovery.py +856 -0
- superqode/agent/__init__.py +22 -0
- superqode/agent/edit_strategies.py +334 -0
- superqode/agent/loop.py +892 -0
- superqode/agent/qe_report_templates.py +39 -0
- superqode/agent/system_prompts.py +353 -0
- superqode/agent_output.py +721 -0
- superqode/agent_stream.py +953 -0
- superqode/agents/__init__.py +59 -0
- superqode/agents/acp_registry.py +305 -0
- superqode/agents/client.py +249 -0
- superqode/agents/data/augmentcode.com.toml +51 -0
- superqode/agents/data/cagent.dev.toml +51 -0
- superqode/agents/data/claude.com.toml +60 -0
- superqode/agents/data/codeassistant.dev.toml +51 -0
- superqode/agents/data/codex.openai.com.toml +57 -0
- superqode/agents/data/fastagent.ai.toml +66 -0
- superqode/agents/data/geminicli.com.toml +77 -0
- superqode/agents/data/goose.block.xyz.toml +54 -0
- superqode/agents/data/junie.jetbrains.com.toml +56 -0
- superqode/agents/data/kimi.moonshot.cn.toml +57 -0
- superqode/agents/data/llmlingagent.dev.toml +51 -0
- superqode/agents/data/molt.bot.toml +49 -0
- superqode/agents/data/opencode.ai.toml +60 -0
- superqode/agents/data/stakpak.dev.toml +51 -0
- superqode/agents/data/vtcode.dev.toml +51 -0
- superqode/agents/discovery.py +266 -0
- superqode/agents/messaging.py +160 -0
- superqode/agents/persona.py +166 -0
- superqode/agents/registry.py +421 -0
- superqode/agents/schema.py +72 -0
- superqode/agents/unified.py +367 -0
- superqode/app/__init__.py +111 -0
- superqode/app/constants.py +314 -0
- superqode/app/css.py +366 -0
- superqode/app/models.py +118 -0
- superqode/app/suggester.py +125 -0
- superqode/app/widgets.py +1591 -0
- superqode/app_enhanced.py +399 -0
- superqode/app_main.py +17187 -0
- superqode/approval.py +312 -0
- superqode/atomic.py +296 -0
- superqode/commands/__init__.py +1 -0
- superqode/commands/acp.py +965 -0
- superqode/commands/agents.py +180 -0
- superqode/commands/auth.py +278 -0
- superqode/commands/config.py +374 -0
- superqode/commands/init.py +826 -0
- superqode/commands/providers.py +819 -0
- superqode/commands/qe.py +1145 -0
- superqode/commands/roles.py +380 -0
- superqode/commands/serve.py +172 -0
- superqode/commands/suggestions.py +127 -0
- superqode/commands/superqe.py +460 -0
- superqode/config/__init__.py +51 -0
- superqode/config/loader.py +812 -0
- superqode/config/schema.py +498 -0
- superqode/core/__init__.py +111 -0
- superqode/core/roles.py +281 -0
- superqode/danger.py +386 -0
- superqode/data/superqode-template.yaml +1522 -0
- superqode/design_system.py +1080 -0
- superqode/dialogs/__init__.py +6 -0
- superqode/dialogs/base.py +39 -0
- superqode/dialogs/model.py +130 -0
- superqode/dialogs/provider.py +870 -0
- superqode/diff_view.py +919 -0
- superqode/enterprise.py +21 -0
- superqode/evaluation/__init__.py +25 -0
- superqode/evaluation/adapters.py +93 -0
- superqode/evaluation/behaviors.py +89 -0
- superqode/evaluation/engine.py +209 -0
- superqode/evaluation/scenarios.py +96 -0
- superqode/execution/__init__.py +36 -0
- superqode/execution/linter.py +538 -0
- superqode/execution/modes.py +347 -0
- superqode/execution/resolver.py +283 -0
- superqode/execution/runner.py +642 -0
- superqode/file_explorer.py +811 -0
- superqode/file_viewer.py +471 -0
- superqode/flash.py +183 -0
- superqode/guidance/__init__.py +58 -0
- superqode/guidance/config.py +203 -0
- superqode/guidance/prompts.py +71 -0
- superqode/harness/__init__.py +54 -0
- superqode/harness/accelerator.py +291 -0
- superqode/harness/config.py +319 -0
- superqode/harness/validator.py +147 -0
- superqode/history.py +279 -0
- superqode/integrations/superopt_runner.py +124 -0
- superqode/logging/__init__.py +49 -0
- superqode/logging/adapters.py +219 -0
- superqode/logging/formatter.py +923 -0
- superqode/logging/integration.py +341 -0
- superqode/logging/sinks.py +170 -0
- superqode/logging/unified_log.py +417 -0
- superqode/lsp/__init__.py +26 -0
- superqode/lsp/client.py +544 -0
- superqode/main.py +1069 -0
- superqode/mcp/__init__.py +89 -0
- superqode/mcp/auth_storage.py +380 -0
- superqode/mcp/client.py +1236 -0
- superqode/mcp/config.py +319 -0
- superqode/mcp/integration.py +337 -0
- superqode/mcp/oauth.py +436 -0
- superqode/mcp/oauth_callback.py +385 -0
- superqode/mcp/types.py +290 -0
- superqode/memory/__init__.py +31 -0
- superqode/memory/feedback.py +342 -0
- superqode/memory/store.py +522 -0
- superqode/notifications.py +369 -0
- superqode/optimization/__init__.py +5 -0
- superqode/optimization/config.py +33 -0
- superqode/permissions/__init__.py +25 -0
- superqode/permissions/rules.py +488 -0
- superqode/plan.py +323 -0
- superqode/providers/__init__.py +33 -0
- superqode/providers/gateway/__init__.py +165 -0
- superqode/providers/gateway/base.py +228 -0
- superqode/providers/gateway/litellm_gateway.py +1170 -0
- superqode/providers/gateway/openresponses_gateway.py +436 -0
- superqode/providers/health.py +297 -0
- superqode/providers/huggingface/__init__.py +74 -0
- superqode/providers/huggingface/downloader.py +472 -0
- superqode/providers/huggingface/endpoints.py +442 -0
- superqode/providers/huggingface/hub.py +531 -0
- superqode/providers/huggingface/inference.py +394 -0
- superqode/providers/huggingface/transformers_runner.py +516 -0
- superqode/providers/local/__init__.py +100 -0
- superqode/providers/local/base.py +438 -0
- superqode/providers/local/discovery.py +418 -0
- superqode/providers/local/lmstudio.py +256 -0
- superqode/providers/local/mlx.py +457 -0
- superqode/providers/local/ollama.py +486 -0
- superqode/providers/local/sglang.py +268 -0
- superqode/providers/local/tgi.py +260 -0
- superqode/providers/local/tool_support.py +477 -0
- superqode/providers/local/vllm.py +258 -0
- superqode/providers/manager.py +1338 -0
- superqode/providers/models.py +1016 -0
- superqode/providers/models_dev.py +578 -0
- superqode/providers/openresponses/__init__.py +87 -0
- superqode/providers/openresponses/converters/__init__.py +17 -0
- superqode/providers/openresponses/converters/messages.py +343 -0
- superqode/providers/openresponses/converters/tools.py +268 -0
- superqode/providers/openresponses/schema/__init__.py +56 -0
- superqode/providers/openresponses/schema/models.py +585 -0
- superqode/providers/openresponses/streaming/__init__.py +5 -0
- superqode/providers/openresponses/streaming/parser.py +338 -0
- superqode/providers/openresponses/tools/__init__.py +21 -0
- superqode/providers/openresponses/tools/apply_patch.py +352 -0
- superqode/providers/openresponses/tools/code_interpreter.py +290 -0
- superqode/providers/openresponses/tools/file_search.py +333 -0
- superqode/providers/openresponses/tools/mcp_adapter.py +252 -0
- superqode/providers/registry.py +716 -0
- superqode/providers/usage.py +332 -0
- superqode/pure_mode.py +384 -0
- superqode/qr/__init__.py +23 -0
- superqode/qr/dashboard.py +781 -0
- superqode/qr/generator.py +1018 -0
- superqode/qr/templates.py +135 -0
- superqode/safety/__init__.py +41 -0
- superqode/safety/sandbox.py +413 -0
- superqode/safety/warnings.py +256 -0
- superqode/server/__init__.py +33 -0
- superqode/server/lsp_server.py +775 -0
- superqode/server/web.py +250 -0
- superqode/session/__init__.py +25 -0
- superqode/session/persistence.py +580 -0
- superqode/session/sharing.py +477 -0
- superqode/session.py +475 -0
- superqode/sidebar.py +2991 -0
- superqode/stream_view.py +648 -0
- superqode/styles/__init__.py +3 -0
- superqode/superqe/__init__.py +184 -0
- superqode/superqe/acp_runner.py +1064 -0
- superqode/superqe/constitution/__init__.py +62 -0
- superqode/superqe/constitution/evaluator.py +308 -0
- superqode/superqe/constitution/loader.py +432 -0
- superqode/superqe/constitution/schema.py +250 -0
- superqode/superqe/events.py +591 -0
- superqode/superqe/frameworks/__init__.py +65 -0
- superqode/superqe/frameworks/base.py +234 -0
- superqode/superqe/frameworks/e2e.py +263 -0
- superqode/superqe/frameworks/executor.py +237 -0
- superqode/superqe/frameworks/javascript.py +409 -0
- superqode/superqe/frameworks/python.py +373 -0
- superqode/superqe/frameworks/registry.py +92 -0
- superqode/superqe/mcp_tools/__init__.py +47 -0
- superqode/superqe/mcp_tools/core_tools.py +418 -0
- superqode/superqe/mcp_tools/registry.py +230 -0
- superqode/superqe/mcp_tools/testing_tools.py +167 -0
- superqode/superqe/noise.py +89 -0
- superqode/superqe/orchestrator.py +778 -0
- superqode/superqe/roles.py +609 -0
- superqode/superqe/session.py +713 -0
- superqode/superqe/skills/__init__.py +57 -0
- superqode/superqe/skills/base.py +106 -0
- superqode/superqe/skills/core_skills.py +899 -0
- superqode/superqe/skills/registry.py +90 -0
- superqode/superqe/verifier.py +101 -0
- superqode/superqe_cli.py +76 -0
- superqode/tool_call.py +358 -0
- superqode/tools/__init__.py +93 -0
- superqode/tools/agent_tools.py +496 -0
- superqode/tools/base.py +324 -0
- superqode/tools/batch_tool.py +133 -0
- superqode/tools/diagnostics.py +311 -0
- superqode/tools/edit_tools.py +653 -0
- superqode/tools/enhanced_base.py +515 -0
- superqode/tools/file_tools.py +269 -0
- superqode/tools/file_tracking.py +45 -0
- superqode/tools/lsp_tools.py +610 -0
- superqode/tools/network_tools.py +350 -0
- superqode/tools/permissions.py +400 -0
- superqode/tools/question_tool.py +324 -0
- superqode/tools/search_tools.py +598 -0
- superqode/tools/shell_tools.py +259 -0
- superqode/tools/todo_tools.py +121 -0
- superqode/tools/validation.py +80 -0
- superqode/tools/web_tools.py +639 -0
- superqode/tui.py +1152 -0
- superqode/tui_integration.py +875 -0
- superqode/tui_widgets/__init__.py +27 -0
- superqode/tui_widgets/widgets/__init__.py +18 -0
- superqode/tui_widgets/widgets/progress.py +185 -0
- superqode/tui_widgets/widgets/tool_display.py +188 -0
- superqode/undo_manager.py +574 -0
- superqode/utils/__init__.py +5 -0
- superqode/utils/error_handling.py +323 -0
- superqode/utils/fuzzy.py +257 -0
- superqode/widgets/__init__.py +477 -0
- superqode/widgets/agent_collab.py +390 -0
- superqode/widgets/agent_store.py +936 -0
- superqode/widgets/agent_switcher.py +395 -0
- superqode/widgets/animation_manager.py +284 -0
- superqode/widgets/code_context.py +356 -0
- superqode/widgets/command_palette.py +412 -0
- superqode/widgets/connection_status.py +537 -0
- superqode/widgets/conversation_history.py +470 -0
- superqode/widgets/diff_indicator.py +155 -0
- superqode/widgets/enhanced_status_bar.py +385 -0
- superqode/widgets/enhanced_toast.py +476 -0
- superqode/widgets/file_browser.py +809 -0
- superqode/widgets/file_reference.py +585 -0
- superqode/widgets/issue_timeline.py +340 -0
- superqode/widgets/leader_key.py +264 -0
- superqode/widgets/mode_switcher.py +445 -0
- superqode/widgets/model_picker.py +234 -0
- superqode/widgets/permission_preview.py +1205 -0
- superqode/widgets/prompt.py +358 -0
- superqode/widgets/provider_connect.py +725 -0
- superqode/widgets/pty_shell.py +587 -0
- superqode/widgets/qe_dashboard.py +321 -0
- superqode/widgets/resizable_sidebar.py +377 -0
- superqode/widgets/response_changes.py +218 -0
- superqode/widgets/response_display.py +528 -0
- superqode/widgets/rich_tool_display.py +613 -0
- superqode/widgets/sidebar_panels.py +1180 -0
- superqode/widgets/slash_complete.py +356 -0
- superqode/widgets/split_view.py +612 -0
- superqode/widgets/status_bar.py +273 -0
- superqode/widgets/superqode_display.py +786 -0
- superqode/widgets/thinking_display.py +815 -0
- superqode/widgets/throbber.py +87 -0
- superqode/widgets/toast.py +206 -0
- superqode/widgets/unified_output.py +1073 -0
- superqode/workspace/__init__.py +75 -0
- superqode/workspace/artifacts.py +472 -0
- superqode/workspace/coordinator.py +353 -0
- superqode/workspace/diff_tracker.py +429 -0
- superqode/workspace/git_guard.py +373 -0
- superqode/workspace/git_snapshot.py +526 -0
- superqode/workspace/manager.py +750 -0
- superqode/workspace/snapshot.py +357 -0
- superqode/workspace/watcher.py +535 -0
- superqode/workspace/worktree.py +440 -0
- superqode-0.1.5.dist-info/METADATA +204 -0
- superqode-0.1.5.dist-info/RECORD +288 -0
- superqode-0.1.5.dist-info/WHEEL +5 -0
- superqode-0.1.5.dist-info/entry_points.txt +3 -0
- superqode-0.1.5.dist-info/licenses/LICENSE +648 -0
- superqode-0.1.5.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,477 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Session Sharing - Collaborative Session Features.
|
|
3
|
+
|
|
4
|
+
Enables sharing and forking of sessions between users:
|
|
5
|
+
- Export sessions for sharing
|
|
6
|
+
- Import shared sessions
|
|
7
|
+
- Fork sessions to create branches
|
|
8
|
+
- Session links for collaboration
|
|
9
|
+
- Adapted for SuperQode's multi-agent QE workflow
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import base64
|
|
15
|
+
import gzip
|
|
16
|
+
import hashlib
|
|
17
|
+
import json
|
|
18
|
+
import secrets
|
|
19
|
+
from dataclasses import dataclass, field
|
|
20
|
+
from datetime import datetime, timedelta
|
|
21
|
+
from enum import Enum
|
|
22
|
+
from pathlib import Path
|
|
23
|
+
from typing import Any, Dict, List, Optional
|
|
24
|
+
import urllib.parse
|
|
25
|
+
|
|
26
|
+
from .persistence import Session, SessionStore
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class ShareVisibility(Enum):
|
|
30
|
+
"""Visibility of a shared session."""
|
|
31
|
+
|
|
32
|
+
PRIVATE = "private" # Only accessible with link
|
|
33
|
+
UNLISTED = "unlisted" # Not discoverable, but accessible
|
|
34
|
+
PUBLIC = "public" # Discoverable and accessible
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@dataclass
|
|
38
|
+
class ShareConfig:
|
|
39
|
+
"""Configuration for session sharing."""
|
|
40
|
+
|
|
41
|
+
visibility: ShareVisibility = ShareVisibility.PRIVATE
|
|
42
|
+
expires_in: Optional[timedelta] = None
|
|
43
|
+
allow_fork: bool = True
|
|
44
|
+
allow_view_history: bool = True
|
|
45
|
+
password: Optional[str] = None # Optional password protection
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@dataclass
|
|
49
|
+
class SharedSession:
|
|
50
|
+
"""A shared session with access controls."""
|
|
51
|
+
|
|
52
|
+
id: str
|
|
53
|
+
session_id: str
|
|
54
|
+
share_token: str
|
|
55
|
+
visibility: ShareVisibility
|
|
56
|
+
created_at: datetime = field(default_factory=datetime.now)
|
|
57
|
+
expires_at: Optional[datetime] = None
|
|
58
|
+
access_count: int = 0
|
|
59
|
+
fork_count: int = 0
|
|
60
|
+
allow_fork: bool = True
|
|
61
|
+
allow_view_history: bool = True
|
|
62
|
+
password_hash: Optional[str] = None
|
|
63
|
+
created_by: str = ""
|
|
64
|
+
|
|
65
|
+
@property
|
|
66
|
+
def is_expired(self) -> bool:
|
|
67
|
+
"""Check if share has expired."""
|
|
68
|
+
if self.expires_at is None:
|
|
69
|
+
return False
|
|
70
|
+
return datetime.now() > self.expires_at
|
|
71
|
+
|
|
72
|
+
@property
|
|
73
|
+
def share_url(self) -> str:
|
|
74
|
+
"""Get the share URL."""
|
|
75
|
+
return f"/share/{self.share_token}"
|
|
76
|
+
|
|
77
|
+
def to_dict(self) -> dict:
|
|
78
|
+
return {
|
|
79
|
+
"id": self.id,
|
|
80
|
+
"session_id": self.session_id,
|
|
81
|
+
"share_token": self.share_token,
|
|
82
|
+
"visibility": self.visibility.value,
|
|
83
|
+
"created_at": self.created_at.isoformat(),
|
|
84
|
+
"expires_at": self.expires_at.isoformat() if self.expires_at else None,
|
|
85
|
+
"access_count": self.access_count,
|
|
86
|
+
"fork_count": self.fork_count,
|
|
87
|
+
"allow_fork": self.allow_fork,
|
|
88
|
+
"allow_view_history": self.allow_view_history,
|
|
89
|
+
"password_hash": self.password_hash,
|
|
90
|
+
"created_by": self.created_by,
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
@classmethod
|
|
94
|
+
def from_dict(cls, data: dict) -> "SharedSession":
|
|
95
|
+
return cls(
|
|
96
|
+
id=data["id"],
|
|
97
|
+
session_id=data["session_id"],
|
|
98
|
+
share_token=data["share_token"],
|
|
99
|
+
visibility=ShareVisibility(data["visibility"]),
|
|
100
|
+
created_at=datetime.fromisoformat(data["created_at"]),
|
|
101
|
+
expires_at=datetime.fromisoformat(data["expires_at"])
|
|
102
|
+
if data.get("expires_at")
|
|
103
|
+
else None,
|
|
104
|
+
access_count=data.get("access_count", 0),
|
|
105
|
+
fork_count=data.get("fork_count", 0),
|
|
106
|
+
allow_fork=data.get("allow_fork", True),
|
|
107
|
+
allow_view_history=data.get("allow_view_history", True),
|
|
108
|
+
password_hash=data.get("password_hash"),
|
|
109
|
+
created_by=data.get("created_by", ""),
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
@dataclass
|
|
114
|
+
class ExportedSession:
|
|
115
|
+
"""A session exported for sharing."""
|
|
116
|
+
|
|
117
|
+
session_data: dict
|
|
118
|
+
export_format: str = "superqode-session-v1"
|
|
119
|
+
exported_at: datetime = field(default_factory=datetime.now)
|
|
120
|
+
checksum: str = ""
|
|
121
|
+
|
|
122
|
+
def to_json(self) -> str:
|
|
123
|
+
"""Export to JSON string."""
|
|
124
|
+
data = {
|
|
125
|
+
"format": self.export_format,
|
|
126
|
+
"exported_at": self.exported_at.isoformat(),
|
|
127
|
+
"session": self.session_data,
|
|
128
|
+
"checksum": self.checksum,
|
|
129
|
+
}
|
|
130
|
+
return json.dumps(data, indent=2)
|
|
131
|
+
|
|
132
|
+
def to_compressed(self) -> bytes:
|
|
133
|
+
"""Export to compressed bytes."""
|
|
134
|
+
json_data = self.to_json().encode("utf-8")
|
|
135
|
+
return gzip.compress(json_data)
|
|
136
|
+
|
|
137
|
+
def to_base64(self) -> str:
|
|
138
|
+
"""Export to base64 string for URLs."""
|
|
139
|
+
compressed = self.to_compressed()
|
|
140
|
+
return base64.urlsafe_b64encode(compressed).decode("ascii")
|
|
141
|
+
|
|
142
|
+
@classmethod
|
|
143
|
+
def from_json(cls, json_str: str) -> "ExportedSession":
|
|
144
|
+
"""Import from JSON string."""
|
|
145
|
+
data = json.loads(json_str)
|
|
146
|
+
|
|
147
|
+
if data.get("format") != "superqode-session-v1":
|
|
148
|
+
raise ValueError(f"Unknown export format: {data.get('format')}")
|
|
149
|
+
|
|
150
|
+
return cls(
|
|
151
|
+
session_data=data["session"],
|
|
152
|
+
export_format=data["format"],
|
|
153
|
+
exported_at=datetime.fromisoformat(data["exported_at"]),
|
|
154
|
+
checksum=data.get("checksum", ""),
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
@classmethod
|
|
158
|
+
def from_compressed(cls, data: bytes) -> "ExportedSession":
|
|
159
|
+
"""Import from compressed bytes."""
|
|
160
|
+
json_data = gzip.decompress(data).decode("utf-8")
|
|
161
|
+
return cls.from_json(json_data)
|
|
162
|
+
|
|
163
|
+
@classmethod
|
|
164
|
+
def from_base64(cls, b64_str: str) -> "ExportedSession":
|
|
165
|
+
"""Import from base64 string."""
|
|
166
|
+
compressed = base64.urlsafe_b64decode(b64_str)
|
|
167
|
+
return cls.from_compressed(compressed)
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
class SessionSharingManager:
|
|
171
|
+
"""
|
|
172
|
+
Manages session sharing and forking.
|
|
173
|
+
|
|
174
|
+
Usage:
|
|
175
|
+
store = SessionStore()
|
|
176
|
+
sharing = SessionSharingManager(store)
|
|
177
|
+
|
|
178
|
+
# Share a session
|
|
179
|
+
share = sharing.create_share("session-123", ShareConfig())
|
|
180
|
+
print(f"Share URL: {share.share_url}")
|
|
181
|
+
|
|
182
|
+
# Fork a shared session
|
|
183
|
+
forked = await sharing.fork_session(share.share_token, "My Fork")
|
|
184
|
+
|
|
185
|
+
# Export for offline sharing
|
|
186
|
+
exported = sharing.export_session("session-123")
|
|
187
|
+
with open("session.json", "w") as f:
|
|
188
|
+
f.write(exported.to_json())
|
|
189
|
+
"""
|
|
190
|
+
|
|
191
|
+
def __init__(
|
|
192
|
+
self,
|
|
193
|
+
session_store: SessionStore,
|
|
194
|
+
shares_dir: Optional[Path] = None,
|
|
195
|
+
):
|
|
196
|
+
self.session_store = session_store
|
|
197
|
+
self.shares_dir = shares_dir or (session_store.storage_dir / "shares")
|
|
198
|
+
self.shares_dir.mkdir(parents=True, exist_ok=True)
|
|
199
|
+
|
|
200
|
+
self._shares: Dict[str, SharedSession] = {}
|
|
201
|
+
self._load_shares()
|
|
202
|
+
|
|
203
|
+
def _load_shares(self) -> None:
|
|
204
|
+
"""Load shares from disk."""
|
|
205
|
+
index_file = self.shares_dir / "index.json"
|
|
206
|
+
if index_file.exists():
|
|
207
|
+
try:
|
|
208
|
+
data = json.loads(index_file.read_text())
|
|
209
|
+
for share_data in data.get("shares", []):
|
|
210
|
+
share = SharedSession.from_dict(share_data)
|
|
211
|
+
if not share.is_expired:
|
|
212
|
+
self._shares[share.share_token] = share
|
|
213
|
+
except (json.JSONDecodeError, KeyError):
|
|
214
|
+
pass
|
|
215
|
+
|
|
216
|
+
def _save_shares(self) -> None:
|
|
217
|
+
"""Save shares to disk."""
|
|
218
|
+
index_file = self.shares_dir / "index.json"
|
|
219
|
+
|
|
220
|
+
# Remove expired shares
|
|
221
|
+
self._shares = {k: v for k, v in self._shares.items() if not v.is_expired}
|
|
222
|
+
|
|
223
|
+
data = {
|
|
224
|
+
"shares": [s.to_dict() for s in self._shares.values()],
|
|
225
|
+
}
|
|
226
|
+
index_file.write_text(json.dumps(data, indent=2))
|
|
227
|
+
|
|
228
|
+
def _generate_token(self) -> str:
|
|
229
|
+
"""Generate a unique share token."""
|
|
230
|
+
return secrets.token_urlsafe(16)
|
|
231
|
+
|
|
232
|
+
def _hash_password(self, password: str) -> str:
|
|
233
|
+
"""Hash a password for storage."""
|
|
234
|
+
return hashlib.sha256(password.encode()).hexdigest()
|
|
235
|
+
|
|
236
|
+
def _verify_password(self, password: str, hash_value: str) -> bool:
|
|
237
|
+
"""Verify a password against its hash."""
|
|
238
|
+
return self._hash_password(password) == hash_value
|
|
239
|
+
|
|
240
|
+
def create_share(
|
|
241
|
+
self,
|
|
242
|
+
session_id: str,
|
|
243
|
+
config: ShareConfig,
|
|
244
|
+
created_by: str = "",
|
|
245
|
+
) -> Optional[SharedSession]:
|
|
246
|
+
"""Create a share for a session."""
|
|
247
|
+
# Verify session exists
|
|
248
|
+
session = self.session_store.load(session_id)
|
|
249
|
+
if not session:
|
|
250
|
+
return None
|
|
251
|
+
|
|
252
|
+
share_token = self._generate_token()
|
|
253
|
+
share_id = f"share-{int(datetime.now().timestamp())}"
|
|
254
|
+
|
|
255
|
+
expires_at = None
|
|
256
|
+
if config.expires_in:
|
|
257
|
+
expires_at = datetime.now() + config.expires_in
|
|
258
|
+
|
|
259
|
+
password_hash = None
|
|
260
|
+
if config.password:
|
|
261
|
+
password_hash = self._hash_password(config.password)
|
|
262
|
+
|
|
263
|
+
share = SharedSession(
|
|
264
|
+
id=share_id,
|
|
265
|
+
session_id=session_id,
|
|
266
|
+
share_token=share_token,
|
|
267
|
+
visibility=config.visibility,
|
|
268
|
+
expires_at=expires_at,
|
|
269
|
+
allow_fork=config.allow_fork,
|
|
270
|
+
allow_view_history=config.allow_view_history,
|
|
271
|
+
password_hash=password_hash,
|
|
272
|
+
created_by=created_by,
|
|
273
|
+
)
|
|
274
|
+
|
|
275
|
+
self._shares[share_token] = share
|
|
276
|
+
self._save_shares()
|
|
277
|
+
|
|
278
|
+
return share
|
|
279
|
+
|
|
280
|
+
def get_share(
|
|
281
|
+
self,
|
|
282
|
+
share_token: str,
|
|
283
|
+
password: Optional[str] = None,
|
|
284
|
+
) -> Optional[SharedSession]:
|
|
285
|
+
"""Get a share by token."""
|
|
286
|
+
share = self._shares.get(share_token)
|
|
287
|
+
|
|
288
|
+
if not share:
|
|
289
|
+
return None
|
|
290
|
+
|
|
291
|
+
if share.is_expired:
|
|
292
|
+
del self._shares[share_token]
|
|
293
|
+
self._save_shares()
|
|
294
|
+
return None
|
|
295
|
+
|
|
296
|
+
# Check password if required
|
|
297
|
+
if share.password_hash:
|
|
298
|
+
if not password or not self._verify_password(password, share.password_hash):
|
|
299
|
+
return None
|
|
300
|
+
|
|
301
|
+
# Increment access count
|
|
302
|
+
share.access_count += 1
|
|
303
|
+
self._save_shares()
|
|
304
|
+
|
|
305
|
+
return share
|
|
306
|
+
|
|
307
|
+
def get_session_for_share(
|
|
308
|
+
self,
|
|
309
|
+
share_token: str,
|
|
310
|
+
password: Optional[str] = None,
|
|
311
|
+
) -> Optional[Session]:
|
|
312
|
+
"""Get the session for a share."""
|
|
313
|
+
share = self.get_share(share_token, password)
|
|
314
|
+
if not share:
|
|
315
|
+
return None
|
|
316
|
+
|
|
317
|
+
return self.session_store.load(share.session_id)
|
|
318
|
+
|
|
319
|
+
def fork_session(
|
|
320
|
+
self,
|
|
321
|
+
share_token: str,
|
|
322
|
+
new_title: str,
|
|
323
|
+
password: Optional[str] = None,
|
|
324
|
+
) -> Optional[Session]:
|
|
325
|
+
"""Fork a shared session."""
|
|
326
|
+
share = self.get_share(share_token, password)
|
|
327
|
+
|
|
328
|
+
if not share or not share.allow_fork:
|
|
329
|
+
return None
|
|
330
|
+
|
|
331
|
+
session = self.session_store.load(share.session_id)
|
|
332
|
+
if not session:
|
|
333
|
+
return None
|
|
334
|
+
|
|
335
|
+
# Create fork
|
|
336
|
+
forked = session.fork(new_title)
|
|
337
|
+
|
|
338
|
+
# Add fork metadata
|
|
339
|
+
forked.metadata["forked_from"] = {
|
|
340
|
+
"session_id": session.id,
|
|
341
|
+
"share_token": share_token,
|
|
342
|
+
"forked_at": datetime.now().isoformat(),
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
# Save forked session
|
|
346
|
+
self.session_store.save(forked)
|
|
347
|
+
|
|
348
|
+
# Update fork count
|
|
349
|
+
share.fork_count += 1
|
|
350
|
+
self._save_shares()
|
|
351
|
+
|
|
352
|
+
return forked
|
|
353
|
+
|
|
354
|
+
def revoke_share(self, share_token: str) -> bool:
|
|
355
|
+
"""Revoke a share."""
|
|
356
|
+
if share_token in self._shares:
|
|
357
|
+
del self._shares[share_token]
|
|
358
|
+
self._save_shares()
|
|
359
|
+
return True
|
|
360
|
+
return False
|
|
361
|
+
|
|
362
|
+
def list_shares(self, session_id: Optional[str] = None) -> List[SharedSession]:
|
|
363
|
+
"""List all shares, optionally filtered by session."""
|
|
364
|
+
shares = list(self._shares.values())
|
|
365
|
+
|
|
366
|
+
if session_id:
|
|
367
|
+
shares = [s for s in shares if s.session_id == session_id]
|
|
368
|
+
|
|
369
|
+
return sorted(shares, key=lambda s: s.created_at, reverse=True)
|
|
370
|
+
|
|
371
|
+
def export_session(
|
|
372
|
+
self,
|
|
373
|
+
session_id: str,
|
|
374
|
+
include_history: bool = True,
|
|
375
|
+
) -> Optional[ExportedSession]:
|
|
376
|
+
"""Export a session for sharing."""
|
|
377
|
+
session = self.session_store.load(session_id)
|
|
378
|
+
if not session:
|
|
379
|
+
return None
|
|
380
|
+
|
|
381
|
+
session_data = session.to_dict()
|
|
382
|
+
|
|
383
|
+
# Optionally strip history
|
|
384
|
+
if not include_history:
|
|
385
|
+
session_data["messages"] = []
|
|
386
|
+
session_data["tool_executions"] = []
|
|
387
|
+
|
|
388
|
+
# Calculate checksum
|
|
389
|
+
json_str = json.dumps(session_data, sort_keys=True)
|
|
390
|
+
checksum = hashlib.sha256(json_str.encode()).hexdigest()[:16]
|
|
391
|
+
|
|
392
|
+
return ExportedSession(
|
|
393
|
+
session_data=session_data,
|
|
394
|
+
checksum=checksum,
|
|
395
|
+
)
|
|
396
|
+
|
|
397
|
+
def import_session(
|
|
398
|
+
self,
|
|
399
|
+
exported: ExportedSession,
|
|
400
|
+
new_title: Optional[str] = None,
|
|
401
|
+
) -> Session:
|
|
402
|
+
"""Import an exported session."""
|
|
403
|
+
# Verify checksum if present
|
|
404
|
+
if exported.checksum:
|
|
405
|
+
json_str = json.dumps(exported.session_data, sort_keys=True)
|
|
406
|
+
expected = hashlib.sha256(json_str.encode()).hexdigest()[:16]
|
|
407
|
+
if expected != exported.checksum:
|
|
408
|
+
raise ValueError("Session data checksum mismatch")
|
|
409
|
+
|
|
410
|
+
# Create session from data
|
|
411
|
+
session = Session.from_dict(exported.session_data)
|
|
412
|
+
|
|
413
|
+
# Generate new ID for imported session
|
|
414
|
+
session.id = f"session-{int(datetime.now().timestamp())}-import"
|
|
415
|
+
|
|
416
|
+
if new_title:
|
|
417
|
+
session.title = new_title
|
|
418
|
+
|
|
419
|
+
# Add import metadata
|
|
420
|
+
session.metadata["imported"] = {
|
|
421
|
+
"imported_at": datetime.now().isoformat(),
|
|
422
|
+
"original_id": exported.session_data.get("id"),
|
|
423
|
+
"export_format": exported.export_format,
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
# Save session
|
|
427
|
+
self.session_store.save(session)
|
|
428
|
+
|
|
429
|
+
return session
|
|
430
|
+
|
|
431
|
+
def generate_share_link(
|
|
432
|
+
self,
|
|
433
|
+
share_token: str,
|
|
434
|
+
base_url: str = "https://superqode.dev",
|
|
435
|
+
) -> str:
|
|
436
|
+
"""Generate a shareable link."""
|
|
437
|
+
return f"{base_url}/share/{share_token}"
|
|
438
|
+
|
|
439
|
+
def generate_export_link(
|
|
440
|
+
self,
|
|
441
|
+
session_id: str,
|
|
442
|
+
base_url: str = "https://superqode.dev",
|
|
443
|
+
) -> Optional[str]:
|
|
444
|
+
"""Generate a self-contained export link."""
|
|
445
|
+
exported = self.export_session(session_id, include_history=False)
|
|
446
|
+
if not exported:
|
|
447
|
+
return None
|
|
448
|
+
|
|
449
|
+
encoded = exported.to_base64()
|
|
450
|
+
|
|
451
|
+
# URL encode the data
|
|
452
|
+
params = urllib.parse.urlencode({"data": encoded})
|
|
453
|
+
return f"{base_url}/import?{params}"
|
|
454
|
+
|
|
455
|
+
|
|
456
|
+
def create_quick_share(
|
|
457
|
+
session_id: str,
|
|
458
|
+
store: SessionStore,
|
|
459
|
+
expires_hours: int = 24,
|
|
460
|
+
) -> Optional[str]:
|
|
461
|
+
"""Quick function to create a share link.
|
|
462
|
+
|
|
463
|
+
Returns the share URL or None if session not found.
|
|
464
|
+
"""
|
|
465
|
+
manager = SessionSharingManager(store)
|
|
466
|
+
|
|
467
|
+
config = ShareConfig(
|
|
468
|
+
visibility=ShareVisibility.PRIVATE,
|
|
469
|
+
expires_in=timedelta(hours=expires_hours),
|
|
470
|
+
allow_fork=True,
|
|
471
|
+
)
|
|
472
|
+
|
|
473
|
+
share = manager.create_share(session_id, config)
|
|
474
|
+
if share:
|
|
475
|
+
return manager.generate_share_link(share.share_token)
|
|
476
|
+
|
|
477
|
+
return None
|