@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.
- package/.env.example +57 -0
- package/README.md +374 -0
- package/dist/index.js +189 -0
- package/dist/mcp/index.js +1132 -0
- package/docker-compose.prod.yml +91 -0
- package/docker-compose.yml +358 -0
- package/drizzle/0000_dapper_the_professor.sql +159 -0
- package/drizzle/0001_api_keys.sql +51 -0
- package/drizzle/meta/0000_snapshot.json +1532 -0
- package/drizzle/meta/_journal.json +13 -0
- package/drizzle.config.ts +20 -0
- package/package.json +114 -0
- package/scripts/add-extraction-job.ts +122 -0
- package/scripts/benchmark-pgvector.ts +122 -0
- package/scripts/bootstrap.sh +209 -0
- package/scripts/check-runtime-pack.ts +111 -0
- package/scripts/claude-mcp-config.ts +336 -0
- package/scripts/docker-entrypoint.sh +183 -0
- package/scripts/doctor.ts +377 -0
- package/scripts/init-db.sql +33 -0
- package/scripts/install.sh +1110 -0
- package/scripts/mcp-setup.ts +271 -0
- package/scripts/migrations/001_create_pgvector_extension.sql +31 -0
- package/scripts/migrations/002_create_memory_embeddings_table.sql +75 -0
- package/scripts/migrations/003_create_hnsw_index.sql +94 -0
- package/scripts/migrations/004_create_memory_embeddings_standalone.sql +70 -0
- package/scripts/migrations/005_create_chunks_table.sql +95 -0
- package/scripts/migrations/006_create_processing_queue.sql +45 -0
- package/scripts/migrations/generate_test_data.sql +42 -0
- package/scripts/migrations/phase1_comprehensive_test.sql +204 -0
- package/scripts/migrations/run_migrations.sh +286 -0
- package/scripts/migrations/test_hnsw_index.sql +255 -0
- package/scripts/pre-commit-secrets +282 -0
- package/scripts/run-extraction-worker.ts +46 -0
- package/scripts/run-phase1-tests.sh +291 -0
- package/scripts/setup.ts +222 -0
- package/scripts/smoke-install.sh +12 -0
- package/scripts/test-health-endpoint.sh +328 -0
- package/src/api/index.ts +2 -0
- package/src/api/middleware/auth.ts +80 -0
- package/src/api/middleware/csrf.ts +308 -0
- package/src/api/middleware/errorHandler.ts +166 -0
- package/src/api/middleware/rateLimit.ts +360 -0
- package/src/api/middleware/validation.ts +514 -0
- package/src/api/routes/documents.ts +286 -0
- package/src/api/routes/profiles.ts +237 -0
- package/src/api/routes/search.ts +71 -0
- package/src/api/stores/index.ts +58 -0
- package/src/config/bootstrap-env.ts +3 -0
- package/src/config/env.ts +71 -0
- package/src/config/feature-flags.ts +25 -0
- package/src/config/index.ts +140 -0
- package/src/config/secrets.config.ts +291 -0
- package/src/db/client.ts +92 -0
- package/src/db/index.ts +73 -0
- package/src/db/postgres.ts +72 -0
- package/src/db/schema/chunks.schema.ts +31 -0
- package/src/db/schema/containers.schema.ts +46 -0
- package/src/db/schema/documents.schema.ts +49 -0
- package/src/db/schema/embeddings.schema.ts +32 -0
- package/src/db/schema/index.ts +11 -0
- package/src/db/schema/memories.schema.ts +72 -0
- package/src/db/schema/profiles.schema.ts +34 -0
- package/src/db/schema/queue.schema.ts +59 -0
- package/src/db/schema/relationships.schema.ts +42 -0
- package/src/db/schema.ts +223 -0
- package/src/db/worker-connection.ts +47 -0
- package/src/index.ts +235 -0
- package/src/mcp/CLAUDE.md +1 -0
- package/src/mcp/index.ts +1380 -0
- package/src/mcp/legacyState.ts +22 -0
- package/src/mcp/rateLimit.ts +358 -0
- package/src/mcp/resources.ts +309 -0
- package/src/mcp/results.ts +104 -0
- package/src/mcp/tools.ts +401 -0
- package/src/queues/config.ts +119 -0
- package/src/queues/index.ts +289 -0
- package/src/sdk/client.ts +225 -0
- package/src/sdk/errors.ts +266 -0
- package/src/sdk/http.ts +560 -0
- package/src/sdk/index.ts +244 -0
- package/src/sdk/resources/base.ts +65 -0
- package/src/sdk/resources/connections.ts +204 -0
- package/src/sdk/resources/documents.ts +163 -0
- package/src/sdk/resources/index.ts +10 -0
- package/src/sdk/resources/memories.ts +150 -0
- package/src/sdk/resources/search.ts +60 -0
- package/src/sdk/resources/settings.ts +36 -0
- package/src/sdk/types.ts +674 -0
- package/src/services/chunking/index.ts +451 -0
- package/src/services/chunking.service.ts +650 -0
- package/src/services/csrf.service.ts +252 -0
- package/src/services/documents.repository.ts +219 -0
- package/src/services/documents.service.ts +191 -0
- package/src/services/embedding.service.ts +404 -0
- package/src/services/extraction.service.ts +300 -0
- package/src/services/extractors/code.extractor.ts +451 -0
- package/src/services/extractors/index.ts +9 -0
- package/src/services/extractors/markdown.extractor.ts +461 -0
- package/src/services/extractors/pdf.extractor.ts +315 -0
- package/src/services/extractors/text.extractor.ts +118 -0
- package/src/services/extractors/url.extractor.ts +243 -0
- package/src/services/index.ts +235 -0
- package/src/services/ingestion.service.ts +177 -0
- package/src/services/llm/anthropic.ts +400 -0
- package/src/services/llm/base.ts +460 -0
- package/src/services/llm/contradiction-detector.service.ts +526 -0
- package/src/services/llm/heuristics.ts +148 -0
- package/src/services/llm/index.ts +309 -0
- package/src/services/llm/memory-classifier.service.ts +383 -0
- package/src/services/llm/memory-extension-detector.service.ts +523 -0
- package/src/services/llm/mock.ts +470 -0
- package/src/services/llm/openai.ts +398 -0
- package/src/services/llm/prompts.ts +438 -0
- package/src/services/llm/types.ts +373 -0
- package/src/services/memory.repository.ts +1769 -0
- package/src/services/memory.service.ts +1338 -0
- package/src/services/memory.types.ts +234 -0
- package/src/services/persistence/index.ts +295 -0
- package/src/services/pipeline.service.ts +509 -0
- package/src/services/profile.repository.ts +436 -0
- package/src/services/profile.service.ts +560 -0
- package/src/services/profile.types.ts +270 -0
- package/src/services/relationships/detector.ts +1128 -0
- package/src/services/relationships/index.ts +268 -0
- package/src/services/relationships/memory-integration.ts +459 -0
- package/src/services/relationships/strategies.ts +132 -0
- package/src/services/relationships/types.ts +370 -0
- package/src/services/search.service.ts +761 -0
- package/src/services/search.types.ts +220 -0
- package/src/services/secrets.service.ts +384 -0
- package/src/services/vectorstore/base.ts +327 -0
- package/src/services/vectorstore/index.ts +444 -0
- package/src/services/vectorstore/memory.ts +286 -0
- package/src/services/vectorstore/migration.ts +295 -0
- package/src/services/vectorstore/mock.ts +403 -0
- package/src/services/vectorstore/pgvector.ts +695 -0
- package/src/services/vectorstore/types.ts +247 -0
- package/src/startup.ts +389 -0
- package/src/types/api.types.ts +193 -0
- package/src/types/document.types.ts +103 -0
- package/src/types/index.ts +241 -0
- package/src/types/profile.base.ts +133 -0
- package/src/utils/errors.ts +447 -0
- package/src/utils/id.ts +15 -0
- package/src/utils/index.ts +101 -0
- package/src/utils/logger.ts +313 -0
- package/src/utils/sanitization.ts +501 -0
- package/src/utils/secret-validation.ts +273 -0
- package/src/utils/synonyms.ts +188 -0
- package/src/utils/validation.ts +581 -0
- package/src/workers/chunking.worker.ts +242 -0
- package/src/workers/embedding.worker.ts +358 -0
- package/src/workers/extraction.worker.ts +346 -0
- package/src/workers/indexing.worker.ts +505 -0
- package/tsconfig.json +38 -0
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
#!/usr/bin/env tsx
|
|
2
|
+
import { execFileSync } from 'node:child_process'
|
|
3
|
+
import { existsSync, rmSync } from 'node:fs'
|
|
4
|
+
import { tmpdir } from 'node:os'
|
|
5
|
+
import { join, resolve } from 'node:path'
|
|
6
|
+
|
|
7
|
+
interface PackFileEntry {
|
|
8
|
+
path?: string
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
interface PackResult {
|
|
12
|
+
files?: PackFileEntry[]
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const REQUIRED_PATHS = [
|
|
16
|
+
'.env.example',
|
|
17
|
+
'docker-compose.prod.yml',
|
|
18
|
+
'docker-compose.yml',
|
|
19
|
+
'package.json',
|
|
20
|
+
'scripts/doctor.ts',
|
|
21
|
+
'scripts/install.sh',
|
|
22
|
+
'scripts/mcp-setup.ts',
|
|
23
|
+
'drizzle/0000_dapper_the_professor.sql',
|
|
24
|
+
'src/index.ts',
|
|
25
|
+
'src/db/schema.ts',
|
|
26
|
+
'src/mcp/index.ts',
|
|
27
|
+
'tsconfig.json',
|
|
28
|
+
] as const
|
|
29
|
+
|
|
30
|
+
const FORBIDDEN_PATTERNS = [
|
|
31
|
+
/^coverage\//,
|
|
32
|
+
/^data\//,
|
|
33
|
+
/^dist\//,
|
|
34
|
+
/^packages\//,
|
|
35
|
+
/^tests\//,
|
|
36
|
+
/^2026-03-05-npx-first-installer-/,
|
|
37
|
+
] as const
|
|
38
|
+
|
|
39
|
+
function parsePackResult(rawOutput: string): PackResult {
|
|
40
|
+
const parsed = JSON.parse(rawOutput) as unknown
|
|
41
|
+
if (!Array.isArray(parsed) || parsed.length === 0) {
|
|
42
|
+
throw new Error('npm pack --json did not return a pack result array')
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const [firstResult] = parsed
|
|
46
|
+
if (!firstResult || typeof firstResult !== 'object') {
|
|
47
|
+
throw new Error('npm pack --json returned an invalid pack result')
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return firstResult as PackResult
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function collectPackPaths(result: PackResult): string[] {
|
|
54
|
+
const packPaths = result.files
|
|
55
|
+
?.map((entry) => entry.path)
|
|
56
|
+
.filter((path): path is string => typeof path === 'string')
|
|
57
|
+
.sort()
|
|
58
|
+
|
|
59
|
+
if (!packPaths || packPaths.length === 0) {
|
|
60
|
+
throw new Error('npm pack --json did not report packaged file paths')
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return packPaths
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function assertRequiredPaths(packPaths: string[]): void {
|
|
67
|
+
const missingPaths = REQUIRED_PATHS.filter((requiredPath) => !packPaths.includes(requiredPath))
|
|
68
|
+
if (missingPaths.length > 0) {
|
|
69
|
+
throw new Error(`Runtime package is missing required files: ${missingPaths.join(', ')}`)
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function assertForbiddenPaths(packPaths: string[]): void {
|
|
74
|
+
const forbiddenPaths = packPaths.filter((packPath) =>
|
|
75
|
+
FORBIDDEN_PATTERNS.some((pattern) => pattern.test(packPath))
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
if (forbiddenPaths.length > 0) {
|
|
79
|
+
throw new Error(`Runtime package includes forbidden files: ${forbiddenPaths.join(', ')}`)
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function validateRuntimePack(repoRoot = resolve(import.meta.dirname, '..')): string[] {
|
|
84
|
+
const packDestination = join(tmpdir(), `supermemory-runtime-pack-${process.pid}-${Date.now()}`)
|
|
85
|
+
|
|
86
|
+
try {
|
|
87
|
+
const rawOutput = execFileSync(
|
|
88
|
+
'npm',
|
|
89
|
+
['pack', '--json', '--dry-run', '--pack-destination', packDestination],
|
|
90
|
+
{
|
|
91
|
+
cwd: repoRoot,
|
|
92
|
+
encoding: 'utf8',
|
|
93
|
+
}
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
const packPaths = collectPackPaths(parsePackResult(rawOutput))
|
|
97
|
+
assertRequiredPaths(packPaths)
|
|
98
|
+
assertForbiddenPaths(packPaths)
|
|
99
|
+
|
|
100
|
+
return packPaths
|
|
101
|
+
} finally {
|
|
102
|
+
if (existsSync(packDestination)) {
|
|
103
|
+
rmSync(packDestination, { recursive: true, force: true })
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (import.meta.url === new URL(process.argv[1], 'file://').href) {
|
|
109
|
+
const packPaths = validateRuntimePack()
|
|
110
|
+
console.log(`Runtime package validated (${packPaths.length} files)`)
|
|
111
|
+
}
|
|
@@ -0,0 +1,336 @@
|
|
|
1
|
+
#!/usr/bin/env tsx
|
|
2
|
+
import { existsSync, readFileSync, realpathSync } from 'node:fs';
|
|
3
|
+
import { homedir } from 'node:os';
|
|
4
|
+
import { join, resolve } from 'node:path';
|
|
5
|
+
|
|
6
|
+
export type ClaudeMcpScope = 'user' | 'project' | 'local';
|
|
7
|
+
|
|
8
|
+
type JsonObject = Record<string, unknown>;
|
|
9
|
+
|
|
10
|
+
export interface ClaudeMcpRegistrationEntry {
|
|
11
|
+
scope: ClaudeMcpScope;
|
|
12
|
+
sourcePath: string;
|
|
13
|
+
command: string;
|
|
14
|
+
args: string[];
|
|
15
|
+
type: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface ClaudeMcpRegistrationCheck {
|
|
19
|
+
status: 'missing' | 'match' | 'mismatch';
|
|
20
|
+
scope: ClaudeMcpScope;
|
|
21
|
+
projectKey: string;
|
|
22
|
+
expectedCommand: string;
|
|
23
|
+
expectedArgs: string[];
|
|
24
|
+
entries: ClaudeMcpRegistrationEntry[];
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
interface CheckOptions {
|
|
28
|
+
expectedArgs: string[];
|
|
29
|
+
expectedCommand: string;
|
|
30
|
+
homeDir?: string;
|
|
31
|
+
name: string;
|
|
32
|
+
projectDir?: string;
|
|
33
|
+
scope: ClaudeMcpScope;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const HOME_CONFIG_CANDIDATES = ['.claude/settings.json', '.claude.json'] as const;
|
|
37
|
+
|
|
38
|
+
function isObject(value: unknown): value is JsonObject {
|
|
39
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function readJsonObject(path: string): JsonObject | null {
|
|
43
|
+
if (!existsSync(path)) {
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
try {
|
|
48
|
+
const parsed = JSON.parse(readFileSync(path, 'utf-8')) as unknown;
|
|
49
|
+
return isObject(parsed) ? parsed : null;
|
|
50
|
+
} catch {
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function resolveRealPath(path: string): string {
|
|
56
|
+
try {
|
|
57
|
+
return realpathSync.native(path);
|
|
58
|
+
} catch {
|
|
59
|
+
return resolve(path);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function normalizeComparableValue(value: string): string {
|
|
64
|
+
const trimmed = value.trim();
|
|
65
|
+
if (!trimmed) {
|
|
66
|
+
return trimmed;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (trimmed.startsWith('-')) {
|
|
70
|
+
return trimmed;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const absoluteCandidate = trimmed.startsWith('/') ? trimmed : resolve(trimmed);
|
|
74
|
+
if (existsSync(absoluteCandidate)) {
|
|
75
|
+
return resolveRealPath(absoluteCandidate);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return trimmed;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function getHomeConfigPaths(homeDir = homedir()): string[] {
|
|
82
|
+
const resolvedHome = resolve(homeDir);
|
|
83
|
+
return HOME_CONFIG_CANDIDATES.map((relativePath) => join(resolvedHome, relativePath));
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function getClaudeProjectKey(projectDir = process.cwd()): string {
|
|
87
|
+
return resolveRealPath(projectDir);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function getEntryFromHomeConfig(
|
|
91
|
+
configPath: string,
|
|
92
|
+
scope: ClaudeMcpScope,
|
|
93
|
+
name: string,
|
|
94
|
+
projectKey: string
|
|
95
|
+
): ClaudeMcpRegistrationEntry | null {
|
|
96
|
+
const config = readJsonObject(configPath);
|
|
97
|
+
if (!config) {
|
|
98
|
+
return null;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
let serverConfig: JsonObject | null = null;
|
|
102
|
+
if (scope === 'user') {
|
|
103
|
+
const mcpServers = isObject(config.mcpServers) ? config.mcpServers : null;
|
|
104
|
+
serverConfig = mcpServers && isObject(mcpServers[name]) ? (mcpServers[name] as JsonObject) : null;
|
|
105
|
+
} else {
|
|
106
|
+
const projects = isObject(config.projects) ? config.projects : null;
|
|
107
|
+
const projectConfig = projects && isObject(projects[projectKey]) ? (projects[projectKey] as JsonObject) : null;
|
|
108
|
+
const mcpServers = projectConfig && isObject(projectConfig.mcpServers) ? projectConfig.mcpServers : null;
|
|
109
|
+
serverConfig = mcpServers && isObject(mcpServers[name]) ? (mcpServers[name] as JsonObject) : null;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (!serverConfig) {
|
|
113
|
+
return null;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return {
|
|
117
|
+
scope,
|
|
118
|
+
sourcePath: configPath,
|
|
119
|
+
command: typeof serverConfig.command === 'string' ? serverConfig.command : '',
|
|
120
|
+
args: Array.isArray(serverConfig.args)
|
|
121
|
+
? serverConfig.args.filter((arg): arg is string => typeof arg === 'string')
|
|
122
|
+
: [],
|
|
123
|
+
type: typeof serverConfig.type === 'string' ? serverConfig.type : '',
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function getEntryFromProjectConfig(
|
|
128
|
+
projectDir: string,
|
|
129
|
+
name: string
|
|
130
|
+
): ClaudeMcpRegistrationEntry | null {
|
|
131
|
+
const configPath = join(resolve(projectDir), '.mcp.json');
|
|
132
|
+
const config = readJsonObject(configPath);
|
|
133
|
+
if (!config) {
|
|
134
|
+
return null;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const mcpServers = isObject(config.mcpServers) ? config.mcpServers : null;
|
|
138
|
+
const serverConfig = mcpServers && isObject(mcpServers[name]) ? (mcpServers[name] as JsonObject) : null;
|
|
139
|
+
if (!serverConfig) {
|
|
140
|
+
return null;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return {
|
|
144
|
+
scope: 'project',
|
|
145
|
+
sourcePath: configPath,
|
|
146
|
+
command: typeof serverConfig.command === 'string' ? serverConfig.command : '',
|
|
147
|
+
args: Array.isArray(serverConfig.args)
|
|
148
|
+
? serverConfig.args.filter((arg): arg is string => typeof arg === 'string')
|
|
149
|
+
: [],
|
|
150
|
+
type: typeof serverConfig.type === 'string' ? serverConfig.type : '',
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function getRegistrationEntries(
|
|
155
|
+
scope: ClaudeMcpScope,
|
|
156
|
+
name: string,
|
|
157
|
+
projectDir: string,
|
|
158
|
+
homeDir?: string
|
|
159
|
+
): ClaudeMcpRegistrationEntry[] {
|
|
160
|
+
if (scope === 'project') {
|
|
161
|
+
const projectEntry = getEntryFromProjectConfig(projectDir, name);
|
|
162
|
+
return projectEntry ? [projectEntry] : [];
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const projectKey = getClaudeProjectKey(projectDir);
|
|
166
|
+
return getHomeConfigPaths(homeDir)
|
|
167
|
+
.map((configPath) => getEntryFromHomeConfig(configPath, scope, name, projectKey))
|
|
168
|
+
.filter((entry): entry is ClaudeMcpRegistrationEntry => entry !== null);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
export function findClaudeMcpRegistrations(
|
|
172
|
+
name: string,
|
|
173
|
+
projectDir = process.cwd(),
|
|
174
|
+
homeDir?: string
|
|
175
|
+
): ClaudeMcpRegistrationEntry[] {
|
|
176
|
+
const scopes: ClaudeMcpScope[] = ['project', 'local', 'user'];
|
|
177
|
+
return scopes.flatMap((scope) => getRegistrationEntries(scope, name, projectDir, homeDir));
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function isExpectedRegistration(
|
|
181
|
+
entry: ClaudeMcpRegistrationEntry,
|
|
182
|
+
expectedCommand: string,
|
|
183
|
+
expectedArgs: string[]
|
|
184
|
+
): boolean {
|
|
185
|
+
if (entry.type && entry.type !== 'stdio') {
|
|
186
|
+
return false;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
if (entry.command !== expectedCommand) {
|
|
190
|
+
return false;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const normalizedExpectedArgs = expectedArgs.map(normalizeComparableValue);
|
|
194
|
+
const normalizedActualArgs = entry.args.map(normalizeComparableValue);
|
|
195
|
+
|
|
196
|
+
if (normalizedExpectedArgs.length !== normalizedActualArgs.length) {
|
|
197
|
+
return false;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
return normalizedExpectedArgs.every((arg, index) => arg === normalizedActualArgs[index]);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
export function checkClaudeMcpRegistration(options: CheckOptions): ClaudeMcpRegistrationCheck {
|
|
204
|
+
const projectDir = resolve(options.projectDir ?? process.cwd());
|
|
205
|
+
const projectKey = getClaudeProjectKey(projectDir);
|
|
206
|
+
const entries = getRegistrationEntries(options.scope, options.name, projectDir, options.homeDir);
|
|
207
|
+
const expectedArgs = options.expectedArgs.map(normalizeComparableValue);
|
|
208
|
+
|
|
209
|
+
if (entries.length === 0) {
|
|
210
|
+
return {
|
|
211
|
+
status: 'missing',
|
|
212
|
+
scope: options.scope,
|
|
213
|
+
projectKey,
|
|
214
|
+
expectedCommand: options.expectedCommand,
|
|
215
|
+
expectedArgs,
|
|
216
|
+
entries: [],
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
if (entries.some((entry) => isExpectedRegistration(entry, options.expectedCommand, expectedArgs))) {
|
|
221
|
+
return {
|
|
222
|
+
status: 'match',
|
|
223
|
+
scope: options.scope,
|
|
224
|
+
projectKey,
|
|
225
|
+
expectedCommand: options.expectedCommand,
|
|
226
|
+
expectedArgs,
|
|
227
|
+
entries,
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
return {
|
|
232
|
+
status: 'mismatch',
|
|
233
|
+
scope: options.scope,
|
|
234
|
+
projectKey,
|
|
235
|
+
expectedCommand: options.expectedCommand,
|
|
236
|
+
expectedArgs,
|
|
237
|
+
entries,
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function parseScope(value: string): ClaudeMcpScope {
|
|
242
|
+
if (value === 'user' || value === 'project' || value === 'local') {
|
|
243
|
+
return value;
|
|
244
|
+
}
|
|
245
|
+
throw new Error(`Invalid scope: ${value}`);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function parseCliArgs(): CheckOptions {
|
|
249
|
+
const args = process.argv.slice(2);
|
|
250
|
+
if (args[0] !== 'check') {
|
|
251
|
+
throw new Error('Usage: claude-mcp-config.ts check --scope <scope> --name <name> --command <command> [--arg <value>]');
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
let scope: ClaudeMcpScope | undefined;
|
|
255
|
+
let name = '';
|
|
256
|
+
let expectedCommand = '';
|
|
257
|
+
const expectedArgs: string[] = [];
|
|
258
|
+
let projectDir: string | undefined;
|
|
259
|
+
let homeDir: string | undefined;
|
|
260
|
+
|
|
261
|
+
for (let index = 1; index < args.length; index += 1) {
|
|
262
|
+
const arg = args[index];
|
|
263
|
+
|
|
264
|
+
if (arg === '--scope') {
|
|
265
|
+
scope = parseScope(args[index + 1] ?? '');
|
|
266
|
+
index += 1;
|
|
267
|
+
continue;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
if (arg === '--name') {
|
|
271
|
+
name = args[index + 1] ?? '';
|
|
272
|
+
index += 1;
|
|
273
|
+
continue;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
if (arg === '--command') {
|
|
277
|
+
expectedCommand = args[index + 1] ?? '';
|
|
278
|
+
index += 1;
|
|
279
|
+
continue;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
if (arg === '--arg') {
|
|
283
|
+
expectedArgs.push(args[index + 1] ?? '');
|
|
284
|
+
index += 1;
|
|
285
|
+
continue;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
if (arg === '--project-dir') {
|
|
289
|
+
projectDir = args[index + 1];
|
|
290
|
+
index += 1;
|
|
291
|
+
continue;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
if (arg === '--home-dir') {
|
|
295
|
+
homeDir = args[index + 1];
|
|
296
|
+
index += 1;
|
|
297
|
+
continue;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
throw new Error(`Unknown argument: ${arg}`);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
if (!scope || !name || !expectedCommand) {
|
|
304
|
+
throw new Error('Missing required arguments for check');
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
return { scope, name, expectedCommand, expectedArgs, projectDir, homeDir };
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
function runCli(): void {
|
|
311
|
+
const result = checkClaudeMcpRegistration(parseCliArgs());
|
|
312
|
+
console.log(JSON.stringify(result));
|
|
313
|
+
|
|
314
|
+
switch (result.status) {
|
|
315
|
+
case 'match':
|
|
316
|
+
process.exit(0);
|
|
317
|
+
case 'missing':
|
|
318
|
+
process.exit(10);
|
|
319
|
+
case 'mismatch':
|
|
320
|
+
process.exit(11);
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
const executedPath = process.argv[1];
|
|
325
|
+
if (executedPath) {
|
|
326
|
+
const currentPath = resolveRealPath(executedPath);
|
|
327
|
+
const modulePath = resolveRealPath(new URL(import.meta.url).pathname);
|
|
328
|
+
if (currentPath === modulePath) {
|
|
329
|
+
try {
|
|
330
|
+
runCli();
|
|
331
|
+
} catch (error) {
|
|
332
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
333
|
+
process.exit(2);
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
}
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
#!/bin/sh
|
|
2
|
+
# =============================================================================
|
|
3
|
+
# Docker Entrypoint Script for SuperMemory Clone API
|
|
4
|
+
# =============================================================================
|
|
5
|
+
# This script runs before the main application starts:
|
|
6
|
+
# 1. Waits for required services (database, redis) to be ready
|
|
7
|
+
# 2. Runs database migrations
|
|
8
|
+
# 3. Starts the application
|
|
9
|
+
#
|
|
10
|
+
# Environment variables:
|
|
11
|
+
# DATABASE_URL - Database connection string
|
|
12
|
+
# REDIS_URL - Redis connection string (optional)
|
|
13
|
+
# SKIP_MIGRATIONS - Set to "true" to skip migrations
|
|
14
|
+
# MAX_RETRIES - Max connection attempts (default: 30)
|
|
15
|
+
# RETRY_INTERVAL - Seconds between retries (default: 2)
|
|
16
|
+
# =============================================================================
|
|
17
|
+
|
|
18
|
+
set -e
|
|
19
|
+
|
|
20
|
+
# -----------------------------------------------------------------------------
|
|
21
|
+
# Configuration
|
|
22
|
+
# -----------------------------------------------------------------------------
|
|
23
|
+
MAX_RETRIES="${MAX_RETRIES:-30}"
|
|
24
|
+
RETRY_INTERVAL="${RETRY_INTERVAL:-2}"
|
|
25
|
+
|
|
26
|
+
# Colors for output (if terminal supports it)
|
|
27
|
+
RED='\033[0;31m'
|
|
28
|
+
GREEN='\033[0;32m'
|
|
29
|
+
YELLOW='\033[1;33m'
|
|
30
|
+
NC='\033[0m' # No Color
|
|
31
|
+
|
|
32
|
+
# -----------------------------------------------------------------------------
|
|
33
|
+
# Logging functions
|
|
34
|
+
# -----------------------------------------------------------------------------
|
|
35
|
+
log_info() {
|
|
36
|
+
echo "${GREEN}[INFO]${NC} $1"
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
log_warn() {
|
|
40
|
+
echo "${YELLOW}[WARN]${NC} $1"
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
log_error() {
|
|
44
|
+
echo "${RED}[ERROR]${NC} $1"
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
# -----------------------------------------------------------------------------
|
|
48
|
+
# Wait for PostgreSQL to be ready
|
|
49
|
+
# -----------------------------------------------------------------------------
|
|
50
|
+
wait_for_postgres() {
|
|
51
|
+
if [ -z "$DATABASE_URL" ]; then
|
|
52
|
+
log_warn "DATABASE_URL not set, skipping PostgreSQL check"
|
|
53
|
+
return 0
|
|
54
|
+
fi
|
|
55
|
+
|
|
56
|
+
# Check if this is a PostgreSQL connection string
|
|
57
|
+
if echo "$DATABASE_URL" | grep -q "postgresql://\|postgres://"; then
|
|
58
|
+
log_info "Waiting for PostgreSQL to be ready..."
|
|
59
|
+
|
|
60
|
+
# Extract host and port from DATABASE_URL
|
|
61
|
+
# Format: postgresql://user:pass@host:port/dbname
|
|
62
|
+
DB_HOST=$(echo "$DATABASE_URL" | sed -n 's/.*@\([^:]*\):.*/\1/p')
|
|
63
|
+
DB_PORT=$(echo "$DATABASE_URL" | sed -n 's/.*:\([0-9]*\)\/.*/\1/p')
|
|
64
|
+
|
|
65
|
+
DB_HOST="${DB_HOST:-localhost}"
|
|
66
|
+
DB_PORT="${DB_PORT:-5432}"
|
|
67
|
+
|
|
68
|
+
RETRIES=0
|
|
69
|
+
until nc -z "$DB_HOST" "$DB_PORT" 2>/dev/null; do
|
|
70
|
+
RETRIES=$((RETRIES + 1))
|
|
71
|
+
if [ $RETRIES -ge $MAX_RETRIES ]; then
|
|
72
|
+
log_error "PostgreSQL is not available after $MAX_RETRIES attempts"
|
|
73
|
+
exit 1
|
|
74
|
+
fi
|
|
75
|
+
log_info "PostgreSQL is unavailable - attempt $RETRIES/$MAX_RETRIES - sleeping ${RETRY_INTERVAL}s"
|
|
76
|
+
sleep $RETRY_INTERVAL
|
|
77
|
+
done
|
|
78
|
+
|
|
79
|
+
log_info "PostgreSQL is ready!"
|
|
80
|
+
else
|
|
81
|
+
log_info "Using SQLite database, no connection wait needed"
|
|
82
|
+
fi
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
# -----------------------------------------------------------------------------
|
|
86
|
+
# Wait for Redis to be ready (optional)
|
|
87
|
+
# -----------------------------------------------------------------------------
|
|
88
|
+
wait_for_redis() {
|
|
89
|
+
if [ -z "$REDIS_URL" ]; then
|
|
90
|
+
log_warn "REDIS_URL not set, skipping Redis check"
|
|
91
|
+
return 0
|
|
92
|
+
fi
|
|
93
|
+
|
|
94
|
+
log_info "Waiting for Redis to be ready..."
|
|
95
|
+
|
|
96
|
+
# Extract host and port from REDIS_URL
|
|
97
|
+
# Format: redis://host:port
|
|
98
|
+
REDIS_HOST=$(echo "$REDIS_URL" | sed -n 's/redis:\/\/\([^:]*\):.*/\1/p')
|
|
99
|
+
REDIS_PORT=$(echo "$REDIS_URL" | sed -n 's/redis:\/\/[^:]*:\([0-9]*\).*/\1/p')
|
|
100
|
+
|
|
101
|
+
REDIS_HOST="${REDIS_HOST:-localhost}"
|
|
102
|
+
REDIS_PORT="${REDIS_PORT:-6379}"
|
|
103
|
+
|
|
104
|
+
RETRIES=0
|
|
105
|
+
until nc -z "$REDIS_HOST" "$REDIS_PORT" 2>/dev/null; do
|
|
106
|
+
RETRIES=$((RETRIES + 1))
|
|
107
|
+
if [ $RETRIES -ge $MAX_RETRIES ]; then
|
|
108
|
+
log_error "Redis is not available after $MAX_RETRIES attempts"
|
|
109
|
+
exit 1
|
|
110
|
+
fi
|
|
111
|
+
log_info "Redis is unavailable - attempt $RETRIES/$MAX_RETRIES - sleeping ${RETRY_INTERVAL}s"
|
|
112
|
+
sleep $RETRY_INTERVAL
|
|
113
|
+
done
|
|
114
|
+
|
|
115
|
+
log_info "Redis is ready!"
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
# -----------------------------------------------------------------------------
|
|
119
|
+
# Run database migrations
|
|
120
|
+
# -----------------------------------------------------------------------------
|
|
121
|
+
run_migrations() {
|
|
122
|
+
if [ "$SKIP_MIGRATIONS" = "true" ]; then
|
|
123
|
+
log_warn "SKIP_MIGRATIONS is set, skipping database migrations"
|
|
124
|
+
return 0
|
|
125
|
+
fi
|
|
126
|
+
|
|
127
|
+
log_info "Running database migrations..."
|
|
128
|
+
|
|
129
|
+
# Check if drizzle directory exists with migrations
|
|
130
|
+
if [ -d "/app/drizzle" ] && [ "$(ls -A /app/drizzle 2>/dev/null)" ]; then
|
|
131
|
+
# For production, use drizzle-kit migrate
|
|
132
|
+
# This requires drizzle-kit to be installed
|
|
133
|
+
if command -v npx >/dev/null 2>&1; then
|
|
134
|
+
npx drizzle-kit migrate || {
|
|
135
|
+
log_warn "Migration failed, continuing anyway (migrations may already be applied)"
|
|
136
|
+
}
|
|
137
|
+
else
|
|
138
|
+
log_warn "npx not available, skipping drizzle migrations"
|
|
139
|
+
fi
|
|
140
|
+
else
|
|
141
|
+
log_info "No migrations found in /app/drizzle, skipping"
|
|
142
|
+
fi
|
|
143
|
+
|
|
144
|
+
log_info "Database migrations complete!"
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
# -----------------------------------------------------------------------------
|
|
148
|
+
# Create data directory if needed
|
|
149
|
+
# -----------------------------------------------------------------------------
|
|
150
|
+
setup_directories() {
|
|
151
|
+
log_info "Setting up directories..."
|
|
152
|
+
|
|
153
|
+
# Ensure data directory exists and is writable
|
|
154
|
+
mkdir -p /app/data
|
|
155
|
+
|
|
156
|
+
log_info "Directories ready!"
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
# -----------------------------------------------------------------------------
|
|
160
|
+
# Main entrypoint
|
|
161
|
+
# -----------------------------------------------------------------------------
|
|
162
|
+
main() {
|
|
163
|
+
log_info "Starting SuperMemory Clone API..."
|
|
164
|
+
log_info "Environment: ${NODE_ENV:-development}"
|
|
165
|
+
|
|
166
|
+
# Setup directories
|
|
167
|
+
setup_directories
|
|
168
|
+
|
|
169
|
+
# Wait for services
|
|
170
|
+
wait_for_postgres
|
|
171
|
+
wait_for_redis
|
|
172
|
+
|
|
173
|
+
# Run migrations
|
|
174
|
+
run_migrations
|
|
175
|
+
|
|
176
|
+
log_info "Starting application..."
|
|
177
|
+
|
|
178
|
+
# Execute the main command (node dist/index.js)
|
|
179
|
+
exec node dist/index.js
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
# Run main function
|
|
183
|
+
main "$@"
|