@memorylayerai/sdk 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json ADDED
@@ -0,0 +1,47 @@
1
+ {
2
+ "name": "@memorylayerai/sdk",
3
+ "version": "0.1.0",
4
+ "private": false,
5
+ "description": "Official Node.js/TypeScript SDK for MemoryLayer",
6
+ "main": "dist/index.js",
7
+ "module": "dist/index.mjs",
8
+ "types": "dist/index.d.ts",
9
+ "type": "module",
10
+ "exports": {
11
+ ".": {
12
+ "import": "./dist/index.mjs",
13
+ "require": "./dist/index.js",
14
+ "types": "./dist/index.d.ts"
15
+ }
16
+ },
17
+ "scripts": {
18
+ "build": "tsup src/index.ts --format cjs,esm --dts",
19
+ "test": "vitest",
20
+ "test:watch": "vitest --watch",
21
+ "lint": "eslint src --ext .ts",
22
+ "typecheck": "tsc --noEmit"
23
+ },
24
+ "keywords": [
25
+ "memorylayer",
26
+ "memory",
27
+ "ai",
28
+ "llm",
29
+ "sdk",
30
+ "typescript"
31
+ ],
32
+ "author": "MemoryLayer",
33
+ "license": "MIT",
34
+ "dependencies": {
35
+ "zod": "^3.22.4"
36
+ },
37
+ "devDependencies": {
38
+ "@types/node": "^20.11.30",
39
+ "fast-check": "^3.17.1",
40
+ "tsup": "^8.0.2",
41
+ "typescript": "^5.4.3",
42
+ "vitest": "^1.4.0"
43
+ },
44
+ "engines": {
45
+ "node": ">=16.0.0"
46
+ }
47
+ }
package/src/client.ts ADDED
@@ -0,0 +1,116 @@
1
+ import { HTTPClient, ClientConfig as HTTPClientConfig } from './http-client.js';
2
+ import { ValidationError } from './errors.js';
3
+
4
+ /**
5
+ * Configuration options for the MemoryLayer client
6
+ */
7
+ export interface ClientConfig {
8
+ /** API key for authentication (can also be set via MEMORYLAYER_API_KEY env var) */
9
+ apiKey?: string;
10
+ /** Base URL for the API (default: https://api.memorylayer.com) */
11
+ baseURL?: string;
12
+ /** Request timeout in milliseconds (default: 30000) */
13
+ timeout?: number;
14
+ /** Maximum number of retry attempts (default: 3) */
15
+ maxRetries?: number;
16
+ /** Initial retry delay in milliseconds (default: 1000) */
17
+ retryDelay?: number;
18
+ /** Custom headers to include in all requests */
19
+ headers?: Record<string, string>;
20
+ /** Logger for debugging */
21
+ logger?: {
22
+ debug(message: string, ...args: any[]): void;
23
+ info(message: string, ...args: any[]): void;
24
+ warn(message: string, ...args: any[]): void;
25
+ error(message: string, ...args: any[]): void;
26
+ };
27
+ }
28
+
29
+ /**
30
+ * Main MemoryLayer SDK client
31
+ */
32
+ export class MemoryLayerClient {
33
+ private httpClient: HTTPClient;
34
+ private _memories?: any;
35
+ private _search?: any;
36
+ private _ingest?: any;
37
+ private _router?: any;
38
+
39
+ constructor(config: ClientConfig = {}) {
40
+ // Get API key from config or environment variable
41
+ const apiKey = config.apiKey || process.env.MEMORYLAYER_API_KEY;
42
+
43
+ if (!apiKey) {
44
+ throw new ValidationError(
45
+ 'API key is required. Provide it via config.apiKey or MEMORYLAYER_API_KEY environment variable.',
46
+ [{ field: 'apiKey', message: 'API key is required' }]
47
+ );
48
+ }
49
+
50
+ // Validate API key format (basic check)
51
+ if (typeof apiKey !== 'string' || apiKey.trim().length === 0) {
52
+ throw new ValidationError(
53
+ 'API key must be a non-empty string',
54
+ [{ field: 'apiKey', message: 'API key must be a non-empty string' }]
55
+ );
56
+ }
57
+
58
+ // Initialize HTTP client
59
+ const httpConfig: HTTPClientConfig = {
60
+ apiKey,
61
+ baseURL: config.baseURL,
62
+ timeout: config.timeout,
63
+ maxRetries: config.maxRetries,
64
+ retryDelay: config.retryDelay,
65
+ headers: config.headers,
66
+ logger: config.logger,
67
+ };
68
+
69
+ this.httpClient = new HTTPClient(httpConfig);
70
+ }
71
+
72
+ /**
73
+ * Access memory operations
74
+ */
75
+ get memories() {
76
+ if (!this._memories) {
77
+ // Lazy load to avoid circular dependencies
78
+ const { MemoriesResource } = require('./resources/memories.js');
79
+ this._memories = new MemoriesResource(this.httpClient);
80
+ }
81
+ return this._memories;
82
+ }
83
+
84
+ /**
85
+ * Access search operations
86
+ */
87
+ get search() {
88
+ if (!this._search) {
89
+ const { SearchResource } = require('./resources/search.js');
90
+ this._search = new SearchResource(this.httpClient);
91
+ }
92
+ return this._search;
93
+ }
94
+
95
+ /**
96
+ * Access ingestion operations
97
+ */
98
+ get ingest() {
99
+ if (!this._ingest) {
100
+ const { IngestResource } = require('./resources/ingest.js');
101
+ this._ingest = new IngestResource(this.httpClient);
102
+ }
103
+ return this._ingest;
104
+ }
105
+
106
+ /**
107
+ * Access router operations
108
+ */
109
+ get router() {
110
+ if (!this._router) {
111
+ const { RouterResource } = require('./resources/router.js');
112
+ this._router = new RouterResource(this.httpClient);
113
+ }
114
+ return this._router;
115
+ }
116
+ }
package/src/errors.ts ADDED
@@ -0,0 +1,91 @@
1
+ /**
2
+ * Base error class for all MemoryLayer SDK errors
3
+ */
4
+ export class MemoryLayerError extends Error {
5
+ constructor(
6
+ message: string,
7
+ public statusCode?: number,
8
+ public requestId?: string,
9
+ public details?: any
10
+ ) {
11
+ super(message);
12
+ this.name = 'MemoryLayerError';
13
+ Object.setPrototypeOf(this, MemoryLayerError.prototype);
14
+ }
15
+ }
16
+
17
+ /**
18
+ * Error thrown when authentication fails (401)
19
+ */
20
+ export class AuthenticationError extends MemoryLayerError {
21
+ constructor(message: string, requestId?: string) {
22
+ super(message, 401, requestId);
23
+ this.name = 'AuthenticationError';
24
+ Object.setPrototypeOf(this, AuthenticationError.prototype);
25
+ }
26
+ }
27
+
28
+ /**
29
+ * Error thrown when rate limit is exceeded (429)
30
+ */
31
+ export class RateLimitError extends MemoryLayerError {
32
+ constructor(
33
+ message: string,
34
+ public retryAfter: number,
35
+ requestId?: string
36
+ ) {
37
+ super(message, 429, requestId);
38
+ this.name = 'RateLimitError';
39
+ Object.setPrototypeOf(this, RateLimitError.prototype);
40
+ }
41
+ }
42
+
43
+ /**
44
+ * Validation error detail
45
+ */
46
+ export interface ValidationErrorDetail {
47
+ field: string;
48
+ message: string;
49
+ }
50
+
51
+ /**
52
+ * Error thrown when request validation fails (400)
53
+ */
54
+ export class ValidationError extends MemoryLayerError {
55
+ constructor(
56
+ message: string,
57
+ public errors: ValidationErrorDetail[],
58
+ requestId?: string
59
+ ) {
60
+ super(message, 400, requestId);
61
+ this.name = 'ValidationError';
62
+ Object.setPrototypeOf(this, ValidationError.prototype);
63
+ }
64
+ }
65
+
66
+ /**
67
+ * Error thrown when network request fails
68
+ */
69
+ export class NetworkError extends MemoryLayerError {
70
+ constructor(message: string, public cause: Error) {
71
+ super(message);
72
+ this.name = 'NetworkError';
73
+ Object.setPrototypeOf(this, NetworkError.prototype);
74
+ }
75
+ }
76
+
77
+ /**
78
+ * Error thrown for other API errors
79
+ */
80
+ export class APIError extends MemoryLayerError {
81
+ constructor(
82
+ message: string,
83
+ statusCode: number,
84
+ requestId?: string,
85
+ details?: any
86
+ ) {
87
+ super(message, statusCode, requestId, details);
88
+ this.name = 'APIError';
89
+ Object.setPrototypeOf(this, APIError.prototype);
90
+ }
91
+ }
@@ -0,0 +1,304 @@
1
+ import {
2
+ MemoryLayerError,
3
+ AuthenticationError,
4
+ RateLimitError,
5
+ ValidationError,
6
+ NetworkError,
7
+ APIError,
8
+ } from './errors.js';
9
+
10
+ /**
11
+ * Configuration for the HTTP client
12
+ */
13
+ export interface ClientConfig {
14
+ apiKey: string;
15
+ baseURL?: string;
16
+ timeout?: number;
17
+ maxRetries?: number;
18
+ retryDelay?: number;
19
+ headers?: Record<string, string>;
20
+ logger?: Logger;
21
+ }
22
+
23
+ /**
24
+ * Logger interface
25
+ */
26
+ export interface Logger {
27
+ debug(message: string, ...args: any[]): void;
28
+ info(message: string, ...args: any[]): void;
29
+ warn(message: string, ...args: any[]): void;
30
+ error(message: string, ...args: any[]): void;
31
+ }
32
+
33
+ /**
34
+ * Retry configuration
35
+ */
36
+ export interface RetryConfig {
37
+ maxRetries: number;
38
+ initialDelay: number;
39
+ maxDelay: number;
40
+ backoffMultiplier: number;
41
+ retryableStatusCodes: number[];
42
+ }
43
+
44
+ /**
45
+ * Request options
46
+ */
47
+ export interface RequestOptions {
48
+ method: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH';
49
+ path: string;
50
+ body?: any;
51
+ query?: Record<string, string>;
52
+ headers?: Record<string, string>;
53
+ timeout?: number;
54
+ }
55
+
56
+ /**
57
+ * HTTP client with retry logic and error handling
58
+ */
59
+ export class HTTPClient {
60
+ private config: ClientConfig;
61
+ private retryConfig: RetryConfig;
62
+ private baseURL: string;
63
+
64
+ constructor(config: ClientConfig, retryConfig?: Partial<RetryConfig>) {
65
+ this.config = config;
66
+ this.baseURL = config.baseURL || 'https://api.memorylayer.com';
67
+ this.retryConfig = {
68
+ maxRetries: config.maxRetries ?? 3,
69
+ initialDelay: config.retryDelay ?? 1000,
70
+ maxDelay: 30000,
71
+ backoffMultiplier: 2,
72
+ retryableStatusCodes: [429, 500, 502, 503, 504],
73
+ ...retryConfig,
74
+ };
75
+ }
76
+
77
+ /**
78
+ * Make an HTTP request with retry logic
79
+ */
80
+ async request<T>(options: RequestOptions): Promise<T> {
81
+ let lastError: Error | null = null;
82
+
83
+ for (let attempt = 0; attempt <= this.retryConfig.maxRetries; attempt++) {
84
+ try {
85
+ return await this.makeRequest<T>(options);
86
+ } catch (error) {
87
+ lastError = error as Error;
88
+
89
+ if (!this.shouldRetry(error as MemoryLayerError, attempt)) {
90
+ throw error;
91
+ }
92
+
93
+ const delay = this.getRetryDelay(error as MemoryLayerError, attempt);
94
+ this.config.logger?.debug(`Retrying request after ${delay}ms (attempt ${attempt + 1}/${this.retryConfig.maxRetries})`);
95
+ await this.sleep(delay);
96
+ }
97
+ }
98
+
99
+ throw lastError;
100
+ }
101
+
102
+ /**
103
+ * Make a streaming request
104
+ */
105
+ async *stream(options: RequestOptions): AsyncIterable<any> {
106
+ const url = this.buildURL(options.path, options.query);
107
+ const headers = this.buildHeaders(options.headers);
108
+
109
+ try {
110
+ const response = await fetch(url, {
111
+ method: options.method,
112
+ headers,
113
+ body: options.body ? JSON.stringify(options.body) : undefined,
114
+ signal: AbortSignal.timeout(options.timeout || this.config.timeout || 60000),
115
+ });
116
+
117
+ if (!response.ok) {
118
+ throw await this.handleErrorResponse(response);
119
+ }
120
+
121
+ if (!response.body) {
122
+ throw new NetworkError('Response body is null', new Error('No response body'));
123
+ }
124
+
125
+ const reader = response.body.getReader();
126
+ const decoder = new TextDecoder();
127
+ let buffer = '';
128
+
129
+ try {
130
+ while (true) {
131
+ const { done, value } = await reader.read();
132
+
133
+ if (done) {
134
+ break;
135
+ }
136
+
137
+ buffer += decoder.decode(value, { stream: true });
138
+ const lines = buffer.split('\n');
139
+ buffer = lines.pop() || '';
140
+
141
+ for (const line of lines) {
142
+ if (line.trim() === '') continue;
143
+ if (line.startsWith('data: ')) {
144
+ const data = line.slice(6);
145
+ if (data === '[DONE]') continue;
146
+ try {
147
+ yield JSON.parse(data);
148
+ } catch (e) {
149
+ this.config.logger?.warn('Failed to parse streaming chunk:', data);
150
+ }
151
+ }
152
+ }
153
+ }
154
+ } finally {
155
+ reader.releaseLock();
156
+ }
157
+ } catch (error) {
158
+ if (error instanceof MemoryLayerError) {
159
+ throw error;
160
+ }
161
+ throw new NetworkError('Streaming request failed', error as Error);
162
+ }
163
+ }
164
+
165
+ /**
166
+ * Make the actual HTTP request
167
+ */
168
+ private async makeRequest<T>(options: RequestOptions): Promise<T> {
169
+ const url = this.buildURL(options.path, options.query);
170
+ const headers = this.buildHeaders(options.headers);
171
+
172
+ try {
173
+ const response = await fetch(url, {
174
+ method: options.method,
175
+ headers,
176
+ body: options.body ? JSON.stringify(options.body) : undefined,
177
+ signal: AbortSignal.timeout(options.timeout || this.config.timeout || 30000),
178
+ });
179
+
180
+ if (!response.ok) {
181
+ throw await this.handleErrorResponse(response);
182
+ }
183
+
184
+ return await response.json();
185
+ } catch (error) {
186
+ if (error instanceof MemoryLayerError) {
187
+ throw error;
188
+ }
189
+ throw new NetworkError('Request failed', error as Error);
190
+ }
191
+ }
192
+
193
+ /**
194
+ * Build the full URL with query parameters
195
+ */
196
+ private buildURL(path: string, query?: Record<string, string>): string {
197
+ const url = new URL(path, this.baseURL);
198
+ if (query) {
199
+ Object.entries(query).forEach(([key, value]) => {
200
+ url.searchParams.append(key, value);
201
+ });
202
+ }
203
+ return url.toString();
204
+ }
205
+
206
+ /**
207
+ * Build request headers
208
+ */
209
+ private buildHeaders(additionalHeaders?: Record<string, string>): Record<string, string> {
210
+ return {
211
+ 'Content-Type': 'application/json',
212
+ 'Authorization': `Bearer ${this.config.apiKey}`,
213
+ 'X-SDK-Version': '0.1.0',
214
+ 'X-API-Version': 'v1',
215
+ ...this.config.headers,
216
+ ...additionalHeaders,
217
+ };
218
+ }
219
+
220
+ /**
221
+ * Handle error responses from the API
222
+ */
223
+ private async handleErrorResponse(response: Response): Promise<MemoryLayerError> {
224
+ const requestId = response.headers.get('x-request-id') || undefined;
225
+ let errorData: any;
226
+
227
+ try {
228
+ errorData = await response.json();
229
+ } catch {
230
+ errorData = { message: response.statusText };
231
+ }
232
+
233
+ const message = errorData.error?.message || errorData.message || 'Unknown error';
234
+ const details = errorData.error?.details || errorData.details;
235
+
236
+ switch (response.status) {
237
+ case 401:
238
+ return new AuthenticationError(message, requestId);
239
+ case 429:
240
+ const retryAfter = parseInt(response.headers.get('retry-after') || '60', 10);
241
+ return new RateLimitError(message, retryAfter, requestId);
242
+ case 400:
243
+ const errors = errorData.error?.errors || [];
244
+ return new ValidationError(message, errors, requestId);
245
+ default:
246
+ return new APIError(message, response.status, requestId, details);
247
+ }
248
+ }
249
+
250
+ /**
251
+ * Determine if an error should be retried
252
+ */
253
+ private shouldRetry(error: MemoryLayerError, attempt: number): boolean {
254
+ if (attempt >= this.retryConfig.maxRetries) {
255
+ return false;
256
+ }
257
+
258
+ // Retry network errors
259
+ if (error instanceof NetworkError) {
260
+ return true;
261
+ }
262
+
263
+ // Retry rate limit errors
264
+ if (error instanceof RateLimitError) {
265
+ return true;
266
+ }
267
+
268
+ // Retry specific status codes
269
+ if (error.statusCode && this.retryConfig.retryableStatusCodes.includes(error.statusCode)) {
270
+ return true;
271
+ }
272
+
273
+ // Don't retry client errors (4xx except 429)
274
+ if (error.statusCode && error.statusCode >= 400 && error.statusCode < 500) {
275
+ return false;
276
+ }
277
+
278
+ return false;
279
+ }
280
+
281
+ /**
282
+ * Calculate retry delay with exponential backoff
283
+ */
284
+ private getRetryDelay(error: MemoryLayerError, attempt: number): number {
285
+ // Respect Retry-After header for rate limit errors
286
+ if (error instanceof RateLimitError) {
287
+ return error.retryAfter * 1000;
288
+ }
289
+
290
+ // Exponential backoff with jitter
291
+ const exponentialDelay = this.retryConfig.initialDelay * Math.pow(this.retryConfig.backoffMultiplier, attempt);
292
+ const jitter = Math.random() * 0.1 * exponentialDelay; // 10% jitter
293
+ const delay = Math.min(exponentialDelay + jitter, this.retryConfig.maxDelay);
294
+
295
+ return delay;
296
+ }
297
+
298
+ /**
299
+ * Sleep for a specified duration
300
+ */
301
+ private sleep(ms: number): Promise<void> {
302
+ return new Promise(resolve => setTimeout(resolve, ms));
303
+ }
304
+ }
package/src/index.ts ADDED
@@ -0,0 +1,50 @@
1
+ /**
2
+ * MemoryLayer Node.js/TypeScript SDK
3
+ *
4
+ * Official SDK for integrating MemoryLayer into your applications.
5
+ *
6
+ * @packageDocumentation
7
+ */
8
+
9
+ // Main client
10
+ export { MemoryLayerClient } from './client.js';
11
+ export type { ClientConfig } from './client.js';
12
+
13
+ // Error classes
14
+ export {
15
+ MemoryLayerError,
16
+ AuthenticationError,
17
+ RateLimitError,
18
+ ValidationError,
19
+ NetworkError,
20
+ APIError,
21
+ } from './errors.js';
22
+ export type { ValidationErrorDetail } from './errors.js';
23
+
24
+ // Type definitions
25
+ export type {
26
+ Memory,
27
+ CreateMemoryRequest,
28
+ UpdateMemoryRequest,
29
+ ListMemoriesRequest,
30
+ SearchRequest,
31
+ SearchResult,
32
+ SearchResponse,
33
+ IngestFileRequest,
34
+ IngestTextRequest,
35
+ IngestResponse,
36
+ Message,
37
+ RouterRequest,
38
+ RouterResponse,
39
+ Usage,
40
+ Choice,
41
+ StreamChunk,
42
+ StreamChoice,
43
+ StreamDelta,
44
+ } from './types.js';
45
+
46
+ // Resources (exported for advanced use cases)
47
+ export { MemoriesResource } from './resources/memories.js';
48
+ export { SearchResource } from './resources/search.js';
49
+ export { IngestResource } from './resources/ingest.js';
50
+ export { RouterResource } from './resources/router.js';
@@ -0,0 +1,77 @@
1
+ import { HTTPClient } from '../http-client.js';
2
+ import { IngestFileRequest, IngestTextRequest, IngestResponse } from '../types.js';
3
+ import { ValidationError } from '../errors.js';
4
+
5
+ /**
6
+ * Resource for ingestion operations
7
+ */
8
+ export class IngestResource {
9
+ constructor(private httpClient: HTTPClient) {}
10
+
11
+ /**
12
+ * Ingest a file
13
+ * @param request - File ingestion request
14
+ * @returns Ingestion response with created memory IDs
15
+ */
16
+ async file(request: IngestFileRequest): Promise<IngestResponse> {
17
+ // Validate request
18
+ if (!request.file) {
19
+ throw new ValidationError(
20
+ 'File is required',
21
+ [{ field: 'file', message: 'File is required' }]
22
+ );
23
+ }
24
+
25
+ if (!request.projectId || request.projectId.trim().length === 0) {
26
+ throw new ValidationError(
27
+ 'Project ID is required',
28
+ [{ field: 'projectId', message: 'Project ID is required' }]
29
+ );
30
+ }
31
+
32
+ // For file uploads, we need to use FormData
33
+ // This is a simplified implementation - in production, you'd handle multipart/form-data properly
34
+ const body = {
35
+ projectId: request.projectId,
36
+ metadata: request.metadata,
37
+ chunkSize: request.chunkSize,
38
+ chunkOverlap: request.chunkOverlap,
39
+ // In a real implementation, you'd convert the file to base64 or use FormData
40
+ file: request.file,
41
+ };
42
+
43
+ return this.httpClient.request<IngestResponse>({
44
+ method: 'POST',
45
+ path: '/v1/ingest/file',
46
+ body,
47
+ });
48
+ }
49
+
50
+ /**
51
+ * Ingest text
52
+ * @param request - Text ingestion request
53
+ * @returns Ingestion response with created memory IDs
54
+ */
55
+ async text(request: IngestTextRequest): Promise<IngestResponse> {
56
+ // Validate request
57
+ if (!request.text || request.text.trim().length === 0) {
58
+ throw new ValidationError(
59
+ 'Text cannot be empty',
60
+ [{ field: 'text', message: 'Text is required and cannot be empty' }]
61
+ );
62
+ }
63
+
64
+ if (!request.projectId || request.projectId.trim().length === 0) {
65
+ throw new ValidationError(
66
+ 'Project ID is required',
67
+ [{ field: 'projectId', message: 'Project ID is required' }]
68
+ );
69
+ }
70
+
71
+ return this.httpClient.request<IngestResponse>({
72
+ method: 'POST',
73
+ path: '/v1/ingest/text',
74
+ body: request,
75
+ });
76
+ }
77
+ }