Skip to main content

Periscope SDKs

Typed client libraries for integrating browser context into your agents and applications

The Periscope SDKs wrap both the REST API and WebSocket API into ergonomic, type-safe clients. They handle authentication, heartbeats, reconnection with exponential backoff, and channel subscription management automatically.

Choosing REST vs WebSocket

The SDKs support both modes. Choose based on your use case:

Use caseModeSDK method
Get what the user is viewing right nowRESTgetCurrentContext()
Search browsing historyRESTsearchContexts()
React to navigation in real timeWebSocketsubscribe("current-context", ...)
Monitor all browsing activityWebSocketsubscribe("user-contexts", ...)
Track tab switching patternsWebSocketsubscribe("tab-events", ...)

Most agents use WebSocket for real-time awareness and REST for historical queries. See the Agent Integration Guide for recommended patterns.


TypeScript SDK

Installation

bash
pnpm add @lovelace-ai/periscope-client

Configuration

typescript
import { PeriscopeClient } from "@lovelace-ai/periscope-client";

const client = new PeriscopeClient({
  token: "YOUR_API_KEY",
  userId: "user_456",
  baseUrl: "https://periscope.uselovelace.com", // optional, this is the default
});

Constructor options:

OptionTypeDefaultDescription
tokenstringAPI key for authentication (required)
userIdstringUser ID to scope requests to (required)
baseUrlstringhttps://periscope.uselovelace.comAPI base URL
reconnectbooleantrueAuto-reconnect on WebSocket disconnect
maxReconnectAttemptsnumber10Maximum reconnection attempts before giving up
reconnectBackoffstring"exponential"Backoff strategy: "exponential" or "linear"

REST API Methods

typescript
// Get current browser context
const current = await client.getCurrentContext();
console.log(current.url, current.title);

// Get a specific context by ID
const context = await client.getContext("ctx_abc123");

// Get context history with filters
const history = await client.getContextHistory({
  limit: 20,
  since: new Date("2026-03-01"),
  type: "page",
});

// Search contexts by content or URL
const results = await client.searchContexts({
  query: "authentication",
  urlPattern: "docs.example.com",
  limit: 10,
});
for (const ctx of results) {
  console.log(ctx.url, ctx.snippet, ctx.relevance);
}

// Delete a context
await client.deleteContext("ctx_abc123");

WebSocket Streaming

typescript
// Connect to WebSocket
await client.connect();

// Subscribe to real-time context updates
client.subscribe("current-context", (context) => {
  console.log("Now viewing:", context.url);
  console.log("Title:", context.title);
  console.log("Type:", context.type);
});

// Subscribe to tab events
client.subscribe("tab-events", (event) => {
  console.log(`Tab ${event.type}:`, event.tabId, event.url);
});

// Subscribe to all context changes
client.subscribe("user-contexts", (context) => {
  console.log("Context changed:", context.contextId, context.url);
});

// Unsubscribe from a channel
client.unsubscribe("tab-events");

// Handle connection events
client.on("disconnect", (reason) => {
  console.log("Disconnected:", reason);
  // Client auto-reconnects by default
});

client.on("reconnect", () => {
  console.log("Reconnected successfully");
  // Subscriptions are automatically restored
});

client.on("error", (err) => {
  console.error("Connection error:", err);
});

// Disconnect when done
client.disconnect();

Error Handling

The SDK throws typed errors that can be caught and handled:

typescript
import {
  PeriscopeClient,
  PeriscopeError,
  type ContextId,
} from "@lovelace-ai/periscope-client";

try {
  const context = await client.getContext("ctx_nonexistent" as ContextId);
} catch (error) {
  if (error instanceof PeriscopeError) {
    switch (error.code) {
      case "not_found":
        console.log("Context does not exist");
        break;
      case "unauthorized":
        console.log("Invalid API key");
        break;
      case "rate_limited":
        console.log(`Rate limited, retry after ${error.retryAfter}s`);
        break;
      default:
        console.error("API error:", error.code, error.message);
    }
  }
}

Type Safety

The SDK uses branded types for compile-time safety, preventing accidental misuse of identifiers:

typescript
import {
  type ContextId,
  type UserId,
  type TabId,
  type BrowserContext,
} from "@lovelace-ai/periscope-client";

// These are distinct branded string types — you can't mix them up
function processContext(contextId: ContextId, userId: UserId): void {
  // TypeScript prevents passing a UserId where ContextId is expected
}

// BrowserContext is fully typed with all 7 context types
function handleContext(ctx: BrowserContext): void {
  switch (ctx.type) {
    case "page":
      console.log("Page content:", ctx.content);
      break;
    case "selection":
      console.log("Selected text:", ctx.selection?.text);
      break;
    case "form":
      console.log("Form fields:", ctx.formData?.fields);
      break;
  }
}

Rust SDK

Installation

toml
# Cargo.toml
[dependencies]
periscope-client = "0.1"
tokio = { version = "1", features = ["full"] }

Basic Usage

rust
use periscope_client::PeriscopeClient;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let client = PeriscopeClient::builder()
        .token("YOUR_API_KEY")
        .user_id("user_456")
        .build()?;

    // Get current context
    let current = client.get_current_context().await?;
    println!("URL: {}", current.url);
    println!("Title: {}", current.title);

    // Search history
    let results = client.search_contexts("authentication")
        .limit(10)
        .send()
        .await?;

    for ctx in results {
        println!("{}: {}", ctx.url, ctx.timestamp);
    }

    Ok(())
}

Real-Time Streaming

rust
use periscope_client::{PeriscopeClient, Channel};

let client = PeriscopeClient::builder()
    .token("YOUR_API_KEY")
    .user_id("user_456")
    .build()?;

let mut stream = client.connect_websocket().await?;

stream.subscribe(Channel::CurrentContext).await?;
stream.subscribe(Channel::TabEvents).await?;

while let Some(event) = stream.next().await {
    match event {
        Ok(msg) => {
            println!("Event: {} - {}", msg.event_type, msg.payload.url);
        }
        Err(e) => {
            eprintln!("Stream error: {}", e);
            // Client handles reconnection automatically
        }
    }
}

Error Handling

rust
use periscope_client::{PeriscopeClient, PeriscopeError};

match client.get_context("ctx_abc123").await {
    Ok(context) => println!("Found: {}", context.url),
    Err(PeriscopeError::NotFound) => println!("Context not found"),
    Err(PeriscopeError::Unauthorized) => println!("Invalid API key"),
    Err(PeriscopeError::RateLimited { retry_after }) => {
        println!("Rate limited, retry after {}s", retry_after);
    }
    Err(e) => eprintln!("Unexpected error: {}", e),
}

Type Safety

The Rust SDK uses newtype wrappers for identifiers:

rust
use periscope_client::{ContextId, UserId, TabId};

// These are distinct types — the compiler prevents mixing them
let context_id: ContextId = ContextId::new("ctx_abc123");
let user_id: UserId = UserId::new("user_456");

// This would fail to compile:
// let wrong: ContextId = user_id;  // Error: expected ContextId, found UserId

Next Steps