@bjlee2024/claude-mem 13.4.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 (101) hide show
  1. package/.agents/plugins/marketplace.json +20 -0
  2. package/.codex-plugin/plugin.json +46 -0
  3. package/LICENSE +202 -0
  4. package/README.md +419 -0
  5. package/dist/npx-cli/index.js +10001 -0
  6. package/dist/opencode-plugin/index.js +67 -0
  7. package/openclaw/Dockerfile.e2e +46 -0
  8. package/openclaw/SKILL.md +462 -0
  9. package/openclaw/TESTING.md +279 -0
  10. package/openclaw/dist/index.js +15 -0
  11. package/openclaw/e2e-verify.sh +222 -0
  12. package/openclaw/install.sh +1653 -0
  13. package/openclaw/openclaw.plugin.json +98 -0
  14. package/openclaw/package.json +21 -0
  15. package/openclaw/src/index.test.ts +1124 -0
  16. package/openclaw/src/index.ts +1092 -0
  17. package/openclaw/test-e2e.sh +40 -0
  18. package/openclaw/test-install.sh +2086 -0
  19. package/openclaw/test-sse-consumer.js +98 -0
  20. package/openclaw/tsconfig.json +26 -0
  21. package/package.json +211 -0
  22. package/plugin/.claude-plugin/plugin.json +24 -0
  23. package/plugin/.codex-plugin/plugin.json +46 -0
  24. package/plugin/.mcp.json +12 -0
  25. package/plugin/hooks/bugfixes-2026-01-10.md +92 -0
  26. package/plugin/hooks/codex-hooks.json +74 -0
  27. package/plugin/hooks/hooks.json +87 -0
  28. package/plugin/modes/code--ar.json +24 -0
  29. package/plugin/modes/code--bn.json +24 -0
  30. package/plugin/modes/code--chill.json +8 -0
  31. package/plugin/modes/code--cs.json +24 -0
  32. package/plugin/modes/code--da.json +24 -0
  33. package/plugin/modes/code--de.json +24 -0
  34. package/plugin/modes/code--el.json +24 -0
  35. package/plugin/modes/code--es.json +24 -0
  36. package/plugin/modes/code--fi.json +24 -0
  37. package/plugin/modes/code--fr.json +24 -0
  38. package/plugin/modes/code--he.json +24 -0
  39. package/plugin/modes/code--hi.json +24 -0
  40. package/plugin/modes/code--hu.json +24 -0
  41. package/plugin/modes/code--id.json +24 -0
  42. package/plugin/modes/code--it.json +24 -0
  43. package/plugin/modes/code--ja.json +24 -0
  44. package/plugin/modes/code--ko.json +24 -0
  45. package/plugin/modes/code--nl.json +24 -0
  46. package/plugin/modes/code--no.json +24 -0
  47. package/plugin/modes/code--pl.json +24 -0
  48. package/plugin/modes/code--pt-br.json +24 -0
  49. package/plugin/modes/code--ro.json +24 -0
  50. package/plugin/modes/code--ru.json +24 -0
  51. package/plugin/modes/code--sv.json +24 -0
  52. package/plugin/modes/code--th.json +24 -0
  53. package/plugin/modes/code--tr.json +24 -0
  54. package/plugin/modes/code--uk.json +24 -0
  55. package/plugin/modes/code--ur.json +25 -0
  56. package/plugin/modes/code--vi.json +24 -0
  57. package/plugin/modes/code--zh.json +24 -0
  58. package/plugin/modes/code.json +139 -0
  59. package/plugin/modes/email-investigation.json +120 -0
  60. package/plugin/modes/law-study--chill.json +7 -0
  61. package/plugin/modes/law-study-CLAUDE.md +85 -0
  62. package/plugin/modes/law-study.json +120 -0
  63. package/plugin/modes/meme-tokens.json +125 -0
  64. package/plugin/package.json +46 -0
  65. package/plugin/scripts/bun-runner.js +216 -0
  66. package/plugin/scripts/context-generator.cjs +795 -0
  67. package/plugin/scripts/mcp-server.cjs +239 -0
  68. package/plugin/scripts/server-beta-service.cjs +9856 -0
  69. package/plugin/scripts/statusline-counts.js +40 -0
  70. package/plugin/scripts/version-check.js +69 -0
  71. package/plugin/scripts/worker-cli.js +19 -0
  72. package/plugin/scripts/worker-service.cjs +2368 -0
  73. package/plugin/scripts/worker-wrapper.cjs +2 -0
  74. package/plugin/skills/babysit/SKILL.md +87 -0
  75. package/plugin/skills/design-is/SKILL.md +312 -0
  76. package/plugin/skills/do/SKILL.md +45 -0
  77. package/plugin/skills/how-it-works/SKILL.md +22 -0
  78. package/plugin/skills/how-it-works/onboarding-explainer.md +17 -0
  79. package/plugin/skills/knowledge-agent/SKILL.md +80 -0
  80. package/plugin/skills/learn-codebase/SKILL.md +21 -0
  81. package/plugin/skills/make-plan/SKILL.md +67 -0
  82. package/plugin/skills/mem-search/SKILL.md +131 -0
  83. package/plugin/skills/oh-my-issues/SKILL.md +226 -0
  84. package/plugin/skills/pathfinder/SKILL.md +111 -0
  85. package/plugin/skills/smart-explore/SKILL.md +190 -0
  86. package/plugin/skills/timeline-report/SKILL.md +211 -0
  87. package/plugin/skills/version-bump/SKILL.md +68 -0
  88. package/plugin/skills/version-bump/scripts/generate_changelog.js +34 -0
  89. package/plugin/skills/weekly-digests/SKILL.md +262 -0
  90. package/plugin/skills/wowerpoint/SKILL.md +205 -0
  91. package/plugin/ui/assets/fonts/monaspace-radon-var.woff +0 -0
  92. package/plugin/ui/assets/fonts/monaspace-radon-var.woff2 +0 -0
  93. package/plugin/ui/claude-mem-logo-for-dark-mode.webp +0 -0
  94. package/plugin/ui/claude-mem-logo-stylized.png +0 -0
  95. package/plugin/ui/claude-mem-logomark.webp +0 -0
  96. package/plugin/ui/icon-thick-completed.svg +8 -0
  97. package/plugin/ui/icon-thick-investigated.svg +8 -0
  98. package/plugin/ui/icon-thick-learned.svg +12 -0
  99. package/plugin/ui/icon-thick-next-steps.svg +8 -0
  100. package/plugin/ui/viewer-bundle.js +65 -0
  101. package/plugin/ui/viewer.html +3145 -0
@@ -0,0 +1,1653 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ readonly MIN_BUN_VERSION="1.1.14"
5
+ readonly INSTALLER_VERSION="1.0.0"
6
+
7
+ NON_INTERACTIVE=""
8
+ CLI_PROVIDER=""
9
+ CLI_API_KEY=""
10
+ UPGRADE_MODE=""
11
+ CLI_BRANCH=""
12
+
13
+ while [[ $# -gt 0 ]]; do
14
+ case "$1" in
15
+ --non-interactive)
16
+ NON_INTERACTIVE="true"
17
+ shift
18
+ ;;
19
+ --upgrade)
20
+ UPGRADE_MODE="true"
21
+ shift
22
+ ;;
23
+ --branch=*)
24
+ CLI_BRANCH="${1#--branch=}"
25
+ shift
26
+ ;;
27
+ --branch)
28
+ CLI_BRANCH="${2:-}"
29
+ shift 2
30
+ ;;
31
+ --provider=*)
32
+ CLI_PROVIDER="${1#--provider=}"
33
+ shift
34
+ ;;
35
+ --provider)
36
+ CLI_PROVIDER="${2:-}"
37
+ shift 2
38
+ ;;
39
+ --api-key=*)
40
+ CLI_API_KEY="${1#--api-key=}"
41
+ shift
42
+ ;;
43
+ --api-key)
44
+ CLI_API_KEY="${2:-}"
45
+ shift 2
46
+ ;;
47
+ *)
48
+ shift
49
+ ;;
50
+ esac
51
+ done
52
+
53
+ TTY_FD=0
54
+
55
+ setup_tty() {
56
+ if [[ -t 0 ]]; then
57
+ TTY_FD=0
58
+ elif [[ "$NON_INTERACTIVE" == "true" ]]; then
59
+ TTY_FD=0
60
+ elif [[ -r /dev/tty ]]; then
61
+ exec 3</dev/tty
62
+ TTY_FD=3
63
+ else
64
+ echo "Error: No terminal available for interactive prompts." >&2
65
+ echo "Use --non-interactive or run directly: bash install.sh" >&2
66
+ exit 1
67
+ fi
68
+ }
69
+
70
+ if [[ -t 1 ]] && [[ "${TERM:-}" != "dumb" ]]; then
71
+ readonly COLOR_RED='\033[0;31m'
72
+ readonly COLOR_GREEN='\033[0;32m'
73
+ readonly COLOR_YELLOW='\033[0;33m'
74
+ readonly COLOR_BLUE='\033[0;34m'
75
+ readonly COLOR_MAGENTA='\033[0;35m'
76
+ readonly COLOR_CYAN='\033[0;36m'
77
+ readonly COLOR_BOLD='\033[1m'
78
+ readonly COLOR_RESET='\033[0m'
79
+ else
80
+ readonly COLOR_RED=''
81
+ readonly COLOR_GREEN=''
82
+ readonly COLOR_YELLOW=''
83
+ readonly COLOR_BLUE=''
84
+ readonly COLOR_MAGENTA=''
85
+ readonly COLOR_CYAN=''
86
+ readonly COLOR_BOLD=''
87
+ readonly COLOR_RESET=''
88
+ fi
89
+
90
+ info() { echo -e "${COLOR_BLUE}ℹ${COLOR_RESET} $*"; }
91
+ success() { echo -e "${COLOR_GREEN}✓${COLOR_RESET} $*"; }
92
+ warn() { echo -e "${COLOR_YELLOW}⚠${COLOR_RESET} $*"; }
93
+ error() { echo -e "${COLOR_RED}✗${COLOR_RESET} $*" >&2; }
94
+
95
+ prompt_user() {
96
+ if [[ "$NON_INTERACTIVE" == "true" ]]; then
97
+ error "Cannot prompt in non-interactive mode: $*"
98
+ return 1
99
+ fi
100
+ echo -en "${COLOR_CYAN}?${COLOR_RESET} $* "
101
+ }
102
+
103
+ read_tty() {
104
+ read "$@" <&"$TTY_FD"
105
+ }
106
+
107
+ CLEANUP_DIRS=()
108
+
109
+ register_cleanup_dir() {
110
+ CLEANUP_DIRS+=("$1")
111
+ }
112
+
113
+ cleanup_on_exit() {
114
+ local exit_code=$?
115
+ for dir in "${CLEANUP_DIRS[@]+"${CLEANUP_DIRS[@]}"}"; do
116
+ if [[ -d "$dir" ]]; then
117
+ rm -rf "$dir"
118
+ fi
119
+ done
120
+ if [[ $exit_code -ne 0 ]]; then
121
+ echo "" >&2
122
+ error "Installation failed (exit code: ${exit_code})"
123
+ error "Any temporary files have been cleaned up."
124
+ error "Fix the issue above and re-run the installer."
125
+ fi
126
+ }
127
+
128
+ trap cleanup_on_exit EXIT
129
+
130
+ check_git() {
131
+ if command -v git &>/dev/null; then
132
+ return 0
133
+ fi
134
+
135
+ error "git is not installed"
136
+ echo "" >&2
137
+ case "${PLATFORM:-}" in
138
+ macos)
139
+ error "Install git on macOS with:"
140
+ error " xcode-select --install"
141
+ error " # or: brew install git"
142
+ ;;
143
+ linux)
144
+ error "Install git on Linux with:"
145
+ error " sudo apt install git # Debian/Ubuntu"
146
+ error " sudo dnf install git # Fedora/RHEL"
147
+ error " sudo pacman -S git # Arch"
148
+ ;;
149
+ *)
150
+ error "Please install git and re-run this installer."
151
+ ;;
152
+ esac
153
+ exit 1
154
+ }
155
+
156
+ check_port_37777() {
157
+ local port_in_use=""
158
+
159
+ if command -v lsof &>/dev/null; then
160
+ if lsof -i :37777 -sTCP:LISTEN &>/dev/null; then
161
+ port_in_use="true"
162
+ fi
163
+ elif command -v ss &>/dev/null; then
164
+ if ss -tlnp 2>/dev/null | grep -q ':37777 '; then
165
+ port_in_use="true"
166
+ fi
167
+ elif command -v curl &>/dev/null; then
168
+ local response
169
+ response="$(curl -s -o /dev/null -w "%{http_code}" "http://127.0.0.1:37777/api/health" 2>/dev/null)" || true
170
+ if [[ "$response" == "200" ]]; then
171
+ port_in_use="true"
172
+ fi
173
+ fi
174
+
175
+ if [[ "$port_in_use" == "true" ]]; then
176
+ return 0
177
+ fi
178
+ return 1
179
+ }
180
+
181
+ is_claude_mem_installed() {
182
+ if find_claude_mem_install_dir 2>/dev/null; then
183
+ return 0
184
+ fi
185
+ return 1
186
+ }
187
+
188
+ ensure_jq_or_fallback() {
189
+ local json_file="$1"
190
+ shift
191
+ local jq_filter="$1"
192
+ shift
193
+
194
+ if command -v jq &>/dev/null; then
195
+ local tmp_file
196
+ tmp_file="$(mktemp)"
197
+ jq "$@" "$jq_filter" "$json_file" > "$tmp_file" && mv "$tmp_file" "$json_file"
198
+ return $?
199
+ fi
200
+
201
+ if command -v python3 &>/dev/null; then
202
+ :
203
+ fi
204
+
205
+ warn "jq not found — using node for JSON manipulation"
206
+ return 1
207
+ }
208
+
209
+ parse_health_json() {
210
+ local raw_json="$1"
211
+
212
+ WORKER_VERSION=""
213
+ WORKER_AI_PROVIDER=""
214
+ WORKER_AI_AUTH_METHOD=""
215
+ WORKER_INITIALIZED=""
216
+ WORKER_REPORTED_PID=""
217
+ WORKER_UPTIME=""
218
+
219
+ if [[ -z "$raw_json" ]]; then
220
+ return 0
221
+ fi
222
+
223
+ if command -v jq &>/dev/null; then
224
+ WORKER_VERSION="$(echo "$raw_json" | jq -r '.version // empty' 2>/dev/null)" || true
225
+ WORKER_AI_PROVIDER="$(echo "$raw_json" | jq -r '.ai.provider // empty' 2>/dev/null)" || true
226
+ WORKER_AI_AUTH_METHOD="$(echo "$raw_json" | jq -r '.ai.authMethod // empty' 2>/dev/null)" || true
227
+ WORKER_INITIALIZED="$(echo "$raw_json" | jq -r '.initialized // empty' 2>/dev/null)" || true
228
+ WORKER_REPORTED_PID="$(echo "$raw_json" | jq -r '.pid // empty' 2>/dev/null)" || true
229
+ WORKER_UPTIME="$(echo "$raw_json" | jq -r '.uptime // empty' 2>/dev/null)" || true
230
+ return 0
231
+ fi
232
+
233
+ if command -v python3 &>/dev/null; then
234
+ local parsed
235
+ parsed="$(INSTALLER_HEALTH_JSON="$raw_json" python3 -c "
236
+ import json, os, sys
237
+ try:
238
+ data = json.loads(os.environ['INSTALLER_HEALTH_JSON'])
239
+ ai = data.get('ai') or {}
240
+ fields = [
241
+ str(data.get('version', '')),
242
+ str(ai.get('provider', '')),
243
+ str(ai.get('authMethod', '')),
244
+ str(data.get('initialized', '')),
245
+ str(data.get('pid', '')),
246
+ str(data.get('uptime', '')),
247
+ ]
248
+ sys.stdout.write('\n'.join(fields))
249
+ except Exception:
250
+ sys.stdout.write('\n\n\n\n\n')
251
+ " 2>/dev/null)" || true
252
+
253
+ if [[ -n "$parsed" ]]; then
254
+ local -a health_fields
255
+ IFS=$'\n' read -r -d '' -a health_fields <<< "$parsed" || true
256
+ WORKER_VERSION="${health_fields[0]:-}"
257
+ WORKER_AI_PROVIDER="${health_fields[1]:-}"
258
+ WORKER_AI_AUTH_METHOD="${health_fields[2]:-}"
259
+ WORKER_INITIALIZED="${health_fields[3]:-}"
260
+ WORKER_REPORTED_PID="${health_fields[4]:-}"
261
+ WORKER_UPTIME="${health_fields[5]:-}"
262
+ [[ "$WORKER_VERSION" == "None" ]] && WORKER_VERSION=""
263
+ [[ "$WORKER_AI_PROVIDER" == "None" ]] && WORKER_AI_PROVIDER=""
264
+ [[ "$WORKER_AI_AUTH_METHOD" == "None" ]] && WORKER_AI_AUTH_METHOD=""
265
+ [[ "$WORKER_INITIALIZED" == "None" ]] && WORKER_INITIALIZED=""
266
+ [[ "$WORKER_REPORTED_PID" == "None" ]] && WORKER_REPORTED_PID=""
267
+ [[ "$WORKER_UPTIME" == "None" ]] && WORKER_UPTIME=""
268
+ fi
269
+ return 0
270
+ fi
271
+
272
+ local parsed
273
+ parsed="$(INSTALLER_HEALTH_JSON="$raw_json" node -e "
274
+ try {
275
+ const data = JSON.parse(process.env.INSTALLER_HEALTH_JSON);
276
+ const ai = data.ai || {};
277
+ const fields = [
278
+ data.version ?? '',
279
+ ai.provider ?? '',
280
+ ai.authMethod ?? '',
281
+ data.initialized != null ? String(data.initialized) : '',
282
+ data.pid != null ? String(data.pid) : '',
283
+ data.uptime != null ? String(data.uptime) : '',
284
+ ];
285
+ process.stdout.write(fields.join('\n'));
286
+ } catch (e) {
287
+ process.stdout.write('\n\n\n\n\n');
288
+ }
289
+ " 2>/dev/null)" || true
290
+
291
+ if [[ -n "$parsed" ]]; then
292
+ local -a health_fields
293
+ IFS=$'\n' read -r -d '' -a health_fields <<< "$parsed" || true
294
+ WORKER_VERSION="${health_fields[0]:-}"
295
+ WORKER_AI_PROVIDER="${health_fields[1]:-}"
296
+ WORKER_AI_AUTH_METHOD="${health_fields[2]:-}"
297
+ WORKER_INITIALIZED="${health_fields[3]:-}"
298
+ WORKER_REPORTED_PID="${health_fields[4]:-}"
299
+ WORKER_UPTIME="${health_fields[5]:-}"
300
+ fi
301
+ }
302
+
303
+ format_uptime_ms() {
304
+ local ms="$1"
305
+ local secs=$((ms / 1000))
306
+ if (( secs >= 3600 )); then
307
+ echo "$((secs / 3600))h $((secs % 3600 / 60))m"
308
+ elif (( secs >= 60 )); then
309
+ echo "$((secs / 60))m $((secs % 60))s"
310
+ else
311
+ echo "${secs}s"
312
+ fi
313
+ }
314
+
315
+ print_banner() {
316
+ echo -e "${COLOR_MAGENTA}${COLOR_BOLD}"
317
+ cat << 'BANNER'
318
+ ┌─────────────────────────────────────────┐
319
+ │ claude-mem × OpenClaw │
320
+ │ Persistent Memory Plugin Installer │
321
+ └─────────────────────────────────────────┘
322
+ BANNER
323
+ echo -e "${COLOR_RESET}"
324
+ info "Installer v${INSTALLER_VERSION}"
325
+ echo ""
326
+ }
327
+
328
+ PLATFORM=""
329
+ IS_WSL=""
330
+
331
+ detect_platform() {
332
+ local uname_out
333
+ uname_out="$(uname -s)"
334
+
335
+ case "${uname_out}" in
336
+ Darwin*)
337
+ PLATFORM="macos"
338
+ ;;
339
+ Linux*)
340
+ if grep -qi microsoft /proc/version 2>/dev/null; then
341
+ PLATFORM="linux"
342
+ IS_WSL="true"
343
+ else
344
+ PLATFORM="linux"
345
+ fi
346
+ ;;
347
+ MINGW*|MSYS*|CYGWIN*)
348
+ PLATFORM="windows"
349
+ ;;
350
+ *)
351
+ error "Unsupported platform: ${uname_out}"
352
+ exit 1
353
+ ;;
354
+ esac
355
+
356
+ info "Detected platform: ${PLATFORM}${IS_WSL:+ (WSL)}"
357
+ }
358
+
359
+ version_gte() {
360
+ local v1="$1" v2="$2"
361
+ local -a parts1 parts2
362
+ IFS='.' read -ra parts1 <<< "$v1"
363
+ IFS='.' read -ra parts2 <<< "$v2"
364
+
365
+ for i in 0 1 2; do
366
+ local p1="${parts1[$i]:-0}"
367
+ local p2="${parts2[$i]:-0}"
368
+ if (( p1 > p2 )); then return 0; fi
369
+ if (( p1 < p2 )); then return 1; fi
370
+ done
371
+ return 0
372
+ }
373
+
374
+ BUN_PATH=""
375
+
376
+ find_bun_path() {
377
+ if command -v bun &>/dev/null; then
378
+ BUN_PATH="$(command -v bun)"
379
+ return 0
380
+ fi
381
+
382
+ local -a bun_paths=(
383
+ "${HOME}/.bun/bin/bun"
384
+ "/usr/local/bin/bun"
385
+ "/opt/homebrew/bin/bun"
386
+ )
387
+
388
+ for candidate in "${bun_paths[@]}"; do
389
+ if [[ -x "$candidate" ]]; then
390
+ BUN_PATH="$candidate"
391
+ return 0
392
+ fi
393
+ done
394
+
395
+ BUN_PATH=""
396
+ return 1
397
+ }
398
+
399
+ check_bun() {
400
+ if ! find_bun_path; then
401
+ return 1
402
+ fi
403
+
404
+ local bun_version
405
+ bun_version="$("$BUN_PATH" --version 2>/dev/null)" || return 1
406
+
407
+ if version_gte "$bun_version" "$MIN_BUN_VERSION"; then
408
+ success "Bun ${bun_version} found at ${BUN_PATH}"
409
+ return 0
410
+ else
411
+ warn "Bun ${bun_version} is below minimum required version ${MIN_BUN_VERSION}"
412
+ return 1
413
+ fi
414
+ }
415
+
416
+ install_bun() {
417
+ info "Installing Bun runtime..."
418
+
419
+ if ! curl -fsSL https://bun.sh/install | bash; then
420
+ error "Failed to install Bun automatically"
421
+ error "Please install manually:"
422
+ error " curl -fsSL https://bun.sh/install | bash"
423
+ error " Or: brew install oven-sh/bun/bun (macOS)"
424
+ error "Then restart your terminal and re-run this installer."
425
+ exit 1
426
+ fi
427
+
428
+ if ! find_bun_path; then
429
+ error "Bun installation completed but binary not found in expected locations"
430
+ error "Please restart your terminal and re-run this installer."
431
+ exit 1
432
+ fi
433
+
434
+ local bun_version
435
+ bun_version="$("$BUN_PATH" --version 2>/dev/null)" || true
436
+ success "Bun ${bun_version} installed at ${BUN_PATH}"
437
+ }
438
+
439
+ UV_PATH=""
440
+
441
+ find_uv_path() {
442
+ if command -v uv &>/dev/null; then
443
+ UV_PATH="$(command -v uv)"
444
+ return 0
445
+ fi
446
+
447
+ local -a uv_paths=(
448
+ "${HOME}/.local/bin/uv"
449
+ "${HOME}/.cargo/bin/uv"
450
+ "/usr/local/bin/uv"
451
+ "/opt/homebrew/bin/uv"
452
+ )
453
+
454
+ for candidate in "${uv_paths[@]}"; do
455
+ if [[ -x "$candidate" ]]; then
456
+ UV_PATH="$candidate"
457
+ return 0
458
+ fi
459
+ done
460
+
461
+ UV_PATH=""
462
+ return 1
463
+ }
464
+
465
+ check_uv() {
466
+ if ! find_uv_path; then
467
+ return 1
468
+ fi
469
+
470
+ local uv_version
471
+ uv_version="$("$UV_PATH" --version 2>/dev/null)" || return 1
472
+ success "uv ${uv_version} found at ${UV_PATH}"
473
+ return 0
474
+ }
475
+
476
+ install_uv() {
477
+ info "Installing uv (Python package manager for Chroma support)..."
478
+
479
+ if ! curl -LsSf https://astral.sh/uv/install.sh | sh; then
480
+ error "Failed to install uv automatically"
481
+ error "Please install manually:"
482
+ error " curl -LsSf https://astral.sh/uv/install.sh | sh"
483
+ error " Or: brew install uv (macOS)"
484
+ error "Then restart your terminal and re-run this installer."
485
+ exit 1
486
+ fi
487
+
488
+ if ! find_uv_path; then
489
+ error "uv installation completed but binary not found in expected locations"
490
+ error "Please restart your terminal and re-run this installer."
491
+ exit 1
492
+ fi
493
+
494
+ local uv_version
495
+ uv_version="$("$UV_PATH" --version 2>/dev/null)" || true
496
+ success "uv ${uv_version} installed at ${UV_PATH}"
497
+ }
498
+
499
+ OPENCLAW_PATH=""
500
+
501
+ find_openclaw() {
502
+ for bin_name in openclaw openclaw.mjs; do
503
+ if command -v "$bin_name" &>/dev/null; then
504
+ OPENCLAW_PATH="$(command -v "$bin_name")"
505
+ return 0
506
+ fi
507
+ done
508
+
509
+ local -a openclaw_paths=(
510
+ "${HOME}/.openclaw/openclaw.mjs"
511
+ "/usr/local/bin/openclaw.mjs"
512
+ "/usr/local/bin/openclaw"
513
+ "/usr/local/lib/node_modules/openclaw/openclaw.mjs"
514
+ "${HOME}/.npm-global/lib/node_modules/openclaw/openclaw.mjs"
515
+ "${HOME}/.npm-global/bin/openclaw"
516
+ )
517
+
518
+ if [[ -n "${NODE_PATH:-}" ]]; then
519
+ openclaw_paths+=("${NODE_PATH}/openclaw/openclaw.mjs")
520
+ fi
521
+
522
+ for candidate in "${openclaw_paths[@]}"; do
523
+ if [[ -f "$candidate" ]]; then
524
+ OPENCLAW_PATH="$candidate"
525
+ return 0
526
+ fi
527
+ done
528
+
529
+ OPENCLAW_PATH=""
530
+ return 1
531
+ }
532
+
533
+ check_openclaw() {
534
+ if ! find_openclaw; then
535
+ error "OpenClaw gateway not found"
536
+ error ""
537
+ error "The claude-mem plugin requires an OpenClaw gateway to be installed."
538
+ error "Please install OpenClaw first:"
539
+ error ""
540
+ error " npm install -g openclaw"
541
+ error " # or visit: https://openclaw.dev/docs/installation"
542
+ error ""
543
+ error "Then re-run this installer."
544
+ exit 1
545
+ fi
546
+
547
+ success "OpenClaw gateway found at ${OPENCLAW_PATH}"
548
+ }
549
+
550
+ run_openclaw() {
551
+ if [[ "$OPENCLAW_PATH" == *.mjs ]]; then
552
+ node "$OPENCLAW_PATH" "$@"
553
+ else
554
+ "$OPENCLAW_PATH" "$@"
555
+ fi
556
+ }
557
+
558
+ CLAUDE_MEM_REPO="https://github.com/bjlee2024/claude-mem.git"
559
+ CLAUDE_MEM_BRANCH="${CLI_BRANCH:-main}"
560
+ PLUGIN_FRESHLY_INSTALLED=""
561
+
562
+ resolve_extension_dir() {
563
+ local oc_config="${HOME}/.openclaw/openclaw.json"
564
+ if [[ -f "$oc_config" ]] && command -v node &>/dev/null; then
565
+ local existing_path
566
+ existing_path="$(node -e "
567
+ try {
568
+ const c = require('$oc_config');
569
+ const p = c?.plugins?.installs?.['claude-mem']?.installPath;
570
+ if (p) console.log(p);
571
+ } catch {}
572
+ " 2>/dev/null)" || true
573
+ if [[ -n "$existing_path" ]]; then
574
+ echo "$existing_path"
575
+ return
576
+ fi
577
+ local load_path
578
+ load_path="$(node -e "
579
+ try {
580
+ const c = require('$oc_config');
581
+ const paths = c?.plugins?.load?.paths || [];
582
+ const p = paths.find(p => p.endsWith('/claude-mem'));
583
+ if (p) console.log(p);
584
+ } catch {}
585
+ " 2>/dev/null)" || true
586
+ if [[ -n "$load_path" ]]; then
587
+ echo "$load_path"
588
+ return
589
+ fi
590
+ fi
591
+ echo "${HOME}/.openclaw/extensions/claude-mem"
592
+ }
593
+
594
+ CLAUDE_MEM_EXTENSION_DIR=""
595
+
596
+ install_plugin() {
597
+ check_git
598
+
599
+ CLAUDE_MEM_EXTENSION_DIR="$(resolve_extension_dir)"
600
+
601
+ local existing_plugin_dir="$CLAUDE_MEM_EXTENSION_DIR"
602
+ if [[ -d "$existing_plugin_dir" ]]; then
603
+ info "Removing existing claude-mem plugin at ${existing_plugin_dir}..."
604
+ rm -rf "$existing_plugin_dir"
605
+ fi
606
+
607
+ local build_dir
608
+ build_dir="$(mktemp -d)"
609
+ register_cleanup_dir "$build_dir"
610
+
611
+ info "Cloning claude-mem repository (branch: ${CLAUDE_MEM_BRANCH})..."
612
+ if ! git clone --depth 1 --branch "$CLAUDE_MEM_BRANCH" "$CLAUDE_MEM_REPO" "$build_dir/claude-mem" 2>&1; then
613
+ error "Failed to clone claude-mem repository"
614
+ error "Check your internet connection and try again."
615
+ exit 1
616
+ fi
617
+
618
+ local plugin_src="${build_dir}/claude-mem/openclaw"
619
+
620
+ info "Building TypeScript plugin..."
621
+ if ! (cd "$plugin_src" && NODE_ENV=development npm install --ignore-scripts 2>&1 && npx tsc 2>&1); then
622
+ error "Failed to build the claude-mem OpenClaw plugin"
623
+ error "Make sure Node.js and npm are installed."
624
+ exit 1
625
+ fi
626
+
627
+ local installable_dir="${build_dir}/claude-mem-installable"
628
+ mkdir -p "${installable_dir}/dist"
629
+
630
+ cp "${plugin_src}/dist/index.js" "${installable_dir}/dist/"
631
+ cp "${plugin_src}/dist/index.d.ts" "${installable_dir}/dist/" 2>/dev/null || true
632
+ cp "${plugin_src}/openclaw.plugin.json" "${installable_dir}/"
633
+
634
+ INSTALLER_PACKAGE_DIR="$installable_dir" node -e "
635
+ const pkg = {
636
+ name: 'claude-mem',
637
+ version: '1.0.0',
638
+ type: 'module',
639
+ main: 'dist/index.js',
640
+ openclaw: { extensions: ['./dist/index.js'] }
641
+ };
642
+ require('fs').writeFileSync(process.env.INSTALLER_PACKAGE_DIR + '/package.json', JSON.stringify(pkg, null, 2));
643
+ "
644
+
645
+ local oc_config="${HOME}/.openclaw/openclaw.json"
646
+ local saved_plugin_config=""
647
+ if [[ -f "$oc_config" ]]; then
648
+ saved_plugin_config=$(INSTALLER_CONFIG_FILE="$oc_config" node -e "
649
+ const fs = require('fs');
650
+ const configPath = process.env.INSTALLER_CONFIG_FILE;
651
+ const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
652
+ const entry = config?.plugins?.entries?.['claude-mem'];
653
+ const allowHasClaudeMem = Array.isArray(config?.plugins?.allow) && config.plugins.allow.includes('claude-mem');
654
+ if (entry || config?.plugins?.slots?.memory === 'claude-mem' || allowHasClaudeMem) {
655
+ // Save the config block so we can restore it after install
656
+ process.stdout.write(JSON.stringify(entry?.config || {}));
657
+ // Remove the stale entry so OpenClaw CLI can run
658
+ if (entry) delete config.plugins.entries['claude-mem'];
659
+ // Also remove stale allowlist reference — this alone can block ALL CLI commands
660
+ if (Array.isArray(config?.plugins?.allow)) {
661
+ config.plugins.allow = config.plugins.allow.filter((x) => x !== 'claude-mem');
662
+ }
663
+ // Also remove the slot reference — if the slot points to a plugin
664
+ // that isn't in entries, OpenClaw's config validator rejects ALL commands
665
+ if (config?.plugins?.slots?.memory === 'claude-mem') {
666
+ delete config.plugins.slots.memory;
667
+ }
668
+ fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
669
+ }
670
+ " 2>/dev/null) || true
671
+ fi
672
+
673
+ info "Installing claude-mem plugin into OpenClaw..."
674
+ if ! run_openclaw plugins install "$installable_dir" 2>&1; then
675
+ error "Failed to install claude-mem plugin"
676
+ error "Try manually: ${OPENCLAW_PATH} plugins install <path>"
677
+ exit 1
678
+ fi
679
+
680
+ info "Enabling claude-mem plugin..."
681
+ if ! run_openclaw plugins enable claude-mem 2>&1; then
682
+ error "Failed to enable claude-mem plugin"
683
+ error "Try manually: ${OPENCLAW_PATH} plugins enable claude-mem"
684
+ exit 1
685
+ fi
686
+
687
+ if [[ -f "$oc_config" ]]; then
688
+ if ! INSTALLER_CONFIG_FILE="$oc_config" node -e "
689
+ const fs = require('fs');
690
+ const configPath = process.env.INSTALLER_CONFIG_FILE;
691
+ const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
692
+ if (!config.plugins) config.plugins = {};
693
+ if (!Array.isArray(config.plugins.allow)) config.plugins.allow = [];
694
+ if (!config.plugins.allow.includes('claude-mem')) {
695
+ config.plugins.allow.push('claude-mem');
696
+ fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
697
+ console.log('Added claude-mem to plugins.allow');
698
+ } else {
699
+ console.log('claude-mem already in plugins.allow');
700
+ }
701
+ " 2>&1; then
702
+ warn "Failed to write plugins.allow — claude-mem may need manual allowlisting"
703
+ fi
704
+ else
705
+ info "OpenClaw config not yet materialized; will ensure allowlist in post-install"
706
+ if run_openclaw status --json >/dev/null 2>&1 && [[ -f "$oc_config" ]]; then
707
+ if ! INSTALLER_CONFIG_FILE="$oc_config" node -e "
708
+ const fs = require('fs');
709
+ const configPath = process.env.INSTALLER_CONFIG_FILE;
710
+ const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
711
+ if (!config.plugins) config.plugins = {};
712
+ if (!Array.isArray(config.plugins.allow)) config.plugins.allow = [];
713
+ if (!config.plugins.allow.includes('claude-mem')) {
714
+ config.plugins.allow.push('claude-mem');
715
+ fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
716
+ console.log('Added claude-mem to plugins.allow (post-materialization)');
717
+ }
718
+ " 2>&1; then
719
+ warn "Failed to write plugins.allow after materialization — configure manually"
720
+ fi
721
+ fi
722
+ fi
723
+
724
+ if [[ -n "$saved_plugin_config" && "$saved_plugin_config" != "{}" ]]; then
725
+ info "Restoring previous plugin configuration..."
726
+ INSTALLER_CONFIG_FILE="$oc_config" INSTALLER_SAVED_CONFIG="$saved_plugin_config" node -e "
727
+ const fs = require('fs');
728
+ const configPath = process.env.INSTALLER_CONFIG_FILE;
729
+ const savedConfig = JSON.parse(process.env.INSTALLER_SAVED_CONFIG);
730
+ const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
731
+ if (config?.plugins?.entries?.['claude-mem']) {
732
+ config.plugins.entries['claude-mem'].config = savedConfig;
733
+ fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
734
+ }
735
+ " 2>/dev/null || warn "Could not restore previous plugin config — configure manually"
736
+ fi
737
+
738
+ success "claude-mem plugin installed and enabled"
739
+
740
+ local extension_dir="$CLAUDE_MEM_EXTENSION_DIR"
741
+ local repo_root="${build_dir}/claude-mem"
742
+
743
+ if [[ -d "$extension_dir" && -d "${repo_root}/plugin" ]]; then
744
+ info "Copying core plugin files to ${extension_dir}..."
745
+
746
+ cp -R "${repo_root}/plugin" "${extension_dir}/"
747
+
748
+ local root_version
749
+ root_version="$(node -e "console.log(require('${repo_root}/package.json').version)")"
750
+ node -e "
751
+ const fs = require('fs');
752
+ const pkgPath = '${extension_dir}/package.json';
753
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
754
+ pkg.version = '${root_version}';
755
+ fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + '\n');
756
+ "
757
+
758
+ success "Core plugin files updated at ${extension_dir}"
759
+ else
760
+ warn "Could not copy core plugin files — worker may need manual update"
761
+ fi
762
+
763
+ PLUGIN_FRESHLY_INSTALLED="true"
764
+ }
765
+
766
+ configure_memory_slot() {
767
+ local config_dir="${HOME}/.openclaw"
768
+ local config_file="${config_dir}/openclaw.json"
769
+
770
+ mkdir -p "$config_dir"
771
+
772
+ if [[ ! -f "$config_file" ]]; then
773
+ info "Creating OpenClaw configuration with claude-mem memory slot..."
774
+ INSTALLER_CONFIG_FILE="$config_file" node -e "
775
+ const config = {
776
+ plugins: {
777
+ slots: { memory: 'claude-mem' },
778
+ entries: {
779
+ 'claude-mem': {
780
+ enabled: true,
781
+ config: {
782
+ workerPort: 37777,
783
+ syncMemoryFile: true
784
+ }
785
+ }
786
+ }
787
+ }
788
+ };
789
+ require('fs').writeFileSync(process.env.INSTALLER_CONFIG_FILE, JSON.stringify(config, null, 2));
790
+ "
791
+ success "Created ${config_file} with memory slot set to claude-mem"
792
+ return 0
793
+ fi
794
+
795
+ info "Updating OpenClaw configuration to use claude-mem memory slot..."
796
+
797
+ INSTALLER_CONFIG_FILE="$config_file" node -e "
798
+ const fs = require('fs');
799
+ const configPath = process.env.INSTALLER_CONFIG_FILE;
800
+ const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
801
+
802
+ // Ensure plugins structure exists
803
+ if (!config.plugins) config.plugins = {};
804
+ if (!config.plugins.slots) config.plugins.slots = {};
805
+ if (!config.plugins.entries) config.plugins.entries = {};
806
+
807
+ // Set memory slot to claude-mem
808
+ config.plugins.slots.memory = 'claude-mem';
809
+
810
+ // Ensure claude-mem entry exists and is enabled
811
+ if (!config.plugins.entries['claude-mem']) {
812
+ config.plugins.entries['claude-mem'] = {
813
+ enabled: true,
814
+ config: {
815
+ workerPort: 37777,
816
+ syncMemoryFile: true
817
+ }
818
+ };
819
+ } else {
820
+ config.plugins.entries['claude-mem'].enabled = true;
821
+ // Remove unrecognized keys that cause OpenClaw config validation errors
822
+ const allowedKeys = new Set(['enabled', 'config']);
823
+ for (const key of Object.keys(config.plugins.entries['claude-mem'])) {
824
+ if (!allowedKeys.has(key)) {
825
+ delete config.plugins.entries['claude-mem'][key];
826
+ }
827
+ }
828
+ }
829
+
830
+ fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
831
+ "
832
+
833
+ success "Memory slot set to claude-mem in ${config_file}"
834
+ }
835
+
836
+ AI_PROVIDER=""
837
+ AI_PROVIDER_API_KEY=""
838
+
839
+ mask_api_key() {
840
+ local key="$1"
841
+ local len=${#key}
842
+ if (( len <= 4 )); then
843
+ echo "****"
844
+ else
845
+ local masked_len=$((len - 4))
846
+ local mask=""
847
+ for (( i=0; i<masked_len; i++ )); do
848
+ mask+="*"
849
+ done
850
+ echo "${mask}${key: -4}"
851
+ fi
852
+ }
853
+
854
+ setup_ai_provider() {
855
+ echo ""
856
+ info "AI Provider Configuration"
857
+ echo ""
858
+
859
+ if [[ -n "$CLI_PROVIDER" ]]; then
860
+ case "$CLI_PROVIDER" in
861
+ claude)
862
+ AI_PROVIDER="claude"
863
+ success "Selected via --provider: Claude Max Plan (CLI authentication)"
864
+ ;;
865
+ gemini)
866
+ AI_PROVIDER="gemini"
867
+ AI_PROVIDER_API_KEY="${CLI_API_KEY}"
868
+ if [[ -n "$AI_PROVIDER_API_KEY" ]]; then
869
+ success "Selected via --provider: Gemini (API key set via --api-key)"
870
+ else
871
+ warn "Selected via --provider: Gemini (no API key — add later in ~/.claude-mem/settings.json)"
872
+ fi
873
+ ;;
874
+ openrouter)
875
+ AI_PROVIDER="openrouter"
876
+ AI_PROVIDER_API_KEY="${CLI_API_KEY}"
877
+ if [[ -n "$AI_PROVIDER_API_KEY" ]]; then
878
+ success "Selected via --provider: OpenRouter (API key set via --api-key)"
879
+ else
880
+ warn "Selected via --provider: OpenRouter (no API key — add later in ~/.claude-mem/settings.json)"
881
+ fi
882
+ ;;
883
+ *)
884
+ error "Unknown provider: ${CLI_PROVIDER}"
885
+ error "Valid providers: claude, gemini, openrouter"
886
+ exit 1
887
+ ;;
888
+ esac
889
+ return 0
890
+ fi
891
+
892
+ if [[ "$NON_INTERACTIVE" == "true" ]]; then
893
+ info "Non-interactive mode: defaulting to Claude Max Plan (no API key needed)"
894
+ AI_PROVIDER="claude"
895
+ return 0
896
+ fi
897
+
898
+ echo -e " Choose your AI provider for claude-mem:"
899
+ echo ""
900
+ echo -e " ${COLOR_BOLD}1)${COLOR_RESET} Claude Max Plan ${COLOR_GREEN}(recommended)${COLOR_RESET}"
901
+ echo -e " Uses your existing subscription, no API key needed"
902
+ echo ""
903
+ echo -e " ${COLOR_BOLD}2)${COLOR_RESET} Gemini"
904
+ echo -e " Free tier available — requires API key from ai.google.dev"
905
+ echo ""
906
+ echo -e " ${COLOR_BOLD}3)${COLOR_RESET} OpenRouter"
907
+ echo -e " Pay-per-use — requires API key from openrouter.ai"
908
+ echo ""
909
+
910
+ local choice
911
+ while true; do
912
+ prompt_user "Enter choice [1/2/3] (default: 1):"
913
+ read_tty -r choice
914
+ choice="${choice:-1}"
915
+
916
+ case "$choice" in
917
+ 1)
918
+ AI_PROVIDER="claude"
919
+ success "Selected: Claude Max Plan (CLI authentication)"
920
+ break
921
+ ;;
922
+ 2)
923
+ AI_PROVIDER="gemini"
924
+ echo ""
925
+ prompt_user "Enter your Gemini API key (from https://ai.google.dev):"
926
+ read_tty -rs AI_PROVIDER_API_KEY
927
+ echo ""
928
+ if [[ -z "$AI_PROVIDER_API_KEY" ]]; then
929
+ warn "No API key provided — you can add it later in ~/.claude-mem/settings.json"
930
+ else
931
+ success "Gemini API key set ($(mask_api_key "$AI_PROVIDER_API_KEY"))"
932
+ fi
933
+ break
934
+ ;;
935
+ 3)
936
+ AI_PROVIDER="openrouter"
937
+ echo ""
938
+ prompt_user "Enter your OpenRouter API key (from https://openrouter.ai):"
939
+ read_tty -rs AI_PROVIDER_API_KEY
940
+ echo ""
941
+ if [[ -z "$AI_PROVIDER_API_KEY" ]]; then
942
+ warn "No API key provided — you can add it later in ~/.claude-mem/settings.json"
943
+ else
944
+ success "OpenRouter API key set ($(mask_api_key "$AI_PROVIDER_API_KEY"))"
945
+ fi
946
+ break
947
+ ;;
948
+ *)
949
+ warn "Invalid choice. Please enter 1, 2, or 3."
950
+ ;;
951
+ esac
952
+ done
953
+ }
954
+
955
+ write_settings() {
956
+ local settings_dir="${HOME}/.claude-mem"
957
+ local settings_file="${settings_dir}/settings.json"
958
+
959
+ mkdir -p "$settings_dir"
960
+
961
+ INSTALLER_AI_PROVIDER="$AI_PROVIDER" \
962
+ INSTALLER_AI_API_KEY="$AI_PROVIDER_API_KEY" \
963
+ INSTALLER_SETTINGS_FILE="$settings_file" \
964
+ node -e "
965
+ const fs = require('fs');
966
+ const path = require('path');
967
+ const homedir = require('os').homedir();
968
+ const provider = process.env.INSTALLER_AI_PROVIDER;
969
+ const apiKey = process.env.INSTALLER_AI_API_KEY || '';
970
+ const settingsPath = process.env.INSTALLER_SETTINGS_FILE;
971
+
972
+ // All defaults from SettingsDefaultsManager.ts
973
+ const defaults = {
974
+ CLAUDE_MEM_MODEL: 'claude-sonnet-4-6',
975
+ CLAUDE_MEM_CONTEXT_OBSERVATIONS: '50',
976
+ CLAUDE_MEM_WORKER_PORT: '37777',
977
+ CLAUDE_MEM_WORKER_HOST: '127.0.0.1',
978
+ CLAUDE_MEM_SKIP_TOOLS: 'ListMcpResourcesTool,SlashCommand,Skill,TodoWrite,AskUserQuestion',
979
+ CLAUDE_MEM_PROVIDER: 'claude',
980
+ CLAUDE_MEM_CLAUDE_AUTH_METHOD: 'cli',
981
+ CLAUDE_MEM_GEMINI_API_KEY: '',
982
+ CLAUDE_MEM_GEMINI_MODEL: 'gemini-2.5-flash-lite',
983
+ CLAUDE_MEM_GEMINI_RATE_LIMITING_ENABLED: 'true',
984
+ CLAUDE_MEM_OPENROUTER_API_KEY: '',
985
+ CLAUDE_MEM_OPENROUTER_MODEL: 'xiaomi/mimo-v2-flash:free',
986
+ CLAUDE_MEM_OPENROUTER_SITE_URL: '',
987
+ CLAUDE_MEM_OPENROUTER_APP_NAME: 'claude-mem',
988
+ CLAUDE_MEM_OPENROUTER_MAX_CONTEXT_MESSAGES: '20',
989
+ CLAUDE_MEM_OPENROUTER_MAX_TOKENS: '100000',
990
+ CLAUDE_MEM_DATA_DIR: path.join(homedir, '.claude-mem'),
991
+ CLAUDE_MEM_LOG_LEVEL: 'INFO',
992
+ CLAUDE_MEM_PYTHON_VERSION: '3.13',
993
+ CLAUDE_CODE_PATH: '',
994
+ CLAUDE_MEM_MODE: 'code',
995
+ CLAUDE_MEM_CONTEXT_SHOW_READ_TOKENS: 'true',
996
+ CLAUDE_MEM_CONTEXT_SHOW_WORK_TOKENS: 'true',
997
+ CLAUDE_MEM_CONTEXT_SHOW_SAVINGS_AMOUNT: 'true',
998
+ CLAUDE_MEM_CONTEXT_SHOW_SAVINGS_PERCENT: 'true',
999
+ CLAUDE_MEM_CONTEXT_OBSERVATION_TYPES: 'bugfix,feature,refactor,discovery,decision,change',
1000
+ CLAUDE_MEM_CONTEXT_OBSERVATION_CONCEPTS: 'how-it-works,why-it-exists,what-changed,problem-solution,gotcha,pattern,trade-off',
1001
+ CLAUDE_MEM_CONTEXT_FULL_COUNT: '5',
1002
+ CLAUDE_MEM_CONTEXT_FULL_FIELD: 'narrative',
1003
+ CLAUDE_MEM_CONTEXT_SESSION_COUNT: '10',
1004
+ CLAUDE_MEM_CONTEXT_SHOW_LAST_SUMMARY: 'true',
1005
+ CLAUDE_MEM_CONTEXT_SHOW_LAST_MESSAGE: 'false',
1006
+ CLAUDE_MEM_FOLDER_CLAUDEMD_ENABLED: 'false',
1007
+ CLAUDE_MEM_EXCLUDED_PROJECTS: '',
1008
+ CLAUDE_MEM_FOLDER_MD_EXCLUDE: '[]'
1009
+ };
1010
+
1011
+ // Build provider-specific overrides safely from environment variables
1012
+ const overrides = { CLAUDE_MEM_PROVIDER: provider };
1013
+ if (provider === 'claude') {
1014
+ overrides.CLAUDE_MEM_CLAUDE_AUTH_METHOD = 'cli';
1015
+ } else if (provider === 'gemini') {
1016
+ overrides.CLAUDE_MEM_GEMINI_API_KEY = apiKey;
1017
+ overrides.CLAUDE_MEM_GEMINI_MODEL = 'gemini-2.5-flash-lite';
1018
+ } else if (provider === 'openrouter') {
1019
+ overrides.CLAUDE_MEM_OPENROUTER_API_KEY = apiKey;
1020
+ overrides.CLAUDE_MEM_OPENROUTER_MODEL = 'xiaomi/mimo-v2-flash:free';
1021
+ }
1022
+
1023
+ const settings = Object.assign(defaults, overrides);
1024
+
1025
+ // If settings file already exists, merge (preserve user customizations)
1026
+ if (fs.existsSync(settingsPath)) {
1027
+ try {
1028
+ let existing = JSON.parse(fs.readFileSync(settingsPath, 'utf8'));
1029
+ // Handle old nested schema
1030
+ if (existing.env && typeof existing.env === 'object') {
1031
+ existing = existing.env;
1032
+ }
1033
+ // Existing settings take priority, except for provider settings we just set
1034
+ for (const key of Object.keys(existing)) {
1035
+ if (!(key in overrides) && key in defaults) {
1036
+ settings[key] = existing[key];
1037
+ }
1038
+ }
1039
+ } catch (e) {
1040
+ // Corrupted file — overwrite with fresh defaults
1041
+ }
1042
+ }
1043
+
1044
+ fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2));
1045
+ "
1046
+
1047
+ success "Settings written to ${settings_file}"
1048
+ }
1049
+
1050
+ CLAUDE_MEM_INSTALL_DIR=""
1051
+
1052
+ find_claude_mem_install_dir() {
1053
+ local resolved_dir
1054
+ resolved_dir="$(resolve_extension_dir)"
1055
+ local -a search_paths=(
1056
+ "$resolved_dir"
1057
+ "${HOME}/.openclaw/extensions/claude-mem"
1058
+ "${HOME}/.claude/plugins/marketplaces/bjlee2024"
1059
+ "${HOME}/.openclaw/plugins/claude-mem"
1060
+ )
1061
+
1062
+ for candidate in "${search_paths[@]}"; do
1063
+ if [[ -f "${candidate}/plugin/scripts/worker-service.cjs" ]]; then
1064
+ CLAUDE_MEM_INSTALL_DIR="$candidate"
1065
+ return 0
1066
+ fi
1067
+ done
1068
+
1069
+ local -a roots=(
1070
+ "${HOME}/.openclaw"
1071
+ "${HOME}/.claude/plugins"
1072
+ )
1073
+ for root in "${roots[@]}"; do
1074
+ if [[ -d "$root" ]]; then
1075
+ local found
1076
+ found="$(find "$root" -name "worker-service.cjs" -path "*/plugin/scripts/*" 2>/dev/null | head -n 1)" || true
1077
+ if [[ -n "$found" ]]; then
1078
+ CLAUDE_MEM_INSTALL_DIR="${found%/plugin/scripts/worker-service.cjs}"
1079
+ return 0
1080
+ fi
1081
+ fi
1082
+ done
1083
+
1084
+ CLAUDE_MEM_INSTALL_DIR=""
1085
+ return 1
1086
+ }
1087
+
1088
+ WORKER_PID=""
1089
+ WORKER_VERSION=""
1090
+ WORKER_AI_PROVIDER=""
1091
+ WORKER_AI_AUTH_METHOD=""
1092
+ WORKER_INITIALIZED=""
1093
+ WORKER_REPORTED_PID=""
1094
+ WORKER_UPTIME=""
1095
+
1096
+ start_worker() {
1097
+ info "Starting claude-mem worker service..."
1098
+
1099
+ if ! find_claude_mem_install_dir; then
1100
+ error "Cannot find claude-mem plugin installation directory"
1101
+ error "Expected worker-service.cjs in one of:"
1102
+ error " ~/.openclaw/extensions/claude-mem/plugin/scripts/"
1103
+ error " ~/.claude/plugins/marketplaces/bjlee2024/plugin/scripts/"
1104
+ error ""
1105
+ error "Try reinstalling the plugin and re-running this installer."
1106
+ return 1
1107
+ fi
1108
+
1109
+ local worker_script="${CLAUDE_MEM_INSTALL_DIR}/plugin/scripts/worker-service.cjs"
1110
+ local log_dir="${HOME}/.claude-mem/logs"
1111
+ local log_date
1112
+ log_date="$(date +%Y-%m-%d)"
1113
+ local log_file="${log_dir}/worker-${log_date}.log"
1114
+
1115
+ mkdir -p "$log_dir"
1116
+
1117
+ if [[ -z "$BUN_PATH" ]]; then
1118
+ if ! find_bun_path; then
1119
+ error "Bun not found — cannot start worker service"
1120
+ return 1
1121
+ fi
1122
+ fi
1123
+
1124
+ CLAUDE_MEM_WORKER_PORT=37777 nohup "$BUN_PATH" "$worker_script" \
1125
+ >> "$log_file" 2>&1 &
1126
+ WORKER_PID=$!
1127
+
1128
+ local pid_file="${HOME}/.claude-mem/worker.pid"
1129
+ mkdir -p "${HOME}/.claude-mem"
1130
+ INSTALLER_PID_FILE="$pid_file" INSTALLER_WORKER_PID="$WORKER_PID" node -e "
1131
+ const info = {
1132
+ pid: parseInt(process.env.INSTALLER_WORKER_PID, 10),
1133
+ port: 37777,
1134
+ startedAt: new Date().toISOString(),
1135
+ version: 'installer'
1136
+ };
1137
+ require('fs').writeFileSync(process.env.INSTALLER_PID_FILE, JSON.stringify(info, null, 2));
1138
+ "
1139
+
1140
+ success "Worker process started (PID: ${WORKER_PID})"
1141
+ info "Logs: ${log_file}"
1142
+ }
1143
+
1144
+ verify_health() {
1145
+ local max_attempts=30
1146
+ local attempt=1
1147
+ local health_url="http://127.0.0.1:37777/api/health"
1148
+ local readiness_url="http://127.0.0.1:37777/api/readiness"
1149
+ local health_alive=false
1150
+
1151
+ info "Verifying worker health..."
1152
+
1153
+ while (( attempt <= max_attempts )); do
1154
+ local http_status
1155
+ http_status="$(curl -s -o /dev/null -w "%{http_code}" "$health_url" 2>/dev/null)" || true
1156
+
1157
+ if [[ "$http_status" == "200" ]]; then
1158
+ health_alive=true
1159
+
1160
+ local body
1161
+ body="$(curl -s "$health_url" 2>/dev/null)" || true
1162
+ parse_health_json "$body"
1163
+
1164
+ success "Worker is alive, waiting for initialization..."
1165
+
1166
+ break
1167
+ fi
1168
+
1169
+ info "Waiting for worker to start... (attempt ${attempt}/${max_attempts})"
1170
+ sleep 1
1171
+ attempt=$((attempt + 1))
1172
+ done
1173
+
1174
+ if [[ "$health_alive" != "true" ]]; then
1175
+ warn "Worker health check timed out after ${max_attempts} attempts"
1176
+ warn "The worker may still be starting up. Check status with:"
1177
+ warn " curl http://127.0.0.1:37777/api/health"
1178
+ warn " Or check logs: ~/.claude-mem/logs/"
1179
+ return 1
1180
+ fi
1181
+
1182
+ attempt=$((attempt + 1))
1183
+ while (( attempt <= max_attempts )); do
1184
+ local readiness_status
1185
+ readiness_status="$(curl -s -o /dev/null -w "%{http_code}" "$readiness_url" 2>/dev/null)" || true
1186
+
1187
+ if [[ "$readiness_status" == "200" ]]; then
1188
+ success "Worker is ready!"
1189
+ return 0
1190
+ fi
1191
+
1192
+ info "Waiting for worker to initialize... (attempt ${attempt}/${max_attempts})"
1193
+ sleep 1
1194
+ attempt=$((attempt + 1))
1195
+ done
1196
+
1197
+ warn "Worker is running but initialization is still in progress"
1198
+ warn "This is normal on first run — the worker will finish initializing in the background."
1199
+ warn "Check readiness with: curl http://127.0.0.1:37777/api/readiness"
1200
+ return 0
1201
+ }
1202
+
1203
+ FEED_CHANNEL=""
1204
+ FEED_TARGET_ID=""
1205
+ FEED_CONFIGURED=false
1206
+
1207
+ setup_observation_feed() {
1208
+ echo ""
1209
+ echo -e " ${COLOR_BOLD}Real-Time Observation Feed${COLOR_RESET}"
1210
+ echo ""
1211
+ echo " claude-mem can stream AI-compressed observations to a messaging"
1212
+ echo " channel in real time. Every time an agent learns something,"
1213
+ echo " you'll see it in your chat."
1214
+ echo ""
1215
+
1216
+ if [[ "$NON_INTERACTIVE" == "true" ]]; then
1217
+ info "Non-interactive mode: skipping observation feed setup"
1218
+ info "Configure later in ~/.openclaw/openclaw.json under"
1219
+ info " plugins.entries.claude-mem.config.observationFeed"
1220
+ return 0
1221
+ fi
1222
+
1223
+ prompt_user "Would you like to set up real-time observation streaming to a messaging channel? (y/n)"
1224
+ local answer
1225
+ read_tty -r answer
1226
+ answer="${answer:-n}"
1227
+
1228
+ if [[ "$answer" != [yY] && "$answer" != [yY][eE][sS] ]]; then
1229
+ echo ""
1230
+ info "Skipped observation feed setup."
1231
+ info "You can configure it later by re-running this installer or"
1232
+ info "editing ~/.openclaw/openclaw.json under"
1233
+ info " plugins.entries.claude-mem.config.observationFeed"
1234
+ return 0
1235
+ fi
1236
+
1237
+ echo ""
1238
+ echo -e " ${COLOR_BOLD}Select your messaging channel:${COLOR_RESET}"
1239
+ echo ""
1240
+ echo -e " ${COLOR_BOLD}1)${COLOR_RESET} Telegram"
1241
+ echo -e " ${COLOR_BOLD}2)${COLOR_RESET} Discord"
1242
+ echo -e " ${COLOR_BOLD}3)${COLOR_RESET} Slack"
1243
+ echo -e " ${COLOR_BOLD}4)${COLOR_RESET} Signal"
1244
+ echo -e " ${COLOR_BOLD}5)${COLOR_RESET} WhatsApp"
1245
+ echo -e " ${COLOR_BOLD}6)${COLOR_RESET} LINE"
1246
+ echo ""
1247
+
1248
+ local channel_choice
1249
+ while true; do
1250
+ prompt_user "Enter choice [1-6]:"
1251
+ read_tty -r channel_choice
1252
+
1253
+ case "$channel_choice" in
1254
+ 1)
1255
+ FEED_CHANNEL="telegram"
1256
+ echo ""
1257
+ echo -e " ${COLOR_CYAN}How to find your Telegram chat ID:${COLOR_RESET}"
1258
+ echo " Message @userinfobot on Telegram (https://t.me/userinfobot)"
1259
+ echo " — it replies with your numeric chat ID."
1260
+ echo " For groups, the ID is negative (e.g., -1001234567890)."
1261
+ break
1262
+ ;;
1263
+ 2)
1264
+ FEED_CHANNEL="discord"
1265
+ echo ""
1266
+ echo -e " ${COLOR_CYAN}How to find your Discord channel ID:${COLOR_RESET}"
1267
+ echo " Enable Developer Mode (Settings → Advanced → Developer Mode),"
1268
+ echo " right-click the target channel → Copy Channel ID"
1269
+ break
1270
+ ;;
1271
+ 3)
1272
+ FEED_CHANNEL="slack"
1273
+ echo ""
1274
+ echo -e " ${COLOR_CYAN}How to find your Slack channel ID:${COLOR_RESET}"
1275
+ echo " Open the channel, click the channel name at top,"
1276
+ echo " scroll to bottom — ID looks like C01ABC2DEFG"
1277
+ break
1278
+ ;;
1279
+ 4)
1280
+ FEED_CHANNEL="signal"
1281
+ echo ""
1282
+ echo -e " ${COLOR_CYAN}How to find your Signal target ID:${COLOR_RESET}"
1283
+ echo " Use the phone number or group ID from your"
1284
+ echo " OpenClaw Signal plugin config"
1285
+ break
1286
+ ;;
1287
+ 5)
1288
+ FEED_CHANNEL="whatsapp"
1289
+ echo ""
1290
+ echo -e " ${COLOR_CYAN}How to find your WhatsApp target ID:${COLOR_RESET}"
1291
+ echo " Use the phone number or group JID from your"
1292
+ echo " OpenClaw WhatsApp plugin config"
1293
+ break
1294
+ ;;
1295
+ 6)
1296
+ FEED_CHANNEL="line"
1297
+ echo ""
1298
+ echo -e " ${COLOR_CYAN}How to find your LINE target ID:${COLOR_RESET}"
1299
+ echo " Use the user ID or group ID from the"
1300
+ echo " LINE Developer Console"
1301
+ break
1302
+ ;;
1303
+ *)
1304
+ warn "Invalid choice. Please enter a number between 1 and 6."
1305
+ ;;
1306
+ esac
1307
+ done
1308
+
1309
+ echo ""
1310
+ prompt_user "Enter your ${FEED_CHANNEL} target ID:"
1311
+ read_tty -r FEED_TARGET_ID
1312
+
1313
+ if [[ -z "$FEED_TARGET_ID" ]]; then
1314
+ warn "No target ID provided — skipping observation feed setup."
1315
+ warn "You can configure it later in ~/.openclaw/openclaw.json"
1316
+ FEED_CHANNEL=""
1317
+ return 0
1318
+ fi
1319
+
1320
+ success "Observation feed: ${FEED_CHANNEL} → ${FEED_TARGET_ID}"
1321
+ FEED_CONFIGURED=true
1322
+ }
1323
+
1324
+ write_observation_feed_config() {
1325
+ if [[ "$FEED_CONFIGURED" != "true" ]]; then
1326
+ return 0
1327
+ fi
1328
+
1329
+ local config_file="${HOME}/.openclaw/openclaw.json"
1330
+
1331
+ if [[ ! -f "$config_file" ]]; then
1332
+ warn "OpenClaw config file not found at ${config_file}"
1333
+ warn "Cannot write observation feed config."
1334
+ return 1
1335
+ fi
1336
+
1337
+ info "Writing observation feed configuration..."
1338
+
1339
+ if command -v jq &>/dev/null; then
1340
+ local tmp_file
1341
+ tmp_file="$(mktemp)"
1342
+ jq --arg channel "$FEED_CHANNEL" --arg target "$FEED_TARGET_ID" '
1343
+ .plugins //= {} |
1344
+ .plugins.entries //= {} |
1345
+ .plugins.entries["claude-mem"] //= {"enabled": true, "config": {}} |
1346
+ .plugins.entries["claude-mem"].config //= {} |
1347
+ .plugins.entries["claude-mem"].config.observationFeed = {
1348
+ "enabled": true,
1349
+ "channel": $channel,
1350
+ "to": $target
1351
+ }
1352
+ ' "$config_file" > "$tmp_file" && mv "$tmp_file" "$config_file"
1353
+ elif command -v python3 &>/dev/null; then
1354
+ INSTALLER_FEED_CHANNEL="$FEED_CHANNEL" \
1355
+ INSTALLER_FEED_TARGET_ID="$FEED_TARGET_ID" \
1356
+ INSTALLER_CONFIG_FILE="$config_file" \
1357
+ python3 -c "
1358
+ import json, os
1359
+ config_path = os.environ['INSTALLER_CONFIG_FILE']
1360
+ channel = os.environ['INSTALLER_FEED_CHANNEL']
1361
+ target_id = os.environ['INSTALLER_FEED_TARGET_ID']
1362
+
1363
+ with open(config_path) as f:
1364
+ config = json.load(f)
1365
+
1366
+ config.setdefault('plugins', {})
1367
+ config['plugins'].setdefault('entries', {})
1368
+ config['plugins']['entries'].setdefault('claude-mem', {'enabled': True, 'config': {}})
1369
+ config['plugins']['entries']['claude-mem'].setdefault('config', {})
1370
+ config['plugins']['entries']['claude-mem']['config']['observationFeed'] = {
1371
+ 'enabled': True,
1372
+ 'channel': channel,
1373
+ 'to': target_id
1374
+ }
1375
+
1376
+ with open(config_path, 'w') as f:
1377
+ json.dump(config, f, indent=2)
1378
+ "
1379
+ else
1380
+ INSTALLER_FEED_CHANNEL="$FEED_CHANNEL" \
1381
+ INSTALLER_FEED_TARGET_ID="$FEED_TARGET_ID" \
1382
+ INSTALLER_CONFIG_FILE="$config_file" \
1383
+ node -e "
1384
+ const fs = require('fs');
1385
+ const configPath = process.env.INSTALLER_CONFIG_FILE;
1386
+ const channel = process.env.INSTALLER_FEED_CHANNEL;
1387
+ const targetId = process.env.INSTALLER_FEED_TARGET_ID;
1388
+
1389
+ const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
1390
+
1391
+ if (!config.plugins) config.plugins = {};
1392
+ if (!config.plugins.entries) config.plugins.entries = {};
1393
+ if (!config.plugins.entries['claude-mem']) {
1394
+ config.plugins.entries['claude-mem'] = { enabled: true, config: {} };
1395
+ }
1396
+ if (!config.plugins.entries['claude-mem'].config) {
1397
+ config.plugins.entries['claude-mem'].config = {};
1398
+ }
1399
+
1400
+ config.plugins.entries['claude-mem'].config.observationFeed = {
1401
+ enabled: true,
1402
+ channel: channel,
1403
+ to: targetId
1404
+ };
1405
+
1406
+ fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
1407
+ "
1408
+ fi
1409
+
1410
+ success "Observation feed config written to ${config_file}"
1411
+ echo ""
1412
+ echo -e " ${COLOR_BOLD}Observation feed summary:${COLOR_RESET}"
1413
+ echo -e " Channel: ${COLOR_CYAN}${FEED_CHANNEL}${COLOR_RESET}"
1414
+ echo -e " Target: ${COLOR_CYAN}${FEED_TARGET_ID}${COLOR_RESET}"
1415
+ echo -e " Enabled: ${COLOR_GREEN}yes${COLOR_RESET}"
1416
+ echo ""
1417
+ info "Restart your OpenClaw gateway to activate the observation feed."
1418
+ info "You should see these log lines:"
1419
+ echo " [claude-mem] Observation feed starting — channel: ${FEED_CHANNEL}, target: ${FEED_TARGET_ID}"
1420
+ echo ""
1421
+ info "After restarting, run /claude-mem-feed in any OpenClaw chat to verify"
1422
+ info "the feed is connected."
1423
+ }
1424
+
1425
+ print_completion_summary() {
1426
+ local provider_display=""
1427
+ case "$AI_PROVIDER" in
1428
+ claude) provider_display="Claude Max Plan (CLI authentication)" ;;
1429
+ gemini) provider_display="Gemini (gemini-2.5-flash-lite)" ;;
1430
+ openrouter) provider_display="OpenRouter (xiaomi/mimo-v2-flash:free)" ;;
1431
+ *) provider_display="$AI_PROVIDER" ;;
1432
+ esac
1433
+
1434
+ echo ""
1435
+ echo -e "${COLOR_MAGENTA}${COLOR_BOLD}"
1436
+ echo " ┌──────────────────────────────────────────┐"
1437
+ echo " │ Installation Complete! │"
1438
+ echo " └──────────────────────────────────────────┘"
1439
+ echo -e "${COLOR_RESET}"
1440
+
1441
+ echo -e " ${COLOR_GREEN}✓${COLOR_RESET} Dependencies installed (Bun, uv)"
1442
+ echo -e " ${COLOR_GREEN}✓${COLOR_RESET} OpenClaw gateway detected"
1443
+
1444
+ if [[ -n "$WORKER_VERSION" ]]; then
1445
+ echo -e " ${COLOR_GREEN}✓${COLOR_RESET} claude-mem v${COLOR_BOLD}${WORKER_VERSION}${COLOR_RESET} installed and running"
1446
+ else
1447
+ echo -e " ${COLOR_GREEN}✓${COLOR_RESET} claude-mem plugin installed and enabled"
1448
+ fi
1449
+
1450
+ echo -e " ${COLOR_GREEN}✓${COLOR_RESET} Memory slot configured"
1451
+
1452
+ if [[ -n "$WORKER_AI_AUTH_METHOD" ]]; then
1453
+ echo -e " ${COLOR_GREEN}✓${COLOR_RESET} AI provider: ${COLOR_BOLD}${WORKER_AI_PROVIDER} (${WORKER_AI_AUTH_METHOD})${COLOR_RESET}"
1454
+ else
1455
+ echo -e " ${COLOR_GREEN}✓${COLOR_RESET} AI provider: ${COLOR_BOLD}${provider_display}${COLOR_RESET}"
1456
+ fi
1457
+
1458
+ echo -e " ${COLOR_GREEN}✓${COLOR_RESET} Settings written to ~/.claude-mem/settings.json"
1459
+
1460
+ if [[ -n "$WORKER_PID" ]] && kill -0 "$WORKER_PID" 2>/dev/null; then
1461
+ echo -e " ${COLOR_GREEN}✓${COLOR_RESET} Worker running on port ${COLOR_BOLD}37777${COLOR_RESET} (PID: ${WORKER_PID})"
1462
+ elif [[ -n "$WORKER_UPTIME" && "$WORKER_UPTIME" =~ ^[0-9]+$ ]] && (( WORKER_UPTIME > 0 )); then
1463
+ local uptime_formatted
1464
+ uptime_formatted="$(format_uptime_ms "$WORKER_UPTIME")"
1465
+ echo -e " ${COLOR_GREEN}✓${COLOR_RESET} Worker running on port ${COLOR_BOLD}37777${COLOR_RESET} (PID: ${WORKER_REPORTED_PID}, uptime: ${uptime_formatted})"
1466
+ else
1467
+ echo -e " ${COLOR_YELLOW}⚠${COLOR_RESET} Worker may not be running — check logs at ~/.claude-mem/logs/"
1468
+ fi
1469
+
1470
+ if [[ "$WORKER_INITIALIZED" != "true" ]] && { [[ -n "$WORKER_REPORTED_PID" ]] || { [[ -n "$WORKER_PID" ]] && kill -0 "$WORKER_PID" 2>/dev/null; }; }; then
1471
+ echo -e " ${COLOR_YELLOW}⚠${COLOR_RESET} Worker is starting but still initializing (this is normal on first run)"
1472
+ fi
1473
+
1474
+ if [[ "$FEED_CONFIGURED" == "true" ]]; then
1475
+ echo -e " ${COLOR_GREEN}✓${COLOR_RESET} Observation feed: ${COLOR_BOLD}${FEED_CHANNEL}${COLOR_RESET} → ${FEED_TARGET_ID}"
1476
+ else
1477
+ echo -e " ${COLOR_YELLOW}─${COLOR_RESET} Observation feed: not configured (optional)"
1478
+ echo -e " Configure later in ~/.openclaw/openclaw.json under"
1479
+ echo -e " plugins.entries.claude-mem.config.observationFeed"
1480
+ fi
1481
+
1482
+ echo ""
1483
+ echo -e " ${COLOR_BOLD}What's next?${COLOR_RESET}"
1484
+ echo ""
1485
+ echo -e " ${COLOR_CYAN}1.${COLOR_RESET} Restart your OpenClaw gateway to load the plugin"
1486
+ echo -e " ${COLOR_CYAN}2.${COLOR_RESET} Verify with ${COLOR_BOLD}/claude-mem-status${COLOR_RESET} in any OpenClaw chat"
1487
+ echo -e " ${COLOR_CYAN}3.${COLOR_RESET} Check the viewer UI at ${COLOR_BOLD}http://localhost:37777${COLOR_RESET}"
1488
+ if [[ "$FEED_CONFIGURED" == "true" ]]; then
1489
+ echo -e " ${COLOR_CYAN}4.${COLOR_RESET} Run ${COLOR_BOLD}/claude-mem-feed${COLOR_RESET} to check feed status"
1490
+ fi
1491
+ echo ""
1492
+ echo -e " ${COLOR_BOLD}To re-run this installer:${COLOR_RESET}"
1493
+ echo " bash <(curl -fsSL https://install.cmem.ai/openclaw.sh)"
1494
+ echo ""
1495
+ }
1496
+
1497
+ main() {
1498
+ setup_tty
1499
+ print_banner
1500
+ detect_platform
1501
+
1502
+ echo ""
1503
+ info "${COLOR_BOLD}[1/8]${COLOR_RESET} Checking dependencies..."
1504
+ echo ""
1505
+
1506
+ if ! check_bun; then
1507
+ install_bun
1508
+ fi
1509
+
1510
+ if ! check_uv; then
1511
+ install_uv
1512
+ fi
1513
+
1514
+ echo ""
1515
+ success "All dependencies satisfied"
1516
+
1517
+ echo ""
1518
+ info "${COLOR_BOLD}[2/8]${COLOR_RESET} Locating OpenClaw gateway..."
1519
+ check_openclaw
1520
+
1521
+ echo ""
1522
+ info "${COLOR_BOLD}[3/8]${COLOR_RESET} Installing claude-mem plugin..."
1523
+
1524
+ if [[ "$UPGRADE_MODE" == "true" ]] && is_claude_mem_installed; then
1525
+ success "claude-mem already installed at ${CLAUDE_MEM_INSTALL_DIR}"
1526
+ info "Upgrade mode: skipping clone/build/register, updating settings only"
1527
+ else
1528
+ install_plugin
1529
+ fi
1530
+
1531
+ echo ""
1532
+ info "${COLOR_BOLD}[4/8]${COLOR_RESET} Configuring memory slot..."
1533
+ configure_memory_slot
1534
+
1535
+ echo ""
1536
+ info "${COLOR_BOLD}[5/8]${COLOR_RESET} AI provider setup..."
1537
+ setup_ai_provider
1538
+
1539
+ echo ""
1540
+ info "${COLOR_BOLD}[6/8]${COLOR_RESET} Writing settings..."
1541
+ write_settings
1542
+
1543
+ echo ""
1544
+ info "${COLOR_BOLD}[7/8]${COLOR_RESET} Starting worker service..."
1545
+
1546
+ if check_port_37777; then
1547
+ warn "Port 37777 is already in use (worker may already be running)"
1548
+ info "Checking if the existing service is healthy..."
1549
+ if verify_health; then
1550
+ local expected_version=""
1551
+ if [[ -n "$CLAUDE_MEM_INSTALL_DIR" ]] || find_claude_mem_install_dir; then
1552
+ expected_version="$(INSTALLER_PKG="${CLAUDE_MEM_INSTALL_DIR}/package.json" node -e "
1553
+ try { process.stdout.write(JSON.parse(require('fs').readFileSync(process.env.INSTALLER_PKG, 'utf8')).version || ''); }
1554
+ catch(e) {}
1555
+ " 2>/dev/null)" || true
1556
+ fi
1557
+
1558
+ local needs_restart=""
1559
+
1560
+ if [[ "$PLUGIN_FRESHLY_INSTALLED" == "true" ]]; then
1561
+ if [[ -n "$WORKER_VERSION" && -n "$expected_version" && "$WORKER_VERSION" != "$expected_version" ]]; then
1562
+ info "Upgrading worker from v${WORKER_VERSION} to v${expected_version}..."
1563
+ else
1564
+ info "Plugin files updated — restarting worker to load new code..."
1565
+ fi
1566
+ needs_restart="true"
1567
+ fi
1568
+
1569
+ if [[ "$needs_restart" != "true" && -n "$WORKER_VERSION" && -n "$expected_version" && "$WORKER_VERSION" != "$expected_version" ]]; then
1570
+ info "Upgrading worker from v${WORKER_VERSION} to v${expected_version}..."
1571
+ needs_restart="true"
1572
+ fi
1573
+
1574
+ if [[ "$needs_restart" != "true" && -n "$WORKER_AI_PROVIDER" && -n "$AI_PROVIDER" && "$WORKER_AI_PROVIDER" != "$AI_PROVIDER" ]]; then
1575
+ warn "Worker is using ${WORKER_AI_PROVIDER} but you configured ${AI_PROVIDER} — restarting to apply"
1576
+ needs_restart="true"
1577
+ fi
1578
+
1579
+ if [[ "$needs_restart" == "true" ]]; then
1580
+ info "Stopping existing worker..."
1581
+ curl -s -X POST "http://127.0.0.1:37777/api/admin/shutdown" >/dev/null 2>&1 || true
1582
+ sleep 2
1583
+
1584
+ if check_port_37777; then
1585
+ if [[ -n "$WORKER_REPORTED_PID" ]]; then
1586
+ kill "$WORKER_REPORTED_PID" 2>/dev/null || true
1587
+ sleep 1
1588
+ fi
1589
+ local pid_file="${HOME}/.claude-mem/worker.pid"
1590
+ if [[ -f "$pid_file" ]]; then
1591
+ local file_pid
1592
+ file_pid="$(INSTALLER_PID_FILE="$pid_file" node -e "
1593
+ try { process.stdout.write(String(JSON.parse(require('fs').readFileSync(process.env.INSTALLER_PID_FILE, 'utf8')).pid || '')); }
1594
+ catch(e) {}
1595
+ " 2>/dev/null)" || true
1596
+ if [[ -n "$file_pid" ]]; then
1597
+ kill "$file_pid" 2>/dev/null || true
1598
+ sleep 1
1599
+ fi
1600
+ fi
1601
+ fi
1602
+
1603
+ if start_worker; then
1604
+ verify_health || true
1605
+ else
1606
+ warn "Worker restart failed — you can start it manually later"
1607
+ fi
1608
+ else
1609
+ local uptime_display=""
1610
+ if [[ -n "$WORKER_UPTIME" && "$WORKER_UPTIME" =~ ^[0-9]+$ && "$WORKER_UPTIME" != "0" ]]; then
1611
+ uptime_display="$(format_uptime_ms "$WORKER_UPTIME")"
1612
+ fi
1613
+
1614
+ local status_parts=""
1615
+ if [[ -n "$WORKER_VERSION" ]]; then
1616
+ status_parts="v${WORKER_VERSION}"
1617
+ fi
1618
+ if [[ -n "$WORKER_AI_PROVIDER" ]]; then
1619
+ status_parts="${status_parts:+${status_parts}, }${WORKER_AI_PROVIDER}"
1620
+ fi
1621
+ if [[ -n "$uptime_display" ]]; then
1622
+ status_parts="${status_parts:+${status_parts}, }uptime: ${uptime_display}"
1623
+ fi
1624
+
1625
+ if [[ -n "$status_parts" ]]; then
1626
+ success "Existing worker is healthy (${status_parts}) — skipping startup"
1627
+ else
1628
+ success "Existing worker is healthy — skipping startup"
1629
+ fi
1630
+ fi
1631
+ else
1632
+ warn "Port 37777 is occupied but not responding to health checks"
1633
+ warn "Another process may be using this port. Stop it and re-run the installer,"
1634
+ warn "or change CLAUDE_MEM_WORKER_PORT in ~/.claude-mem/settings.json"
1635
+ fi
1636
+ else
1637
+ if start_worker; then
1638
+ verify_health || true
1639
+ else
1640
+ warn "Worker startup failed — you can start it manually later"
1641
+ warn " cd ~/.openclaw/extensions/claude-mem && bun plugin/scripts/worker-service.cjs"
1642
+ fi
1643
+ fi
1644
+
1645
+ echo ""
1646
+ info "${COLOR_BOLD}[8/8]${COLOR_RESET} Observation feed setup..."
1647
+ setup_observation_feed
1648
+ write_observation_feed_config
1649
+
1650
+ print_completion_summary
1651
+ }
1652
+
1653
+ main "$@"