/** * MemoryStack React Native SDK - Core Client * * A lightweight, offline-first client for React Native applications * with full API parity to the Node.js SDK. */ import { MemoryStackConfig, Message, CreateMemoryResponse, UpdateMemoryResponse, DeleteMemoryResponse, BatchDeleteResponse, ListMemoriesRequest, ListMemoriesResponse, Memory, SearchMemoriesResponse, ReflectionOptions, ReflectionResponse, ConsolidationOptions, ConsolidationResponse, UsageStats, GraphData, ListAgentMemoriesOptions, ListAgentMemoriesResponse, ExportOptions, ImportMemory, ImportResponse, RetryConfig, Logger, OfflineQueueItem, OfflineQueueStatus, } from './types'; import { MemoryStackError, AuthenticationError, RateLimitError, ValidationError, NotFoundError, NetworkError, ServerError, OfflineError, isRetryableError, } from './errors'; // ============================================================================ // Default Configuration // ============================================================================ const DEFAULT_BASE_URL = 'https://www.memorystack.app'; const DEFAULT_TIMEOUT = 30000; const SDK_VERSION = '1.0.0'; const DEFAULT_RETRY_CONFIG: Required = { maxRetries: 3, retryDelay: 1000, maxRetryDelay: 10000, retryableStatusCodes: [408, 429, 500, 502, 503, 504], }; // ============================================================================ // Utility Functions // ============================================================================ /** * Sleep for specified milliseconds */ const sleep = (ms: number): Promise => new Promise(resolve => setTimeout(resolve, ms)); /** * Generate unique request ID */ const generateRequestId = (): string => `req_${Date.now()}_${Math.random().toString(36).substring(2, 11)}`; /** * Generate unique ID for offline queue items */ const generateOfflineId = (): string => `offline_${Date.now()}_${Math.random().toString(36).substring(2, 11)}`; // ============================================================================ // Offline Storage Interface // ============================================================================ interface OfflineStorage { getItem: (key: string) => Promise; setItem: (key: string, value: string) => Promise; removeItem: (key: string) => Promise; } // Default in-memory storage (fallback when AsyncStorage not available) const createInMemoryStorage = (): OfflineStorage => { const store = new Map(); return { getItem: async (key: string) => store.get(key) ?? null, setItem: async (key: string, value: string) => { store.set(key, value); }, removeItem: async (key: string) => { store.delete(key); }, }; }; // ============================================================================ // Network State Interface // ============================================================================ interface NetworkStateProvider { isConnected: () => Promise; addEventListener?: (callback: (isConnected: boolean) => void) => () => void; } // Default network state (assumes always connected) const createDefaultNetworkProvider = (): NetworkStateProvider => ({ isConnected: async () => true, }); // ============================================================================ // Main Client Class // ============================================================================ export class MemoryStackClient { private readonly apiKey: string; private readonly baseUrl: string; private readonly timeout: number; private readonly retryConfig: Required; private readonly enableLogging: boolean; private readonly logger: Required; private readonly enableOffline: boolean; private readonly storageKeyPrefix: string; // Agent context private agentId?: string; private projectId?: string; private sessionId?: string; private agentName?: string; private agentType?: string; // Offline support private storage: OfflineStorage; private networkProvider: NetworkStateProvider; private isOnline: boolean = true; private offlineQueue: OfflineQueueItem[] = []; private isProcessingQueue: boolean = false; constructor(config: MemoryStackConfig) { // Validate API key if (!config.apiKey) { throw new ValidationError('API key is required'); } this.apiKey = config.apiKey; // Configure base URL let baseUrl = (config.baseUrl || DEFAULT_BASE_URL).trim(); if (baseUrl.endsWith('/api/v1')) { baseUrl = baseUrl.replace(/\/api\/v1$/, ''); } this.baseUrl = `${baseUrl}/api/v1`; // Client configuration this.timeout = config.timeout ?? DEFAULT_TIMEOUT; this.enableLogging = config.enableLogging ?? false; this.enableOffline = config.enableOffline ?? false; this.storageKeyPrefix = config.storageKeyPrefix ?? '@memorystack'; // Logger configuration this.logger = { debug: config.logger?.debug ?? (() => { }), info: config.logger?.info ?? ((msg, data) => console.log(`[MemoryStack] ${msg}`, data || '')), warn: config.logger?.warn ?? ((msg, data) => console.warn(`[MemoryStack] ${msg}`, data || '')), error: config.logger?.error ?? ((msg, data) => console.error(`[MemoryStack] ${msg}`, data || '')), }; // Retry configuration this.retryConfig = { maxRetries: config.retryConfig?.maxRetries ?? DEFAULT_RETRY_CONFIG.maxRetries, retryDelay: config.retryConfig?.retryDelay ?? DEFAULT_RETRY_CONFIG.retryDelay, maxRetryDelay: config.retryConfig?.maxRetryDelay ?? DEFAULT_RETRY_CONFIG.maxRetryDelay, retryableStatusCodes: config.retryConfig?.retryableStatusCodes ?? DEFAULT_RETRY_CONFIG.retryableStatusCodes, }; // Agent context this.sessionId = config.sessionId; this.agentName = config.agentName; this.agentType = config.agentType; // Initialize storage (will be replaced by setStorage) this.storage = createInMemoryStorage(); this.networkProvider = createDefaultNetworkProvider(); // Load offline queue if enabled if (this.enableOffline) { this.loadOfflineQueue().catch(err => { this.logger.warn('Failed to load offline queue', err); }); } } // ============================================================================ // Storage & Network Configuration // ============================================================================ /** * Set custom storage provider (e.g., AsyncStorage) * Call this before using offline features */ setStorage(storage: OfflineStorage): void { this.storage = storage; if (this.enableOffline) { this.loadOfflineQueue().catch(err => { this.logger.warn('Failed to load offline queue after setting storage', err); }); } } /** * Set custom network state provider (e.g., NetInfo) */ setNetworkProvider(provider: NetworkStateProvider): void { this.networkProvider = provider; // Subscribe to network changes if (provider.addEventListener) { provider.addEventListener((isConnected) => { const wasOffline = !this.isOnline; this.isOnline = isConnected; if (wasOffline && isConnected && this.offlineQueue.length > 0) { this.processPendingOperations().catch(err => { this.logger.error('Failed to process pending operations', err); }); } }); } } // ============================================================================ // HTTP Request Handler // ============================================================================ private async request( method: 'GET' | 'POST' | 'PATCH' | 'DELETE', endpoint: string, options?: { body?: unknown; params?: Record; retryCount?: number; } ): Promise { const requestId = generateRequestId(); const retryCount = options?.retryCount ?? 0; // Build URL with query params let url = `${this.baseUrl}${endpoint}`; if (options?.params && Object.keys(options.params).length > 0) { const searchParams = new URLSearchParams(options.params); url = `${url}?${searchParams.toString()}`; } // Log request if (this.enableLogging) { this.logger.debug('Request', { method, url, requestId, retryCount, }); } // Check network state const isConnected = await this.networkProvider.isConnected(); if (!isConnected) { throw new NetworkError('Device is offline', undefined, false, true); } // Build request options const fetchOptions: RequestInit = { method, headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${this.apiKey}`, 'X-API-Key': this.apiKey, 'X-SDK-Version': SDK_VERSION, 'X-SDK-Platform': 'react-native', 'X-Request-ID': requestId, }, }; if (options?.body) { fetchOptions.body = JSON.stringify(options.body); } try { // Create timeout promise const timeoutPromise = new Promise((_, reject) => { setTimeout(() => reject(new NetworkError('Request timeout', undefined, true)), this.timeout); }); // Make request with timeout const response = await Promise.race([ fetch(url, fetchOptions), timeoutPromise, ]) as Response; // Log response if (this.enableLogging) { this.logger.debug('Response', { status: response.status, url, requestId, }); } // Parse response const data = await response.json().catch(() => ({})); // Handle errors if (!response.ok) { const message = data.error || data.message || 'API request failed'; const details = data.details; switch (response.status) { case 401: throw new AuthenticationError(message, details); case 429: { const limit = response.headers.get('x-ratelimit-limit'); const remaining = response.headers.get('x-ratelimit-remaining'); const reset = response.headers.get('x-ratelimit-reset'); const retryAfter = response.headers.get('retry-after'); throw new RateLimitError( message, limit ? parseInt(limit) : undefined, remaining ? parseInt(remaining) : undefined, reset ? parseInt(reset) : undefined, retryAfter ? parseInt(retryAfter) : undefined ); } case 400: throw new ValidationError(message, details); case 404: throw new NotFoundError(message); case 409: throw new MemoryStackError(message, 409, details, requestId); default: if (response.status >= 500) { throw new ServerError(message, response.status, details); } throw new MemoryStackError(message, response.status, details, requestId); } } return data as T; } catch (error) { // Handle retry logic if (isRetryableError(error) && retryCount < this.retryConfig.maxRetries) { const delay = Math.min( this.retryConfig.retryDelay * Math.pow(2, retryCount), this.retryConfig.maxRetryDelay ); if (this.enableLogging) { this.logger.warn( `Retrying request (attempt ${retryCount + 1}/${this.retryConfig.maxRetries}) after ${delay}ms`, { url, requestId, error: error instanceof Error ? error.message : 'Unknown' } ); } await sleep(delay); return this.request(method, endpoint, { ...options, retryCount: retryCount + 1, }); } // Log error if (this.enableLogging) { this.logger.error('Request failed', { url, requestId, error: error instanceof Error ? error.message : 'Unknown', }); } // Re-throw typed errors if (error instanceof MemoryStackError) { throw error; } // Wrap unknown errors if (error instanceof Error) { throw new NetworkError(error.message, error); } throw new MemoryStackError('Unknown error occurred'); } } // ============================================================================ // Offline Queue Management // ============================================================================ private async loadOfflineQueue(): Promise { try { const stored = await this.storage.getItem(`${this.storageKeyPrefix}_queue`); if (stored) { this.offlineQueue = JSON.parse(stored); } } catch (error) { this.logger.warn('Failed to load offline queue', error); } } private async saveOfflineQueue(): Promise { try { await this.storage.setItem( `${this.storageKeyPrefix}_queue`, JSON.stringify(this.offlineQueue) ); } catch (error) { this.logger.warn('Failed to save offline queue', error); } } private async addToOfflineQueue( type: OfflineQueueItem['type'], payload: unknown ): Promise { const item: OfflineQueueItem = { id: generateOfflineId(), type, payload, createdAt: Date.now(), retryCount: 0, }; this.offlineQueue.push(item); await this.saveOfflineQueue(); return item.id; } /** * Get pending offline operations */ async getPendingOperations(): Promise { return { pendingCount: this.offlineQueue.length, items: [...this.offlineQueue], isProcessing: this.isProcessingQueue, }; } /** * Process all pending offline operations */ async processPendingOperations(): Promise<{ processed: number; failed: number; remaining: number; }> { if (this.isProcessingQueue || this.offlineQueue.length === 0) { return { processed: 0, failed: 0, remaining: this.offlineQueue.length }; } this.isProcessingQueue = true; let processed = 0; let failed = 0; try { const isConnected = await this.networkProvider.isConnected(); if (!isConnected) { return { processed: 0, failed: 0, remaining: this.offlineQueue.length }; } const itemsToProcess = [...this.offlineQueue]; for (const item of itemsToProcess) { try { await this.executeQueueItem(item); // Remove from queue on success this.offlineQueue = this.offlineQueue.filter(q => q.id !== item.id); processed++; } catch (error) { item.retryCount++; item.lastError = error instanceof Error ? error.message : 'Unknown error'; // Remove if max retries exceeded if (item.retryCount >= this.retryConfig.maxRetries) { this.offlineQueue = this.offlineQueue.filter(q => q.id !== item.id); failed++; this.logger.error('Offline operation failed permanently', { item, error }); } } } await this.saveOfflineQueue(); } finally { this.isProcessingQueue = false; } return { processed, failed, remaining: this.offlineQueue.length }; } private async executeQueueItem(item: OfflineQueueItem): Promise { switch (item.type) { case 'add': { const payload = item.payload as { content: string | Message[]; options?: unknown }; await this.add(payload.content, payload.options as Parameters[1]); break; } case 'update': { const payload = item.payload as { memoryId: string; updates: unknown }; await this.updateMemory(payload.memoryId, payload.updates as Parameters[1]); break; } case 'delete': { const payload = item.payload as { memoryId: string; hard: boolean }; await this.deleteMemory(payload.memoryId, payload.hard); break; } case 'deleteBatch': { const payload = item.payload as { memoryIds: string[]; hard: boolean }; await this.deleteMemories(payload.memoryIds, payload.hard); break; } } } /** * Clear all pending offline operations */ async clearPendingOperations(): Promise { this.offlineQueue = []; await this.saveOfflineQueue(); } // ============================================================================ // Memory Operations // ============================================================================ /** * Add a memory - the simplest way to store information * * @example * // Simple text * await client.add("User prefers dark mode"); * * // With user ID (for B2B apps) * await client.add("User prefers dark mode", { userId: "user_123" }); * * // Conversation format * await client.add([ * { role: "user", content: "I love TypeScript" }, * { role: "assistant", content: "Great choice!" } * ]); */ async add( content: string | Message[], options?: { userId?: string; metadata?: Record; agentId?: string; teamId?: string; sessionId?: string; conversationId?: string; } ): Promise { // Normalize content to messages array let messages: Message[]; if (typeof content === 'string') { messages = [{ role: 'user', content }]; } else if (Array.isArray(content)) { messages = content; } else { throw new ValidationError('Content must be a string or array of messages'); } // Validate messages if (messages.length === 0) { throw new ValidationError('At least one message is required'); } if (messages.length > 100) { throw new ValidationError('Maximum 100 messages per request'); } for (const msg of messages) { if (!msg.role || !msg.content) { throw new ValidationError(`Message missing role or content: ${JSON.stringify(msg)}`); } } // Build payload const agentId = options?.agentId || this.agentId; const teamId = options?.teamId; const sessionId = options?.sessionId || this.sessionId; const conversationId = options?.conversationId; // Filter out scoping fields from metadata const cleanMetadata = options?.metadata ? Object.fromEntries( Object.entries(options.metadata).filter(([key]) => !['agent_id', 'team_id', 'session_id', 'conversation_id'].includes(key) ) ) : undefined; const payload: Record = { messages, ...(options?.userId && { user_id: options.userId }), ...(agentId && { agent_id: agentId }), ...(teamId && { team_id: teamId }), ...(sessionId && { session_id: sessionId }), ...(conversationId && { conversation_id: conversationId }), ...(this.agentName && { agent_name: this.agentName }), ...(this.agentType && { agent_type: this.agentType }), ...(cleanMetadata && Object.keys(cleanMetadata).length > 0 && { metadata: cleanMetadata }), }; // Handle offline mode if (this.enableOffline) { const isConnected = await this.networkProvider.isConnected(); if (!isConnected) { await this.addToOfflineQueue('add', { content, options }); throw new OfflineError( 'Operation queued for sync when online', 'add', true ); } } const endpoint = this.agentName ? '/memories/agents' : '/memories'; const response = await this.request('POST', endpoint, { body: payload }); // Store IDs for future use if (response.agent_id) this.agentId = response.agent_id; if (response.project_id) this.projectId = response.project_id; return response; } /** * @deprecated Use add() instead - simpler API */ async createMemory(request: { messages: Message[]; user_id?: string; metadata?: Record; }): Promise { return this.add(request.messages, { userId: request.user_id, metadata: request.metadata, }); } /** * Get a single memory by ID */ async getMemory(memoryId: string): Promise { if (!memoryId || memoryId.trim().length === 0) { throw new ValidationError('Memory ID is required'); } const response = await this.request<{ success: boolean; memory: Memory }>( 'GET', `/memories/${memoryId}` ); return response.memory; } /** * Update an existing memory */ async updateMemory( memoryId: string, updates: { content?: string; memory_type?: string; confidence?: number; metadata?: Record; } ): Promise { if (!memoryId || memoryId.trim().length === 0) { throw new ValidationError('Memory ID is required'); } if (!updates.content && updates.memory_type === undefined && updates.confidence === undefined && updates.metadata === undefined) { throw new ValidationError('At least one field must be provided for update'); } if (updates.content !== undefined && updates.content.trim().length === 0) { throw new ValidationError('Content cannot be empty'); } if (updates.confidence !== undefined) { if (typeof updates.confidence !== 'number' || updates.confidence < 0 || updates.confidence > 1) { throw new ValidationError( `Invalid confidence: ${updates.confidence}. Must be a number between 0 and 1` ); } } // Handle offline mode if (this.enableOffline) { const isConnected = await this.networkProvider.isConnected(); if (!isConnected) { await this.addToOfflineQueue('update', { memoryId, updates }); throw new OfflineError('Operation queued for sync when online', 'update', true); } } return this.request('PATCH', `/memories/${memoryId}`, { body: updates }); } /** * Delete a single memory */ async deleteMemory(memoryId: string, hard: boolean = false): Promise { if (!memoryId || memoryId.trim().length === 0) { throw new ValidationError('Memory ID is required'); } // Handle offline mode if (this.enableOffline) { const isConnected = await this.networkProvider.isConnected(); if (!isConnected) { await this.addToOfflineQueue('delete', { memoryId, hard }); throw new OfflineError('Operation queued for sync when online', 'delete', true); } } return this.request( 'DELETE', `/memories/${memoryId}`, { params: { hard: hard.toString() } } ); } /** * Delete multiple memories at once */ async deleteMemories(memoryIds: string[], hard: boolean = false): Promise { if (!memoryIds || memoryIds.length === 0) { throw new ValidationError('At least one memory ID is required'); } // Handle offline mode if (this.enableOffline) { const isConnected = await this.networkProvider.isConnected(); if (!isConnected) { await this.addToOfflineQueue('deleteBatch', { memoryIds, hard }); throw new OfflineError('Operation queued for sync when online', 'deleteBatch', true); } } return this.request('DELETE', '/memories/batch', { body: { memory_ids: memoryIds, hard }, }); } /** * List memories with optional filtering and pagination */ async listMemories(request: ListMemoriesRequest = {}): Promise { // Validate limit if (request.limit !== undefined) { if (!Number.isInteger(request.limit) || request.limit < 1 || request.limit > 100) { throw new ValidationError( `Invalid limit: ${request.limit}. Must be an integer between 1 and 100` ); } } // Validate order if (request.order && !['asc', 'desc'].includes(request.order)) { throw new ValidationError(`Invalid order: "${request.order}". Must be 'asc' or 'desc'`); } // Validate min_confidence if (request.min_confidence !== undefined) { if (typeof request.min_confidence !== 'number' || request.min_confidence < 0 || request.min_confidence > 1) { throw new ValidationError( `Invalid min_confidence: ${request.min_confidence}. Must be a number between 0 and 1` ); } } const params: Record = {}; if (request.user_id !== undefined) params.user_id = request.user_id; if (request.limit !== undefined) params.limit = request.limit.toString(); if (request.cursor) params.cursor = request.cursor; if (request.order) params.order = request.order; if (request.memory_type) params.memory_type = request.memory_type; if (request.min_confidence !== undefined) params.min_confidence = request.min_confidence.toString(); if (request.include_embedding !== undefined) params.include_embedding = request.include_embedding.toString(); return this.request('GET', '/memories', { params }); } /** * List memories with optional filtering * @deprecated Use listMemories() for more options */ async list(options?: { userId?: string; limit?: number }): Promise { return this.listMemories({ user_id: options?.userId, limit: options?.limit ?? 20, }); } // ============================================================================ // Search // ============================================================================ /** * Search memories - find relevant information * * @example * // Simple search * const results = await client.search("user preferences"); * * // With user ID * const results = await client.search("user preferences", { userId: "user_123" }); * * // With limit * const results = await client.search("user preferences", { limit: 5 }); */ async search( query: string, options?: { userId?: string; limit?: number; } ): Promise { if (!query || query.trim().length === 0) { throw new ValidationError('Search query is required'); } const limit = options?.limit ?? 10; if (limit < 1 || limit > 50) { throw new ValidationError('Limit must be between 1 and 50'); } const params: Record = { query, limit: limit.toString(), mode: 'hybrid', }; if (options?.userId) params.user_id = options.userId; return this.request('GET', '/memories/search', { params }); } /** * @deprecated Use search() instead - simpler API */ async searchMemories(options: { query: string; userId?: string; limit?: number; mode?: string; }): Promise { return this.search(options.query, { userId: options.userId, limit: options.limit, }); } // ============================================================================ // Agent Operations // ============================================================================ /** * List memories for the current agent */ async listAgentMemories(options?: ListAgentMemoriesOptions): Promise { if (options?.limit !== undefined) { if (!Number.isInteger(options.limit) || options.limit < 1 || options.limit > 100) { throw new ValidationError( `Invalid limit: ${options.limit}. Must be an integer between 1 and 100` ); } } if (!this.agentId) { throw new ValidationError( 'Agent not initialized. Create a memory first or provide agentName in constructor.' ); } const params: Record = { agent_id: this.agentId }; if (options?.limit) params.limit = options.limit.toString(); if (options?.cursor) params.cursor = options.cursor; if (options?.includeTeam !== undefined) params.include_team = options.includeTeam.toString(); if (options?.includeProject !== undefined) params.include_project = options.includeProject.toString(); return this.request('GET', '/memories/agents', { params }); } // ============================================================================ // Analysis Operations // ============================================================================ /** * Run reflection analysis on memories to generate insights */ async reflectOnMemories(options?: ReflectionOptions): Promise { if (options?.timeWindowDays !== undefined) { if (!Number.isInteger(options.timeWindowDays) || options.timeWindowDays < 1 || options.timeWindowDays > 90) { throw new ValidationError( `Invalid timeWindowDays: ${options.timeWindowDays}. Must be an integer between 1 and 90` ); } } if (options?.analysisDepth && !['shallow', 'deep'].includes(options.analysisDepth)) { throw new ValidationError( `Invalid analysisDepth: "${options.analysisDepth}". Must be 'shallow' or 'deep'` ); } if (options?.minPatternStrength !== undefined) { if (typeof options.minPatternStrength !== 'number' || options.minPatternStrength < 0 || options.minPatternStrength > 1) { throw new ValidationError( `Invalid minPatternStrength: ${options.minPatternStrength}. Must be a number between 0 and 1` ); } } const payload = { timeWindowDays: options?.timeWindowDays || 7, analysisDepth: options?.analysisDepth || 'shallow', dryRun: options?.dryRun || false, minPatternStrength: options?.minPatternStrength || 0.7, }; return this.request('POST', '/memories/reflect', { body: payload }); } /** * Consolidate duplicate or similar memories */ async consolidateMemories(options?: ConsolidationOptions): Promise { if (options?.similarityThreshold !== undefined) { if (typeof options.similarityThreshold !== 'number' || options.similarityThreshold < 0 || options.similarityThreshold > 1) { throw new ValidationError( `Invalid similarityThreshold: ${options.similarityThreshold}. Must be a number between 0 and 1` ); } } const payload = { similarity_threshold: options?.similarityThreshold || 0.90, check_redundancy: options?.checkRedundancy !== false, dry_run: options?.dryRun || false, }; return this.request('POST', '/memories/consolidate', { body: payload }); } // ============================================================================ // Stats & Graph // ============================================================================ /** * Get usage statistics */ async getStats(): Promise { return this.request('GET', '/stats'); } /** * Get knowledge graph data */ async getGraph(): Promise { return this.request('GET', '/graph'); } // ============================================================================ // Import/Export // ============================================================================ /** * Export memories */ async exportMemories(options?: ExportOptions): Promise { const payload: Record = { format: options?.format || 'json', }; if (options?.userId) payload.user_id = options.userId; return this.request('POST', '/memories/export', { body: payload }); } /** * Import memories */ async importMemories(memories: ImportMemory[]): Promise { if (!memories || memories.length === 0) { throw new ValidationError('At least one memory is required for import'); } return this.request('POST', '/memories/import', { body: { memories } }); } // ============================================================================ // Auto-Maintenance Configuration // ============================================================================ /** * Get current auto-maintenance configuration */ async getAutoMaintenanceConfig(): Promise { return this.request('GET', '/auto-maintenance/config'); } /** * Update auto-maintenance configuration */ async updateAutoMaintenanceConfig(config: { consolidation_enabled?: boolean; consolidation_frequency?: 'daily' | 'weekly' | 'monthly'; reflection_enabled?: boolean; reflection_frequency?: 'daily' | 'weekly' | 'monthly'; }): Promise { return this.request('POST', '/auto-maintenance/config', { body: config }); } // ============================================================================ // Utility Methods // ============================================================================ /** * Get current network status */ async getNetworkStatus(): Promise { return this.networkProvider.isConnected(); } /** * Get the current agent ID (if initialized) */ getAgentId(): string | undefined { return this.agentId; } /** * Get the current project ID (if initialized) */ getProjectId(): string | undefined { return this.projectId; } } // ============================================================================ // Re-export errors for convenience // ============================================================================ export { MemoryStackError, AuthenticationError, RateLimitError, ValidationError, NotFoundError, NetworkError, ServerError, OfflineError, isMemoryStackError, isRetryableError, } from './errors';