Skip to content

Feature/clipboard source tracking - #3

Merged
ckaraca merged 5 commits into
feature/clipboard-clientfrom
feature/clipboard-source-tracking
Nov 29, 2025
Merged

ckaraca merged 5 commits into
feature/clipboard-clientfrom
feature/clipboard-source-tracking

Conversation

@ckaraca

@ckaraca ckaraca commented Nov 29, 2025 •

Copy link
Copy Markdown
Contributor

Summary by Sourcery

Add clipboard source tracking to SDK clipboard entries and enhance clipboard operations documentation.

New Features:

  • Introduce ClipboardSource enum and default clipboard source for tracking the origin of clipboard entries (UI, SDK, MCP).
  • Expose clipboard source metadata and defaults through the public package API.

Bug Fixes:

  • Prevent clear_all from over-counting deletions by only incrementing when delete operations succeed.
  • Validate ttl_seconds explicitly and reject non-positive TTL values for clipboard entries.

Enhancements:

  • Automatically set clipboard entry source to 'sdk' for all clipboard set and push operations to support source attribution.

Documentation:

  • Document clipboard operations, entry structure, and source behavior in the README, including examples for set, get, push, pop, list, and delete.

- Add ClipboardSource enum (ui/sdk/mcp)
- Add source field to ClipboardEntry model
- Hardcode source as 'sdk' in sync/async set/push methods
- Add DEFAULT_CLIPBOARD_SOURCE constant for backward compatibility
- Make ClipboardEntry.source Optional to handle older API responses
- Export constant from __init__.py

Addresses code review feedback about breaking existing call sites.
- Add Clipboard Operations section to README
- Document set, get, push, pop, list, and delete operations
- Add ClipboardEntry structure with source field
- Note that source is auto-set to ClipboardSource.SDK when using this SDK
- Add Clipboard/Memory to Features list
- Use 'is not None' check for ttl_seconds instead of truthiness
- Add explicit validation that ttl_seconds > 0 when provided
- Fix clear_all to only increment counter when delete() returns True
- Applied to both sync and async service classes (4 locations each)
@sourcery-ai

sourcery-ai Bot commented Nov 29, 2025 •

Copy link
Copy Markdown

Reviewer's Guide

Adds clipboard source tracking to the SDK’s clipboard operations and exposes it in the public types, while improving TTL validation and making clear_all only count successful deletions. Also documents clipboard usage and the new source behavior in the README.

Sequence diagram for clipboard set operation with source tracking and TTL validation

sequenceDiagram
    actor Caller
    participant ClipboardClient
    participant HttpClient
    participant ClipboardAPI

    Caller->>ClipboardClient: set(name, value, content_type, encoding, visibility, created_by_tool, created_by_model, ttl_seconds)
    alt ttl_seconds is not None and ttl_seconds <= 0
        ClipboardClient-->>Caller: raise ValueError
    else valid ttl_seconds or None
        ClipboardClient->>ClipboardClient: build payload
        ClipboardClient->>ClipboardClient: set source = sdk
        opt ttl_seconds is not None
            ClipboardClient->>ClipboardClient: add ttlSeconds to payload
        end
        ClipboardClient->>HttpClient: request(POST, /api/clipboard, json=payload)
        HttpClient->>ClipboardAPI: POST /api/clipboard
        ClipboardAPI-->>HttpClient: 200 ClipboardEntry JSON
        HttpClient-->>ClipboardClient: ClipboardEntry JSON
        ClipboardClient-->>Caller: ClipboardEntry(source=sdk)
    end
Loading

Sequence diagram for clear_all counting only successful clipboard deletions

sequenceDiagram
    actor Caller
    participant ClipboardClient
    participant HttpClient
    participant ClipboardAPI

    Caller->>ClipboardClient: clear_all()
    ClipboardClient->>HttpClient: request(GET, /api/clipboard)
    HttpClient->>ClipboardAPI: GET /api/clipboard
    ClipboardAPI-->>HttpClient: list ClipboardEntry JSON
    HttpClient-->>ClipboardClient: entries
    ClipboardClient->>ClipboardClient: deleted = 0
    loop for each entry in entries
        alt entry has name
            ClipboardClient->>ClipboardClient: success = delete(name)
        else entry has idx
            ClipboardClient->>ClipboardClient: success = delete(idx)
        else no name or idx
            ClipboardClient->>ClipboardClient: success = False
        end
        alt success is True
            ClipboardClient->>ClipboardClient: deleted += 1
        else success is False
            ClipboardClient->>ClipboardClient: skip increment
        end
        opt delete raises PluggedInError
            ClipboardClient->>ClipboardClient: ignore error
        end
    end
    ClipboardClient-->>Caller: deleted
Loading

Class diagram for updated clipboard types with source tracking

classDiagram
    class ClipboardVisibility {
      <<enumeration>>
      PRIVATE
      WORKSPACE
      PUBLIC
    }

    class ClipboardSource {
      <<enumeration>>
      UI
      SDK
      MCP
    }

    class ClipboardEntry {
      +str uuid
      +str name
      +int idx
      +str value
      +str content_type
      +ClipboardEncoding encoding
      +int size_bytes
      +ClipboardVisibility visibility
      +str created_by_tool
      +str created_by_model
      +ClipboardSource source
      +datetime created_at
      +datetime updated_at
      +datetime expires_at
    }

    class ClipboardEncoding {
      <<enumeration>>
      UTF8
      BASE64
      HEX
    }

    class DEFAULT_CLIPBOARD_SOURCE {
      <<constant>>
      +ClipboardSource value
    }

    ClipboardEntry --> ClipboardVisibility : uses
    ClipboardEntry --> ClipboardEncoding : uses
    ClipboardEntry --> ClipboardSource : uses
    DEFAULT_CLIPBOARD_SOURCE --> ClipboardSource : default_for_legacy_entries
Loading

File-Level Changes

Change Details Files
Add clipboard source tracking to entries created via the SDK.
  • Extend clipboard set and push payloads to always include a hardcoded 'source': 'sdk' field for both sync and async methods.
  • Introduce ClipboardSource enum (ui/sdk/mcp) and DEFAULT_CLIPBOARD_SOURCE constant for backward-compatible typing of clipboard entry source.
  • Update ClipboardEntry model to include an optional source field with a default value for older API responses.
  • Re-export ClipboardSource and DEFAULT_CLIPBOARD_SOURCE from the package root so users can reference them.
src/pluggedinkit/services/clipboard.py
src/pluggedinkit/types.py
src/pluggedinkit/__init__.py
Tighten TTL handling and deletion accounting for clipboard operations.
  • Change ttl_seconds handling in set/push (sync and async) to check explicitly for None and raise ValueError if a non-positive value is provided.
  • Update clear_all (sync and async) to track whether delete() calls succeed and increment the deleted count only on successful deletions, while still swallowing PluggedInError exceptions.
src/pluggedinkit/services/clipboard.py
Document clipboard features and the new source behavior in the README.
  • Add clipboard/memory as a documented feature in the feature list.
  • Add a new 'Clipboard Operations' section with examples for set/get, push/pop, list/delete, and entry structure.
  • Document that the source field is auto-set to ClipboardSource.SDK when using this SDK and describe its purpose.
README.md

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey there - I've reviewed your changes - here's some feedback:

  • In ClipboardEntry, consider making source a non-optional ClipboardSource with a default (or using Field(default=DEFAULT_CLIPBOARD_SOURCE)), since it now always has a meaningful default and your comment implies it should only be absent in older API responses.
  • In the clipboard service, you currently hardcode the string literal "sdk" for the payload source field; using the ClipboardSource.SDK.value enum instead would avoid a magic string and keep things consistent with the new type.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- In `ClipboardEntry`, consider making `source` a non-optional `ClipboardSource` with a default (or using `Field(default=DEFAULT_CLIPBOARD_SOURCE)`), since it now always has a meaningful default and your comment implies it should only be absent in older API responses.
- In the clipboard service, you currently hardcode the string literal `"sdk"` for the payload `source` field; using the `ClipboardSource.SDK.value` enum instead would avoid a magic string and keep things consistent with the new type.

## Individual Comments

### Comment 1
<location> `src/pluggedinkit/services/clipboard.py:176-179` </location>
<code_context>
+                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:
</code_context>

<issue_to_address>
**issue (bug_risk):** clear_all now depends on delete returning a truthy value, which may change existing behavior.

`deleted` used to increment after every `delete` call unless an exception was raised. Now it only increments when `delete` returns truthy, which changes semantics if `delete` historically returned `None` or didn’t signal success. That could cause `clear_all` to underreport deletions. Consider keeping the old behavior (increment on non-exception) or update `delete` to reliably return a boolean and document the change in behavior.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment on lines +176 to +179
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:

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue (bug_risk): clear_all now depends on delete returning a truthy value, which may change existing behavior.

deleted used to increment after every delete call unless an exception was raised. Now it only increments when delete returns truthy, which changes semantics if delete historically returned None or didn’t signal success. That could cause clear_all to underreport deletions. Consider keeping the old behavior (increment on non-exception) or update delete to reliably return a boolean and document the change in behavior.

@claude

claude Bot commented Nov 29, 2025

Copy link
Copy Markdown

Pull Request Review: Feature/clipboard source tracking

Overview

This PR adds clipboard source tracking to distinguish between entries created via UI, SDK, or MCP. The implementation includes new enums, automatic source attribution, bug fixes, and comprehensive documentation.

✅ Strengths

1. Code Quality & Best Practices

  • Clean enum implementation: ClipboardSource follows the established pattern used by other enums in the codebase (DocumentSource, ClipboardVisibility, etc.)
  • Type safety: Proper use of Pydantic models with field aliasing and validation
  • Backward compatibility: Using Optional[ClipboardSource] with a sensible default (DEFAULT_CLIPBOARD_SOURCE = ClipboardSource.UI) ensures existing data without source fields won't break
  • Consistent API: The source: "sdk" is automatically set in all clipboard operations (set and push), removing burden from SDK users
  • Documentation: Excellent README additions with clear examples and field descriptions

2. Bug Fixes

  • clear_all over-counting fix (lines 174-180, 340-346): Now correctly tracks only successful deletions by checking the return value before incrementing. This prevents misleading deletion counts.
  • TTL validation (lines 88-90, 124-126, 254-256, 290-292): Explicit validation of ttl_seconds to reject non-positive values is a good defensive programming practice

3. API Design

  • Automatic source attribution: Hardcoding "source": "sdk" removes potential for user error and ensures consistent tracking
  • Default value strategy: Using DEFAULT_CLIPBOARD_SOURCE.UI as the default is reasonable for backward compatibility

⚠️ Issues & Recommendations

1. CRITICAL: Inconsistent Field Checking

Location: src/pluggedinkit/services/clipboard.py:88-90, 124-126, 254-256, 290-292

Issue: The TTL validation change from if ttl_seconds: to if ttl_seconds is not None: is correct, but there's an inconsistency:

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

Problem: ttl_seconds <= 0 allows 0, but the error message says "must be greater than 0". Should be:

if ttl_seconds <= 0:  # This rejects 0 and negative

OR change message to "must be positive" if 0 should also be rejected.

Recommendation: Clarify if ttl_seconds=0 should be valid (no expiration?) or invalid. The validation should match the intent.

2. MEDIUM: Type Inconsistency in Pydantic Model

Location: src/pluggedinkit/types.py:334

Issue: The Pydantic model defines:

ttl_seconds: Optional[int] = Field(None, alias="ttlSeconds", gt=0)

This uses Pydantic's gt=0 validator (greater than 0), which will reject 0 automatically. However, the manual validation in the service layer checks <= 0.

Recommendation:

  • Remove the manual validation in clipboard.py since Pydantic already validates gt=0
  • OR keep manual validation but remove gt=0 from the Field definition to avoid redundancy
  • Consistency is key here

3. MEDIUM: clear_all() Logic Issue

Location: src/pluggedinkit/services/clipboard.py:174-180, 340-346

Issue: The fix initializes success = False but then checks it conditionally:

success = False
if entry.name:
    success = self.delete(name=entry.name)
elif entry.idx is not None:
    success = self.delete(idx=entry.idx)
if success:
    deleted += 1

Problem: If entry.name is None/empty AND entry.idx is None, success remains False and the entry is silently skipped. This might be correct behavior, but should be explicit.

Recommendation: Add logging or handle the edge case explicitly:

success = False
if entry.name:
    success = self.delete(name=entry.name)
elif entry.idx is not None:
    success = self.delete(idx=entry.idx)
else:
    # Log warning: entry without name or idx
    continue
if success:
    deleted += 1

4. LOW: Hardcoded String Literal

Location: src/pluggedinkit/services/clipboard.py:81, 117, 247, 283

Issue: "source": "sdk" is hardcoded as a string literal instead of using the enum.

Recommendation: Use the enum for consistency:

"source": ClipboardSource.SDK.value  # or just ClipboardSource.SDK if the API serializes enums

This prevents typos and makes the code more maintainable.

5. LOW: Missing Type Hints

The code follows the repository's type hinting patterns well, but the delete method return type isn't explicitly shown in the diff. Ensure it returns bool consistently.

🔒 Security Considerations

✅ Good

  • No new security vulnerabilities introduced
  • Source tracking helps with audit trails and attribution
  • TTL validation prevents potential DoS via immediate expirations

⚠️ Consider

  • Source spoofing: The hardcoded "source": "sdk" is good, but ensure the backend API also validates/enforces this. A malicious client could potentially bypass SDK validation and send different source values directly to the API.
  • TTL edge cases: Ensure the backend handles very large ttl_seconds values (e.g., INT_MAX) gracefully to prevent overflow issues.

⚡ Performance Considerations

✅ Good

  • No performance regressions introduced
  • The additional validation is negligible overhead

💡 Suggestion

  • clear_all() iterates through all entries and deletes them one-by-one. For large datasets, consider asking if the backend API supports bulk delete operations that could be more efficient.

🧪 Test Coverage

❌ CRITICAL: No Tests Included

Issue: This PR adds new functionality and bug fixes but includes no test files. The repository has pytest configured but no tests directory exists.

Required Tests:

  1. Source tracking: Verify source field is automatically set to "sdk"
  2. TTL validation: Test that ttl_seconds=0 and negative values are rejected
  3. clear_all bug fix: Verify deletion count is accurate
  4. Backward compatibility: Test that entries without source field work correctly
  5. Edge cases: Test entries with neither name nor idx

Recommendation: Add a tests/test_clipboard.py file with comprehensive unit tests before merging.

📋 Documentation

✅ Excellent

  • README additions are clear and comprehensive
  • Examples cover all major operations
  • Field documentation is thorough
  • Note about automatic source setting is helpful

💡 Minor Suggestions

  1. Add a "Source Tracking" section explaining the three source types and their meanings
  2. Document the TTL behavior more clearly (what happens when TTL expires?)
  3. Consider adding error handling examples

Summary

Category Rating Notes
Code Quality ⭐⭐⭐⭐ Clean, follows patterns, good practices
Bug Fixes ⭐⭐⭐⭐ Addresses real issues correctly
Security ⭐⭐⭐⭐ No new vulnerabilities, good audit trail
Performance ⭐⭐⭐⭐⭐ No concerns
Documentation ⭐⭐⭐⭐⭐ Excellent coverage
Test Coverage ⭐ Missing - must add

🚦 Recommendation: Request Changes

Required Before Merge:

  1. ✅ Fix TTL validation inconsistency (message vs logic)
  2. ✅ Resolve Pydantic vs manual validation redundancy
  3. ✅ Add test coverage (at minimum: source tracking, TTL validation, clear_all accuracy)

Suggested Improvements:

  1. Use ClipboardSource.SDK.value instead of string literal
  2. Handle edge case in clear_all() for entries without name/idx
  3. Verify backend API enforces source attribution

Once the required items are addressed, this will be a solid addition to the SDK. The feature is well-designed and the documentation is exemplary. Great work overall! 🎉


Review generated with Claude Code

- Import ClipboardSource from types
- Replace hardcoded "sdk" string with ClipboardSource.SDK.value
- Consistent with enum usage across the SDK
@claude

claude Bot commented Nov 29, 2025

Copy link
Copy Markdown

Pull Request Review: Clipboard Source Tracking

Overview

This PR introduces clipboard source tracking functionality to distinguish between entries created via UI, SDK, or MCP. The implementation includes new enums, model updates, validation improvements, and comprehensive documentation.

Strengths

Code Quality

  • Excellent enum usage: The ClipboardSource enum provides type-safe source attribution
  • Backward compatibility: Making source optional with DEFAULT_CLIPBOARD_SOURCE fallback prevents breaking existing API integrations
  • Consistent implementation: Source tracking is correctly applied in all 4 locations
  • DRY principle: Both sync and async classes maintain identical logic patterns

Bug Fixes

  • TTL validation improvement: Changed from truthiness check to explicit is not None check
  • clear_all counter fix: Now only increments when delete() returns True

Documentation

  • Comprehensive README updates with clear examples
  • Well-documented ClipboardEntry structure with all fields explained

Critical Issues

1. Incorrect Default Source Value
Location: src/pluggedinkit/types.py:310

The default value is set to ClipboardSource.UI, but when SDK creates entries, they should have source=sdk. If the API does not return a source field, entries created by the SDK will incorrectly show source=ui instead of source=sdk.

Recommendation: Set default to None and handle the fallback logic in a property accessor or Pydantic validator.

2. Missing Test Coverage
No tests found for the new functionality. Need tests for:

  • Source field correctly set to sdk in set() and push()
  • Source field deserialization with/without value from API
  • TTL validation edge cases
  • clear_all() counter accuracy
  • Backward compatibility

High Priority Issues

3. TTL Validation Security
No upper bound validation. Extremely large TTL values could cause integer overflow on the backend or resource exhaustion.

Recommendation: Add reasonable upper bound (e.g., 1 year = 31,536,000 seconds)

4. Type Safety in Parameters
Parameters use str instead of proper enum types, allowing invalid values.

Recommendation: Use Union[str, EnumType] in function signatures for better IDE autocomplete and type safety.

Medium Priority Issues

5. clear_all() Performance
Makes N+1 API calls (1 to list, N to delete). For 100 entries, makes 101 HTTP requests. Silently swallows all exceptions.

Recommendations:

  • Consider a dedicated DELETE /api/clipboard/all endpoint
  • Add logging for failed deletions
  • Document the N+1 behavior as a known limitation

6. Missing Async Examples
README shows clipboard examples only with sync client. Add async examples for consistency.

7. Code Duplication
TTL validation logic is duplicated 4 times. Extract into a helper function.

Minor Issues

8. Unused Type Imports in README
Documentation imports ClipboardEntry and ClipboardSource but does not use them in code examples.

Recommendations Priority

Critical (Must Fix Before Merge)

  1. Fix default source value in Pydantic model
  2. Add test coverage for source tracking

High Priority
3. Add TTL upper bound validation
4. Improve type safety with enums in function signatures

Medium Priority
5. Document clear_all() performance limitation
6. Add async examples to README
7. Reduce code duplication

Low Priority
8. Clean up unused imports in docs

Final Verdict

Status: Needs changes before merge

The PR demonstrates solid engineering with excellent backward compatibility handling and comprehensive documentation. However, the default source value issue could cause incorrect attribution, and the lack of tests is concerning for a feature tracking data provenance.

Estimated effort: 2-3 hours

  • Fix default source: 30 minutes
  • Add basic test coverage: 1-2 hours
  • Address other issues: 30-60 minutes

Great work on the careful refactoring across commits!

@ckaraca
ckaraca merged commit f78568c into feature/clipboard-client Nov 29, 2025
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant