@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,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 "$@"