diff --git a/README.md b/README.md index 088858a..71ac9b7 100644 --- a/README.md +++ b/README.md @@ -66,6 +66,7 @@ asyncio.run(main()) - 📄 **Document Management** - Full CRUD operations with type safety - 🔍 **Semantic Search** - AI-powered document search - 🤖 **RAG Integration** - Natural language queries to your knowledge base +- 📋 **Clipboard/Memory** - Persistent key-value storage for MCP tools and AI agents - 📤 **File Uploads** - Support for various file formats - 🔄 **Version Control** - Document versioning and history - ⚡ **Async Support** - Both sync and async clients @@ -377,6 +378,96 @@ async def rag_operations(): asyncio.run(rag_operations()) ``` +### Clipboard Operations + +The clipboard provides persistent key-value storage for MCP tools and AI agents. + +#### Set Named Entry + +```python +entry = client.clipboard.set( + name="user_preferences", + value='{"theme": "dark", "lang": "en"}', + content_type="application/json", + encoding="utf-8", + visibility="private", + ttl_seconds=86400 # 24 hours +) + +print(f"Created entry: {entry.uuid}") +print(f"Source: {entry.source}") # 'sdk' - automatically set +``` + +#### Get Entry + +```python +# By name +entry = client.clipboard.get(name="user_preferences") +print(entry.value) + +# By index (stack access) +latest = client.clipboard.get(idx=0) +``` + +#### Push to Stack + +```python +entry = client.clipboard.push( + value="Processing step 1 result", + content_type="text/plain" +) + +print(f"Pushed to index: {entry.idx}") +``` + +#### Pop from Stack + +```python +entry = client.clipboard.pop() +if entry: + print(f"Popped value: {entry.value}") +``` + +#### List and Delete + +```python +# List all entries +response = client.clipboard.list() +for entry in response.entries: + label = entry.name or f"idx:{entry.idx}" + print(f"{label} - source: {entry.source}") + +# Delete by name +client.clipboard.delete(name="old_entry") + +# Clear all +client.clipboard.clear_all() +``` + +#### Clipboard Entry Structure + +```python +from pluggedinkit import ClipboardEntry, ClipboardSource + +# ClipboardEntry fields: +# - uuid: str +# - name: Optional[str] # Semantic key +# - idx: Optional[int] # Stack index +# - value: str +# - content_type: str +# - encoding: "utf-8" | "base64" | "hex" +# - size_bytes: int +# - visibility: "private" | "workspace" | "public" +# - created_by_tool: Optional[str] +# - created_by_model: Optional[str] +# - source: Optional[ClipboardSource] # Auto-set: ui, sdk, or mcp +# - created_at: datetime +# - updated_at: datetime +# - expires_at: Optional[datetime] +``` + +> **Note**: The `source` field is automatically set to `ClipboardSource.SDK` when using this SDK. It indicates how the entry was created (UI, SDK, or MCP proxy). + ### Error Handling ```python diff --git a/src/pluggedinkit/__init__.py b/src/pluggedinkit/__init__.py index 074966e..6e5e6c8 100644 --- a/src/pluggedinkit/__init__.py +++ b/src/pluggedinkit/__init__.py @@ -14,7 +14,22 @@ NotFoundError, ValidationError, ) +from .services.clipboard import ClearAllResult from .types import ( + # Clipboard types + ClipboardEntry, + ClipboardListResponse, + ClipboardSetRequest, + ClipboardPushRequest, + ClipboardGetFilters, + ClipboardDeleteRequest, + ClipboardResponse, + ClipboardDeleteResponse, + ClipboardEncoding, + ClipboardVisibility, + ClipboardSource, + DEFAULT_CLIPBOARD_SOURCE, + # Document types Document, DocumentWithContent, DocumentListResponse, @@ -38,6 +53,21 @@ "RateLimitError", "NotFoundError", "ValidationError", + # Clipboard types + "ClipboardEntry", + "ClipboardListResponse", + "ClipboardSetRequest", + "ClipboardPushRequest", + "ClipboardGetFilters", + "ClipboardDeleteRequest", + "ClipboardResponse", + "ClipboardDeleteResponse", + "ClipboardEncoding", + "ClipboardVisibility", + "ClipboardSource", + "ClearAllResult", + "DEFAULT_CLIPBOARD_SOURCE", + # Document types "Document", "DocumentWithContent", "DocumentListResponse", diff --git a/src/pluggedinkit/client.py b/src/pluggedinkit/client.py index 549a72d..c99e11a 100644 --- a/src/pluggedinkit/client.py +++ b/src/pluggedinkit/client.py @@ -12,6 +12,7 @@ PluggedInError, RateLimitError, ) +from .services.clipboard import ClipboardService, AsyncClipboardService from .services.documents import DocumentService, AsyncDocumentService from .services.rag import RagService, AsyncRagService from .services.uploads import UploadService, AsyncUploadService @@ -96,6 +97,7 @@ def __init__( ) # Initialize services + self.clipboard = ClipboardService(self) self.documents = DocumentService(self) self.rag = RagService(self) self.uploads = UploadService(self) @@ -195,6 +197,7 @@ def __init__( ) # Initialize async services + self.clipboard = AsyncClipboardService(self) self.documents = AsyncDocumentService(self) self.rag = AsyncRagService(self) self.uploads = AsyncUploadService(self) diff --git a/src/pluggedinkit/services/__init__.py b/src/pluggedinkit/services/__init__.py index be59a48..cff8617 100644 --- a/src/pluggedinkit/services/__init__.py +++ b/src/pluggedinkit/services/__init__.py @@ -1,10 +1,14 @@ """Service modules for Plugged.in SDK""" +from .clipboard import AsyncClipboardService, ClearAllResult, ClipboardService from .documents import AsyncDocumentService, DocumentService from .rag import AsyncRagService, RagService from .uploads import AsyncUploadService, UploadService __all__ = [ + "ClipboardService", + "AsyncClipboardService", + "ClearAllResult", "DocumentService", "AsyncDocumentService", "RagService", diff --git a/src/pluggedinkit/services/clipboard.py b/src/pluggedinkit/services/clipboard.py new file mode 100644 index 0000000..134a89a --- /dev/null +++ b/src/pluggedinkit/services/clipboard.py @@ -0,0 +1,605 @@ +"""Clipboard service for Plugged.in SDK + +This module provides both synchronous and asynchronous clipboard services +for persistent key-value and stack-based storage in AI workflows. +""" + +from dataclasses import dataclass +from typing import TYPE_CHECKING, Any, Dict, List, Optional, Union + +from ..exceptions import PluggedInError +from ..types import ( + ClipboardDeleteRequest, + ClipboardEncoding, + ClipboardEntry, + ClipboardGetFilters, + ClipboardListResponse, + ClipboardSource, + ClipboardVisibility, +) + +if TYPE_CHECKING: + from ..client import AsyncPluggedInClient, PluggedInClient + + +# ----------------------------------------------------------------------------- +# Shared Helper Functions +# ----------------------------------------------------------------------------- + + +def _normalize_encoding( + encoding: Union[ClipboardEncoding, str] +) -> ClipboardEncoding: + """Normalize encoding to ClipboardEncoding enum.""" + if isinstance(encoding, ClipboardEncoding): + return encoding + return ClipboardEncoding(encoding) + + +def _normalize_visibility( + visibility: Union[ClipboardVisibility, str] +) -> ClipboardVisibility: + """Normalize visibility to ClipboardVisibility enum.""" + if isinstance(visibility, ClipboardVisibility): + return visibility + return ClipboardVisibility(visibility) + + +def _build_clipboard_payload( + value: str, + name: Optional[str] = None, + content_type: str = "text/plain", + encoding: Union[ClipboardEncoding, str] = ClipboardEncoding.UTF8, + visibility: Union[ClipboardVisibility, str] = ClipboardVisibility.PRIVATE, + created_by_tool: Optional[str] = None, + created_by_model: Optional[str] = None, + ttl_seconds: Optional[int] = None, +) -> Dict[str, Any]: + """Build a clipboard request payload with common fields. + + Normalizes encoding and visibility enums in one place. + """ + # Normalize enums here - callers don't need to + normalized_encoding = _normalize_encoding(encoding) + normalized_visibility = _normalize_visibility(visibility) + + payload: Dict[str, Any] = { + "value": value, + "contentType": content_type, + "encoding": normalized_encoding.value, + "visibility": normalized_visibility.value, + "source": ClipboardSource.SDK.value, + } + + if name: + payload["name"] = name + if created_by_tool: + payload["createdByTool"] = created_by_tool + if created_by_model: + payload["createdByModel"] = created_by_model + if ttl_seconds is not None: + if ttl_seconds <= 0: + raise ValueError("ttl_seconds must be greater than 0 when provided") + payload["ttlSeconds"] = ttl_seconds + + return payload + + +def _parse_list_response(data: Dict[str, Any]) -> List[ClipboardEntry]: + """Parse list response and raise on failure.""" + if not data.get("success"): + raise PluggedInError(data.get("error", "Failed to list clipboard entries")) + return ClipboardListResponse(**data).entries + + +def _parse_entry_response( + data: Dict[str, Any], + error_msg: str = "Operation failed", + raise_on_failure: bool = True, +) -> Optional[ClipboardEntry]: + """Parse entry response with consistent error handling. + + Args: + data: API response data + error_msg: Error message to use if raising + raise_on_failure: If True, raises PluggedInError on failure. + If False, returns None on failure. + + Returns: + ClipboardEntry on success, None on failure (if not raising) + + Raises: + PluggedInError: If raise_on_failure is True and operation failed + """ + if not data.get("success"): + if raise_on_failure: + raise PluggedInError(data.get("error", error_msg)) + return None + + if "entry" in data and data["entry"]: + return ClipboardEntry(**data["entry"]) + + if raise_on_failure: + raise PluggedInError(data.get("error", error_msg)) + return None + + +def _parse_delete_response(data: Dict[str, Any]) -> int: + """Parse delete response and return deleted count. + + Returns: + Number of deleted entries (0 if failed) + """ + if not data.get("success"): + return 0 + deleted = data.get("deleted", 0) + return int(deleted) if deleted is not None else 0 + + +def _build_get_params(filters: ClipboardGetFilters) -> Dict[str, str]: + """Build query params from validated filters.""" + params: Dict[str, str] = {} + if filters.name is not None: + params["name"] = filters.name + if filters.idx is not None: + params["idx"] = str(filters.idx) + return params + + +def _build_delete_payload(request: ClipboardDeleteRequest) -> Dict[str, Any]: + """Build delete payload from validated request.""" + payload: Dict[str, Any] = {} + if request.name is not None: + payload["name"] = request.name + if request.idx is not None: + payload["idx"] = request.idx + return payload + + +# ----------------------------------------------------------------------------- +# Result Types +# ----------------------------------------------------------------------------- + + +@dataclass +class ClearAllResult: + """Result of clear_all operation with structured feedback. + + Attributes: + deleted: Number of successfully deleted entries + failed: Number of entries that failed to delete + total: Total entries attempted (deleted + failed) + """ + deleted: int + failed: int + + @property + def total(self) -> int: + """Total entries attempted.""" + return self.deleted + self.failed + + @property + def success(self) -> bool: + """True if all entries were deleted.""" + return self.failed == 0 + + +# ----------------------------------------------------------------------------- +# Synchronous Clipboard Service +# ----------------------------------------------------------------------------- + + +class ClipboardService: + """Synchronous clipboard service for Plugged.in. + + Provides persistent key-value and stack-based storage for AI workflows. + """ + + def __init__(self, client: "PluggedInClient"): + self.client = client + + def list(self) -> List[ClipboardEntry]: + """List all clipboard entries. + + Returns: + List of ClipboardEntry objects + + Raises: + PluggedInError: If the API request fails + """ + response = self.client.request("GET", "/api/clipboard") + return _parse_list_response(response.json()) + + def get( + self, + name: Optional[str] = None, + idx: Optional[int] = None, + ) -> Optional[ClipboardEntry]: + """Get a clipboard entry by name or index. + + Args: + name: Entry name for key-value lookup + idx: Entry index for stack-based lookup + + Returns: + ClipboardEntry if found, None if not found + + Raises: + ValueError: If neither name nor idx is provided + """ + # Use Pydantic model for validation + filters = ClipboardGetFilters(name=name, idx=idx) + params = _build_get_params(filters) + + response = self.client.request("GET", "/api/clipboard", params=params) + return _parse_entry_response( + response.json(), + error_msg="Failed to get clipboard entry", + raise_on_failure=False, + ) + + def set( + self, + name: str, + value: str, + content_type: str = "text/plain", + encoding: Union[ClipboardEncoding, str] = ClipboardEncoding.UTF8, + visibility: Union[ClipboardVisibility, str] = ClipboardVisibility.PRIVATE, + created_by_tool: Optional[str] = None, + created_by_model: Optional[str] = None, + ttl_seconds: Optional[int] = None, + ) -> ClipboardEntry: + """Set a named clipboard entry (upsert). + + Args: + name: Entry name + value: Value to store + content_type: MIME type (default: text/plain) + encoding: Content encoding (default: utf-8) + visibility: Visibility level (default: private) + created_by_tool: Tool name for attribution + created_by_model: Model name for attribution + ttl_seconds: Time-to-live in seconds + + Returns: + The created/updated ClipboardEntry + + Raises: + PluggedInError: If the API request fails + ValueError: If ttl_seconds is <= 0 + """ + payload = _build_clipboard_payload( + value=value, + name=name, + content_type=content_type, + encoding=encoding, + visibility=visibility, + created_by_tool=created_by_tool, + created_by_model=created_by_model, + ttl_seconds=ttl_seconds, + ) + + response = self.client.request("POST", "/api/clipboard", json=payload) + result = _parse_entry_response( + response.json(), + error_msg="Failed to set clipboard entry", + raise_on_failure=True, + ) + # result is guaranteed non-None when raise_on_failure=True + assert result is not None + return result + + def push( + self, + value: str, + content_type: str = "text/plain", + encoding: Union[ClipboardEncoding, str] = ClipboardEncoding.UTF8, + visibility: Union[ClipboardVisibility, str] = ClipboardVisibility.PRIVATE, + created_by_tool: Optional[str] = None, + created_by_model: Optional[str] = None, + ttl_seconds: Optional[int] = None, + ) -> ClipboardEntry: + """Push a value to the indexed clipboard (auto-increment index). + + Args: + value: Value to store + content_type: MIME type (default: text/plain) + encoding: Content encoding (default: utf-8) + visibility: Visibility level (default: private) + created_by_tool: Tool name for attribution + created_by_model: Model name for attribution + ttl_seconds: Time-to-live in seconds + + Returns: + The created ClipboardEntry with assigned index + + Raises: + PluggedInError: If the API request fails + ValueError: If ttl_seconds is <= 0 + """ + payload = _build_clipboard_payload( + value=value, + content_type=content_type, + encoding=encoding, + visibility=visibility, + created_by_tool=created_by_tool, + created_by_model=created_by_model, + ttl_seconds=ttl_seconds, + ) + + response = self.client.request("POST", "/api/clipboard/push", json=payload) + result = _parse_entry_response( + response.json(), + error_msg="Failed to push to clipboard", + raise_on_failure=True, + ) + assert result is not None + return result + + def pop(self) -> Optional[ClipboardEntry]: + """Pop the last indexed entry from clipboard (LIFO). + + Returns: + The popped ClipboardEntry, or None if clipboard is empty + """ + response = self.client.request("POST", "/api/clipboard/pop") + return _parse_entry_response( + response.json(), + error_msg="Failed to pop from clipboard", + raise_on_failure=False, + ) + + def delete( + self, + name: Optional[str] = None, + idx: Optional[int] = None, + ) -> int: + """Delete a clipboard entry by name or index. + + Args: + name: Entry name to delete + idx: Entry index to delete + + Returns: + Number of deleted entries (0 or 1) + + Raises: + ValueError: If neither name nor idx is provided + """ + # Use Pydantic model for validation + request = ClipboardDeleteRequest(name=name, idx=idx) + payload = _build_delete_payload(request) + + response = self.client.request("DELETE", "/api/clipboard", json=payload) + return _parse_delete_response(response.json()) + + def clear_all(self) -> ClearAllResult: + """Clear all clipboard entries using bulk delete API. + + Returns: + ClearAllResult with deleted count and status + + Raises: + PluggedInError: If the API request fails + """ + response = self.client.request( + "DELETE", + "/api/clipboard", + json={"clearAll": True} + ) + data = response.json() + + if not data.get("success"): + raise PluggedInError(data.get("error", "Failed to clear clipboard")) + + deleted = data.get("deleted", 0) + return ClearAllResult(deleted=deleted, failed=0) + + +# ----------------------------------------------------------------------------- +# Asynchronous Clipboard Service +# ----------------------------------------------------------------------------- + + +class AsyncClipboardService: + """Asynchronous clipboard service for Plugged.in. + + Provides persistent key-value and stack-based storage for AI workflows. + """ + + def __init__(self, client: "AsyncPluggedInClient"): + self.client = client + + async def list(self) -> List[ClipboardEntry]: + """List all clipboard entries. + + Returns: + List of ClipboardEntry objects + + Raises: + PluggedInError: If the API request fails + """ + response = await self.client.request("GET", "/api/clipboard") + return _parse_list_response(response.json()) + + async def get( + self, + name: Optional[str] = None, + idx: Optional[int] = None, + ) -> Optional[ClipboardEntry]: + """Get a clipboard entry by name or index. + + Args: + name: Entry name for key-value lookup + idx: Entry index for stack-based lookup + + Returns: + ClipboardEntry if found, None if not found + + Raises: + ValueError: If neither name nor idx is provided + """ + filters = ClipboardGetFilters(name=name, idx=idx) + params = _build_get_params(filters) + + response = await self.client.request("GET", "/api/clipboard", params=params) + return _parse_entry_response( + response.json(), + error_msg="Failed to get clipboard entry", + raise_on_failure=False, + ) + + async def set( + self, + name: str, + value: str, + content_type: str = "text/plain", + encoding: Union[ClipboardEncoding, str] = ClipboardEncoding.UTF8, + visibility: Union[ClipboardVisibility, str] = ClipboardVisibility.PRIVATE, + created_by_tool: Optional[str] = None, + created_by_model: Optional[str] = None, + ttl_seconds: Optional[int] = None, + ) -> ClipboardEntry: + """Set a named clipboard entry (upsert). + + Args: + name: Entry name + value: Value to store + content_type: MIME type (default: text/plain) + encoding: Content encoding (default: utf-8) + visibility: Visibility level (default: private) + created_by_tool: Tool name for attribution + created_by_model: Model name for attribution + ttl_seconds: Time-to-live in seconds + + Returns: + The created/updated ClipboardEntry + + Raises: + PluggedInError: If the API request fails + ValueError: If ttl_seconds is <= 0 + """ + payload = _build_clipboard_payload( + value=value, + name=name, + content_type=content_type, + encoding=encoding, + visibility=visibility, + created_by_tool=created_by_tool, + created_by_model=created_by_model, + ttl_seconds=ttl_seconds, + ) + + response = await self.client.request("POST", "/api/clipboard", json=payload) + result = _parse_entry_response( + response.json(), + error_msg="Failed to set clipboard entry", + raise_on_failure=True, + ) + assert result is not None + return result + + async def push( + self, + value: str, + content_type: str = "text/plain", + encoding: Union[ClipboardEncoding, str] = ClipboardEncoding.UTF8, + visibility: Union[ClipboardVisibility, str] = ClipboardVisibility.PRIVATE, + created_by_tool: Optional[str] = None, + created_by_model: Optional[str] = None, + ttl_seconds: Optional[int] = None, + ) -> ClipboardEntry: + """Push a value to the indexed clipboard (auto-increment index). + + Args: + value: Value to store + content_type: MIME type (default: text/plain) + encoding: Content encoding (default: utf-8) + visibility: Visibility level (default: private) + created_by_tool: Tool name for attribution + created_by_model: Model name for attribution + ttl_seconds: Time-to-live in seconds + + Returns: + The created ClipboardEntry with assigned index + + Raises: + PluggedInError: If the API request fails + ValueError: If ttl_seconds is <= 0 + """ + payload = _build_clipboard_payload( + value=value, + content_type=content_type, + encoding=encoding, + visibility=visibility, + created_by_tool=created_by_tool, + created_by_model=created_by_model, + ttl_seconds=ttl_seconds, + ) + + response = await self.client.request("POST", "/api/clipboard/push", json=payload) + result = _parse_entry_response( + response.json(), + error_msg="Failed to push to clipboard", + raise_on_failure=True, + ) + assert result is not None + return result + + async def pop(self) -> Optional[ClipboardEntry]: + """Pop the last indexed entry from clipboard (LIFO). + + Returns: + The popped ClipboardEntry, or None if clipboard is empty + """ + response = await self.client.request("POST", "/api/clipboard/pop") + return _parse_entry_response( + response.json(), + error_msg="Failed to pop from clipboard", + raise_on_failure=False, + ) + + async def delete( + self, + name: Optional[str] = None, + idx: Optional[int] = None, + ) -> int: + """Delete a clipboard entry by name or index. + + Args: + name: Entry name to delete + idx: Entry index to delete + + Returns: + Number of deleted entries (0 or 1) + + Raises: + ValueError: If neither name nor idx is provided + """ + request = ClipboardDeleteRequest(name=name, idx=idx) + payload = _build_delete_payload(request) + + response = await self.client.request("DELETE", "/api/clipboard", json=payload) + return _parse_delete_response(response.json()) + + async def clear_all(self) -> ClearAllResult: + """Clear all clipboard entries using bulk delete API. + + Returns: + ClearAllResult with deleted count and status + + Raises: + PluggedInError: If the API request fails + """ + response = await self.client.request( + "DELETE", + "/api/clipboard", + json={"clearAll": True} + ) + data = response.json() + + if not data.get("success"): + raise PluggedInError(data.get("error", "Failed to clear clipboard")) + + deleted = data.get("deleted", 0) + return ClearAllResult(deleted=deleted, failed=0) diff --git a/src/pluggedinkit/types.py b/src/pluggedinkit/types.py index 8cbb42d..aaaf352 100644 --- a/src/pluggedinkit/types.py +++ b/src/pluggedinkit/types.py @@ -4,7 +4,7 @@ from enum import Enum from typing import Any, Dict, List, Literal, Optional, Union -from pydantic import BaseModel, Field +from pydantic import BaseModel, Field, model_validator class DocumentSource(str, Enum): @@ -266,3 +266,124 @@ class RagStorageStats(BaseModel): vectors_count: Optional[int] = None embedding_dimension: Optional[int] = None is_estimate: Optional[bool] = None + + +# Clipboard types +class ClipboardEncoding(str, Enum): + """Clipboard content encoding""" + UTF8 = "utf-8" + BASE64 = "base64" + HEX = "hex" + + +class ClipboardVisibility(str, Enum): + """Clipboard visibility levels""" + PRIVATE = "private" + WORKSPACE = "workspace" + PUBLIC = "public" + + +class ClipboardSource(str, Enum): + """Clipboard data source""" + UI = "ui" + SDK = "sdk" + MCP = "mcp" + + +# Default source for backward compatibility with older data +DEFAULT_CLIPBOARD_SOURCE = ClipboardSource.UI + + +class ClipboardEntry(BaseModel): + """Clipboard entry model""" + uuid: str + name: Optional[str] = None + idx: Optional[int] = None + value: str + content_type: str = Field(alias="contentType", default="text/plain") + encoding: ClipboardEncoding = ClipboardEncoding.UTF8 + size_bytes: int = Field(alias="sizeBytes") + visibility: ClipboardVisibility = ClipboardVisibility.PRIVATE + created_by_tool: Optional[str] = Field(None, alias="createdByTool") + created_by_model: Optional[str] = Field(None, alias="createdByModel") + # Optional for backward compatibility with older API responses + source: Optional[ClipboardSource] = DEFAULT_CLIPBOARD_SOURCE + created_at: datetime = Field(alias="createdAt") + updated_at: datetime = Field(alias="updatedAt") + expires_at: Optional[datetime] = Field(None, alias="expiresAt") + + class Config: + populate_by_name = True + + +class ClipboardListResponse(BaseModel): + """Response for clipboard listing""" + success: bool + entries: List[ClipboardEntry] + + +class ClipboardSetRequest(BaseModel): + """Request for setting a named clipboard entry""" + name: str = Field(..., min_length=1, max_length=255) + value: str + content_type: Optional[str] = Field(None, alias="contentType", max_length=256) + encoding: Optional[ClipboardEncoding] = None + visibility: Optional[ClipboardVisibility] = None + created_by_tool: Optional[str] = Field(None, alias="createdByTool", max_length=255) + created_by_model: Optional[str] = Field(None, alias="createdByModel", max_length=255) + ttl_seconds: Optional[int] = Field(None, alias="ttlSeconds", gt=0) + + class Config: + populate_by_name = True + + +class ClipboardPushRequest(BaseModel): + """Request for pushing to indexed clipboard""" + value: str + content_type: Optional[str] = Field(None, alias="contentType", max_length=256) + encoding: Optional[ClipboardEncoding] = None + visibility: Optional[ClipboardVisibility] = None + created_by_tool: Optional[str] = Field(None, alias="createdByTool", max_length=255) + created_by_model: Optional[str] = Field(None, alias="createdByModel", max_length=255) + ttl_seconds: Optional[int] = Field(None, alias="ttlSeconds", gt=0) + + class Config: + populate_by_name = True + + +class ClipboardGetFilters(BaseModel): + """Filters for getting a clipboard entry""" + name: Optional[str] = None + idx: Optional[int] = None + + @model_validator(mode='after') + def check_name_or_idx(self) -> 'ClipboardGetFilters': + if self.name is None and self.idx is None: + raise ValueError("Either 'name' or 'idx' must be provided") + return self + + +class ClipboardDeleteRequest(BaseModel): + """Request for deleting a clipboard entry""" + name: Optional[str] = None + idx: Optional[int] = None + + @model_validator(mode='after') + def check_name_or_idx(self) -> 'ClipboardDeleteRequest': + if self.name is None and self.idx is None: + raise ValueError("Either 'name' or 'idx' must be provided") + return self + + +class ClipboardResponse(BaseModel): + """Response for clipboard operations""" + success: bool + entry: Optional[ClipboardEntry] = None + error: Optional[str] = None + + +class ClipboardDeleteResponse(BaseModel): + """Response for clipboard delete operations""" + success: bool + deleted: Optional[bool] = None + error: Optional[str] = None