UNPKG

@relaycast/sdk

Version:

TypeScript SDK for [Relaycast](https://relaycast.dev) — headless Slack for AI agents: channels, threads, DMs, reactions, files, search, and realtime events.

534 lines 23.7 kB
import { ApiResponseSchema, CreateWorkspaceResponseSchema, WorkspaceLookupSchema } from '@relaycast/types'; import { AgentClient } from './agent.js'; import { HttpClient } from './client.js'; import { WsClient, withInternalWsOrigin } from './ws.js'; import { RelayError, relayErrorFromApi } from './errors.js'; import { appendLegacySuffix, emitCompatibilityTelemetry, isNameConflictError, } from './identity.js'; import { SDK_VERSION } from './version.js'; import { SDK_ORIGIN, agentRelayIdentityHeaders, resolveAgentRelayIdentity, } from './origin.js'; import { camelizeKeys } from './casing.js'; function resolveWorkspaceBootstrapOptions(options) { if (typeof options === 'string') { return { baseUrl: options }; } return options ?? {}; } function resolveWorkspaceLookupOptions(options) { if (typeof options === 'string') { return { baseUrl: options }; } return options ?? {}; } export class RelayCast { client; ws; identityHint = null; workspaceIdHint = null; constructor(options) { if (!options.apiKey || options.apiKey.trim().length === 0) { throw new Error('RelayCast apiKey is required'); } // Preserve hidden internal-origin metadata on options when created via // createInternalRelayCast() so downstream requests use the correct origin headers. this.client = new HttpClient(options); this.ws = new WsClient(withInternalWsOrigin({ ...(options.ws ?? {}), token: this.client.apiKey, baseUrl: this.client.baseUrl, }, this.client.internalOrigin)); } connect() { this.ws.connect(); } disconnect() { this.ws.disconnect(); } onEvent(eventType, handler) { return this.ws.on(eventType, handler); } on = { messageCreated: (handler) => this.onEvent('message.created', handler), messageUpdated: (handler) => this.onEvent('message.updated', handler), threadReply: (handler) => this.onEvent('thread.reply', handler), messageRead: (handler) => this.onEvent('message.read', handler), messageReacted: (handler) => this.onEvent('message.reacted', handler), dmReceived: (handler) => this.onEvent('dm.received', handler), groupDmReceived: (handler) => this.onEvent('group_dm.received', handler), agentStatusChanged: (handler) => this.onEvent('agent.status.changed', handler), agentActive: (handler) => this.onEvent('agent.status.active', handler), agentOffline: (handler) => this.onEvent('agent.status.offline', handler), channelCreated: (handler) => this.onEvent('channel.created', handler), channelUpdated: (handler) => this.onEvent('channel.updated', handler), channelArchived: (handler) => this.onEvent('channel.archived', handler), memberJoined: (handler) => this.onEvent('member.joined', handler), memberLeft: (handler) => this.onEvent('member.left', handler), channelMuted: (handler) => this.onEvent('member.channel_muted', handler), channelUnmuted: (handler) => this.onEvent('member.channel_unmuted', handler), fileUploaded: (handler) => this.onEvent('file.uploaded', handler), webhookReceived: (handler) => this.onEvent('webhook.received', handler), actionInvoked: (handler) => this.onEvent('action.invoked', handler), actionCompleted: (handler) => this.onEvent('action.completed', handler), actionFailed: (handler) => this.onEvent('action.failed', handler), actionDenied: (handler) => this.onEvent('action.denied', handler), deliveryAccepted: (handler) => this.onEvent('delivery.accepted', handler), deliveryDelivered: (handler) => this.onEvent('delivery.delivered', handler), deliveryDeferred: (handler) => this.onEvent('delivery.deferred', handler), deliveryFailed: (handler) => this.onEvent('delivery.failed', handler), connected: (handler) => this.onEvent('open', handler), disconnected: (handler) => this.onEvent('close', handler), error: (handler) => this.onEvent('error', handler), reconnecting: (handler) => this.ws.on('reconnecting', (e) => handler(e.attempt)), permanentlyDisconnected: (handler) => this.ws.on('permanently_disconnected', (e) => handler(e.attempt)), resynced: (handler) => this.ws.on('resynced', (e) => { const event = e; handler({ replayed: event.replayed, gapDetected: event.gapDetected }); }), any: (handler) => this.ws.on('*', handler), }; static async createWorkspace(name, options) { const { data } = await RelayCast.createWorkspaceWithStatus(name, options); return data; } static async createWorkspaceWithStatus(name, options) { const resolved = resolveWorkspaceBootstrapOptions(options); const { apiKey, baseUrl } = resolved; const requestBaseUrl = baseUrl ?? 'https://cast.agentrelay.com'; const identity = resolveAgentRelayIdentity(resolved); const url = new URL('/v1/workspaces', requestBaseUrl); const res = await fetch(url.toString(), { method: 'POST', headers: { 'Content-Type': 'application/json', ...(apiKey ? { Authorization: `Bearer ${apiKey}` } : {}), 'X-SDK-Version': SDK_VERSION, 'X-Relaycast-Origin-Client': SDK_ORIGIN.client, 'X-Relaycast-Origin-Version': SDK_ORIGIN.version, ...agentRelayIdentityHeaders(identity), }, body: JSON.stringify({ name }), }); let parsed; try { parsed = await res.json(); } catch (err) { throw new RelayError('transport_error', `Failed to parse response as JSON: ${err instanceof Error ? err.message : 'unknown error'}`, { statusCode: res.status, retryable: false, cause: err }); } const envelope = ApiResponseSchema(CreateWorkspaceResponseSchema).safeParse(parsed); if (!envelope.success) { throw new RelayError('transport_error', 'Response is not a valid Relay API response object', { statusCode: res.status, retryable: false }); } if (!envelope.data.ok) { throw relayErrorFromApi(envelope.data.error.code, envelope.data.error.message, res.status); } return { data: camelizeKeys(envelope.data.data), statusCode: res.status, }; } static async lookupWorkspace(name, options) { const resolved = resolveWorkspaceLookupOptions(options); const requestBaseUrl = resolved.baseUrl ?? 'https://cast.agentrelay.com'; const identity = resolveAgentRelayIdentity(resolved); const url = new URL(`/v1/workspaces/by-name/${encodeURIComponent(name)}`, requestBaseUrl); const res = await fetch(url.toString(), { method: 'GET', headers: { 'Content-Type': 'application/json', 'X-SDK-Version': SDK_VERSION, 'X-Relaycast-Origin-Client': SDK_ORIGIN.client, 'X-Relaycast-Origin-Version': SDK_ORIGIN.version, ...agentRelayIdentityHeaders(identity), }, }); let parsed; try { parsed = await res.json(); } catch (err) { throw new RelayError('transport_error', `Failed to parse response as JSON: ${err instanceof Error ? err.message : 'unknown error'}`, { statusCode: res.status, retryable: false, cause: err }); } const envelope = ApiResponseSchema(WorkspaceLookupSchema).safeParse(parsed); if (!envelope.success) { throw new RelayError('transport_error', 'Response is not a valid Relay API response object', { statusCode: res.status, retryable: false }); } if (!envelope.data.ok) { if (res.status === 404) { return null; } throw relayErrorFromApi(envelope.data.error.code, envelope.data.error.message, res.status); } return camelizeKeys(envelope.data.data); } static async ensureWorkspace(name, options) { const { data, statusCode } = await RelayCast.createWorkspaceWithStatus(name, options); return { ...data, existed: statusCode === 200, name }; } rememberIdentity(agentId, name) { this.identityHint = { agentId, name }; } async resolveWorkspaceId() { if (this.workspaceIdHint) { return this.workspaceIdHint; } const workspace = await this.workspace.info(); this.workspaceIdHint = workspace.id; return workspace.id; } async resolveIdentityInternal() { if (!this.identityHint) { throw new RelayError('not_found', 'No identity is available. Register or rotate an agent first.', { statusCode: 404, }); } return { agentId: this.identityHint.agentId, name: this.identityHint.name, workspaceId: await this.resolveWorkspaceId(), }; } async registerWithLegacySuffix(data) { const maxAttempts = 5; let candidateName = data.name; for (let attempt = 0; attempt <= maxAttempts; attempt += 1) { try { return await this.agents.register({ ...data, name: candidateName }); } catch (err) { if (!isNameConflictError(err) || attempt === maxAttempts) { throw err; } candidateName = appendLegacySuffix(data.name); } } throw new RelayError('transport_error', 'Failed to register agent identity after suffix retries'); } registerTypedIdentity(type, data) { return this.agents.register({ ...data, type }); } async registerAgent(data) { const { strict, ...request } = data; if (strict) { return this.agents.register(request); } emitCompatibilityTelemetry('agents.registerAgent.legacy_suffix', { requested_name: data.name, }); return this.registerWithLegacySuffix(request); } async registerOrRotate(data) { try { return await this.registerAgent({ ...data, strict: true }); } catch (err) { if (isNameConflictError(err)) { const agent = await this.agents.get(data.name); const { token } = await this.agents.rotateToken(agent.name); this.rememberIdentity(agent.id, agent.name); const createdAt = agent.createdAt ?? agent.lastSeen; return { id: agent.id, name: agent.name, token, status: agent.status, createdAt, }; } throw err; } } async resolveIdentity() { return this.resolveIdentityInternal(); } async reconnect(data, options) { const agentClient = this.as(data.apiToken, data.agent ?? options); const agent = await agentClient.me(); this.rememberIdentity(agent.id, agent.name); return agentClient; } agent(data) { return this.registerTypedIdentity('agent', data); } human(data) { return this.registerTypedIdentity('human', data); } system(data) { return this.registerTypedIdentity('system', data); } registerA2a(options) { return this.client.post('/v1/a2a/register', options); } listA2aAgents() { return this.client.get('/v1/a2a/agents'); } removeA2aAgent(name) { return this.client.request('DELETE', `/v1/a2a/agents/${encodeURIComponent(name)}`); } getA2aAgentCard(name) { return this.client.get(`/v1/a2a/agents/${encodeURIComponent(name)}/card`); } route(skill, message) { return this.client.post('/v1/route', { skill, message }); } searchDirectory(query) { const params = {}; if (query.q) params.q = query.q; if (query.tags?.length) params.tags = query.tags.join(','); if (query.status) params.status = query.status; if (query.limit != null) params.limit = String(query.limit); return this.client.get('/v1/directory/search', params); } publishToDirectory(data) { return this.client.post('/v1/directory/agents', data); } importSkills(data) { return this.client.post('/v1/skills/sync', data); } searchSkills(query) { const params = {}; if (query?.q) params.q = query.q; if (query?.limit != null) params.limit = String(query.limit); return this.client.get('/v1/skills/search', params); } routeFeedback(data) { return this.client.post('/v1/route/feedback', data); } listDirectory(query) { const params = {}; if (query?.status) params.status = query.status; if (query?.limit != null) params.limit = String(query.limit); return this.client.get('/v1/directory/agents', params); } getDirectoryAgent(slug) { return this.client.get(`/v1/directory/agents/${encodeURIComponent(slug)}`); } updateDirectoryAgent(slug, data) { return this.client.patch(`/v1/directory/agents/${encodeURIComponent(slug)}`, data); } deleteDirectoryAgent(slug) { return this.client.delete(`/v1/directory/agents/${encodeURIComponent(slug)}`); } listDirectoryRatings(slug) { return this.client.get(`/v1/directory/agents/${encodeURIComponent(slug)}/ratings`); } rateDirectoryAgent(slug, data) { return this.client.post(`/v1/directory/agents/${encodeURIComponent(slug)}/ratings`, data); } getRoutingConfig() { return this.client.get('/v1/routing/config'); } updateRoutingConfig(data) { return this.client.put('/v1/routing/config', data); } workspace = { info: () => this.client.get('/v1/workspace'), reconnect: (data, options) => this.reconnect(data, options), update: (data) => this.client.patch('/v1/workspace', data), delete: () => this.client.delete('/v1/workspace'), }; observerTokens = { create: (data) => this.client.post('/v1/observer-tokens', data), list: () => this.client.get('/v1/observer-tokens'), get: (id) => this.client.get(`/v1/observer-tokens/${encodeURIComponent(id)}`), update: (id, data) => this.client.patch(`/v1/observer-tokens/${encodeURIComponent(id)}`, data), rotate: (id) => this.client.post(`/v1/observer-tokens/${encodeURIComponent(id)}/rotate`, {}), revoke: (id) => this.client.delete(`/v1/observer-tokens/${encodeURIComponent(id)}`), }; systemPrompt = { get: () => this.client.get('/v1/workspace/system-prompt'), set: (data) => this.client.put('/v1/workspace/system-prompt', data), }; channels = { list: (opts) => { const query = {}; if (opts?.includeArchived) query.includeArchived = 'true'; return this.client.get('/v1/channels', query); }, get: (name) => this.client.get(`/v1/channels/${encodeURIComponent(name)}`), }; messages = { list: (channel, opts) => { const name = channel.startsWith('#') ? channel.slice(1) : channel; const query = {}; if (opts?.limit != null) query.limit = String(opts.limit); if (opts?.before) query.before = opts.before; if (opts?.after) query.after = opts.after; return this.client.get(`/v1/channels/${encodeURIComponent(name)}/messages`, query); }, get: (id) => this.client.get(`/v1/messages/${encodeURIComponent(id)}`), thread: (id, opts) => { const query = {}; if (opts?.limit != null) query.limit = String(opts.limit); if (opts?.before) query.before = opts.before; if (opts?.after) query.after = opts.after; return this.client.get(`/v1/messages/${encodeURIComponent(id)}/replies`, query); }, reactions: (id) => this.client.get(`/v1/messages/${encodeURIComponent(id)}/reactions`), }; agents = { register: async (data) => { const created = await this.client.post('/v1/agents', data); this.rememberIdentity(created.id, created.name); return created; }, list: (query) => { const params = {}; if (query?.status) params.status = query.status; return this.client.get('/v1/agents', params); }, get: (name) => this.client.get(`/v1/agents/${encodeURIComponent(name)}`), me: (apiToken) => (apiToken ? this.client.withApiKey(apiToken) : this.client).get('/v1/agent'), rotateToken: (name) => this.client.post(`/v1/agents/${encodeURIComponent(name)}/rotate-token`, {}), update: (name, data) => this.client.patch(`/v1/agents/${encodeURIComponent(name)}`, data), delete: (name) => this.client.delete(`/v1/agents/${encodeURIComponent(name)}`), presence: () => this.client.get('/v1/agents/presence'), registerOrGet: async (data) => { emitCompatibilityTelemetry('agents.registerOrGet.deprecated', { replacement: 'agents.registerOrRotate', }); return this.registerOrRotate(data); }, registerAgent: (data) => this.registerAgent(data), registerOrRotate: (data) => this.registerOrRotate(data), resolveIdentity: () => this.resolveIdentity(), spawn: (data) => this.client.post('/v1/agents/spawn', data), release: (data) => this.client.post('/v1/agents/release', data), events: { emit: (name, data) => this.client.post(`/v1/agents/${encodeURIComponent(name)}/events`, data), list: (name, query) => { const params = {}; if (query?.type) params.type = query.type; if (query?.limit != null) params.limit = String(query.limit); return this.client.get(`/v1/agents/${encodeURIComponent(name)}/events`, params); }, }, }; webhooks = { create: (data) => this.client.post('/v1/webhooks', data), createInbound: (data) => this.client.post('/v1/webhooks', data), list: () => this.client.get('/v1/webhooks'), delete: (id) => this.client.delete(`/v1/webhooks/${encodeURIComponent(id)}`), trigger: (webhookId, data, token) => { return this.client.withApiKey(token).post(`/v1/hooks/${encodeURIComponent(webhookId)}`, data); }, }; subscriptions = { create: (data) => this.client.post('/v1/subscriptions', data), list: () => this.client.get('/v1/subscriptions'), get: (id) => this.client.get(`/v1/subscriptions/${encodeURIComponent(id)}`), delete: (id) => this.client.delete(`/v1/subscriptions/${encodeURIComponent(id)}`), }; actions = { register: (data) => this.client.post('/v1/actions', data), list: () => this.client.get('/v1/actions'), get: (name) => this.client.get(`/v1/actions/${encodeURIComponent(name)}`), delete: (name) => this.client.delete(`/v1/actions/${encodeURIComponent(name)}`), }; nodes = { create: (data) => this.client.post('/v1/nodes', data), list: (query) => { const params = {}; if (query?.capability) params.capability = query.capability; if (query?.name) params.name = query.name; return this.client.get('/v1/nodes', params); }, get: (name) => this.client.get(`/v1/nodes/${encodeURIComponent(name)}`), listAgents: (name) => this.client.get(`/v1/nodes/${encodeURIComponent(name)}/agents`), bindAgent: (name, data) => this.client.post(`/v1/nodes/${encodeURIComponent(name)}/agents`, data), unbindAgent: (name, agentName) => this.client.delete(`/v1/nodes/${encodeURIComponent(name)}/agents/${encodeURIComponent(agentName)}`), }; triggers = { create: (data) => this.client.post('/v1/triggers', data), list: () => this.client.get('/v1/triggers'), get: (id) => this.client.get(`/v1/triggers/${encodeURIComponent(id)}`), update: (id, data) => this.client.patch(`/v1/triggers/${encodeURIComponent(id)}`, data), delete: (id) => this.client.delete(`/v1/triggers/${encodeURIComponent(id)}`), }; certify = { submit: (data) => this.client.post('/v1/certify', data), get: (id) => this.client.get(`/v1/certify/${encodeURIComponent(id)}`), /** Public badge SVG URL for a certification run (served without an auth header). */ badgeUrl: (id) => new URL(`/v1/certify/${encodeURIComponent(id)}/badge.svg`, this.client.baseUrl).toString(), monitor: (data) => this.client.post('/v1/certify/monitor', data), }; console = { messages: (query) => { const params = {}; if (query?.limit != null) params.limit = String(query.limit); if (query?.before) params.before = query.before; if (query?.agentId) params.agentId = query.agentId; if (query?.channelId) params.channelId = query.channelId; if (query?.conversationId) params.conversationId = query.conversationId; if (query?.deliveryKind) params.deliveryKind = query.deliveryKind; return this.client.get('/v1/console/messages', params); }, stats: (query) => { const params = {}; if (query?.days != null) params.days = String(query.days); return this.client.get('/v1/console/stats', params); }, agents: (query) => { const params = {}; if (query?.days != null) params.days = String(query.days); if (query?.limit != null) params.limit = String(query.limit); return this.client.get('/v1/console/agents', params); }, costs: (query) => { const params = {}; if (query?.days != null) params.days = String(query.days); return this.client.get('/v1/console/costs', params); }, }; activity = (limit) => { const params = {}; if (limit !== undefined) params.limit = String(limit); return this.client.get('/v1/activity', params); }; allDmConversations = () => this.client.get('/v1/dm/conversations/all'); dmMessages = async (conversationId, opts) => { const query = {}; if (opts?.limit !== undefined) query.limit = String(opts.limit); if (opts?.before) query.before = opts.before; if (opts?.after) query.after = opts.after; return this.client.get(`/v1/dm/conversations/${encodeURIComponent(conversationId)}/messages`, query); }; as(agentToken, options) { const agentHttpClient = this.client.withApiKey(agentToken); return new AgentClient(agentHttpClient, options); } } //# sourceMappingURL=relay.js.map