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 case | Mode | SDK method |
|---|---|---|
| Get what the user is viewing right now | REST | getCurrentContext() |
| Search browsing history | REST | searchContexts() |
| React to navigation in real time | WebSocket | subscribe("current-context", ...) |
| Monitor all browsing activity | WebSocket | subscribe("user-contexts", ...) |
| Track tab switching patterns | WebSocket | subscribe("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:
| Option | Type | Default | Description |
|---|---|---|---|
token | string | — | API key for authentication (required) |
userId | string | — | User ID to scope requests to (required) |
baseUrl | string | https://periscope.uselovelace.com | API base URL |
reconnect | boolean | true | Auto-reconnect on WebSocket disconnect |
maxReconnectAttempts | number | 10 | Maximum reconnection attempts before giving up |
reconnectBackoff | string | "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
- Getting Started — Make your first API call
- REST API Reference — Complete endpoint documentation
- WebSocket API Reference — Real-time streaming protocol
- Agent Integration Guide — How agents consume browser context
- Privacy & Security — Configure privacy levels and filtering