@twelvehart/supermemory-runtime 1.0.0-next.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.
Files changed (156) hide show
  1. package/.env.example +57 -0
  2. package/README.md +374 -0
  3. package/dist/index.js +189 -0
  4. package/dist/mcp/index.js +1132 -0
  5. package/docker-compose.prod.yml +91 -0
  6. package/docker-compose.yml +358 -0
  7. package/drizzle/0000_dapper_the_professor.sql +159 -0
  8. package/drizzle/0001_api_keys.sql +51 -0
  9. package/drizzle/meta/0000_snapshot.json +1532 -0
  10. package/drizzle/meta/_journal.json +13 -0
  11. package/drizzle.config.ts +20 -0
  12. package/package.json +114 -0
  13. package/scripts/add-extraction-job.ts +122 -0
  14. package/scripts/benchmark-pgvector.ts +122 -0
  15. package/scripts/bootstrap.sh +209 -0
  16. package/scripts/check-runtime-pack.ts +111 -0
  17. package/scripts/claude-mcp-config.ts +336 -0
  18. package/scripts/docker-entrypoint.sh +183 -0
  19. package/scripts/doctor.ts +377 -0
  20. package/scripts/init-db.sql +33 -0
  21. package/scripts/install.sh +1110 -0
  22. package/scripts/mcp-setup.ts +271 -0
  23. package/scripts/migrations/001_create_pgvector_extension.sql +31 -0
  24. package/scripts/migrations/002_create_memory_embeddings_table.sql +75 -0
  25. package/scripts/migrations/003_create_hnsw_index.sql +94 -0
  26. package/scripts/migrations/004_create_memory_embeddings_standalone.sql +70 -0
  27. package/scripts/migrations/005_create_chunks_table.sql +95 -0
  28. package/scripts/migrations/006_create_processing_queue.sql +45 -0
  29. package/scripts/migrations/generate_test_data.sql +42 -0
  30. package/scripts/migrations/phase1_comprehensive_test.sql +204 -0
  31. package/scripts/migrations/run_migrations.sh +286 -0
  32. package/scripts/migrations/test_hnsw_index.sql +255 -0
  33. package/scripts/pre-commit-secrets +282 -0
  34. package/scripts/run-extraction-worker.ts +46 -0
  35. package/scripts/run-phase1-tests.sh +291 -0
  36. package/scripts/setup.ts +222 -0
  37. package/scripts/smoke-install.sh +12 -0
  38. package/scripts/test-health-endpoint.sh +328 -0
  39. package/src/api/index.ts +2 -0
  40. package/src/api/middleware/auth.ts +80 -0
  41. package/src/api/middleware/csrf.ts +308 -0
  42. package/src/api/middleware/errorHandler.ts +166 -0
  43. package/src/api/middleware/rateLimit.ts +360 -0
  44. package/src/api/middleware/validation.ts +514 -0
  45. package/src/api/routes/documents.ts +286 -0
  46. package/src/api/routes/profiles.ts +237 -0
  47. package/src/api/routes/search.ts +71 -0
  48. package/src/api/stores/index.ts +58 -0
  49. package/src/config/bootstrap-env.ts +3 -0
  50. package/src/config/env.ts +71 -0
  51. package/src/config/feature-flags.ts +25 -0
  52. package/src/config/index.ts +140 -0
  53. package/src/config/secrets.config.ts +291 -0
  54. package/src/db/client.ts +92 -0
  55. package/src/db/index.ts +73 -0
  56. package/src/db/postgres.ts +72 -0
  57. package/src/db/schema/chunks.schema.ts +31 -0
  58. package/src/db/schema/containers.schema.ts +46 -0
  59. package/src/db/schema/documents.schema.ts +49 -0
  60. package/src/db/schema/embeddings.schema.ts +32 -0
  61. package/src/db/schema/index.ts +11 -0
  62. package/src/db/schema/memories.schema.ts +72 -0
  63. package/src/db/schema/profiles.schema.ts +34 -0
  64. package/src/db/schema/queue.schema.ts +59 -0
  65. package/src/db/schema/relationships.schema.ts +42 -0
  66. package/src/db/schema.ts +223 -0
  67. package/src/db/worker-connection.ts +47 -0
  68. package/src/index.ts +235 -0
  69. package/src/mcp/CLAUDE.md +1 -0
  70. package/src/mcp/index.ts +1380 -0
  71. package/src/mcp/legacyState.ts +22 -0
  72. package/src/mcp/rateLimit.ts +358 -0
  73. package/src/mcp/resources.ts +309 -0
  74. package/src/mcp/results.ts +104 -0
  75. package/src/mcp/tools.ts +401 -0
  76. package/src/queues/config.ts +119 -0
  77. package/src/queues/index.ts +289 -0
  78. package/src/sdk/client.ts +225 -0
  79. package/src/sdk/errors.ts +266 -0
  80. package/src/sdk/http.ts +560 -0
  81. package/src/sdk/index.ts +244 -0
  82. package/src/sdk/resources/base.ts +65 -0
  83. package/src/sdk/resources/connections.ts +204 -0
  84. package/src/sdk/resources/documents.ts +163 -0
  85. package/src/sdk/resources/index.ts +10 -0
  86. package/src/sdk/resources/memories.ts +150 -0
  87. package/src/sdk/resources/search.ts +60 -0
  88. package/src/sdk/resources/settings.ts +36 -0
  89. package/src/sdk/types.ts +674 -0
  90. package/src/services/chunking/index.ts +451 -0
  91. package/src/services/chunking.service.ts +650 -0
  92. package/src/services/csrf.service.ts +252 -0
  93. package/src/services/documents.repository.ts +219 -0
  94. package/src/services/documents.service.ts +191 -0
  95. package/src/services/embedding.service.ts +404 -0
  96. package/src/services/extraction.service.ts +300 -0
  97. package/src/services/extractors/code.extractor.ts +451 -0
  98. package/src/services/extractors/index.ts +9 -0
  99. package/src/services/extractors/markdown.extractor.ts +461 -0
  100. package/src/services/extractors/pdf.extractor.ts +315 -0
  101. package/src/services/extractors/text.extractor.ts +118 -0
  102. package/src/services/extractors/url.extractor.ts +243 -0
  103. package/src/services/index.ts +235 -0
  104. package/src/services/ingestion.service.ts +177 -0
  105. package/src/services/llm/anthropic.ts +400 -0
  106. package/src/services/llm/base.ts +460 -0
  107. package/src/services/llm/contradiction-detector.service.ts +526 -0
  108. package/src/services/llm/heuristics.ts +148 -0
  109. package/src/services/llm/index.ts +309 -0
  110. package/src/services/llm/memory-classifier.service.ts +383 -0
  111. package/src/services/llm/memory-extension-detector.service.ts +523 -0
  112. package/src/services/llm/mock.ts +470 -0
  113. package/src/services/llm/openai.ts +398 -0
  114. package/src/services/llm/prompts.ts +438 -0
  115. package/src/services/llm/types.ts +373 -0
  116. package/src/services/memory.repository.ts +1769 -0
  117. package/src/services/memory.service.ts +1338 -0
  118. package/src/services/memory.types.ts +234 -0
  119. package/src/services/persistence/index.ts +295 -0
  120. package/src/services/pipeline.service.ts +509 -0
  121. package/src/services/profile.repository.ts +436 -0
  122. package/src/services/profile.service.ts +560 -0
  123. package/src/services/profile.types.ts +270 -0
  124. package/src/services/relationships/detector.ts +1128 -0
  125. package/src/services/relationships/index.ts +268 -0
  126. package/src/services/relationships/memory-integration.ts +459 -0
  127. package/src/services/relationships/strategies.ts +132 -0
  128. package/src/services/relationships/types.ts +370 -0
  129. package/src/services/search.service.ts +761 -0
  130. package/src/services/search.types.ts +220 -0
  131. package/src/services/secrets.service.ts +384 -0
  132. package/src/services/vectorstore/base.ts +327 -0
  133. package/src/services/vectorstore/index.ts +444 -0
  134. package/src/services/vectorstore/memory.ts +286 -0
  135. package/src/services/vectorstore/migration.ts +295 -0
  136. package/src/services/vectorstore/mock.ts +403 -0
  137. package/src/services/vectorstore/pgvector.ts +695 -0
  138. package/src/services/vectorstore/types.ts +247 -0
  139. package/src/startup.ts +389 -0
  140. package/src/types/api.types.ts +193 -0
  141. package/src/types/document.types.ts +103 -0
  142. package/src/types/index.ts +241 -0
  143. package/src/types/profile.base.ts +133 -0
  144. package/src/utils/errors.ts +447 -0
  145. package/src/utils/id.ts +15 -0
  146. package/src/utils/index.ts +101 -0
  147. package/src/utils/logger.ts +313 -0
  148. package/src/utils/sanitization.ts +501 -0
  149. package/src/utils/secret-validation.ts +273 -0
  150. package/src/utils/synonyms.ts +188 -0
  151. package/src/utils/validation.ts +581 -0
  152. package/src/workers/chunking.worker.ts +242 -0
  153. package/src/workers/embedding.worker.ts +358 -0
  154. package/src/workers/extraction.worker.ts +346 -0
  155. package/src/workers/indexing.worker.ts +505 -0
  156. package/tsconfig.json +38 -0
@@ -0,0 +1,22 @@
1
+ import { access, readFile, rename } from 'node:fs/promises'
2
+
3
+ export async function pathExists(filePath: string): Promise<boolean> {
4
+ try {
5
+ await access(filePath)
6
+ return true
7
+ } catch {
8
+ return false
9
+ }
10
+ }
11
+
12
+ export async function readJsonFile<T>(filePath: string): Promise<T> {
13
+ const raw = await readFile(filePath, 'utf-8')
14
+ return JSON.parse(raw) as T
15
+ }
16
+
17
+ export async function archiveFileWithSuffix(filePath: string, suffix: string = '.migrated'): Promise<string> {
18
+ const archiveBasePath = `${filePath}${suffix}`
19
+ const archivePath = (await pathExists(archiveBasePath)) ? `${archiveBasePath}.${Date.now()}` : archiveBasePath
20
+ await rename(filePath, archivePath)
21
+ return archivePath
22
+ }
@@ -0,0 +1,358 @@
1
+ /**
2
+ * MCP Rate Limiter
3
+ *
4
+ * Provides rate limiting for MCP tool calls using the existing
5
+ * RateLimitStore infrastructure. Supports both per-tool limits
6
+ * and global limits per containerTag.
7
+ */
8
+
9
+ import { RateLimitStore, MemoryRateLimitStore, RedisRateLimitStore } from '../api/middleware/rateLimit.js'
10
+ import { createMcpEnvelopeError, createToolResponse } from './results.js'
11
+
12
+ // ============================================================================
13
+ // Types
14
+ // ============================================================================
15
+
16
+ /**
17
+ * Result of a rate limit check
18
+ */
19
+ export interface RateLimitResult {
20
+ /** Whether the request is allowed */
21
+ allowed: boolean
22
+ /** Remaining requests in the current window */
23
+ remaining: number
24
+ /** Seconds until the limit resets */
25
+ resetIn: number
26
+ /** The limit that applies */
27
+ limit: number
28
+ /** Which limit was hit (if any) */
29
+ limitType?: 'tool' | 'global'
30
+ }
31
+
32
+ /**
33
+ * Configuration for a rate limit
34
+ */
35
+ export interface RateLimitConfig {
36
+ /** Maximum requests allowed */
37
+ maxRequests: number
38
+ /** Time window in milliseconds */
39
+ windowMs: number
40
+ }
41
+
42
+ /**
43
+ * Tool-specific rate limit configurations
44
+ */
45
+ export interface ToolLimits {
46
+ [toolName: string]: RateLimitConfig
47
+ }
48
+
49
+ // ============================================================================
50
+ // Default Limits
51
+ // ============================================================================
52
+
53
+ /**
54
+ * Tool-specific rate limits
55
+ *
56
+ * Limits are based on:
57
+ * - Computational cost (embedding generation is expensive)
58
+ * - Side effects (delete is destructive)
59
+ * - Read vs write operations
60
+ */
61
+ const DEFAULT_TOOL_LIMITS: ToolLimits = {
62
+ // Write operations with embedding generation - most expensive
63
+ supermemory_add: {
64
+ maxRequests: 50,
65
+ windowMs: 60 * 1000, // 50 req/min
66
+ },
67
+
68
+ // Search operations - moderately expensive
69
+ supermemory_search: {
70
+ maxRequests: 100,
71
+ windowMs: 60 * 1000, // 100 req/min
72
+ },
73
+
74
+ // Profile operations - moderately expensive
75
+ supermemory_profile: {
76
+ maxRequests: 50,
77
+ windowMs: 60 * 1000, // 50 req/min
78
+ },
79
+
80
+ // Remember - creates facts with potential embedding
81
+ supermemory_remember: {
82
+ maxRequests: 100,
83
+ windowMs: 60 * 1000, // 100 req/min
84
+ },
85
+
86
+ // Recall - semantic search over facts
87
+ supermemory_recall: {
88
+ maxRequests: 200,
89
+ windowMs: 60 * 1000, // 200 req/min
90
+ },
91
+
92
+ // List - lightweight read operation
93
+ supermemory_list: {
94
+ maxRequests: 200,
95
+ windowMs: 60 * 1000, // 200 req/min
96
+ },
97
+
98
+ // Delete - destructive, strict limit
99
+ supermemory_delete: {
100
+ maxRequests: 20,
101
+ windowMs: 60 * 1000, // 20 req/min
102
+ },
103
+ }
104
+
105
+ /**
106
+ * Global rate limit per containerTag
107
+ * Applies across all tools to prevent abuse
108
+ */
109
+ const DEFAULT_GLOBAL_LIMIT: RateLimitConfig = {
110
+ maxRequests: 1000,
111
+ windowMs: 15 * 60 * 1000, // 1000 req/15min
112
+ }
113
+
114
+ // ============================================================================
115
+ // MCPRateLimiter Class
116
+ // ============================================================================
117
+
118
+ /**
119
+ * Rate limiter for MCP tool calls
120
+ *
121
+ * Provides two levels of rate limiting:
122
+ * 1. Per-tool limits: Different limits for each tool based on cost
123
+ * 2. Global limit: Overall limit per containerTag across all tools
124
+ *
125
+ * Uses the same store infrastructure as the API rate limiter,
126
+ * supporting both in-memory (single instance) and Redis (distributed).
127
+ */
128
+ export class MCPRateLimiter {
129
+ private store: RateLimitStore
130
+ private toolLimits: ToolLimits
131
+ private globalLimit: RateLimitConfig
132
+ private readonly keyPrefix = 'mcp:'
133
+
134
+ constructor(options?: {
135
+ store?: RateLimitStore
136
+ toolLimits?: { [toolName: string]: RateLimitConfig }
137
+ globalLimit?: Partial<RateLimitConfig>
138
+ }) {
139
+ // Initialize store - use Redis if available, fallback to memory
140
+ this.store = options?.store ?? this.createDefaultStore()
141
+
142
+ // Merge tool limits with defaults
143
+ this.toolLimits = { ...DEFAULT_TOOL_LIMITS }
144
+ if (options?.toolLimits) {
145
+ for (const [key, value] of Object.entries(options.toolLimits)) {
146
+ this.toolLimits[key] = value
147
+ }
148
+ }
149
+
150
+ // Merge global limit with defaults
151
+ this.globalLimit = {
152
+ ...DEFAULT_GLOBAL_LIMIT,
153
+ ...options?.globalLimit,
154
+ }
155
+ }
156
+
157
+ /**
158
+ * Create the default rate limit store
159
+ * Uses Redis if REDIS_URL is set, otherwise in-memory
160
+ */
161
+ private createDefaultStore(): RateLimitStore {
162
+ if (process.env.REDIS_URL) {
163
+ return new RedisRateLimitStore()
164
+ }
165
+ return new MemoryRateLimitStore()
166
+ }
167
+
168
+ /**
169
+ * Get the rate limit configuration for a tool
170
+ * Returns a default configuration for unknown tools
171
+ */
172
+ getToolLimit(toolName: string): RateLimitConfig {
173
+ return (
174
+ this.toolLimits[toolName] ?? {
175
+ maxRequests: 100,
176
+ windowMs: 60 * 1000, // Default: 100 req/min
177
+ }
178
+ )
179
+ }
180
+
181
+ /**
182
+ * Get the global rate limit configuration
183
+ */
184
+ getGlobalLimit(): RateLimitConfig {
185
+ return this.globalLimit
186
+ }
187
+
188
+ /**
189
+ * Check if a request is allowed under rate limits
190
+ *
191
+ * @param containerTag - The container tag (user/context identifier)
192
+ * @param toolName - The MCP tool being called
193
+ * @returns Rate limit result with allowed status and metadata
194
+ */
195
+ async checkLimit(containerTag: string, toolName: string): Promise<RateLimitResult> {
196
+ const now = Date.now()
197
+ const tag = containerTag || 'default'
198
+
199
+ // Check tool-specific limit first
200
+ const toolLimit = this.getToolLimit(toolName)
201
+ const toolKey = `${this.keyPrefix}tool:${tag}:${toolName}`
202
+ const toolEntry = await this.store.increment(toolKey, toolLimit.windowMs)
203
+
204
+ const toolRemaining = Math.max(0, toolLimit.maxRequests - toolEntry.count)
205
+ const toolResetIn = Math.ceil((toolEntry.resetTime - now) / 1000)
206
+
207
+ if (toolEntry.count > toolLimit.maxRequests) {
208
+ return {
209
+ allowed: false,
210
+ remaining: 0,
211
+ resetIn: toolResetIn,
212
+ limit: toolLimit.maxRequests,
213
+ limitType: 'tool',
214
+ }
215
+ }
216
+
217
+ // Check global limit
218
+ const globalKey = `${this.keyPrefix}global:${tag}`
219
+ const globalEntry = await this.store.increment(globalKey, this.globalLimit.windowMs)
220
+
221
+ const globalRemaining = Math.max(0, this.globalLimit.maxRequests - globalEntry.count)
222
+ const globalResetIn = Math.ceil((globalEntry.resetTime - now) / 1000)
223
+
224
+ if (globalEntry.count > this.globalLimit.maxRequests) {
225
+ return {
226
+ allowed: false,
227
+ remaining: 0,
228
+ resetIn: globalResetIn,
229
+ limit: this.globalLimit.maxRequests,
230
+ limitType: 'global',
231
+ }
232
+ }
233
+
234
+ // Request is allowed - return the more restrictive remaining count
235
+ return {
236
+ allowed: true,
237
+ remaining: Math.min(toolRemaining, globalRemaining),
238
+ resetIn: Math.min(toolResetIn, globalResetIn),
239
+ limit: toolLimit.maxRequests,
240
+ }
241
+ }
242
+
243
+ /**
244
+ * Get current rate limit status without incrementing counters
245
+ *
246
+ * @param containerTag - The container tag (user/context identifier)
247
+ * @param toolName - The MCP tool to check
248
+ * @returns Current rate limit status
249
+ */
250
+ async getStatus(containerTag: string, toolName: string): Promise<RateLimitResult> {
251
+ const now = Date.now()
252
+ const tag = containerTag || 'default'
253
+
254
+ // Get tool-specific status
255
+ const toolLimit = this.getToolLimit(toolName)
256
+ const toolKey = `${this.keyPrefix}tool:${tag}:${toolName}`
257
+ const toolEntry = await this.store.get(toolKey)
258
+
259
+ let toolRemaining = toolLimit.maxRequests
260
+ let toolResetIn = 0
261
+
262
+ if (toolEntry && toolEntry.resetTime > now) {
263
+ toolRemaining = Math.max(0, toolLimit.maxRequests - toolEntry.count)
264
+ toolResetIn = Math.ceil((toolEntry.resetTime - now) / 1000)
265
+ }
266
+
267
+ // Get global status
268
+ const globalKey = `${this.keyPrefix}global:${tag}`
269
+ const globalEntry = await this.store.get(globalKey)
270
+
271
+ let globalRemaining = this.globalLimit.maxRequests
272
+ let globalResetIn = 0
273
+
274
+ if (globalEntry && globalEntry.resetTime > now) {
275
+ globalRemaining = Math.max(0, this.globalLimit.maxRequests - globalEntry.count)
276
+ globalResetIn = Math.ceil((globalEntry.resetTime - now) / 1000)
277
+ }
278
+
279
+ // Check if either limit is exceeded (or at capacity - next request would be blocked)
280
+ // Use >= because when count equals maxRequests, the next request would be blocked
281
+ if (toolEntry && toolEntry.count >= toolLimit.maxRequests && toolEntry.resetTime > now) {
282
+ return {
283
+ allowed: false,
284
+ remaining: 0,
285
+ resetIn: toolResetIn,
286
+ limit: toolLimit.maxRequests,
287
+ limitType: 'tool',
288
+ }
289
+ }
290
+
291
+ if (globalEntry && globalEntry.count >= this.globalLimit.maxRequests && globalEntry.resetTime > now) {
292
+ return {
293
+ allowed: false,
294
+ remaining: 0,
295
+ resetIn: globalResetIn,
296
+ limit: this.globalLimit.maxRequests,
297
+ limitType: 'global',
298
+ }
299
+ }
300
+
301
+ return {
302
+ allowed: true,
303
+ remaining: Math.min(toolRemaining, globalRemaining),
304
+ resetIn: Math.max(toolResetIn, globalResetIn),
305
+ limit: toolLimit.maxRequests,
306
+ }
307
+ }
308
+ }
309
+
310
+ // ============================================================================
311
+ // Singleton Instance
312
+ // ============================================================================
313
+
314
+ let rateLimiterInstance: MCPRateLimiter | null = null
315
+
316
+ /**
317
+ * Get or create the global MCP rate limiter instance
318
+ */
319
+ export function getMCPRateLimiter(): MCPRateLimiter {
320
+ if (!rateLimiterInstance) {
321
+ rateLimiterInstance = new MCPRateLimiter()
322
+ }
323
+ return rateLimiterInstance
324
+ }
325
+
326
+ /**
327
+ * Create a rate limit error response for MCP
328
+ *
329
+ * @param result - The rate limit result
330
+ * @param toolName - The tool that was rate limited
331
+ * @returns MCP-compatible error response
332
+ */
333
+ export function createRateLimitErrorResponse(
334
+ result: RateLimitResult,
335
+ toolName: string
336
+ ) {
337
+ const limitTypeMessage =
338
+ result.limitType === 'global' ? 'Global rate limit exceeded' : `Rate limit exceeded for ${toolName}`
339
+
340
+ return createToolResponse({
341
+ tool: toolName,
342
+ ok: false,
343
+ data: null,
344
+ errors: [
345
+ createMcpEnvelopeError(
346
+ 'RATE_LIMIT_EXCEEDED',
347
+ `${limitTypeMessage}. Try again in ${result.resetIn} seconds. (Limit: ${result.limit} requests)`,
348
+ {
349
+ limit: result.limit,
350
+ limitType: result.limitType ?? 'tool',
351
+ remaining: result.remaining,
352
+ resetIn: result.resetIn,
353
+ },
354
+ true
355
+ ),
356
+ ],
357
+ })
358
+ }
@@ -0,0 +1,309 @@
1
+ /**
2
+ * MCP Resource Definitions for Supermemory
3
+ *
4
+ * Exposes supermemory data as MCP resources that can be read by clients.
5
+ * Resources use URI patterns to identify different data types.
6
+ */
7
+
8
+ import { ValidationError } from '../utils/errors.js'
9
+
10
+ // ============================================================================
11
+ // Resource URI Patterns
12
+ // ============================================================================
13
+
14
+ /**
15
+ * Resource URI patterns:
16
+ * - memory://profiles/{containerTag} - User profile for a container
17
+ * - memory://documents/{id} - Specific document by ID
18
+ * - memory://search?q={query}&container={containerTag} - Search results
19
+ * - memory://facts/{containerTag} - Facts for a container
20
+ * - memory://stats - Overall statistics
21
+ */
22
+
23
+ export const RESOURCE_TEMPLATES = [
24
+ {
25
+ uriTemplate: 'memory://profiles/{containerTag}',
26
+ name: 'User Profile',
27
+ description: 'Get the user profile containing extracted facts for a specific container',
28
+ mimeType: 'application/json',
29
+ },
30
+ {
31
+ uriTemplate: 'memory://documents/{id}',
32
+ name: 'Document',
33
+ description: 'Get a specific document by its ID',
34
+ mimeType: 'application/json',
35
+ },
36
+ {
37
+ uriTemplate: 'memory://search',
38
+ name: 'Search Results',
39
+ description: 'Search for memories. Query params: q (query), container (containerTag), limit, mode',
40
+ mimeType: 'application/json',
41
+ },
42
+ {
43
+ uriTemplate: 'memory://facts/{containerTag}',
44
+ name: 'Container Facts',
45
+ description: 'Get all facts for a specific container',
46
+ mimeType: 'application/json',
47
+ },
48
+ {
49
+ uriTemplate: 'memory://stats',
50
+ name: 'Statistics',
51
+ description: 'Get overall supermemory statistics',
52
+ mimeType: 'application/json',
53
+ },
54
+ ]
55
+
56
+ // ============================================================================
57
+ // Resource Parser
58
+ // ============================================================================
59
+
60
+ export interface ParsedResourceUri {
61
+ type: 'profile' | 'document' | 'search' | 'facts' | 'stats' | 'unknown'
62
+ params: Record<string, string>
63
+ }
64
+
65
+ /**
66
+ * Parse a resource URI into its components
67
+ */
68
+ export function parseResourceUri(uri: string): ParsedResourceUri {
69
+ // Handle memory:// protocol
70
+ if (!uri.startsWith('memory://')) {
71
+ return { type: 'unknown', params: {} }
72
+ }
73
+
74
+ const path = uri.substring('memory://'.length)
75
+ const [pathPart, queryPart] = path.split('?')
76
+
77
+ // Parse query parameters
78
+ const params: Record<string, string> = {}
79
+ if (queryPart) {
80
+ const searchParams = new URLSearchParams(queryPart)
81
+ for (const [key, value] of searchParams.entries()) {
82
+ params[key] = value
83
+ }
84
+ }
85
+
86
+ // Parse path
87
+ if (!pathPart) {
88
+ return { type: 'unknown', params }
89
+ }
90
+
91
+ const segments = pathPart.split('/').filter(Boolean)
92
+
93
+ if (segments[0] === 'profiles' && segments[1]) {
94
+ return { type: 'profile', params: { containerTag: segments[1], ...params } }
95
+ }
96
+
97
+ if (segments[0] === 'documents' && segments[1]) {
98
+ return { type: 'document', params: { id: segments[1], ...params } }
99
+ }
100
+
101
+ if (segments[0] === 'search') {
102
+ return { type: 'search', params }
103
+ }
104
+
105
+ if (segments[0] === 'facts' && segments[1]) {
106
+ return { type: 'facts', params: { containerTag: segments[1], ...params } }
107
+ }
108
+
109
+ if (segments[0] === 'stats') {
110
+ return { type: 'stats', params }
111
+ }
112
+
113
+ return { type: 'unknown', params }
114
+ }
115
+
116
+ /**
117
+ * Build a resource URI from components
118
+ */
119
+ export function buildResourceUri(
120
+ type: 'profile' | 'document' | 'search' | 'facts' | 'stats',
121
+ params?: Record<string, string>
122
+ ): string {
123
+ switch (type) {
124
+ case 'profile':
125
+ if (!params?.containerTag) {
126
+ throw new ValidationError('containerTag required for profile URI', {
127
+ containerTag: ['containerTag parameter is required for profile URIs'],
128
+ })
129
+ }
130
+ return `memory://profiles/${encodeURIComponent(params.containerTag)}`
131
+
132
+ case 'document':
133
+ if (!params?.id) {
134
+ throw new ValidationError('id required for document URI', {
135
+ id: ['id parameter is required for document URIs'],
136
+ })
137
+ }
138
+ return `memory://documents/${encodeURIComponent(params.id)}`
139
+
140
+ case 'search': {
141
+ const searchParams = new URLSearchParams()
142
+ if (params?.q) searchParams.set('q', params.q)
143
+ if (params?.container) searchParams.set('container', params.container)
144
+ if (params?.limit) searchParams.set('limit', params.limit)
145
+ if (params?.mode) searchParams.set('mode', params.mode)
146
+ const queryString = searchParams.toString()
147
+ return queryString ? `memory://search?${queryString}` : 'memory://search'
148
+ }
149
+
150
+ case 'facts':
151
+ if (!params?.containerTag) {
152
+ throw new ValidationError('containerTag required for facts URI', {
153
+ containerTag: ['containerTag parameter is required for facts URIs'],
154
+ })
155
+ }
156
+ return `memory://facts/${encodeURIComponent(params.containerTag)}`
157
+
158
+ case 'stats':
159
+ return 'memory://stats'
160
+
161
+ default:
162
+ throw new ValidationError(`Unknown resource type: ${type}`, {
163
+ type: [`Invalid resource type '${type}'. Valid types: profiles, documents, search, facts, stats`],
164
+ })
165
+ }
166
+ }
167
+
168
+ // ============================================================================
169
+ // Resource Response Types
170
+ // ============================================================================
171
+
172
+ export interface ProfileResource {
173
+ uri: string
174
+ containerTag: string
175
+ staticFacts: Array<{
176
+ id: string
177
+ content: string
178
+ category?: string
179
+ confidence: number
180
+ extractedAt: string
181
+ }>
182
+ dynamicFacts: Array<{
183
+ id: string
184
+ content: string
185
+ category?: string
186
+ expiresAt?: string
187
+ extractedAt: string
188
+ }>
189
+ createdAt: string
190
+ updatedAt: string
191
+ version: number
192
+ }
193
+
194
+ export interface DocumentResource {
195
+ uri: string
196
+ id: string
197
+ title?: string
198
+ content: string
199
+ contentType: string
200
+ containerTag?: string
201
+ sourceUrl?: string
202
+ metadata?: Record<string, unknown>
203
+ createdAt: string
204
+ updatedAt: string
205
+ }
206
+
207
+ export interface SearchResource {
208
+ uri: string
209
+ query: string
210
+ results: Array<{
211
+ id: string
212
+ content: string
213
+ similarity: number
214
+ containerTag?: string
215
+ metadata?: Record<string, unknown>
216
+ }>
217
+ totalCount: number
218
+ searchTimeMs: number
219
+ }
220
+
221
+ export interface FactsResource {
222
+ uri: string
223
+ containerTag: string
224
+ facts: Array<{
225
+ id: string
226
+ content: string
227
+ type: 'static' | 'dynamic'
228
+ category?: string
229
+ confidence: number
230
+ createdAt: string
231
+ expiresAt?: string
232
+ }>
233
+ totalCount: number
234
+ }
235
+
236
+ export interface StatsResource {
237
+ uri: string
238
+ totalDocuments: number
239
+ totalMemories: number
240
+ totalFacts: number
241
+ containerTags: string[]
242
+ indexedVectors: number
243
+ lastUpdated: string
244
+ }
245
+
246
+ // ============================================================================
247
+ // Resource List Response
248
+ // ============================================================================
249
+
250
+ export interface ResourceListItem {
251
+ uri: string
252
+ name: string
253
+ description?: string
254
+ mimeType?: string
255
+ }
256
+
257
+ export const MAX_LISTED_RESOURCE_CONTAINERS = 5
258
+ export const MAX_LISTED_RESOURCE_DOCUMENTS = 5
259
+
260
+ /**
261
+ * Generate a list of available resources for a given state
262
+ */
263
+ export function generateResourceList(containerTags: string[], documentIds: string[]): ResourceListItem[] {
264
+ const resources: ResourceListItem[] = []
265
+
266
+ // Add stats resource
267
+ resources.push({
268
+ uri: 'memory://stats',
269
+ name: 'Supermemory Statistics',
270
+ description: 'Overall statistics and health of the memory system',
271
+ mimeType: 'application/json',
272
+ })
273
+
274
+ // Add search resource
275
+ resources.push({
276
+ uri: 'memory://search',
277
+ name: 'Search Memories',
278
+ description: 'Search through stored memories (add ?q=query to search)',
279
+ mimeType: 'application/json',
280
+ })
281
+
282
+ // Add profile resources for each container
283
+ for (const tag of containerTags.slice(0, MAX_LISTED_RESOURCE_CONTAINERS)) {
284
+ resources.push({
285
+ uri: buildResourceUri('profile', { containerTag: tag }),
286
+ name: `Profile: ${tag}`,
287
+ description: `User profile and facts for container "${tag}"`,
288
+ mimeType: 'application/json',
289
+ })
290
+
291
+ resources.push({
292
+ uri: buildResourceUri('facts', { containerTag: tag }),
293
+ name: `Facts: ${tag}`,
294
+ description: `All facts for container "${tag}"`,
295
+ mimeType: 'application/json',
296
+ })
297
+ }
298
+
299
+ // Add only a few recent documents to keep resource discovery bounded.
300
+ for (const id of documentIds.slice(0, MAX_LISTED_RESOURCE_DOCUMENTS)) {
301
+ resources.push({
302
+ uri: buildResourceUri('document', { id }),
303
+ name: `Document: ${id}`,
304
+ mimeType: 'application/json',
305
+ })
306
+ }
307
+
308
+ return resources
309
+ }