@surfjs/client
v0.5.0
Published
Agent-side client SDK for Surf-enabled sites and apps
Readme
@surfjs/client
The agent-side SDK for Surf-enabled websites.
Discover, connect, and execute commands on any Surf-enabled website. Built for AI agents and automation — no scraping, no vision models, just typed commands.
Part of the Surf.js ecosystem. See @surfjs/core for the server-side library.
npm install @surfjs/clientQuick Start
import { SurfClient } from '@surfjs/client';
// Discover and connect
const client = await SurfClient.discover('https://example.com');
// List available commands
console.log(client.commands());
// Execute a command
const results = await client.execute('search', { query: 'shoes' });API
SurfClient.discover(url, options?): Promise<SurfClient>
Discover a Surf-enabled site by fetching its manifest. Tries /.well-known/surf.json first, falls back to HTML <meta name="surf"> tag.
const client = await SurfClient.discover('https://example.com', {
auth: 'bearer-token',
retry: { maxAttempts: 3, backoffMs: 500, backoffMultiplier: 2 },
cache: { ttlMs: 30000, maxSize: 100 },
discoverTimeout: 5000,
fetch: customFetch,
});SurfClient.fromManifest(manifest, options): SurfClient
Create a client with a pre-loaded manifest (skip discovery):
const client = SurfClient.fromManifest(manifest, { baseUrl: 'https://example.com' });SurfClientOptions
interface SurfClientOptions {
baseUrl: string;
auth?: string; // Bearer token
fetch?: typeof globalThis.fetch; // Custom fetch implementation
retry?: RetryConfig;
cache?: CacheConfig;
discoverTimeout?: number; // Discovery timeout in ms (default: 5000)
}Instance Methods
client.commands(): Record<string, ManifestCommand>
Returns all available commands from the manifest.
client.command(name): ManifestCommand | undefined
Get a specific command definition.
client.execute(command, params?): Promise<unknown>
Execute a command via HTTP. Respects retry and cache configuration.
const products = await client.execute('search', { query: 'shoes', maxPrice: 100 });Cache behavior:
- Commands with
hints.sideEffects: truebypass the cache - Use
client.clearCache()orclient.clearCache('commandName')to invalidate
client.pipeline(steps, options?): Promise<PipelineResponse>
Execute multiple commands in a single HTTP round-trip:
const result = await client.pipeline([
{ command: 'search', params: { query: 'shoes' }, as: 'results' },
{ command: 'getProduct', params: { id: '$results[0].id' } },
{ command: 'addToCart', params: { sku: '$results[0].sku' } },
], { sessionId: 'optional', continueOnError: true });
result.results.forEach(step => {
console.log(`${step.command}: ${step.ok ? '✅' : '❌'}`, step.result ?? step.error);
});client.typed<T>(): TypedClient<T>
Returns a typed proxy client with full TypeScript inference:
interface Product { id: string; name: string; price: number; }
const typed = client.typed<{
search: { params: { query: string; maxPrice?: number }; result: Product[] };
addToCart: { params: { sku: string; quantity?: number }; result: { added: boolean } };
}>();
const products = await typed.search({ query: 'shoes' }); // → Product[]
await typed.addToCart({ sku: 'SHOE-001' }); // → { added: boolean }client.startSession(): Promise<SurfSession>
Start a stateful session:
const session = await client.startSession();
console.log(session.id); // Session ID
console.log(session.state); // Current state (updated after each execute)
await session.execute('addToCart', { sku: 'SHOE-001' });
await session.execute('addToCart', { sku: 'HAT-002' });
const cart = await session.execute('getCart');
await session.end();client.connect(): Promise<WebSocketTransport>
Connect via WebSocket for real-time interaction:
const ws = await client.connect();
ws.on('orderUpdate', (data) => console.log(data));
const result = await ws.execute('search', { query: 'shoes' });
ws.close();client.disconnect(): void
Close the WebSocket connection if connected.
client.checkForUpdates(): Promise<UpdateCheckResult>
Re-fetch the manifest and check if the checksum changed:
const { changed, checksum, manifest } = await client.checkForUpdates();
if (changed) console.log('Manifest updated!', manifest);client.clearCache(command?): void
Clear the response cache. Pass a command name to clear only that command's cache.
client.manifest: SurfManifest
The discovered manifest object (read-only).
Transports
HttpTransport
Default transport — used by SurfClient.execute() internally.
import { HttpTransport } from '@surfjs/client';
const http = new HttpTransport({
baseUrl: 'https://example.com',
auth: 'token',
fetch: globalThis.fetch,
});
const response = await http.execute('search', { query: 'shoes' }, sessionId);
await http.startSession();
await http.endSession(sessionId);WebSocketTransport
Real-time bidirectional transport:
import { WebSocketTransport } from '@surfjs/client';
const ws = new WebSocketTransport();
await ws.connect('ws://example.com/surf/ws', 'auth-token');
// Execute commands
const response = await ws.execute('search', { query: 'shoes' });
// Subscribe to events
const unsub = ws.on('orderUpdate', (data) => console.log(data));
// Sessions
const sessionId = await ws.startSession();
await ws.endSession();
// Status
console.log(ws.connected); // true/false
ws.close();WindowTransport
For browser-based agents using window.__surf__:
import { WindowTransport } from '@surfjs/client';
const win = new WindowTransport();
await win.connect(); // Waits for window.__surf__ or surf:ready event
const manifest = win.discover();
const result = await win.execute('search', { query: 'shoes' });
win.on('event', (data) => {});
win.authenticate('token');
console.log(win.connected); // true/falseDiscovery
import { discoverManifest } from '@surfjs/client';
const manifest = await discoverManifest('https://example.com', fetch, 5000);
// Tries /.well-known/surf.json → HTML <meta name="surf"> fallbackRetry Configuration
interface RetryConfig {
maxAttempts: number; // Max retry attempts (default: 3)
backoffMs: number; // Initial backoff in ms (default: 500)
backoffMultiplier: number; // Exponential multiplier (default: 2)
retryOn?: number[]; // HTTP codes to retry (default: [429, 502, 503, 504])
}Cache Configuration
interface CacheConfig {
ttlMs: number; // Cache TTL in milliseconds
maxSize: number; // Maximum cached entries
}Error Handling
import { SurfClientError } from '@surfjs/client';
try {
await client.execute('nonexistent');
} catch (e) {
if (e instanceof SurfClientError) {
console.log(e.code); // 'UNKNOWN_COMMAND'
console.log(e.message); // Human-readable message
console.log(e.statusCode); // HTTP status (if available)
console.log(e.retryAfter); // Seconds to wait (for RATE_LIMITED)
}
}Exports
import {
SurfClient,
SurfClientError,
discoverManifest,
HttpTransport,
WebSocketTransport,
WindowTransport,
// Types
type SurfManifest, type ManifestCommand, type SurfClientOptions,
type SurfSession, type UpdateCheckResult,
type ParamSchema, type ParamType, type TypeRef, type CommandHints,
type AuthConfig, type EventDefinition, type TypeDefinition,
type SurfErrorCode, type ExecuteResponse, type ErrorResponse, type SurfResponse,
type RetryConfig, type CacheConfig,
type TypedCommands, type TypedClient,
type PipelineStep, type PipelineStepResult, type PipelineResponse,
} from '@surfjs/client';