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 7c88733..f4c0650 100644 --- a/src/pluggedinkit/__init__.py +++ b/src/pluggedinkit/__init__.py @@ -26,6 +26,8 @@ ClipboardDeleteResponse, ClipboardEncoding, ClipboardVisibility, + ClipboardSource, + DEFAULT_CLIPBOARD_SOURCE, # Document types Document, DocumentWithContent, @@ -61,6 +63,8 @@ "ClipboardDeleteResponse", "ClipboardEncoding", "ClipboardVisibility", + "ClipboardSource", + "DEFAULT_CLIPBOARD_SOURCE", # Document types "Document", "DocumentWithContent", diff --git a/src/pluggedinkit/services/clipboard.py b/src/pluggedinkit/services/clipboard.py index bf8d1ed..610d3cd 100644 --- a/src/pluggedinkit/services/clipboard.py +++ b/src/pluggedinkit/services/clipboard.py @@ -12,6 +12,7 @@ ClipboardPushRequest, ClipboardResponse, ClipboardSetRequest, + ClipboardSource, ) if TYPE_CHECKING: @@ -78,13 +79,16 @@ def set( "contentType": content_type, "encoding": encoding, "visibility": visibility, + "source": ClipboardSource.SDK.value, # SDK always uses 'sdk' source } if created_by_tool: payload["createdByTool"] = created_by_tool if created_by_model: payload["createdByModel"] = created_by_model - if ttl_seconds: + 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 response = self.client.request("POST", "/api/clipboard", json=payload) @@ -111,13 +115,16 @@ def push( "contentType": content_type, "encoding": encoding, "visibility": visibility, + "source": ClipboardSource.SDK.value, # SDK always uses 'sdk' source } if created_by_tool: payload["createdByTool"] = created_by_tool if created_by_model: payload["createdByModel"] = created_by_model - if ttl_seconds: + 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 response = self.client.request("POST", "/api/clipboard/push", json=payload) @@ -165,11 +172,13 @@ def clear_all(self) -> int: for entry in entries: try: + success = False if entry.name: - self.delete(name=entry.name) + success = self.delete(name=entry.name) elif entry.idx is not None: - self.delete(idx=entry.idx) - deleted += 1 + success = self.delete(idx=entry.idx) + if success: + deleted += 1 except PluggedInError: pass @@ -236,13 +245,16 @@ async def set( "contentType": content_type, "encoding": encoding, "visibility": visibility, + "source": ClipboardSource.SDK.value, # SDK always uses 'sdk' source } if created_by_tool: payload["createdByTool"] = created_by_tool if created_by_model: payload["createdByModel"] = created_by_model - if ttl_seconds: + 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 response = await self.client.request("POST", "/api/clipboard", json=payload) @@ -269,13 +281,16 @@ async def push( "contentType": content_type, "encoding": encoding, "visibility": visibility, + "source": ClipboardSource.SDK.value, # SDK always uses 'sdk' source } if created_by_tool: payload["createdByTool"] = created_by_tool if created_by_model: payload["createdByModel"] = created_by_model - if ttl_seconds: + 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 response = await self.client.request("POST", "/api/clipboard/push", json=payload) @@ -323,11 +338,13 @@ async def clear_all(self) -> int: for entry in entries: try: + success = False if entry.name: - await self.delete(name=entry.name) + success = await self.delete(name=entry.name) elif entry.idx is not None: - await self.delete(idx=entry.idx) - deleted += 1 + success = await self.delete(idx=entry.idx) + if success: + deleted += 1 except PluggedInError: pass diff --git a/src/pluggedinkit/types.py b/src/pluggedinkit/types.py index bff3765..f6bfed4 100644 --- a/src/pluggedinkit/types.py +++ b/src/pluggedinkit/types.py @@ -283,6 +283,17 @@ class ClipboardVisibility(str, Enum): 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 @@ -295,6 +306,8 @@ class ClipboardEntry(BaseModel): 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")