@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,72 @@
1
+ import { drizzle } from 'drizzle-orm/node-postgres'
2
+ import { migrate } from 'drizzle-orm/node-postgres/migrator'
3
+ import pkg from 'pg'
4
+ const { Pool } = pkg
5
+ import * as schema from './schema/index.js'
6
+
7
+ let db: ReturnType<typeof drizzle<typeof schema>> | null = null
8
+ let pool: pkg.Pool | null = null
9
+
10
+ function parsePoolNumberEnv(name: string, fallback: number): number {
11
+ const rawValue = process.env[name]
12
+ if (!rawValue) return fallback
13
+
14
+ const parsed = Number.parseInt(rawValue, 10)
15
+ return Number.isFinite(parsed) ? parsed : fallback
16
+ }
17
+
18
+ export function getPostgresPoolConfig(): Pick<
19
+ pkg.PoolConfig,
20
+ 'min' | 'max' | 'idleTimeoutMillis' | 'connectionTimeoutMillis'
21
+ > {
22
+ return {
23
+ min: parsePoolNumberEnv('SUPERMEMORY_PG_POOL_MIN', 10),
24
+ max: parsePoolNumberEnv('SUPERMEMORY_PG_POOL_MAX', 100),
25
+ idleTimeoutMillis: parsePoolNumberEnv('SUPERMEMORY_PG_POOL_IDLE_TIMEOUT_MS', 30000),
26
+ connectionTimeoutMillis: parsePoolNumberEnv('SUPERMEMORY_PG_POOL_CONNECTION_TIMEOUT_MS', 2000),
27
+ }
28
+ }
29
+
30
+ export function createPostgresDatabase(connectionString: string) {
31
+ // Create connection pool
32
+ pool = new Pool({
33
+ connectionString,
34
+ ...getPostgresPoolConfig(),
35
+ })
36
+
37
+ // Enable pgvector extension on connection
38
+ pool.on('connect', async (client) => {
39
+ try {
40
+ await client.query('CREATE EXTENSION IF NOT EXISTS vector')
41
+ } catch (error) {
42
+ console.error('Error enabling pgvector extension:', error)
43
+ }
44
+ })
45
+
46
+ return drizzle(pool, { schema })
47
+ }
48
+
49
+ export function getPostgresDatabase(connectionString: string) {
50
+ if (!db) {
51
+ db = createPostgresDatabase(connectionString)
52
+ }
53
+ return db
54
+ }
55
+
56
+ export async function runPostgresMigrations(connectionString: string) {
57
+ const database = getPostgresDatabase(connectionString)
58
+ await migrate(database, { migrationsFolder: './drizzle' })
59
+ console.log('PostgreSQL migrations completed successfully')
60
+ }
61
+
62
+ export async function closePostgresDatabase() {
63
+ if (pool) {
64
+ await pool.end()
65
+ pool = null
66
+ db = null
67
+ }
68
+ }
69
+
70
+ export type PostgresDatabaseInstance = ReturnType<typeof createPostgresDatabase>
71
+
72
+ export { schema }
@@ -0,0 +1,31 @@
1
+ import { pgTable, uuid, text, integer, jsonb, timestamp, index } from 'drizzle-orm/pg-core'
2
+ import { sql } from 'drizzle-orm'
3
+ import { memories } from './memories.schema.js'
4
+
5
+ export const chunks = pgTable(
6
+ 'chunks',
7
+ {
8
+ id: uuid('id')
9
+ .primaryKey()
10
+ .default(sql`gen_random_uuid()`),
11
+ memoryId: uuid('memory_id')
12
+ .notNull()
13
+ .references(() => memories.id, { onDelete: 'cascade' }),
14
+ content: text('content').notNull(),
15
+ chunkIndex: integer('chunk_index').notNull(),
16
+ startOffset: integer('start_offset'),
17
+ endOffset: integer('end_offset'),
18
+ tokenCount: integer('token_count'),
19
+ metadata: jsonb('metadata').default(sql`'{}'::jsonb`),
20
+ createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
21
+ },
22
+ (table) => [
23
+ index('idx_chunks_memory_id').on(table.memoryId),
24
+ index('idx_chunks_chunk_index').on(table.memoryId, table.chunkIndex),
25
+ index('idx_chunks_token_count').on(table.tokenCount),
26
+ index('idx_chunks_metadata').using('gin', sql`${table.metadata} jsonb_path_ops`),
27
+ ]
28
+ )
29
+
30
+ export type Chunk = typeof chunks.$inferSelect
31
+ export type NewChunk = typeof chunks.$inferInsert
@@ -0,0 +1,46 @@
1
+ import {
2
+ pgTable,
3
+ uuid,
4
+ varchar,
5
+ text,
6
+ jsonb,
7
+ boolean,
8
+ timestamp,
9
+ index,
10
+ check,
11
+ type AnyPgColumn,
12
+ } from 'drizzle-orm/pg-core'
13
+ import { sql } from 'drizzle-orm'
14
+
15
+ export const containerTags = pgTable(
16
+ 'container_tags',
17
+ {
18
+ id: uuid('id')
19
+ .primaryKey()
20
+ .default(sql`gen_random_uuid()`),
21
+ tag: varchar('tag', { length: 255 }).notNull().unique(),
22
+ parentTag: varchar('parent_tag', { length: 255 }).references((): AnyPgColumn => containerTags.tag, {
23
+ onDelete: 'set null',
24
+ }),
25
+ displayName: varchar('display_name', { length: 255 }),
26
+ description: text('description'),
27
+ metadata: jsonb('metadata').default(sql`'{}'::jsonb`),
28
+ settings: jsonb('settings').default(sql`'{}'::jsonb`),
29
+ isActive: boolean('is_active').notNull().default(true),
30
+ createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
31
+ updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),
32
+ },
33
+ (table) => [
34
+ index('idx_container_tags_parent').on(table.parentTag),
35
+ index('idx_container_tags_active')
36
+ .on(table.isActive)
37
+ .where(sql`${table.isActive} = TRUE`),
38
+ index('idx_container_tags_metadata').using('gin', table.metadata),
39
+ index('idx_container_tags_hierarchy').on(table.tag, table.parentTag),
40
+ check('container_tags_no_self_parent', sql`${table.tag} != ${table.parentTag}`),
41
+ check('container_tags_tag_format', sql`${table.tag} ~ '^[a-zA-Z0-9_-]+$'`),
42
+ ]
43
+ )
44
+
45
+ export type ContainerTag = typeof containerTags.$inferSelect
46
+ export type NewContainerTag = typeof containerTags.$inferInsert
@@ -0,0 +1,49 @@
1
+ import { pgTable, uuid, varchar, text, jsonb, timestamp, index, check, integer } from 'drizzle-orm/pg-core'
2
+ import { sql } from 'drizzle-orm'
3
+
4
+ export const documents = pgTable(
5
+ 'documents',
6
+ {
7
+ id: uuid('id')
8
+ .primaryKey()
9
+ .default(sql`gen_random_uuid()`),
10
+ customId: varchar('custom_id', { length: 255 }),
11
+ content: text('content').notNull(),
12
+ contentType: varchar('content_type', { length: 50 }).notNull().default('text/plain'),
13
+ status: varchar('status', { length: 20 }).notNull().default('pending'),
14
+ containerTag: varchar('container_tag', { length: 255 }).notNull(),
15
+ metadata: jsonb('metadata').default(sql`'{}'::jsonb`),
16
+ contentHash: varchar('content_hash', { length: 64 })
17
+ .generatedAlwaysAs(sql`encode(sha256(content::bytea), 'hex')`)
18
+ .notNull(),
19
+ wordCount: integer('word_count')
20
+ .generatedAlwaysAs(sql`array_length(regexp_split_to_array(content, '\\s+'), 1)`)
21
+ .notNull(),
22
+ createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
23
+ updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),
24
+ },
25
+ (table) => [
26
+ index('idx_documents_container_tag').on(table.containerTag),
27
+ index('idx_documents_status')
28
+ .on(table.status)
29
+ .where(sql`${table.status} != 'processed'`),
30
+ index('idx_documents_custom_id')
31
+ .on(table.customId)
32
+ .where(sql`${table.customId} IS NOT NULL`),
33
+ index('idx_documents_content_hash').on(table.contentHash),
34
+ index('idx_documents_created_at').on(table.createdAt.desc()),
35
+ index('idx_documents_metadata').using('gin', sql`${table.metadata} jsonb_path_ops`),
36
+ index('idx_documents_container_status').on(table.containerTag, table.status, table.createdAt),
37
+ check(
38
+ 'documents_status_check',
39
+ sql`${table.status} IN ('pending', 'processing', 'processed', 'failed', 'archived')`
40
+ ),
41
+ check(
42
+ 'documents_content_type_check',
43
+ sql`${table.contentType} IN ('text/plain', 'text/markdown', 'text/html', 'application/pdf', 'application/json', 'image/png', 'image/jpeg', 'audio/mp3', 'video/mp4')`
44
+ ),
45
+ ]
46
+ )
47
+
48
+ export type Document = typeof documents.$inferSelect
49
+ export type NewDocument = typeof documents.$inferInsert
@@ -0,0 +1,32 @@
1
+ import { pgTable, uuid, varchar, boolean, timestamp, index, check } from 'drizzle-orm/pg-core'
2
+ import { sql } from 'drizzle-orm'
3
+ import { vector } from 'drizzle-orm/pg-core'
4
+ import { memories } from './memories.schema.js'
5
+
6
+ export const memoryEmbeddings = pgTable(
7
+ 'memory_embeddings',
8
+ {
9
+ memoryId: uuid('memory_id')
10
+ .primaryKey()
11
+ .references(() => memories.id, { onDelete: 'cascade' }),
12
+ embedding: vector('embedding', { dimensions: 1536 }).notNull(),
13
+ model: varchar('model', { length: 100 }).notNull().default('text-embedding-3-small'),
14
+ modelVersion: varchar('model_version', { length: 50 }),
15
+ normalized: boolean('normalized').default(true),
16
+ createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
17
+ },
18
+ (table) => [
19
+ // HNSW index for approximate nearest neighbor search with cosine similarity
20
+ index('idx_memory_embeddings_hnsw')
21
+ .using('hnsw', table.embedding.op('vector_cosine_ops'))
22
+ .with({ m: 16, ef_construction: 64 }),
23
+ index('idx_memory_embeddings_model').on(table.model),
24
+ check(
25
+ 'memory_embeddings_model_check',
26
+ sql`${table.model} IN ('text-embedding-3-small', 'text-embedding-3-large', 'text-embedding-ada-002', 'voyage-large-2', 'voyage-code-2', 'cohere-embed-v3', 'bge-large-en-v1.5', 'custom')`
27
+ ),
28
+ ]
29
+ )
30
+
31
+ export type MemoryEmbedding = typeof memoryEmbeddings.$inferSelect
32
+ export type NewMemoryEmbedding = typeof memoryEmbeddings.$inferInsert
@@ -0,0 +1,11 @@
1
+ // PostgreSQL Schema Exports
2
+ // This file aggregates all schema definitions for the supermemory PostgreSQL database
3
+
4
+ export * from './containers.schema.js'
5
+ export * from './documents.schema.js'
6
+ export * from './memories.schema.js'
7
+ export * from './chunks.schema.js'
8
+ export * from './embeddings.schema.js'
9
+ export * from './relationships.schema.js'
10
+ export * from './profiles.schema.js'
11
+ export * from './queue.schema.js'
@@ -0,0 +1,72 @@
1
+ import {
2
+ pgTable,
3
+ uuid,
4
+ text,
5
+ varchar,
6
+ boolean,
7
+ integer,
8
+ decimal,
9
+ jsonb,
10
+ timestamp,
11
+ index,
12
+ check,
13
+ type AnyPgColumn,
14
+ } from 'drizzle-orm/pg-core'
15
+ import { sql } from 'drizzle-orm'
16
+ import { documents } from './documents.schema.js'
17
+
18
+ export const memories = pgTable(
19
+ 'memories',
20
+ {
21
+ id: uuid('id')
22
+ .primaryKey()
23
+ .default(sql`gen_random_uuid()`),
24
+ documentId: uuid('document_id').references(() => documents.id, { onDelete: 'set null' }),
25
+ content: text('content').notNull(),
26
+ memoryType: varchar('memory_type', { length: 20 }).notNull().default('fact'),
27
+ isLatest: boolean('is_latest').notNull().default(true),
28
+ similarityHash: varchar('similarity_hash', { length: 64 }).notNull(),
29
+ version: integer('version').notNull().default(1),
30
+ supersedesId: uuid('supersedes_id').references((): AnyPgColumn => memories.id, {
31
+ onDelete: 'set null',
32
+ }),
33
+ containerTag: varchar('container_tag', { length: 255 }).notNull(),
34
+ confidenceScore: decimal('confidence_score', { precision: 4, scale: 3 }).default('1.000'),
35
+ metadata: jsonb('metadata').default(sql`'{}'::jsonb`),
36
+ createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
37
+ updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),
38
+ },
39
+ (table) => [
40
+ index('idx_memories_document_id')
41
+ .on(table.documentId)
42
+ .where(sql`${table.documentId} IS NOT NULL`),
43
+ index('idx_memories_container_tag').on(table.containerTag),
44
+ index('idx_memories_type').on(table.memoryType),
45
+ index('idx_memories_is_latest')
46
+ .on(table.isLatest)
47
+ .where(sql`${table.isLatest} = TRUE`),
48
+ index('idx_memories_similarity_hash').on(table.similarityHash),
49
+ index('idx_memories_supersedes')
50
+ .on(table.supersedesId)
51
+ .where(sql`${table.supersedesId} IS NOT NULL`),
52
+ index('idx_memories_metadata').using('gin', sql`${table.metadata} jsonb_path_ops`),
53
+ index('idx_memories_created_at').on(table.createdAt.desc()),
54
+ index('idx_memories_container_latest')
55
+ .on(table.containerTag, table.isLatest, table.createdAt)
56
+ .where(sql`${table.isLatest} = TRUE`),
57
+ index('idx_memories_container_type_latest')
58
+ .on(table.containerTag, table.memoryType, table.isLatest)
59
+ .where(sql`${table.isLatest} = TRUE`),
60
+ index('idx_memories_version_chain')
61
+ .on(table.supersedesId, table.version)
62
+ .where(sql`${table.supersedesId} IS NOT NULL`),
63
+ check(
64
+ 'memories_type_check',
65
+ sql`${table.memoryType} IN ('fact', 'preference', 'episode', 'belief', 'skill', 'context')`
66
+ ),
67
+ check('memories_confidence_check', sql`${table.confidenceScore} >= 0 AND ${table.confidenceScore} <= 1`),
68
+ ]
69
+ )
70
+
71
+ export type Memory = typeof memories.$inferSelect
72
+ export type NewMemory = typeof memories.$inferInsert
@@ -0,0 +1,34 @@
1
+ import { pgTable, uuid, varchar, jsonb, timestamp, integer, index } from 'drizzle-orm/pg-core'
2
+ import { sql } from 'drizzle-orm'
3
+ import { containerTags } from './containers.schema.js'
4
+
5
+ export const userProfiles = pgTable(
6
+ 'user_profiles',
7
+ {
8
+ id: uuid('id')
9
+ .primaryKey()
10
+ .default(sql`gen_random_uuid()`),
11
+ containerTag: varchar('container_tag', { length: 255 })
12
+ .notNull()
13
+ .unique()
14
+ .references(() => containerTags.tag, { onDelete: 'cascade' }),
15
+ staticFacts: jsonb('static_facts').default(sql`'[]'::jsonb`),
16
+ dynamicFacts: jsonb('dynamic_facts').default(sql`'[]'::jsonb`),
17
+ preferences: jsonb('preferences').default(sql`'{}'::jsonb`),
18
+ computedTraits: jsonb('computed_traits').default(sql`'{}'::jsonb`),
19
+ lastInteractionAt: timestamp('last_interaction_at', { withTimezone: true }),
20
+ memoryCount: integer('memory_count').default(0),
21
+ createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
22
+ updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),
23
+ },
24
+ (table) => [
25
+ index('idx_user_profiles_container').on(table.containerTag),
26
+ index('idx_user_profiles_static_facts').using('gin', table.staticFacts),
27
+ index('idx_user_profiles_dynamic_facts').using('gin', table.dynamicFacts),
28
+ index('idx_user_profiles_preferences').using('gin', table.preferences),
29
+ index('idx_user_profiles_updated').on(table.updatedAt.desc()),
30
+ ]
31
+ )
32
+
33
+ export type UserProfile = typeof userProfiles.$inferSelect
34
+ export type NewUserProfile = typeof userProfiles.$inferInsert
@@ -0,0 +1,59 @@
1
+ import { pgTable, uuid, varchar, text, integer, jsonb, timestamp, index, check } from 'drizzle-orm/pg-core'
2
+ import { sql } from 'drizzle-orm'
3
+ import { documents } from './documents.schema.js'
4
+
5
+ export const processingQueue = pgTable(
6
+ 'processing_queue',
7
+ {
8
+ id: uuid('id')
9
+ .primaryKey()
10
+ .default(sql`gen_random_uuid()`),
11
+ documentId: uuid('document_id')
12
+ .notNull()
13
+ .references(() => documents.id, { onDelete: 'cascade' }),
14
+ stage: varchar('stage', { length: 30 }).notNull().default('extraction'),
15
+ status: varchar('status', { length: 20 }).notNull().default('pending'),
16
+ priority: integer('priority').default(0),
17
+ error: text('error'),
18
+ errorCode: varchar('error_code', { length: 50 }),
19
+ attempts: integer('attempts').default(0),
20
+ maxAttempts: integer('max_attempts').default(3),
21
+ workerId: varchar('worker_id', { length: 100 }),
22
+ metadata: jsonb('metadata').default(sql`'{}'::jsonb`),
23
+ createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
24
+ startedAt: timestamp('started_at', { withTimezone: true }),
25
+ completedAt: timestamp('completed_at', { withTimezone: true }),
26
+ scheduledAt: timestamp('scheduled_at', { withTimezone: true }).defaultNow(),
27
+ },
28
+ (table) => [
29
+ index('idx_processing_queue_document').on(table.documentId),
30
+ index('idx_processing_queue_status')
31
+ .on(table.status)
32
+ .where(sql`${table.status} IN ('pending', 'retry')`),
33
+ index('idx_processing_queue_stage').on(table.stage),
34
+ index('idx_processing_queue_worker')
35
+ .on(table.workerId)
36
+ .where(sql`${table.workerId} IS NOT NULL`),
37
+ index('idx_processing_queue_priority')
38
+ .on(table.priority.desc(), table.scheduledAt.asc())
39
+ .where(sql`${table.status} IN ('pending', 'retry')`),
40
+ index('idx_processing_queue_stale')
41
+ .on(table.startedAt)
42
+ .where(sql`${table.status} = 'processing'`),
43
+ index('idx_processing_queue_worker_select')
44
+ .on(table.status, table.stage, table.priority, table.scheduledAt)
45
+ .where(sql`${table.status} IN ('pending', 'retry')`),
46
+ check(
47
+ 'processing_queue_stage_check',
48
+ sql`${table.stage} IN ('extraction', 'embedding', 'deduplication', 'relationship', 'profile_update', 'cleanup')`
49
+ ),
50
+ check(
51
+ 'processing_queue_status_check',
52
+ sql`${table.status} IN ('pending', 'processing', 'completed', 'failed', 'cancelled', 'retry')`
53
+ ),
54
+ check('processing_queue_attempts_check', sql`${table.attempts} <= ${table.maxAttempts}`),
55
+ ]
56
+ )
57
+
58
+ export type ProcessingQueue = typeof processingQueue.$inferSelect
59
+ export type NewProcessingQueue = typeof processingQueue.$inferInsert
@@ -0,0 +1,42 @@
1
+ import { pgTable, uuid, varchar, decimal, boolean, jsonb, timestamp, index, check, unique } from 'drizzle-orm/pg-core'
2
+ import { sql } from 'drizzle-orm'
3
+ import { memories } from './memories.schema.js'
4
+
5
+ export const memoryRelationships = pgTable(
6
+ 'memory_relationships',
7
+ {
8
+ id: uuid('id')
9
+ .primaryKey()
10
+ .default(sql`gen_random_uuid()`),
11
+ sourceMemoryId: uuid('source_memory_id')
12
+ .notNull()
13
+ .references(() => memories.id, { onDelete: 'cascade' }),
14
+ targetMemoryId: uuid('target_memory_id')
15
+ .notNull()
16
+ .references(() => memories.id, { onDelete: 'cascade' }),
17
+ relationshipType: varchar('relationship_type', { length: 30 }).notNull(),
18
+ weight: decimal('weight', { precision: 4, scale: 3 }).default('1.000'),
19
+ bidirectional: boolean('bidirectional').default(false),
20
+ metadata: jsonb('metadata').default(sql`'{}'::jsonb`),
21
+ createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
22
+ },
23
+ (table) => [
24
+ index('idx_memory_rel_source').on(table.sourceMemoryId),
25
+ index('idx_memory_rel_target').on(table.targetMemoryId),
26
+ index('idx_memory_rel_type').on(table.relationshipType),
27
+ index('idx_memory_rel_bidirectional')
28
+ .on(table.sourceMemoryId, table.targetMemoryId)
29
+ .where(sql`${table.bidirectional} = TRUE`),
30
+ index('idx_memory_rel_graph').on(table.sourceMemoryId, table.targetMemoryId, table.relationshipType, table.weight),
31
+ check(
32
+ 'memory_relationships_type_check',
33
+ sql`${table.relationshipType} IN ('updates', 'extends', 'derives', 'contradicts', 'supports', 'relates', 'temporal', 'causal', 'part_of', 'similar')`
34
+ ),
35
+ check('memory_relationships_weight_check', sql`${table.weight} >= 0 AND ${table.weight} <= 1`),
36
+ check('memory_relationships_no_self_loop', sql`${table.sourceMemoryId} != ${table.targetMemoryId}`),
37
+ unique('memory_relationships_unique_edge').on(table.sourceMemoryId, table.targetMemoryId, table.relationshipType),
38
+ ]
39
+ )
40
+
41
+ export type MemoryRelationship = typeof memoryRelationships.$inferSelect
42
+ export type NewMemoryRelationship = typeof memoryRelationships.$inferInsert
@@ -0,0 +1,223 @@
1
+ import { sqliteTable, text, integer, blob, index, uniqueIndex } from 'drizzle-orm/sqlite-core'
2
+ import { sql } from 'drizzle-orm'
3
+
4
+ // Users table
5
+ export const users = sqliteTable(
6
+ 'users',
7
+ {
8
+ id: text('id').primaryKey(),
9
+ email: text('email').notNull().unique(),
10
+ name: text('name'),
11
+ apiKey: text('api_key').notNull().unique(),
12
+ createdAt: integer('created_at', { mode: 'timestamp' })
13
+ .notNull()
14
+ .default(sql`(unixepoch())`),
15
+ updatedAt: integer('updated_at', { mode: 'timestamp' })
16
+ .notNull()
17
+ .default(sql`(unixepoch())`),
18
+ },
19
+ (table) => [uniqueIndex('users_email_idx').on(table.email), uniqueIndex('users_api_key_idx').on(table.apiKey)]
20
+ )
21
+
22
+ // Spaces (collections/folders for organizing memories)
23
+ export const spaces = sqliteTable(
24
+ 'spaces',
25
+ {
26
+ id: text('id').primaryKey(),
27
+ userId: text('user_id')
28
+ .notNull()
29
+ .references(() => users.id, { onDelete: 'cascade' }),
30
+ name: text('name').notNull(),
31
+ description: text('description'),
32
+ isDefault: integer('is_default', { mode: 'boolean' }).notNull().default(false),
33
+ createdAt: integer('created_at', { mode: 'timestamp' })
34
+ .notNull()
35
+ .default(sql`(unixepoch())`),
36
+ updatedAt: integer('updated_at', { mode: 'timestamp' })
37
+ .notNull()
38
+ .default(sql`(unixepoch())`),
39
+ },
40
+ (table) => [index('spaces_user_id_idx').on(table.userId)]
41
+ )
42
+
43
+ // Content types enum values
44
+ export const contentTypes = ['note', 'url', 'pdf', 'image', 'tweet', 'document'] as const
45
+ export type ContentType = (typeof contentTypes)[number]
46
+
47
+ // Memories table (main content storage)
48
+ export const memories = sqliteTable(
49
+ 'memories',
50
+ {
51
+ id: text('id').primaryKey(),
52
+ userId: text('user_id')
53
+ .notNull()
54
+ .references(() => users.id, { onDelete: 'cascade' }),
55
+ spaceId: text('space_id').references(() => spaces.id, { onDelete: 'set null' }),
56
+ contentType: text('content_type', { enum: contentTypes }).notNull(),
57
+ title: text('title'),
58
+ content: text('content').notNull(),
59
+ rawContent: text('raw_content'), // Original content before processing
60
+ sourceUrl: text('source_url'),
61
+ metadata: text('metadata', { mode: 'json' }).$type<Record<string, unknown>>(),
62
+ createdAt: integer('created_at', { mode: 'timestamp' })
63
+ .notNull()
64
+ .default(sql`(unixepoch())`),
65
+ updatedAt: integer('updated_at', { mode: 'timestamp' })
66
+ .notNull()
67
+ .default(sql`(unixepoch())`),
68
+ },
69
+ (table) => [
70
+ index('memories_user_id_idx').on(table.userId),
71
+ index('memories_space_id_idx').on(table.spaceId),
72
+ index('memories_content_type_idx').on(table.contentType),
73
+ index('memories_created_at_idx').on(table.createdAt),
74
+ ]
75
+ )
76
+
77
+ // Chunks table (for RAG - split content into searchable chunks)
78
+ export const chunks = sqliteTable(
79
+ 'chunks',
80
+ {
81
+ id: text('id').primaryKey(),
82
+ memoryId: text('memory_id')
83
+ .notNull()
84
+ .references(() => memories.id, { onDelete: 'cascade' }),
85
+ content: text('content').notNull(),
86
+ chunkIndex: integer('chunk_index').notNull(),
87
+ startOffset: integer('start_offset'),
88
+ endOffset: integer('end_offset'),
89
+ tokenCount: integer('token_count'),
90
+ metadata: text('metadata', { mode: 'json' }).$type<Record<string, unknown>>(),
91
+ createdAt: integer('created_at', { mode: 'timestamp' })
92
+ .notNull()
93
+ .default(sql`(unixepoch())`),
94
+ },
95
+ (table) => [index('chunks_memory_id_idx').on(table.memoryId), index('chunks_chunk_index_idx').on(table.chunkIndex)]
96
+ )
97
+
98
+ // Embeddings table (vector storage for semantic search)
99
+ export const embeddings = sqliteTable(
100
+ 'embeddings',
101
+ {
102
+ id: text('id').primaryKey(),
103
+ chunkId: text('chunk_id')
104
+ .notNull()
105
+ .references(() => chunks.id, { onDelete: 'cascade' }),
106
+ embedding: blob('embedding', { mode: 'buffer' }).notNull(), // Stored as binary for efficiency
107
+ model: text('model').notNull(),
108
+ dimensions: integer('dimensions').notNull(),
109
+ createdAt: integer('created_at', { mode: 'timestamp' })
110
+ .notNull()
111
+ .default(sql`(unixepoch())`),
112
+ },
113
+ (table) => [uniqueIndex('embeddings_chunk_id_idx').on(table.chunkId)]
114
+ )
115
+
116
+ // Tags table
117
+ export const tags = sqliteTable(
118
+ 'tags',
119
+ {
120
+ id: text('id').primaryKey(),
121
+ userId: text('user_id')
122
+ .notNull()
123
+ .references(() => users.id, { onDelete: 'cascade' }),
124
+ name: text('name').notNull(),
125
+ color: text('color'),
126
+ createdAt: integer('created_at', { mode: 'timestamp' })
127
+ .notNull()
128
+ .default(sql`(unixepoch())`),
129
+ },
130
+ (table) => [
131
+ index('tags_user_id_idx').on(table.userId),
132
+ uniqueIndex('tags_user_name_idx').on(table.userId, table.name),
133
+ ]
134
+ )
135
+
136
+ // Memory-Tags junction table
137
+ export const memoryTags = sqliteTable(
138
+ 'memory_tags',
139
+ {
140
+ memoryId: text('memory_id')
141
+ .notNull()
142
+ .references(() => memories.id, { onDelete: 'cascade' }),
143
+ tagId: text('tag_id')
144
+ .notNull()
145
+ .references(() => tags.id, { onDelete: 'cascade' }),
146
+ createdAt: integer('created_at', { mode: 'timestamp' })
147
+ .notNull()
148
+ .default(sql`(unixepoch())`),
149
+ },
150
+ (table) => [index('memory_tags_memory_id_idx').on(table.memoryId), index('memory_tags_tag_id_idx').on(table.tagId)]
151
+ )
152
+
153
+ // Search history for analytics and suggestions
154
+ export const searchHistory = sqliteTable(
155
+ 'search_history',
156
+ {
157
+ id: text('id').primaryKey(),
158
+ userId: text('user_id')
159
+ .notNull()
160
+ .references(() => users.id, { onDelete: 'cascade' }),
161
+ query: text('query').notNull(),
162
+ resultCount: integer('result_count').notNull().default(0),
163
+ createdAt: integer('created_at', { mode: 'timestamp' })
164
+ .notNull()
165
+ .default(sql`(unixepoch())`),
166
+ },
167
+ (table) => [
168
+ index('search_history_user_id_idx').on(table.userId),
169
+ index('search_history_created_at_idx').on(table.createdAt),
170
+ ]
171
+ )
172
+
173
+ // API usage tracking
174
+ export const apiUsage = sqliteTable(
175
+ 'api_usage',
176
+ {
177
+ id: text('id').primaryKey(),
178
+ userId: text('user_id')
179
+ .notNull()
180
+ .references(() => users.id, { onDelete: 'cascade' }),
181
+ endpoint: text('endpoint').notNull(),
182
+ method: text('method').notNull(),
183
+ statusCode: integer('status_code').notNull(),
184
+ responseTimeMs: integer('response_time_ms'),
185
+ tokensUsed: integer('tokens_used'),
186
+ createdAt: integer('created_at', { mode: 'timestamp' })
187
+ .notNull()
188
+ .default(sql`(unixepoch())`),
189
+ },
190
+ (table) => [
191
+ index('api_usage_user_id_idx').on(table.userId),
192
+ index('api_usage_endpoint_idx').on(table.endpoint),
193
+ index('api_usage_created_at_idx').on(table.createdAt),
194
+ ]
195
+ )
196
+
197
+ // Type exports for use in application
198
+ export type User = typeof users.$inferSelect
199
+ export type NewUser = typeof users.$inferInsert
200
+
201
+ export type Space = typeof spaces.$inferSelect
202
+ export type NewSpace = typeof spaces.$inferInsert
203
+
204
+ export type Memory = typeof memories.$inferSelect
205
+ export type NewMemory = typeof memories.$inferInsert
206
+
207
+ export type Chunk = typeof chunks.$inferSelect
208
+ export type NewChunk = typeof chunks.$inferInsert
209
+
210
+ export type Embedding = typeof embeddings.$inferSelect
211
+ export type NewEmbedding = typeof embeddings.$inferInsert
212
+
213
+ export type Tag = typeof tags.$inferSelect
214
+ export type NewTag = typeof tags.$inferInsert
215
+
216
+ export type MemoryTag = typeof memoryTags.$inferSelect
217
+ export type NewMemoryTag = typeof memoryTags.$inferInsert
218
+
219
+ export type SearchHistory = typeof searchHistory.$inferSelect
220
+ export type NewSearchHistory = typeof searchHistory.$inferInsert
221
+
222
+ export type ApiUsage = typeof apiUsage.$inferSelect
223
+ export type NewApiUsage = typeof apiUsage.$inferInsert