@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,1110 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
|
|
4
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
5
|
+
REPO_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)"
|
|
6
|
+
cd "$REPO_ROOT"
|
|
7
|
+
|
|
8
|
+
ACTION="install"
|
|
9
|
+
INSTALL_MODE="agent"
|
|
10
|
+
ENV_FILE=""
|
|
11
|
+
PURGE=0
|
|
12
|
+
SKIP_DOCKER=0
|
|
13
|
+
SKIP_API_KEYS=0
|
|
14
|
+
SKIP_CLAUDE=0
|
|
15
|
+
REGISTER_MCP=0
|
|
16
|
+
NON_INTERACTIVE=0
|
|
17
|
+
SKIP_BUILD=0
|
|
18
|
+
SKIP_API_START=0
|
|
19
|
+
MCP_SCOPE="user"
|
|
20
|
+
MCP_SCOPE_EXPLICIT=0
|
|
21
|
+
INSTALLER_BRIEF="${SUPERMEMORY_INSTALLER_BRIEF:-0}"
|
|
22
|
+
INSTALLER_RESULT_FILE="${SUPERMEMORY_INSTALLER_RESULT_FILE:-}"
|
|
23
|
+
CLAUDE_MCP_STATUS="not_requested"
|
|
24
|
+
CLAUDE_MCP_SCOPE_USED=""
|
|
25
|
+
API_KEYS_WERE_SKIPPED=0
|
|
26
|
+
|
|
27
|
+
usage() {
|
|
28
|
+
cat <<'USAGE'
|
|
29
|
+
Usage: ./scripts/install.sh [command] [options]
|
|
30
|
+
|
|
31
|
+
Commands:
|
|
32
|
+
install Install or repair local setup (default, uses --mode agent unless overridden)
|
|
33
|
+
update Reinstall app components, preserve Postgres/Redis data, and attempt DB migrations
|
|
34
|
+
uninstall Remove generated local artifacts and stop project services
|
|
35
|
+
agent Shorthand for: install --mode agent
|
|
36
|
+
api Shorthand for: install --mode api
|
|
37
|
+
full Shorthand for: install --mode full
|
|
38
|
+
|
|
39
|
+
Options:
|
|
40
|
+
--mode <mode> Install/update mode: agent (default), api, or full
|
|
41
|
+
--env-file <path> Env file to use (default precedence: SUPERMEMORY_ENV_FILE, .env.local, .env)
|
|
42
|
+
--skip-docker Skip docker startup (install/update only)
|
|
43
|
+
--skip-api-keys Do not prompt for API keys (install/update only)
|
|
44
|
+
--register-mcp Explicitly register Claude Code MCP integration
|
|
45
|
+
--skip-mcp Skip Claude Code MCP registration/removal
|
|
46
|
+
--skip-claude Alias for --skip-mcp
|
|
47
|
+
--purge With uninstall, also remove env files, Docker volumes, and Claude MCP registrations
|
|
48
|
+
--skip-build Skip npm run build (install/update only)
|
|
49
|
+
--skip-api-start Skip auto-starting the API container after install/update in api/full mode
|
|
50
|
+
--scope <scope> MCP scope override: user (default), project, or local
|
|
51
|
+
--non-interactive Use defaults and avoid prompts
|
|
52
|
+
-h, --help Show this help
|
|
53
|
+
|
|
54
|
+
Examples:
|
|
55
|
+
./scripts/install.sh
|
|
56
|
+
./scripts/install.sh agent
|
|
57
|
+
./scripts/install.sh install --mode api
|
|
58
|
+
./scripts/install.sh agent --env-file /tmp/supermemory.env
|
|
59
|
+
./scripts/install.sh update --skip-claude
|
|
60
|
+
./scripts/install.sh agent --non-interactive --register-mcp --scope project
|
|
61
|
+
./scripts/install.sh full --skip-api-start
|
|
62
|
+
./scripts/install.sh --scope project
|
|
63
|
+
./scripts/install.sh uninstall --non-interactive
|
|
64
|
+
./scripts/install.sh uninstall --purge --non-interactive
|
|
65
|
+
USAGE
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
log() {
|
|
69
|
+
local level="$1"
|
|
70
|
+
shift
|
|
71
|
+
printf '[%s] %s\n' "$level" "$*"
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
fail() {
|
|
75
|
+
log "FAIL" "$*"
|
|
76
|
+
exit 1
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
command_exists() {
|
|
80
|
+
command -v "$1" >/dev/null 2>&1
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
require_command() {
|
|
84
|
+
local name="$1"
|
|
85
|
+
if ! command_exists "$name"; then
|
|
86
|
+
fail "Missing required command: $name"
|
|
87
|
+
fi
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
confirm() {
|
|
91
|
+
local prompt="$1"
|
|
92
|
+
local default_answer="$2" # y or n
|
|
93
|
+
|
|
94
|
+
if [[ "$NON_INTERACTIVE" -eq 1 ]]; then
|
|
95
|
+
[[ "$default_answer" == "y" ]]
|
|
96
|
+
return
|
|
97
|
+
fi
|
|
98
|
+
|
|
99
|
+
local label="[y/N]"
|
|
100
|
+
if [[ "$default_answer" == "y" ]]; then
|
|
101
|
+
label="[Y/n]"
|
|
102
|
+
fi
|
|
103
|
+
|
|
104
|
+
read -r -p "$prompt $label " reply
|
|
105
|
+
reply="${reply,,}"
|
|
106
|
+
|
|
107
|
+
if [[ -z "$reply" ]]; then
|
|
108
|
+
[[ "$default_answer" == "y" ]]
|
|
109
|
+
return
|
|
110
|
+
fi
|
|
111
|
+
|
|
112
|
+
[[ "$reply" == "y" || "$reply" == "yes" ]]
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
validate_mcp_scope() {
|
|
116
|
+
local scope="$1"
|
|
117
|
+
case "$scope" in
|
|
118
|
+
user|project|local)
|
|
119
|
+
return 0
|
|
120
|
+
;;
|
|
121
|
+
*)
|
|
122
|
+
fail "Invalid MCP scope: $scope (expected: user, project, or local)"
|
|
123
|
+
;;
|
|
124
|
+
esac
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
validate_install_mode() {
|
|
128
|
+
local mode="$1"
|
|
129
|
+
case "$mode" in
|
|
130
|
+
agent|api|full)
|
|
131
|
+
return 0
|
|
132
|
+
;;
|
|
133
|
+
*)
|
|
134
|
+
fail "Invalid install mode: $mode (expected: agent, api, or full)"
|
|
135
|
+
;;
|
|
136
|
+
esac
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
resolve_path() {
|
|
140
|
+
local candidate="$1"
|
|
141
|
+
if [[ "$candidate" = /* ]]; then
|
|
142
|
+
printf '%s\n' "$candidate"
|
|
143
|
+
else
|
|
144
|
+
printf '%s\n' "$REPO_ROOT/$candidate"
|
|
145
|
+
fi
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
resolve_default_env_file() {
|
|
149
|
+
if [[ -n "${SUPERMEMORY_ENV_FILE:-}" ]]; then
|
|
150
|
+
resolve_path "$SUPERMEMORY_ENV_FILE"
|
|
151
|
+
return 0
|
|
152
|
+
fi
|
|
153
|
+
|
|
154
|
+
if [[ -f "$REPO_ROOT/.env.local" ]]; then
|
|
155
|
+
printf '%s\n' "$REPO_ROOT/.env.local"
|
|
156
|
+
return 0
|
|
157
|
+
fi
|
|
158
|
+
|
|
159
|
+
printf '%s\n' "$REPO_ROOT/.env"
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
ensure_env_file_path() {
|
|
163
|
+
if [[ -n "$ENV_FILE" ]]; then
|
|
164
|
+
ENV_FILE="$(resolve_path "$ENV_FILE")"
|
|
165
|
+
else
|
|
166
|
+
ENV_FILE="$(resolve_default_env_file)"
|
|
167
|
+
fi
|
|
168
|
+
|
|
169
|
+
export SUPERMEMORY_ENV_FILE="$ENV_FILE"
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
compose_cmd() {
|
|
173
|
+
local -a cmd=(docker compose)
|
|
174
|
+
if [[ -f "$ENV_FILE" ]]; then
|
|
175
|
+
cmd+=(--env-file "$ENV_FILE")
|
|
176
|
+
fi
|
|
177
|
+
"${cmd[@]}" "$@"
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
prompt_mcp_scope() {
|
|
181
|
+
local answer
|
|
182
|
+
read -r -p "MCP registration scope [user/project/local] (user): " answer
|
|
183
|
+
answer="${answer,,}"
|
|
184
|
+
if [[ -z "$answer" ]]; then
|
|
185
|
+
echo "user"
|
|
186
|
+
return 0
|
|
187
|
+
fi
|
|
188
|
+
|
|
189
|
+
validate_mcp_scope "$answer"
|
|
190
|
+
echo "$answer"
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
set_env_value() {
|
|
194
|
+
local key="$1"
|
|
195
|
+
local value="$2"
|
|
196
|
+
local file="$ENV_FILE"
|
|
197
|
+
local tmp_file
|
|
198
|
+
tmp_file="$(mktemp)"
|
|
199
|
+
|
|
200
|
+
if grep -q "^${key}=" "$file"; then
|
|
201
|
+
awk -v k="$key" -v v="$value" '
|
|
202
|
+
BEGIN { FS = OFS = "=" }
|
|
203
|
+
$1 == k { $0 = k "=" v }
|
|
204
|
+
{ print }
|
|
205
|
+
' "$file" > "$tmp_file"
|
|
206
|
+
mv "$tmp_file" "$file"
|
|
207
|
+
else
|
|
208
|
+
printf '%s=%s\n' "$key" "$value" >> "$file"
|
|
209
|
+
rm -f "$tmp_file"
|
|
210
|
+
fi
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
generate_local_secret() {
|
|
214
|
+
node -e "console.log(require('node:crypto').randomBytes(48).toString('base64url'))"
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
scrub_placeholder_env_values() {
|
|
218
|
+
local openai_key
|
|
219
|
+
local anthropic_key
|
|
220
|
+
local llm_provider
|
|
221
|
+
|
|
222
|
+
openai_key="$(read_env_value "OPENAI_API_KEY")"
|
|
223
|
+
anthropic_key="$(read_env_value "ANTHROPIC_API_KEY")"
|
|
224
|
+
llm_provider="$(read_env_value "LLM_PROVIDER")"
|
|
225
|
+
|
|
226
|
+
if [[ "$openai_key" == "sk-your-openai-api-key-here" ]]; then
|
|
227
|
+
set_env_value "OPENAI_API_KEY" ""
|
|
228
|
+
openai_key=""
|
|
229
|
+
fi
|
|
230
|
+
|
|
231
|
+
if [[ "$anthropic_key" == "anthropic-your-api-key-here" ]]; then
|
|
232
|
+
set_env_value "ANTHROPIC_API_KEY" ""
|
|
233
|
+
anthropic_key=""
|
|
234
|
+
fi
|
|
235
|
+
|
|
236
|
+
if [[ -z "$openai_key" && -z "$anthropic_key" ]]; then
|
|
237
|
+
if [[ "$llm_provider" == "openai" || "$llm_provider" == "anthropic" ]]; then
|
|
238
|
+
set_env_value "LLM_PROVIDER" ""
|
|
239
|
+
fi
|
|
240
|
+
|
|
241
|
+
if [[ "$(read_env_value "LLM_MODEL")" == "gpt-5.1-nano" || "$(read_env_value "LLM_MODEL")" == "claude-4-5-haiku-20251001" ]]; then
|
|
242
|
+
set_env_value "LLM_MODEL" ""
|
|
243
|
+
fi
|
|
244
|
+
fi
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
ensure_env_defaults() {
|
|
248
|
+
local created_env="$1"
|
|
249
|
+
local csrf_secret
|
|
250
|
+
|
|
251
|
+
if [[ "$created_env" -eq 1 ]]; then
|
|
252
|
+
set_env_value "DATABASE_URL" "postgresql://supermemory:supermemory_secret@localhost:15432/supermemory"
|
|
253
|
+
set_env_value "REDIS_URL" "redis://localhost:16379"
|
|
254
|
+
set_env_value "API_PORT" "13000"
|
|
255
|
+
set_env_value "API_HOST_PORT" "13000"
|
|
256
|
+
set_env_value "POSTGRES_HOST_PORT" "15432"
|
|
257
|
+
set_env_value "REDIS_HOST_PORT" "16379"
|
|
258
|
+
set_env_value "CSRF_SECRET" "$(generate_local_secret)"
|
|
259
|
+
return
|
|
260
|
+
fi
|
|
261
|
+
|
|
262
|
+
if ! grep -q "^API_HOST_PORT=" "$ENV_FILE"; then
|
|
263
|
+
set_env_value "API_HOST_PORT" "13000"
|
|
264
|
+
fi
|
|
265
|
+
|
|
266
|
+
if ! grep -q "^POSTGRES_HOST_PORT=" "$ENV_FILE"; then
|
|
267
|
+
set_env_value "POSTGRES_HOST_PORT" "15432"
|
|
268
|
+
fi
|
|
269
|
+
|
|
270
|
+
if ! grep -q "^REDIS_HOST_PORT=" "$ENV_FILE"; then
|
|
271
|
+
set_env_value "REDIS_HOST_PORT" "16379"
|
|
272
|
+
fi
|
|
273
|
+
|
|
274
|
+
csrf_secret="$(read_env_value "CSRF_SECRET")"
|
|
275
|
+
if [[ -z "$csrf_secret" ]]; then
|
|
276
|
+
set_env_value "CSRF_SECRET" "$(generate_local_secret)"
|
|
277
|
+
fi
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
read_env_value() {
|
|
281
|
+
local key="$1"
|
|
282
|
+
awk -F= -v k="$key" '$1 == k { print substr($0, index($0, "=") + 1) }' "$ENV_FILE" | tail -n 1
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
migrate_legacy_default_ports() {
|
|
286
|
+
if grep -q "^DATABASE_URL=postgresql://supermemory:supermemory_secret@localhost:5432/supermemory$" "$ENV_FILE"; then
|
|
287
|
+
set_env_value "DATABASE_URL" "postgresql://supermemory:supermemory_secret@localhost:15432/supermemory"
|
|
288
|
+
log "INFO" "Updated DATABASE_URL from localhost:5432 to localhost:15432"
|
|
289
|
+
fi
|
|
290
|
+
|
|
291
|
+
if grep -q "^REDIS_URL=redis://localhost:6379$" "$ENV_FILE"; then
|
|
292
|
+
set_env_value "REDIS_URL" "redis://localhost:16379"
|
|
293
|
+
log "INFO" "Updated REDIS_URL from localhost:6379 to localhost:16379"
|
|
294
|
+
fi
|
|
295
|
+
|
|
296
|
+
if grep -q "^API_PORT=3000$" "$ENV_FILE"; then
|
|
297
|
+
set_env_value "API_PORT" "13000"
|
|
298
|
+
log "INFO" "Updated API_PORT from 3000 to 13000"
|
|
299
|
+
fi
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
load_env_file_into_shell() {
|
|
303
|
+
if [[ ! -f "$ENV_FILE" ]]; then
|
|
304
|
+
return 0
|
|
305
|
+
fi
|
|
306
|
+
|
|
307
|
+
set -a
|
|
308
|
+
# shellcheck disable=SC1090
|
|
309
|
+
source "$ENV_FILE"
|
|
310
|
+
set +a
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
wait_for_container_health() {
|
|
314
|
+
local container="$1"
|
|
315
|
+
local timeout_seconds="$2"
|
|
316
|
+
local elapsed=0
|
|
317
|
+
|
|
318
|
+
while (( elapsed < timeout_seconds )); do
|
|
319
|
+
local status
|
|
320
|
+
status="$(docker inspect --format '{{if .State.Health}}{{.State.Health.Status}}{{else}}{{.State.Status}}{{end}}' "$container" 2>/dev/null || true)"
|
|
321
|
+
|
|
322
|
+
if [[ "$status" == "healthy" || "$status" == "running" ]]; then
|
|
323
|
+
log "OK" "$container is $status"
|
|
324
|
+
return 0
|
|
325
|
+
fi
|
|
326
|
+
|
|
327
|
+
sleep 2
|
|
328
|
+
elapsed=$((elapsed + 2))
|
|
329
|
+
done
|
|
330
|
+
|
|
331
|
+
fail "Timed out waiting for container health: $container"
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
start_compose_services() {
|
|
335
|
+
if compose_cmd up -d postgres redis; then
|
|
336
|
+
return 0
|
|
337
|
+
fi
|
|
338
|
+
|
|
339
|
+
log "WARN" "docker compose up failed, attempting one cleanup+retry"
|
|
340
|
+
compose_cmd down >/dev/null 2>&1 || true
|
|
341
|
+
docker rm -f supermemory-postgres supermemory-redis >/dev/null 2>&1 || true
|
|
342
|
+
docker network rm supermemory-network >/dev/null 2>&1 || true
|
|
343
|
+
|
|
344
|
+
compose_cmd up -d postgres redis
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
start_postgres_service() {
|
|
348
|
+
if compose_cmd up -d postgres; then
|
|
349
|
+
return 0
|
|
350
|
+
fi
|
|
351
|
+
|
|
352
|
+
log "WARN" "docker compose up postgres failed, attempting one cleanup+retry"
|
|
353
|
+
compose_cmd down >/dev/null 2>&1 || true
|
|
354
|
+
docker rm -f supermemory-postgres >/dev/null 2>&1 || true
|
|
355
|
+
docker network rm supermemory-network >/dev/null 2>&1 || true
|
|
356
|
+
|
|
357
|
+
compose_cmd up -d postgres
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
start_api_stack() {
|
|
361
|
+
if compose_cmd -f docker-compose.yml -f docker-compose.prod.yml --profile production up -d api postgres redis; then
|
|
362
|
+
return 0
|
|
363
|
+
fi
|
|
364
|
+
|
|
365
|
+
log "WARN" "docker compose (api stack) up failed, attempting one cleanup+retry"
|
|
366
|
+
compose_cmd -f docker-compose.yml -f docker-compose.prod.yml down >/dev/null 2>&1 || true
|
|
367
|
+
docker rm -f supermemory-api supermemory-postgres supermemory-redis >/dev/null 2>&1 || true
|
|
368
|
+
docker network rm supermemory-network >/dev/null 2>&1 || true
|
|
369
|
+
|
|
370
|
+
compose_cmd -f docker-compose.yml -f docker-compose.prod.yml --profile production up -d api postgres redis
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
verify_api_health() {
|
|
374
|
+
local api_host_port="$1"
|
|
375
|
+
local health_url="http://localhost:${api_host_port}/health"
|
|
376
|
+
local attempt=0
|
|
377
|
+
local max_attempts=30
|
|
378
|
+
|
|
379
|
+
wait_for_container_health "supermemory-api" 120
|
|
380
|
+
|
|
381
|
+
if ! command_exists curl; then
|
|
382
|
+
log "WARN" "curl not found; skipped HTTP health probe. Container health is already ready"
|
|
383
|
+
return 0
|
|
384
|
+
fi
|
|
385
|
+
|
|
386
|
+
until curl --fail --silent --show-error "$health_url" >/dev/null; do
|
|
387
|
+
attempt=$((attempt + 1))
|
|
388
|
+
if [[ "$attempt" -ge "$max_attempts" ]]; then
|
|
389
|
+
fail "Timed out waiting for API health endpoint: $health_url"
|
|
390
|
+
fi
|
|
391
|
+
sleep 2
|
|
392
|
+
done
|
|
393
|
+
|
|
394
|
+
log "OK" "API health endpoint is reachable: $health_url"
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
configure_api_keys() {
|
|
398
|
+
if [[ "$SKIP_API_KEYS" -eq 1 || "$NON_INTERACTIVE" -eq 1 ]]; then
|
|
399
|
+
API_KEYS_WERE_SKIPPED=1
|
|
400
|
+
return 0
|
|
401
|
+
fi
|
|
402
|
+
|
|
403
|
+
if ! confirm "Configure API keys now?" "n"; then
|
|
404
|
+
API_KEYS_WERE_SKIPPED=1
|
|
405
|
+
log "WARN" "Skipping API key setup"
|
|
406
|
+
return 0
|
|
407
|
+
fi
|
|
408
|
+
|
|
409
|
+
read -r -p "OPENAI_API_KEY (optional, press Enter to skip): " openai_key
|
|
410
|
+
read -r -p "ANTHROPIC_API_KEY (optional, press Enter to skip): " anthropic_key
|
|
411
|
+
read -r -p "LLM provider [openai/anthropic/none] (none): " llm_provider
|
|
412
|
+
|
|
413
|
+
llm_provider="${llm_provider,,}"
|
|
414
|
+
case "$llm_provider" in
|
|
415
|
+
openai|anthropic|none|"") ;;
|
|
416
|
+
*) llm_provider="none" ;;
|
|
417
|
+
esac
|
|
418
|
+
|
|
419
|
+
if [[ -n "$openai_key" ]]; then
|
|
420
|
+
set_env_value "OPENAI_API_KEY" "$openai_key"
|
|
421
|
+
fi
|
|
422
|
+
|
|
423
|
+
if [[ -n "$anthropic_key" ]]; then
|
|
424
|
+
set_env_value "ANTHROPIC_API_KEY" "$anthropic_key"
|
|
425
|
+
fi
|
|
426
|
+
|
|
427
|
+
if [[ "$llm_provider" == "openai" ]]; then
|
|
428
|
+
set_env_value "LLM_PROVIDER" "openai"
|
|
429
|
+
set_env_value "LLM_MODEL" "gpt-5.1-nano"
|
|
430
|
+
elif [[ "$llm_provider" == "anthropic" ]]; then
|
|
431
|
+
set_env_value "LLM_PROVIDER" "anthropic"
|
|
432
|
+
set_env_value "LLM_MODEL" "claude-4-5-haiku-20251001"
|
|
433
|
+
else
|
|
434
|
+
set_env_value "LLM_PROVIDER" ""
|
|
435
|
+
set_env_value "LLM_MODEL" ""
|
|
436
|
+
fi
|
|
437
|
+
|
|
438
|
+
log "OK" "Saved API key/provider configuration to $ENV_FILE"
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
configure_claude_mcp() {
|
|
442
|
+
if [[ "$SKIP_CLAUDE" -eq 1 ]]; then
|
|
443
|
+
CLAUDE_MCP_STATUS="skipped"
|
|
444
|
+
log "WARN" "Skipped Claude Code setup"
|
|
445
|
+
return 0
|
|
446
|
+
fi
|
|
447
|
+
|
|
448
|
+
if ! command_exists claude; then
|
|
449
|
+
CLAUDE_MCP_STATUS="missing_cli"
|
|
450
|
+
log "WARN" "Claude CLI not found. Install Claude Code or run npm run mcp:setup later"
|
|
451
|
+
return 0
|
|
452
|
+
fi
|
|
453
|
+
|
|
454
|
+
local should_register=0
|
|
455
|
+
local selected_scope="$MCP_SCOPE"
|
|
456
|
+
CLAUDE_MCP_SCOPE_USED="$selected_scope"
|
|
457
|
+
local register_command
|
|
458
|
+
printf -v register_command 'claude mcp add supermemory --scope %q -- node %q' "$selected_scope" "$REPO_ROOT/dist/mcp/index.js"
|
|
459
|
+
|
|
460
|
+
if [[ "$NON_INTERACTIVE" -eq 1 ]]; then
|
|
461
|
+
if [[ "$REGISTER_MCP" -eq 1 || "$MCP_SCOPE_EXPLICIT" -eq 1 ]]; then
|
|
462
|
+
should_register=1
|
|
463
|
+
else
|
|
464
|
+
CLAUDE_MCP_STATUS="not_requested"
|
|
465
|
+
log "INFO" "Skipped Claude Code MCP registration in non-interactive mode. Pass --register-mcp or --scope to opt in"
|
|
466
|
+
return 0
|
|
467
|
+
fi
|
|
468
|
+
else
|
|
469
|
+
if [[ "$MCP_SCOPE_EXPLICIT" -eq 0 ]]; then
|
|
470
|
+
selected_scope="$(prompt_mcp_scope)"
|
|
471
|
+
CLAUDE_MCP_SCOPE_USED="$selected_scope"
|
|
472
|
+
printf -v register_command 'claude mcp add supermemory --scope %q -- node %q' "$selected_scope" "$REPO_ROOT/dist/mcp/index.js"
|
|
473
|
+
fi
|
|
474
|
+
|
|
475
|
+
if [[ "$REGISTER_MCP" -eq 1 ]] || confirm "Register MCP server with Claude Code in ${selected_scope} scope?" "y"; then
|
|
476
|
+
should_register=1
|
|
477
|
+
fi
|
|
478
|
+
fi
|
|
479
|
+
|
|
480
|
+
if [[ "$should_register" -ne 1 ]]; then
|
|
481
|
+
CLAUDE_MCP_STATUS="declined"
|
|
482
|
+
log "WARN" "Skipped Claude Code MCP registration"
|
|
483
|
+
return 0
|
|
484
|
+
fi
|
|
485
|
+
|
|
486
|
+
if npx tsx scripts/claude-mcp-config.ts check --scope "$selected_scope" --name supermemory --command node --arg "$REPO_ROOT/dist/mcp/index.js" --project-dir "$REPO_ROOT" >/dev/null 2>&1; then
|
|
487
|
+
CLAUDE_MCP_STATUS="match"
|
|
488
|
+
log "OK" "Claude Code MCP server 'supermemory' already matches ${selected_scope} scope and command path"
|
|
489
|
+
return 0
|
|
490
|
+
else
|
|
491
|
+
local inspect_rc=$?
|
|
492
|
+
local repair_required=0
|
|
493
|
+
case "$inspect_rc" in
|
|
494
|
+
10)
|
|
495
|
+
log "INFO" "No Claude Code MCP registration found in ${selected_scope} scope"
|
|
496
|
+
;;
|
|
497
|
+
11)
|
|
498
|
+
log "INFO" "Existing Claude Code MCP registration in ${selected_scope} scope does not match the current command path"
|
|
499
|
+
repair_required=1
|
|
500
|
+
;;
|
|
501
|
+
*)
|
|
502
|
+
log "WARN" "Could not inspect existing Claude Code MCP registration cleanly. Continuing with registration attempt"
|
|
503
|
+
;;
|
|
504
|
+
esac
|
|
505
|
+
|
|
506
|
+
if [[ "$repair_required" -eq 1 ]]; then
|
|
507
|
+
local remove_command
|
|
508
|
+
printf -v remove_command 'claude mcp remove --scope %q supermemory' "$selected_scope"
|
|
509
|
+
log "INFO" "Removing stale Claude Code MCP registration with: $remove_command"
|
|
510
|
+
if ! claude mcp remove --scope "$selected_scope" supermemory; then
|
|
511
|
+
CLAUDE_MCP_STATUS="remove_failed"
|
|
512
|
+
log "WARN" "Could not remove existing Claude Code MCP registration in ${selected_scope} scope"
|
|
513
|
+
return 0
|
|
514
|
+
fi
|
|
515
|
+
fi
|
|
516
|
+
fi
|
|
517
|
+
|
|
518
|
+
log "INFO" "Registering Claude Code MCP server with: $register_command"
|
|
519
|
+
if claude mcp add supermemory --scope "$selected_scope" -- node "$REPO_ROOT/dist/mcp/index.js"; then
|
|
520
|
+
CLAUDE_MCP_STATUS="registered"
|
|
521
|
+
log "OK" "Registered MCP server with Claude Code (${selected_scope} scope)"
|
|
522
|
+
else
|
|
523
|
+
CLAUDE_MCP_STATUS="register_failed"
|
|
524
|
+
log "WARN" "Could not register MCP automatically. Run: npm run mcp:setup"
|
|
525
|
+
fi
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
write_install_result() {
|
|
529
|
+
local api_started="$1"
|
|
530
|
+
local api_host_port="$2"
|
|
531
|
+
local connectivity_ok="$3"
|
|
532
|
+
|
|
533
|
+
if [[ -z "$INSTALLER_RESULT_FILE" ]]; then
|
|
534
|
+
return 0
|
|
535
|
+
fi
|
|
536
|
+
|
|
537
|
+
INSTALL_RESULT_FILE="$INSTALLER_RESULT_FILE" \
|
|
538
|
+
INSTALL_RESULT_ACTION="$ACTION" \
|
|
539
|
+
INSTALL_RESULT_MODE="$INSTALL_MODE" \
|
|
540
|
+
INSTALL_RESULT_DIR="$REPO_ROOT" \
|
|
541
|
+
INSTALL_RESULT_ENV_FILE="$ENV_FILE" \
|
|
542
|
+
INSTALL_RESULT_API_HOST_PORT="$api_host_port" \
|
|
543
|
+
INSTALL_RESULT_API_STARTED="$api_started" \
|
|
544
|
+
INSTALL_RESULT_CONNECTIVITY_OK="$connectivity_ok" \
|
|
545
|
+
INSTALL_RESULT_MCP_SCOPE="$CLAUDE_MCP_SCOPE_USED" \
|
|
546
|
+
INSTALL_RESULT_MCP_STATUS="$CLAUDE_MCP_STATUS" \
|
|
547
|
+
INSTALL_RESULT_SKIP_DOCKER="$SKIP_DOCKER" \
|
|
548
|
+
INSTALL_RESULT_SKIP_API_KEYS="$SKIP_API_KEYS" \
|
|
549
|
+
INSTALL_RESULT_SKIP_API_START="$SKIP_API_START" \
|
|
550
|
+
INSTALL_RESULT_API_KEYS_WERE_SKIPPED="$API_KEYS_WERE_SKIPPED" \
|
|
551
|
+
node <<'NODE'
|
|
552
|
+
const { writeFileSync } = require('node:fs')
|
|
553
|
+
|
|
554
|
+
const resultPath = process.env.INSTALL_RESULT_FILE
|
|
555
|
+
if (!resultPath) {
|
|
556
|
+
process.exit(0)
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
const asBoolean = (value) => value === '1'
|
|
560
|
+
const nullableString = (value) => (value ? value : null)
|
|
561
|
+
|
|
562
|
+
writeFileSync(
|
|
563
|
+
resultPath,
|
|
564
|
+
`${JSON.stringify(
|
|
565
|
+
{
|
|
566
|
+
action: process.env.INSTALL_RESULT_ACTION,
|
|
567
|
+
installMode: process.env.INSTALL_RESULT_MODE,
|
|
568
|
+
installDir: process.env.INSTALL_RESULT_DIR,
|
|
569
|
+
envFile: process.env.INSTALL_RESULT_ENV_FILE,
|
|
570
|
+
apiHostPort: nullableString(process.env.INSTALL_RESULT_API_HOST_PORT),
|
|
571
|
+
apiStarted: asBoolean(process.env.INSTALL_RESULT_API_STARTED),
|
|
572
|
+
connectivityOk: asBoolean(process.env.INSTALL_RESULT_CONNECTIVITY_OK),
|
|
573
|
+
mcp: {
|
|
574
|
+
scope: nullableString(process.env.INSTALL_RESULT_MCP_SCOPE),
|
|
575
|
+
status: process.env.INSTALL_RESULT_MCP_STATUS,
|
|
576
|
+
},
|
|
577
|
+
flags: {
|
|
578
|
+
skipDocker: asBoolean(process.env.INSTALL_RESULT_SKIP_DOCKER),
|
|
579
|
+
skipApiKeys: asBoolean(process.env.INSTALL_RESULT_SKIP_API_KEYS),
|
|
580
|
+
skipApiStart: asBoolean(process.env.INSTALL_RESULT_SKIP_API_START),
|
|
581
|
+
apiKeysWereSkipped: asBoolean(process.env.INSTALL_RESULT_API_KEYS_WERE_SKIPPED),
|
|
582
|
+
},
|
|
583
|
+
},
|
|
584
|
+
null,
|
|
585
|
+
2
|
|
586
|
+
)}\n`
|
|
587
|
+
)
|
|
588
|
+
NODE
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
print_install_summary() {
|
|
592
|
+
local api_started="$1"
|
|
593
|
+
local api_host_port="$2"
|
|
594
|
+
local connectivity_ok="$3"
|
|
595
|
+
local step=1
|
|
596
|
+
|
|
597
|
+
if [[ "$INSTALLER_BRIEF" == "1" ]]; then
|
|
598
|
+
return 0
|
|
599
|
+
fi
|
|
600
|
+
|
|
601
|
+
printf '\nInstall complete.\n'
|
|
602
|
+
printf '\nNext:\n'
|
|
603
|
+
|
|
604
|
+
if [[ "$INSTALL_MODE" == "api" || "$INSTALL_MODE" == "full" ]]; then
|
|
605
|
+
if [[ "$api_started" -eq 1 && -n "$api_host_port" ]]; then
|
|
606
|
+
printf ' %d. Verify the API: curl http://localhost:%s/health\n' "$step" "$api_host_port"
|
|
607
|
+
step=$((step + 1))
|
|
608
|
+
elif [[ "$SKIP_API_START" -eq 1 ]]; then
|
|
609
|
+
printf ' %d. Start the API stack when ready: docker compose -f docker-compose.yml -f docker-compose.prod.yml --profile production up -d api postgres redis\n' "$step"
|
|
610
|
+
step=$((step + 1))
|
|
611
|
+
fi
|
|
612
|
+
fi
|
|
613
|
+
|
|
614
|
+
if [[ "$INSTALL_MODE" == "agent" || "$INSTALL_MODE" == "full" ]]; then
|
|
615
|
+
case "$CLAUDE_MCP_STATUS" in
|
|
616
|
+
registered|match)
|
|
617
|
+
if [[ "$CLAUDE_MCP_SCOPE_USED" == "project" ]]; then
|
|
618
|
+
printf ' %d. Open Claude in this directory\n' "$step"
|
|
619
|
+
else
|
|
620
|
+
printf ' %d. Open Claude and use supermemory_add\n' "$step"
|
|
621
|
+
fi
|
|
622
|
+
step=$((step + 1))
|
|
623
|
+
;;
|
|
624
|
+
*)
|
|
625
|
+
printf ' %d. Register Claude later with: npm run mcp:setup -- --scope project --non-interactive --register-mcp\n' "$step"
|
|
626
|
+
step=$((step + 1))
|
|
627
|
+
;;
|
|
628
|
+
esac
|
|
629
|
+
fi
|
|
630
|
+
|
|
631
|
+
if [[ "$INSTALL_MODE" == "full" && "$CLAUDE_MCP_SCOPE_USED" == "project" ]]; then
|
|
632
|
+
if [[ "$CLAUDE_MCP_STATUS" == "registered" || "$CLAUDE_MCP_STATUS" == "match" ]]; then
|
|
633
|
+
printf ' %d. Ask Claude to use supermemory_add\n' "$step"
|
|
634
|
+
fi
|
|
635
|
+
fi
|
|
636
|
+
|
|
637
|
+
if [[ "$SKIP_API_KEYS" -eq 1 ]]; then
|
|
638
|
+
printf '\nAPI keys were skipped, so extraction quality may be limited until you update %s.\n' "$ENV_FILE"
|
|
639
|
+
fi
|
|
640
|
+
|
|
641
|
+
if [[ "$SKIP_DOCKER" -eq 1 ]]; then
|
|
642
|
+
printf '\nDocker startup was skipped, so start services manually before using the installed surfaces.\n'
|
|
643
|
+
fi
|
|
644
|
+
|
|
645
|
+
if [[ "$connectivity_ok" -ne 1 ]]; then
|
|
646
|
+
printf '\nConnectivity checks found issues. Review the logs above.\n'
|
|
647
|
+
fi
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
remove_claude_mcp() {
|
|
651
|
+
if [[ "$SKIP_CLAUDE" -eq 1 ]]; then
|
|
652
|
+
log "WARN" "Skipped Claude Code MCP cleanup"
|
|
653
|
+
return 0
|
|
654
|
+
fi
|
|
655
|
+
|
|
656
|
+
if ! command_exists claude; then
|
|
657
|
+
log "WARN" "Claude CLI not found. Skipping Claude MCP cleanup"
|
|
658
|
+
return 0
|
|
659
|
+
fi
|
|
660
|
+
|
|
661
|
+
local scope
|
|
662
|
+
for scope in project local user; do
|
|
663
|
+
if claude mcp remove --scope "$scope" supermemory >/dev/null 2>&1; then
|
|
664
|
+
log "OK" "Removed Claude MCP registration in $scope scope"
|
|
665
|
+
else
|
|
666
|
+
log "INFO" "No Claude MCP registration found in $scope scope"
|
|
667
|
+
fi
|
|
668
|
+
done
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
run_migrations_strict() {
|
|
672
|
+
log "INFO" "Running database migrations"
|
|
673
|
+
load_env_file_into_shell
|
|
674
|
+
./scripts/migrations/run_migrations.sh
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
run_migrations_best_effort() {
|
|
678
|
+
log "INFO" "Attempting database migrations for update"
|
|
679
|
+
load_env_file_into_shell
|
|
680
|
+
if ./scripts/migrations/run_migrations.sh; then
|
|
681
|
+
log "OK" "Migration attempt completed"
|
|
682
|
+
else
|
|
683
|
+
log "WARN" "Migration attempt failed during update. Verify database reachability and rerun ./scripts/migrations/run_migrations.sh"
|
|
684
|
+
fi
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
remove_local_runtime_artifacts_for_update() {
|
|
688
|
+
local path
|
|
689
|
+
for path in node_modules dist; do
|
|
690
|
+
if [[ -e "$path" ]]; then
|
|
691
|
+
rm -rf "$path"
|
|
692
|
+
log "OK" "Removed $path for clean reinstall"
|
|
693
|
+
else
|
|
694
|
+
log "INFO" "$path not present; skipping"
|
|
695
|
+
fi
|
|
696
|
+
done
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
remove_local_install_artifacts() {
|
|
700
|
+
local path
|
|
701
|
+
for path in node_modules dist coverage; do
|
|
702
|
+
if [[ -e "$path" ]]; then
|
|
703
|
+
rm -rf "$path"
|
|
704
|
+
log "OK" "Removed $path"
|
|
705
|
+
else
|
|
706
|
+
log "INFO" "$path not present; skipping"
|
|
707
|
+
fi
|
|
708
|
+
done
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
trash_user_owned_path() {
|
|
712
|
+
local path="$1"
|
|
713
|
+
|
|
714
|
+
if [[ ! -e "$path" ]]; then
|
|
715
|
+
log "INFO" "$path not present; skipping"
|
|
716
|
+
return 0
|
|
717
|
+
fi
|
|
718
|
+
|
|
719
|
+
if command_exists trash; then
|
|
720
|
+
trash "$path"
|
|
721
|
+
log "OK" "Moved $path to trash"
|
|
722
|
+
else
|
|
723
|
+
rm -rf "$path"
|
|
724
|
+
log "OK" "Removed $path"
|
|
725
|
+
fi
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
remove_user_env_files() {
|
|
729
|
+
local candidate
|
|
730
|
+
local -a paths_to_remove=()
|
|
731
|
+
|
|
732
|
+
if [[ -n "$ENV_FILE" ]]; then
|
|
733
|
+
paths_to_remove+=("$ENV_FILE")
|
|
734
|
+
fi
|
|
735
|
+
|
|
736
|
+
paths_to_remove+=("$REPO_ROOT/.env" "$REPO_ROOT/.env.local")
|
|
737
|
+
|
|
738
|
+
declare -A seen=()
|
|
739
|
+
for candidate in "${paths_to_remove[@]}"; do
|
|
740
|
+
if [[ -n "$candidate" && -z "${seen[$candidate]+x}" ]]; then
|
|
741
|
+
seen[$candidate]=1
|
|
742
|
+
trash_user_owned_path "$candidate"
|
|
743
|
+
fi
|
|
744
|
+
done
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
stop_docker_services() {
|
|
748
|
+
if ! command_exists docker; then
|
|
749
|
+
log "WARN" "Docker not found. Skipping docker service shutdown"
|
|
750
|
+
return 0
|
|
751
|
+
fi
|
|
752
|
+
|
|
753
|
+
if docker compose version >/dev/null 2>&1; then
|
|
754
|
+
compose_cmd -f docker-compose.yml -f docker-compose.prod.yml stop api postgres redis >/dev/null 2>&1 || true
|
|
755
|
+
compose_cmd stop postgres redis >/dev/null 2>&1 || true
|
|
756
|
+
else
|
|
757
|
+
log "WARN" "Docker Compose plugin not found. Attempting targeted container stop"
|
|
758
|
+
fi
|
|
759
|
+
|
|
760
|
+
local container
|
|
761
|
+
for container in supermemory-api supermemory-postgres supermemory-redis; do
|
|
762
|
+
if docker stop "$container" >/dev/null 2>&1; then
|
|
763
|
+
log "OK" "Stopped container $container"
|
|
764
|
+
else
|
|
765
|
+
log "INFO" "Container $container not present"
|
|
766
|
+
fi
|
|
767
|
+
done
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
purge_docker_resources() {
|
|
771
|
+
if ! command_exists docker; then
|
|
772
|
+
log "WARN" "Docker not found. Skipping docker purge"
|
|
773
|
+
return 0
|
|
774
|
+
fi
|
|
775
|
+
|
|
776
|
+
if docker compose version >/dev/null 2>&1; then
|
|
777
|
+
if compose_cmd -f docker-compose.yml -f docker-compose.prod.yml down --volumes --remove-orphans >/dev/null 2>&1; then
|
|
778
|
+
log "OK" "Removed docker compose services, network, and attached volumes"
|
|
779
|
+
else
|
|
780
|
+
log "WARN" "docker compose down failed; attempting targeted purge"
|
|
781
|
+
fi
|
|
782
|
+
else
|
|
783
|
+
log "WARN" "Docker Compose plugin not found. Attempting targeted purge"
|
|
784
|
+
fi
|
|
785
|
+
|
|
786
|
+
local container
|
|
787
|
+
for container in supermemory-api supermemory-postgres supermemory-redis; do
|
|
788
|
+
if docker rm -f "$container" >/dev/null 2>&1; then
|
|
789
|
+
log "OK" "Removed container $container"
|
|
790
|
+
else
|
|
791
|
+
log "INFO" "Container $container not present"
|
|
792
|
+
fi
|
|
793
|
+
done
|
|
794
|
+
|
|
795
|
+
local volume
|
|
796
|
+
for volume in supermemory_postgres_data supermemory_redis_data supermemory-clone_postgres_data supermemory-clone_redis_data; do
|
|
797
|
+
if docker volume inspect "$volume" >/dev/null 2>&1; then
|
|
798
|
+
if docker volume rm "$volume" >/dev/null 2>&1; then
|
|
799
|
+
log "OK" "Removed volume $volume"
|
|
800
|
+
else
|
|
801
|
+
log "WARN" "Could not remove volume $volume (it may still be in use)"
|
|
802
|
+
fi
|
|
803
|
+
else
|
|
804
|
+
log "INFO" "Volume $volume not present"
|
|
805
|
+
fi
|
|
806
|
+
done
|
|
807
|
+
|
|
808
|
+
local network
|
|
809
|
+
for network in supermemory-network supermemory-clone_default; do
|
|
810
|
+
if docker network inspect "$network" >/dev/null 2>&1; then
|
|
811
|
+
if docker network rm "$network" >/dev/null 2>&1; then
|
|
812
|
+
log "OK" "Removed network $network"
|
|
813
|
+
else
|
|
814
|
+
log "WARN" "Could not remove network $network (it may still be in use)"
|
|
815
|
+
fi
|
|
816
|
+
else
|
|
817
|
+
log "INFO" "Network $network not present"
|
|
818
|
+
fi
|
|
819
|
+
done
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
validate_prerequisites() {
|
|
823
|
+
require_command node
|
|
824
|
+
require_command npm
|
|
825
|
+
|
|
826
|
+
if ! node -e "process.exit(Number(process.versions.node.split('.')[0]) >= 20 ? 0 : 1)"; then
|
|
827
|
+
fail "Node.js >= 20 is required"
|
|
828
|
+
fi
|
|
829
|
+
|
|
830
|
+
if [[ "$SKIP_DOCKER" -eq 0 ]]; then
|
|
831
|
+
require_command docker
|
|
832
|
+
docker compose version >/dev/null 2>&1 || fail "Docker Compose plugin is required"
|
|
833
|
+
fi
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
ensure_env_file() {
|
|
837
|
+
local created_env=0
|
|
838
|
+
mkdir -p "$(dirname "$ENV_FILE")"
|
|
839
|
+
|
|
840
|
+
if [[ ! -f "$ENV_FILE" ]]; then
|
|
841
|
+
cp .env.example "$ENV_FILE"
|
|
842
|
+
created_env=1
|
|
843
|
+
log "OK" "Created $ENV_FILE from .env.example"
|
|
844
|
+
fi
|
|
845
|
+
|
|
846
|
+
ensure_env_defaults "$created_env"
|
|
847
|
+
scrub_placeholder_env_values
|
|
848
|
+
load_env_file_into_shell
|
|
849
|
+
|
|
850
|
+
if [[ "$ACTION" == "install" ]]; then
|
|
851
|
+
migrate_legacy_default_ports
|
|
852
|
+
fi
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
run_install_or_update_flow() {
|
|
856
|
+
local action="$1"
|
|
857
|
+
|
|
858
|
+
log "INFO" "Starting Supermemory ${action} setup (${INSTALL_MODE} mode)"
|
|
859
|
+
validate_prerequisites
|
|
860
|
+
|
|
861
|
+
if [[ "$action" == "update" ]]; then
|
|
862
|
+
log "INFO" "Update mode performs a clean reinstall of app components"
|
|
863
|
+
remove_local_runtime_artifacts_for_update
|
|
864
|
+
fi
|
|
865
|
+
|
|
866
|
+
log "INFO" "Installing npm dependencies"
|
|
867
|
+
npm install
|
|
868
|
+
|
|
869
|
+
ensure_env_file
|
|
870
|
+
configure_api_keys
|
|
871
|
+
load_env_file_into_shell
|
|
872
|
+
|
|
873
|
+
local api_port
|
|
874
|
+
local api_host_port
|
|
875
|
+
local api_started=0
|
|
876
|
+
api_port="$(read_env_value "API_PORT")"
|
|
877
|
+
if [[ -z "$api_port" ]]; then
|
|
878
|
+
api_port="13000"
|
|
879
|
+
fi
|
|
880
|
+
|
|
881
|
+
api_host_port="$(read_env_value "API_HOST_PORT")"
|
|
882
|
+
if [[ -z "$api_host_port" ]]; then
|
|
883
|
+
api_host_port="$api_port"
|
|
884
|
+
fi
|
|
885
|
+
|
|
886
|
+
if [[ "$SKIP_DOCKER" -eq 0 ]]; then
|
|
887
|
+
case "$INSTALL_MODE" in
|
|
888
|
+
agent)
|
|
889
|
+
log "INFO" "Starting Docker services: postgres"
|
|
890
|
+
start_postgres_service
|
|
891
|
+
wait_for_container_health "supermemory-postgres" 120
|
|
892
|
+
;;
|
|
893
|
+
api|full)
|
|
894
|
+
if [[ "$SKIP_API_START" -eq 0 ]]; then
|
|
895
|
+
log "INFO" "Starting Docker services: postgres, redis, api"
|
|
896
|
+
start_api_stack
|
|
897
|
+
wait_for_container_health "supermemory-postgres" 120
|
|
898
|
+
wait_for_container_health "supermemory-redis" 60
|
|
899
|
+
verify_api_health "$api_host_port"
|
|
900
|
+
api_started=1
|
|
901
|
+
else
|
|
902
|
+
log "INFO" "Starting Docker services: postgres, redis"
|
|
903
|
+
start_compose_services
|
|
904
|
+
wait_for_container_health "supermemory-postgres" 120
|
|
905
|
+
wait_for_container_health "supermemory-redis" 60
|
|
906
|
+
fi
|
|
907
|
+
;;
|
|
908
|
+
esac
|
|
909
|
+
|
|
910
|
+
if [[ "$action" == "update" ]]; then
|
|
911
|
+
run_migrations_best_effort
|
|
912
|
+
else
|
|
913
|
+
run_migrations_strict
|
|
914
|
+
fi
|
|
915
|
+
elif [[ "$action" == "update" ]]; then
|
|
916
|
+
run_migrations_best_effort
|
|
917
|
+
else
|
|
918
|
+
log "WARN" "Skipped Docker startup and migrations"
|
|
919
|
+
fi
|
|
920
|
+
|
|
921
|
+
if [[ "$SKIP_BUILD" -eq 0 ]]; then
|
|
922
|
+
log "INFO" "Building project"
|
|
923
|
+
npm run build
|
|
924
|
+
else
|
|
925
|
+
log "WARN" "Skipped project build"
|
|
926
|
+
fi
|
|
927
|
+
|
|
928
|
+
configure_claude_mcp
|
|
929
|
+
|
|
930
|
+
local connectivity_ok=0
|
|
931
|
+
if npm run doctor -- --env-file "$ENV_FILE" --mode "$INSTALL_MODE"; then
|
|
932
|
+
connectivity_ok=1
|
|
933
|
+
log "OK" "Connectivity checks passed"
|
|
934
|
+
else
|
|
935
|
+
log "WARN" "Connectivity checks found issues. Review output above"
|
|
936
|
+
fi
|
|
937
|
+
|
|
938
|
+
if [[ "$SKIP_DOCKER" -eq 1 ]]; then
|
|
939
|
+
case "$INSTALL_MODE" in
|
|
940
|
+
agent)
|
|
941
|
+
log "WARN" "--skip-docker prevents Postgres startup. Start Postgres manually before using MCP or the API"
|
|
942
|
+
;;
|
|
943
|
+
api|full)
|
|
944
|
+
if [[ "$SKIP_API_START" -eq 0 ]]; then
|
|
945
|
+
log "WARN" "--skip-docker prevents API auto-start. Start the stack manually when ready"
|
|
946
|
+
fi
|
|
947
|
+
;;
|
|
948
|
+
esac
|
|
949
|
+
fi
|
|
950
|
+
|
|
951
|
+
write_install_result "$api_started" "$api_host_port" "$connectivity_ok"
|
|
952
|
+
print_install_summary "$api_started" "$api_host_port" "$connectivity_ok"
|
|
953
|
+
|
|
954
|
+
if [[ "$connectivity_ok" -ne 1 ]]; then
|
|
955
|
+
exit 1
|
|
956
|
+
fi
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
run_uninstall_flow() {
|
|
960
|
+
if [[ "$PURGE" -eq 1 ]]; then
|
|
961
|
+
log "INFO" "Starting Supermemory uninstall with purge"
|
|
962
|
+
else
|
|
963
|
+
log "INFO" "Starting Supermemory uninstall"
|
|
964
|
+
fi
|
|
965
|
+
|
|
966
|
+
stop_docker_services
|
|
967
|
+
remove_local_install_artifacts
|
|
968
|
+
|
|
969
|
+
if [[ "$PURGE" -eq 1 ]]; then
|
|
970
|
+
remove_claude_mcp
|
|
971
|
+
purge_docker_resources
|
|
972
|
+
remove_user_env_files
|
|
973
|
+
log "OK" "Uninstall purge completed"
|
|
974
|
+
else
|
|
975
|
+
log "INFO" "Preserved env files, Docker volumes, and Claude MCP registrations. Re-run with uninstall --purge to remove them"
|
|
976
|
+
log "OK" "Uninstall cleanup completed"
|
|
977
|
+
fi
|
|
978
|
+
}
|
|
979
|
+
|
|
980
|
+
parse_args() {
|
|
981
|
+
local action_explicit=0
|
|
982
|
+
local install_mode_explicit=0
|
|
983
|
+
|
|
984
|
+
while [[ "$#" -gt 0 ]]; do
|
|
985
|
+
local arg="$1"
|
|
986
|
+
case "$arg" in
|
|
987
|
+
install|update|uninstall)
|
|
988
|
+
if [[ "$action_explicit" -eq 1 ]]; then
|
|
989
|
+
fail "Only one command is allowed (install, update, or uninstall)"
|
|
990
|
+
fi
|
|
991
|
+
ACTION="$arg"
|
|
992
|
+
action_explicit=1
|
|
993
|
+
;;
|
|
994
|
+
agent|api|full)
|
|
995
|
+
if [[ "$action_explicit" -eq 0 ]]; then
|
|
996
|
+
ACTION="install"
|
|
997
|
+
action_explicit=1
|
|
998
|
+
elif [[ "$ACTION" != "install" && "$ACTION" != "update" ]]; then
|
|
999
|
+
fail "Install modes can only be used with install or update"
|
|
1000
|
+
fi
|
|
1001
|
+
|
|
1002
|
+
if [[ "$install_mode_explicit" -eq 1 ]]; then
|
|
1003
|
+
fail "Only one install mode is allowed (agent, api, or full)"
|
|
1004
|
+
fi
|
|
1005
|
+
|
|
1006
|
+
INSTALL_MODE="$arg"
|
|
1007
|
+
install_mode_explicit=1
|
|
1008
|
+
;;
|
|
1009
|
+
--mode)
|
|
1010
|
+
shift
|
|
1011
|
+
if [[ "$#" -eq 0 ]]; then
|
|
1012
|
+
fail "Missing value for --mode (expected: agent, api, or full)"
|
|
1013
|
+
fi
|
|
1014
|
+
INSTALL_MODE="${1,,}"
|
|
1015
|
+
validate_install_mode "$INSTALL_MODE"
|
|
1016
|
+
install_mode_explicit=1
|
|
1017
|
+
;;
|
|
1018
|
+
--mode=*)
|
|
1019
|
+
INSTALL_MODE="${arg#*=}"
|
|
1020
|
+
INSTALL_MODE="${INSTALL_MODE,,}"
|
|
1021
|
+
validate_install_mode "$INSTALL_MODE"
|
|
1022
|
+
install_mode_explicit=1
|
|
1023
|
+
;;
|
|
1024
|
+
--env-file)
|
|
1025
|
+
shift
|
|
1026
|
+
if [[ "$#" -eq 0 ]]; then
|
|
1027
|
+
fail "Missing value for --env-file"
|
|
1028
|
+
fi
|
|
1029
|
+
ENV_FILE="$1"
|
|
1030
|
+
;;
|
|
1031
|
+
--env-file=*)
|
|
1032
|
+
ENV_FILE="${arg#*=}"
|
|
1033
|
+
;;
|
|
1034
|
+
--skip-docker)
|
|
1035
|
+
SKIP_DOCKER=1
|
|
1036
|
+
;;
|
|
1037
|
+
--skip-api-keys)
|
|
1038
|
+
SKIP_API_KEYS=1
|
|
1039
|
+
;;
|
|
1040
|
+
--register-mcp)
|
|
1041
|
+
REGISTER_MCP=1
|
|
1042
|
+
;;
|
|
1043
|
+
--skip-mcp|--skip-claude)
|
|
1044
|
+
SKIP_CLAUDE=1
|
|
1045
|
+
;;
|
|
1046
|
+
--skip-build)
|
|
1047
|
+
SKIP_BUILD=1
|
|
1048
|
+
;;
|
|
1049
|
+
--purge)
|
|
1050
|
+
PURGE=1
|
|
1051
|
+
;;
|
|
1052
|
+
--skip-api-start)
|
|
1053
|
+
SKIP_API_START=1
|
|
1054
|
+
;;
|
|
1055
|
+
--scope)
|
|
1056
|
+
shift
|
|
1057
|
+
if [[ "$#" -eq 0 ]]; then
|
|
1058
|
+
fail "Missing value for --scope (expected: user, project, or local)"
|
|
1059
|
+
fi
|
|
1060
|
+
MCP_SCOPE="${1,,}"
|
|
1061
|
+
validate_mcp_scope "$MCP_SCOPE"
|
|
1062
|
+
MCP_SCOPE_EXPLICIT=1
|
|
1063
|
+
;;
|
|
1064
|
+
--scope=*)
|
|
1065
|
+
MCP_SCOPE="${arg#*=}"
|
|
1066
|
+
MCP_SCOPE="${MCP_SCOPE,,}"
|
|
1067
|
+
validate_mcp_scope "$MCP_SCOPE"
|
|
1068
|
+
MCP_SCOPE_EXPLICIT=1
|
|
1069
|
+
;;
|
|
1070
|
+
--non-interactive)
|
|
1071
|
+
NON_INTERACTIVE=1
|
|
1072
|
+
;;
|
|
1073
|
+
-h|--help)
|
|
1074
|
+
usage
|
|
1075
|
+
exit 0
|
|
1076
|
+
;;
|
|
1077
|
+
*)
|
|
1078
|
+
fail "Unknown argument: $arg"
|
|
1079
|
+
;;
|
|
1080
|
+
esac
|
|
1081
|
+
shift
|
|
1082
|
+
done
|
|
1083
|
+
}
|
|
1084
|
+
|
|
1085
|
+
main() {
|
|
1086
|
+
parse_args "$@"
|
|
1087
|
+
ensure_env_file_path
|
|
1088
|
+
validate_install_mode "$INSTALL_MODE"
|
|
1089
|
+
|
|
1090
|
+
if [[ "$PURGE" -eq 1 && "$ACTION" != "uninstall" ]]; then
|
|
1091
|
+
fail "--purge is only supported with uninstall"
|
|
1092
|
+
fi
|
|
1093
|
+
|
|
1094
|
+
case "$ACTION" in
|
|
1095
|
+
install)
|
|
1096
|
+
run_install_or_update_flow "install"
|
|
1097
|
+
;;
|
|
1098
|
+
update)
|
|
1099
|
+
run_install_or_update_flow "update"
|
|
1100
|
+
;;
|
|
1101
|
+
uninstall)
|
|
1102
|
+
run_uninstall_flow
|
|
1103
|
+
;;
|
|
1104
|
+
*)
|
|
1105
|
+
fail "Unsupported command: $ACTION"
|
|
1106
|
+
;;
|
|
1107
|
+
esac
|
|
1108
|
+
}
|
|
1109
|
+
|
|
1110
|
+
main "$@"
|