@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,2086 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
5
+ INSTALL_SCRIPT="${SCRIPT_DIR}/install.sh"
6
+
7
+ TESTS_RUN=0
8
+ TESTS_PASSED=0
9
+ TESTS_FAILED=0
10
+
11
+ test_pass() {
12
+ TESTS_RUN=$((TESTS_RUN + 1))
13
+ TESTS_PASSED=$((TESTS_PASSED + 1))
14
+ echo -e "\033[0;32m✓\033[0m $1"
15
+ }
16
+
17
+ test_fail() {
18
+ TESTS_RUN=$((TESTS_RUN + 1))
19
+ TESTS_FAILED=$((TESTS_FAILED + 1))
20
+ echo -e "\033[0;31m✗\033[0m $1"
21
+ if [[ -n "${2:-}" ]]; then
22
+ echo " Detail: $2"
23
+ fi
24
+ }
25
+
26
+ assert_eq() {
27
+ local expected="$1" actual="$2" msg="$3"
28
+ if [[ "$expected" == "$actual" ]]; then
29
+ test_pass "$msg"
30
+ else
31
+ test_fail "$msg" "expected='${expected}' actual='${actual}'"
32
+ fi
33
+ }
34
+
35
+ assert_contains() {
36
+ local haystack="$1" needle="$2" msg="$3"
37
+ if [[ "$haystack" == *"$needle"* ]]; then
38
+ test_pass "$msg"
39
+ else
40
+ test_fail "$msg" "expected string to contain '${needle}'"
41
+ fi
42
+ }
43
+
44
+ assert_file_exists() {
45
+ local filepath="$1" msg="$2"
46
+ if [[ -f "$filepath" ]]; then
47
+ test_pass "$msg"
48
+ else
49
+ test_fail "$msg" "file not found: ${filepath}"
50
+ fi
51
+ }
52
+
53
+ source_install_functions() {
54
+ local tmp_source
55
+ tmp_source="$(mktemp)"
56
+ sed '$ d' "$INSTALL_SCRIPT" > "$tmp_source"
57
+ echo 'main() { :; }' >> "$tmp_source"
58
+ TERM=dumb source "$tmp_source"
59
+ rm -f "$tmp_source"
60
+ }
61
+
62
+ source_install_functions
63
+
64
+ echo ""
65
+ echo "=== detect_platform() ==="
66
+
67
+ test_detect_platform_returns_valid_string() {
68
+ PLATFORM=""
69
+ IS_WSL=""
70
+ detect_platform >/dev/null 2>&1
71
+
72
+ case "$PLATFORM" in
73
+ macos|linux|windows)
74
+ test_pass "detect_platform sets PLATFORM='${PLATFORM}'"
75
+ ;;
76
+ *)
77
+ test_fail "detect_platform returned unexpected PLATFORM='${PLATFORM}'" "expected macos, linux, or windows"
78
+ ;;
79
+ esac
80
+ }
81
+
82
+ test_detect_platform_returns_valid_string
83
+
84
+ test_detect_platform_is_idempotent() {
85
+ PLATFORM=""
86
+ IS_WSL=""
87
+ detect_platform >/dev/null 2>&1
88
+ local first_platform="$PLATFORM"
89
+
90
+ PLATFORM=""
91
+ IS_WSL=""
92
+ detect_platform >/dev/null 2>&1
93
+ local second_platform="$PLATFORM"
94
+
95
+ assert_eq "$first_platform" "$second_platform" "detect_platform returns consistent results"
96
+ }
97
+
98
+ test_detect_platform_is_idempotent
99
+
100
+ test_detect_platform_sets_iswsl_empty_on_non_wsl() {
101
+ PLATFORM=""
102
+ IS_WSL=""
103
+ detect_platform >/dev/null 2>&1
104
+
105
+ if [[ "$PLATFORM" == "linux" ]] && grep -qi microsoft /proc/version 2>/dev/null; then
106
+ assert_eq "true" "$IS_WSL" "IS_WSL is 'true' on WSL"
107
+ else
108
+ assert_eq "" "${IS_WSL:-}" "IS_WSL is empty on non-WSL platform"
109
+ fi
110
+ }
111
+
112
+ test_detect_platform_sets_iswsl_empty_on_non_wsl
113
+
114
+ echo ""
115
+ echo "=== check_bun() ==="
116
+
117
+ test_check_bun_detects_installed_bun() {
118
+ if command -v bun &>/dev/null; then
119
+ BUN_PATH=""
120
+ if check_bun >/dev/null 2>&1; then
121
+ test_pass "check_bun succeeds when bun is installed"
122
+ else
123
+ test_fail "check_bun should succeed when bun is installed"
124
+ fi
125
+
126
+ if [[ -n "$BUN_PATH" ]]; then
127
+ test_pass "check_bun sets BUN_PATH='${BUN_PATH}'"
128
+ else
129
+ test_fail "check_bun should set BUN_PATH when bun is found"
130
+ fi
131
+ else
132
+ test_pass "check_bun test (installed): skipped (bun not installed)"
133
+ test_pass "check_bun BUN_PATH test: skipped (bun not installed)"
134
+ fi
135
+ }
136
+
137
+ test_check_bun_detects_installed_bun
138
+
139
+ test_check_bun_fails_when_not_found() {
140
+ local fake_home
141
+ fake_home="$(mktemp -d)"
142
+ local exit_code=0
143
+ bash -c '
144
+ set -euo pipefail
145
+ TERM=dumb
146
+ export HOME="'"$fake_home"'"
147
+ tmp=$(mktemp)
148
+ sed "$ d" "'"${INSTALL_SCRIPT}"'" > "$tmp"
149
+ echo "main() { :; }" >> "$tmp"
150
+ source "$tmp"
151
+ rm -f "$tmp"
152
+ PATH="/nonexistent"
153
+ BUN_PATH=""
154
+ check_bun
155
+ ' >/dev/null 2>&1 || exit_code=$?
156
+ rm -rf "$fake_home"
157
+
158
+ if [[ "$exit_code" -ne 0 ]]; then
159
+ test_pass "check_bun returns failure when bun is not in PATH"
160
+ else
161
+ test_fail "check_bun should return failure when bun is not in PATH"
162
+ fi
163
+ }
164
+
165
+ test_check_bun_fails_when_not_found
166
+
167
+ test_find_bun_path_checks_home_bun_bin() {
168
+ local fake_home
169
+ fake_home="$(mktemp -d)"
170
+ local saved_home="$HOME"
171
+ HOME="$fake_home"
172
+ BUN_PATH=""
173
+
174
+ mkdir -p "${fake_home}/.bun/bin"
175
+ cat > "${fake_home}/.bun/bin/bun" <<'FAKEBUN'
176
+ echo "1.2.0"
177
+ FAKEBUN
178
+ chmod +x "${fake_home}/.bun/bin/bun"
179
+
180
+ local saved_path="$PATH"
181
+ PATH="/nonexistent"
182
+
183
+ if find_bun_path 2>/dev/null; then
184
+ assert_eq "${fake_home}/.bun/bin/bun" "$BUN_PATH" "find_bun_path finds bun in ~/.bun/bin/"
185
+ else
186
+ test_fail "find_bun_path should find bun in ~/.bun/bin/"
187
+ fi
188
+
189
+ HOME="$saved_home"
190
+ PATH="$saved_path"
191
+ rm -rf "$fake_home"
192
+ }
193
+
194
+ test_find_bun_path_checks_home_bun_bin
195
+
196
+ echo ""
197
+ echo "=== check_uv() ==="
198
+
199
+ test_check_uv_detects_installed_uv() {
200
+ if command -v uv &>/dev/null; then
201
+ UV_PATH=""
202
+ if check_uv >/dev/null 2>&1; then
203
+ test_pass "check_uv succeeds when uv is installed"
204
+ else
205
+ test_fail "check_uv should succeed when uv is installed"
206
+ fi
207
+
208
+ if [[ -n "$UV_PATH" ]]; then
209
+ test_pass "check_uv sets UV_PATH='${UV_PATH}'"
210
+ else
211
+ test_fail "check_uv should set UV_PATH when uv is found"
212
+ fi
213
+ else
214
+ test_pass "check_uv test (installed): skipped (uv not installed)"
215
+ test_pass "check_uv UV_PATH test: skipped (uv not installed)"
216
+ fi
217
+ }
218
+
219
+ test_check_uv_detects_installed_uv
220
+
221
+ test_check_uv_fails_when_not_found() {
222
+ if [[ -x "/usr/local/bin/uv" ]] || [[ -x "/opt/homebrew/bin/uv" ]]; then
223
+ test_pass "check_uv not-found test: skipped (uv installed at system path)"
224
+ return 0
225
+ fi
226
+
227
+ local fake_home
228
+ fake_home="$(mktemp -d)"
229
+ local exit_code=0
230
+ bash -c '
231
+ set -euo pipefail
232
+ TERM=dumb
233
+ export HOME="'"$fake_home"'"
234
+ tmp=$(mktemp)
235
+ sed "$ d" "'"${INSTALL_SCRIPT}"'" > "$tmp"
236
+ echo "main() { :; }" >> "$tmp"
237
+ source "$tmp"
238
+ rm -f "$tmp"
239
+ PATH="/nonexistent"
240
+ UV_PATH=""
241
+ check_uv
242
+ ' >/dev/null 2>&1 || exit_code=$?
243
+ rm -rf "$fake_home"
244
+
245
+ if [[ "$exit_code" -ne 0 ]]; then
246
+ test_pass "check_uv returns failure when uv is not in PATH"
247
+ else
248
+ test_fail "check_uv should return failure when uv is not in PATH"
249
+ fi
250
+ }
251
+
252
+ test_check_uv_fails_when_not_found
253
+
254
+ test_find_uv_path_checks_local_bin() {
255
+ local fake_home
256
+ fake_home="$(mktemp -d)"
257
+ local saved_home="$HOME"
258
+ HOME="$fake_home"
259
+ UV_PATH=""
260
+
261
+ mkdir -p "${fake_home}/.local/bin"
262
+ cat > "${fake_home}/.local/bin/uv" <<'FAKEUV'
263
+ echo "uv 0.4.0"
264
+ FAKEUV
265
+ chmod +x "${fake_home}/.local/bin/uv"
266
+
267
+ local saved_path="$PATH"
268
+ PATH="/nonexistent"
269
+
270
+ if find_uv_path 2>/dev/null; then
271
+ assert_eq "${fake_home}/.local/bin/uv" "$UV_PATH" "find_uv_path finds uv in ~/.local/bin/"
272
+ else
273
+ test_fail "find_uv_path should find uv in ~/.local/bin/"
274
+ fi
275
+
276
+ HOME="$saved_home"
277
+ PATH="$saved_path"
278
+ rm -rf "$fake_home"
279
+ }
280
+
281
+ test_find_uv_path_checks_local_bin
282
+
283
+ echo ""
284
+ echo "=== find_openclaw() ==="
285
+
286
+ ORIGINAL_PATH="$PATH"
287
+ ORIGINAL_HOME="$HOME"
288
+
289
+ test_find_openclaw_not_found() {
290
+ local fake_home
291
+ fake_home="$(mktemp -d)"
292
+ HOME="$fake_home"
293
+ PATH="/nonexistent"
294
+ OPENCLAW_PATH=""
295
+
296
+ if find_openclaw 2>/dev/null; then
297
+ test_fail "find_openclaw should return 1 when openclaw.mjs is not found"
298
+ else
299
+ test_pass "find_openclaw returns 1 when not found"
300
+ fi
301
+
302
+ assert_eq "" "$OPENCLAW_PATH" "OPENCLAW_PATH is empty when not found"
303
+
304
+ HOME="$ORIGINAL_HOME"
305
+ PATH="$ORIGINAL_PATH"
306
+ rm -rf "$fake_home"
307
+ }
308
+
309
+ test_find_openclaw_not_found
310
+
311
+ test_find_openclaw_in_home() {
312
+ local fake_home
313
+ fake_home="$(mktemp -d)"
314
+ mkdir -p "${fake_home}/.openclaw"
315
+ touch "${fake_home}/.openclaw/openclaw.mjs"
316
+
317
+ HOME="$fake_home"
318
+ PATH="/nonexistent"
319
+ OPENCLAW_PATH=""
320
+
321
+ if find_openclaw 2>/dev/null; then
322
+ test_pass "find_openclaw finds openclaw.mjs in ~/.openclaw/"
323
+ assert_eq "${fake_home}/.openclaw/openclaw.mjs" "$OPENCLAW_PATH" "OPENCLAW_PATH set correctly"
324
+ else
325
+ test_fail "find_openclaw should find openclaw.mjs in ~/.openclaw/"
326
+ fi
327
+
328
+ HOME="$ORIGINAL_HOME"
329
+ PATH="$ORIGINAL_PATH"
330
+ rm -rf "$fake_home"
331
+ }
332
+
333
+ test_find_openclaw_in_home
334
+
335
+ echo ""
336
+ echo "=== configure_memory_slot() ==="
337
+
338
+ test_configure_new_config() {
339
+ local fake_home
340
+ fake_home="$(mktemp -d)"
341
+ HOME="$fake_home"
342
+
343
+ configure_memory_slot >/dev/null 2>&1
344
+
345
+ local config_file="${fake_home}/.openclaw/openclaw.json"
346
+ assert_file_exists "$config_file" "Config file created at ~/.openclaw/openclaw.json"
347
+
348
+ local memory_slot
349
+ memory_slot="$(node -e "const c = JSON.parse(require('fs').readFileSync('${config_file}','utf8')); console.log(c.plugins.slots.memory);")"
350
+ assert_eq "claude-mem" "$memory_slot" "Memory slot set to claude-mem in new config"
351
+
352
+ local enabled
353
+ enabled="$(node -e "const c = JSON.parse(require('fs').readFileSync('${config_file}','utf8')); console.log(c.plugins.entries['claude-mem'].enabled);")"
354
+ assert_eq "true" "$enabled" "claude-mem entry is enabled in new config"
355
+
356
+ local worker_port
357
+ worker_port="$(node -e "const c = JSON.parse(require('fs').readFileSync('${config_file}','utf8')); console.log(c.plugins.entries['claude-mem'].config.workerPort);")"
358
+ assert_eq "37777" "$worker_port" "Worker port is 37777 in new config"
359
+
360
+ HOME="$ORIGINAL_HOME"
361
+ rm -rf "$fake_home"
362
+ }
363
+
364
+ test_configure_new_config
365
+
366
+ test_configure_existing_config() {
367
+ local fake_home
368
+ fake_home="$(mktemp -d)"
369
+ HOME="$fake_home"
370
+
371
+ mkdir -p "${fake_home}/.openclaw"
372
+ local config_file="${fake_home}/.openclaw/openclaw.json"
373
+ node -e "
374
+ const config = {
375
+ gateway: { mode: 'local' },
376
+ plugins: {
377
+ slots: { memory: 'memory-core' },
378
+ entries: {
379
+ 'some-other-plugin': { enabled: true }
380
+ }
381
+ }
382
+ };
383
+ require('fs').writeFileSync('${config_file}', JSON.stringify(config, null, 2));
384
+ "
385
+
386
+ configure_memory_slot >/dev/null 2>&1
387
+
388
+ local memory_slot
389
+ memory_slot="$(node -e "const c = JSON.parse(require('fs').readFileSync('${config_file}','utf8')); console.log(c.plugins.slots.memory);")"
390
+ assert_eq "claude-mem" "$memory_slot" "Memory slot updated from memory-core to claude-mem"
391
+
392
+ local gateway_mode
393
+ gateway_mode="$(node -e "const c = JSON.parse(require('fs').readFileSync('${config_file}','utf8')); console.log(c.gateway.mode);")"
394
+ assert_eq "local" "$gateway_mode" "Existing gateway.mode setting preserved"
395
+
396
+ local other_plugin
397
+ other_plugin="$(node -e "const c = JSON.parse(require('fs').readFileSync('${config_file}','utf8')); console.log(c.plugins.entries['some-other-plugin'].enabled);")"
398
+ assert_eq "true" "$other_plugin" "Existing plugin entries preserved"
399
+
400
+ local cm_enabled
401
+ cm_enabled="$(node -e "const c = JSON.parse(require('fs').readFileSync('${config_file}','utf8')); console.log(c.plugins.entries['claude-mem'].enabled);")"
402
+ assert_eq "true" "$cm_enabled" "claude-mem entry added and enabled"
403
+
404
+ HOME="$ORIGINAL_HOME"
405
+ rm -rf "$fake_home"
406
+ }
407
+
408
+ test_configure_existing_config
409
+
410
+ test_configure_preserves_existing_cm_config() {
411
+ local fake_home
412
+ fake_home="$(mktemp -d)"
413
+ HOME="$fake_home"
414
+
415
+ mkdir -p "${fake_home}/.openclaw"
416
+ local config_file="${fake_home}/.openclaw/openclaw.json"
417
+ node -e "
418
+ const config = {
419
+ plugins: {
420
+ slots: { memory: 'memory-core' },
421
+ entries: {
422
+ 'claude-mem': {
423
+ enabled: false,
424
+ config: {
425
+ workerPort: 38888,
426
+ observationFeed: { enabled: true, channel: 'telegram', to: '12345' }
427
+ }
428
+ }
429
+ }
430
+ }
431
+ };
432
+ require('fs').writeFileSync('${config_file}', JSON.stringify(config, null, 2));
433
+ "
434
+
435
+ configure_memory_slot >/dev/null 2>&1
436
+
437
+ local cm_enabled
438
+ cm_enabled="$(node -e "const c = JSON.parse(require('fs').readFileSync('${config_file}','utf8')); console.log(c.plugins.entries['claude-mem'].enabled);")"
439
+ assert_eq "true" "$cm_enabled" "claude-mem entry enabled when previously disabled"
440
+
441
+ local custom_port
442
+ custom_port="$(node -e "const c = JSON.parse(require('fs').readFileSync('${config_file}','utf8')); console.log(c.plugins.entries['claude-mem'].config.workerPort);")"
443
+ assert_eq "38888" "$custom_port" "Existing custom workerPort preserved"
444
+
445
+ local feed_channel
446
+ feed_channel="$(node -e "const c = JSON.parse(require('fs').readFileSync('${config_file}','utf8')); console.log(c.plugins.entries['claude-mem'].config.observationFeed.channel);")"
447
+ assert_eq "telegram" "$feed_channel" "Existing observationFeed config preserved"
448
+
449
+ HOME="$ORIGINAL_HOME"
450
+ rm -rf "$fake_home"
451
+ }
452
+
453
+ test_configure_preserves_existing_cm_config
454
+
455
+ echo ""
456
+ echo "=== version_gte() ==="
457
+
458
+ if version_gte "1.2.0" "1.1.14"; then
459
+ test_pass "version_gte: 1.2.0 >= 1.1.14"
460
+ else
461
+ test_fail "version_gte: 1.2.0 >= 1.1.14"
462
+ fi
463
+
464
+ if version_gte "1.1.14" "1.1.14"; then
465
+ test_pass "version_gte: 1.1.14 >= 1.1.14 (equal)"
466
+ else
467
+ test_fail "version_gte: 1.1.14 >= 1.1.14 (equal)"
468
+ fi
469
+
470
+ if ! version_gte "1.0.0" "1.1.14"; then
471
+ test_pass "version_gte: 1.0.0 < 1.1.14"
472
+ else
473
+ test_fail "version_gte: 1.0.0 < 1.1.14"
474
+ fi
475
+
476
+ echo ""
477
+ echo "=== Script structure ==="
478
+
479
+ for fn in find_openclaw check_openclaw install_plugin configure_memory_slot; do
480
+ if declare -f "$fn" &>/dev/null; then
481
+ test_pass "Function ${fn}() is defined"
482
+ else
483
+ test_fail "Function ${fn}() should be defined"
484
+ fi
485
+ done
486
+
487
+ assert_contains "$CLAUDE_MEM_REPO" "github.com/bjlee2024/claude-mem" "CLAUDE_MEM_REPO points to correct repository"
488
+
489
+ for fn in setup_ai_provider write_settings mask_api_key; do
490
+ if declare -f "$fn" &>/dev/null; then
491
+ test_pass "Function ${fn}() is defined"
492
+ else
493
+ test_fail "Function ${fn}() should be defined"
494
+ fi
495
+ done
496
+
497
+ echo ""
498
+ echo "=== mask_api_key() ==="
499
+
500
+ masked=$(mask_api_key "sk-1234567890abcdef")
501
+ assert_eq "***************cdef" "$masked" "mask_api_key masks all but last 4 chars"
502
+
503
+ masked_short=$(mask_api_key "abcd")
504
+ assert_eq "****" "$masked_short" "mask_api_key masks keys <= 4 chars entirely"
505
+
506
+ masked_five=$(mask_api_key "12345")
507
+ assert_eq "*2345" "$masked_five" "mask_api_key masks 5-char key correctly"
508
+
509
+ echo ""
510
+ echo "=== setup_ai_provider() ==="
511
+
512
+ test_setup_ai_provider_non_interactive() {
513
+ local ai_result
514
+ ai_result="$(bash -c '
515
+ set -euo pipefail
516
+ TERM=dumb
517
+ tmp=$(mktemp)
518
+ sed "$ d" "'"${INSTALL_SCRIPT}"'" > "$tmp"
519
+ echo "main() { :; }" >> "$tmp"
520
+ set -- "--non-interactive"
521
+ source "$tmp"
522
+ rm -f "$tmp"
523
+ setup_ai_provider >/dev/null 2>&1
524
+ echo "$AI_PROVIDER"
525
+ ' 2>/dev/null)" || true
526
+
527
+ assert_eq "claude" "$ai_result" "Non-interactive mode defaults to claude provider"
528
+ }
529
+
530
+ test_setup_ai_provider_non_interactive
531
+
532
+ echo ""
533
+ echo "=== write_settings() ==="
534
+
535
+ test_write_settings_new_file() {
536
+ local fake_home
537
+ fake_home="$(mktemp -d)"
538
+ HOME="$fake_home"
539
+ AI_PROVIDER="claude"
540
+ AI_PROVIDER_API_KEY=""
541
+
542
+ write_settings >/dev/null 2>&1
543
+
544
+ local settings_file="${fake_home}/.claude-mem/settings.json"
545
+ assert_file_exists "$settings_file" "settings.json created at ~/.claude-mem/settings.json"
546
+
547
+ local provider
548
+ provider="$(node -e "const s = JSON.parse(require('fs').readFileSync('${settings_file}','utf8')); console.log(s.CLAUDE_MEM_PROVIDER);")"
549
+ assert_eq "claude" "$provider" "CLAUDE_MEM_PROVIDER set to claude"
550
+
551
+ local auth_method
552
+ auth_method="$(node -e "const s = JSON.parse(require('fs').readFileSync('${settings_file}','utf8')); console.log(s.CLAUDE_MEM_CLAUDE_AUTH_METHOD);")"
553
+ assert_eq "cli" "$auth_method" "CLAUDE_MEM_CLAUDE_AUTH_METHOD set to cli for Claude provider"
554
+
555
+ local worker_port
556
+ worker_port="$(node -e "const s = JSON.parse(require('fs').readFileSync('${settings_file}','utf8')); console.log(s.CLAUDE_MEM_WORKER_PORT);")"
557
+ assert_eq "37777" "$worker_port" "CLAUDE_MEM_WORKER_PORT defaults to 37777"
558
+
559
+ local model
560
+ model="$(node -e "const s = JSON.parse(require('fs').readFileSync('${settings_file}','utf8')); console.log(s.CLAUDE_MEM_MODEL);")"
561
+ assert_eq "claude-sonnet-4-6" "$model" "CLAUDE_MEM_MODEL defaults to claude-sonnet-4-6"
562
+
563
+ HOME="$ORIGINAL_HOME"
564
+ rm -rf "$fake_home"
565
+ }
566
+
567
+ test_write_settings_new_file
568
+
569
+ test_write_settings_gemini() {
570
+ local fake_home
571
+ fake_home="$(mktemp -d)"
572
+ HOME="$fake_home"
573
+ AI_PROVIDER="gemini"
574
+ AI_PROVIDER_API_KEY="test-gemini-key-1234"
575
+
576
+ write_settings >/dev/null 2>&1
577
+
578
+ local settings_file="${fake_home}/.claude-mem/settings.json"
579
+
580
+ local provider
581
+ provider="$(node -e "const s = JSON.parse(require('fs').readFileSync('${settings_file}','utf8')); console.log(s.CLAUDE_MEM_PROVIDER);")"
582
+ assert_eq "gemini" "$provider" "Gemini: CLAUDE_MEM_PROVIDER set to gemini"
583
+
584
+ local api_key
585
+ api_key="$(node -e "const s = JSON.parse(require('fs').readFileSync('${settings_file}','utf8')); console.log(s.CLAUDE_MEM_GEMINI_API_KEY);")"
586
+ assert_eq "test-gemini-key-1234" "$api_key" "Gemini: API key stored in settings"
587
+
588
+ local gemini_model
589
+ gemini_model="$(node -e "const s = JSON.parse(require('fs').readFileSync('${settings_file}','utf8')); console.log(s.CLAUDE_MEM_GEMINI_MODEL);")"
590
+ assert_eq "gemini-2.5-flash-lite" "$gemini_model" "Gemini: model defaults to gemini-2.5-flash-lite"
591
+
592
+ HOME="$ORIGINAL_HOME"
593
+ rm -rf "$fake_home"
594
+ }
595
+
596
+ test_write_settings_gemini
597
+
598
+ test_write_settings_openrouter() {
599
+ local fake_home
600
+ fake_home="$(mktemp -d)"
601
+ HOME="$fake_home"
602
+ AI_PROVIDER="openrouter"
603
+ AI_PROVIDER_API_KEY="sk-or-test-key-5678"
604
+
605
+ write_settings >/dev/null 2>&1
606
+
607
+ local settings_file="${fake_home}/.claude-mem/settings.json"
608
+
609
+ local provider
610
+ provider="$(node -e "const s = JSON.parse(require('fs').readFileSync('${settings_file}','utf8')); console.log(s.CLAUDE_MEM_PROVIDER);")"
611
+ assert_eq "openrouter" "$provider" "OpenRouter: CLAUDE_MEM_PROVIDER set to openrouter"
612
+
613
+ local api_key
614
+ api_key="$(node -e "const s = JSON.parse(require('fs').readFileSync('${settings_file}','utf8')); console.log(s.CLAUDE_MEM_OPENROUTER_API_KEY);")"
615
+ assert_eq "sk-or-test-key-5678" "$api_key" "OpenRouter: API key stored in settings"
616
+
617
+ local or_model
618
+ or_model="$(node -e "const s = JSON.parse(require('fs').readFileSync('${settings_file}','utf8')); console.log(s.CLAUDE_MEM_OPENROUTER_MODEL);")"
619
+ assert_eq "xiaomi/mimo-v2-flash:free" "$or_model" "OpenRouter: model defaults to xiaomi/mimo-v2-flash:free"
620
+
621
+ HOME="$ORIGINAL_HOME"
622
+ rm -rf "$fake_home"
623
+ }
624
+
625
+ test_write_settings_openrouter
626
+
627
+ test_write_settings_preserves_existing() {
628
+ local fake_home
629
+ fake_home="$(mktemp -d)"
630
+ HOME="$fake_home"
631
+
632
+ mkdir -p "${fake_home}/.claude-mem"
633
+ local settings_file="${fake_home}/.claude-mem/settings.json"
634
+ node -e "
635
+ const settings = {
636
+ CLAUDE_MEM_PROVIDER: 'gemini',
637
+ CLAUDE_MEM_GEMINI_API_KEY: 'old-key',
638
+ CLAUDE_MEM_WORKER_PORT: '38888',
639
+ CLAUDE_MEM_LOG_LEVEL: 'DEBUG'
640
+ };
641
+ require('fs').writeFileSync('${settings_file}', JSON.stringify(settings, null, 2));
642
+ "
643
+
644
+ AI_PROVIDER="claude"
645
+ AI_PROVIDER_API_KEY=""
646
+ write_settings >/dev/null 2>&1
647
+
648
+ local provider
649
+ provider="$(node -e "const s = JSON.parse(require('fs').readFileSync('${settings_file}','utf8')); console.log(s.CLAUDE_MEM_PROVIDER);")"
650
+ assert_eq "claude" "$provider" "Preserve: provider updated to new selection"
651
+
652
+ local custom_port
653
+ custom_port="$(node -e "const s = JSON.parse(require('fs').readFileSync('${settings_file}','utf8')); console.log(s.CLAUDE_MEM_WORKER_PORT);")"
654
+ assert_eq "38888" "$custom_port" "Preserve: existing custom WORKER_PORT preserved"
655
+
656
+ local log_level
657
+ log_level="$(node -e "const s = JSON.parse(require('fs').readFileSync('${settings_file}','utf8')); console.log(s.CLAUDE_MEM_LOG_LEVEL);")"
658
+ assert_eq "DEBUG" "$log_level" "Preserve: existing custom LOG_LEVEL preserved"
659
+
660
+ HOME="$ORIGINAL_HOME"
661
+ rm -rf "$fake_home"
662
+ }
663
+
664
+ test_write_settings_preserves_existing
665
+
666
+ test_write_settings_complete_schema() {
667
+ local fake_home
668
+ fake_home="$(mktemp -d)"
669
+ HOME="$fake_home"
670
+ AI_PROVIDER="claude"
671
+ AI_PROVIDER_API_KEY=""
672
+
673
+ write_settings >/dev/null 2>&1
674
+
675
+ local settings_file="${fake_home}/.claude-mem/settings.json"
676
+
677
+ local key_count
678
+ key_count="$(node -e "const s = JSON.parse(require('fs').readFileSync('${settings_file}','utf8')); console.log(Object.keys(s).length);")"
679
+
680
+ if (( key_count >= 30 )); then
681
+ test_pass "Settings file has ${key_count} keys (complete schema)"
682
+ else
683
+ test_fail "Settings file has ${key_count} keys, expected >= 30" "Schema may be incomplete"
684
+ fi
685
+
686
+ local has_env_key
687
+ has_env_key="$(node -e "const s = JSON.parse(require('fs').readFileSync('${settings_file}','utf8')); console.log(s.env !== undefined);")"
688
+ assert_eq "false" "$has_env_key" "Settings uses flat schema (no nested 'env' key)"
689
+
690
+ HOME="$ORIGINAL_HOME"
691
+ rm -rf "$fake_home"
692
+ }
693
+
694
+ test_write_settings_complete_schema
695
+
696
+ echo ""
697
+ echo "=== find_claude_mem_install_dir() ==="
698
+
699
+ test_find_install_dir_not_found() {
700
+ local fake_home
701
+ fake_home="$(mktemp -d)"
702
+ HOME="$fake_home"
703
+ CLAUDE_MEM_INSTALL_DIR=""
704
+
705
+ if find_claude_mem_install_dir 2>/dev/null; then
706
+ test_fail "find_claude_mem_install_dir should return 1 when not found"
707
+ else
708
+ test_pass "find_claude_mem_install_dir returns 1 when not found"
709
+ fi
710
+
711
+ assert_eq "" "$CLAUDE_MEM_INSTALL_DIR" "CLAUDE_MEM_INSTALL_DIR is empty when not found"
712
+
713
+ HOME="$ORIGINAL_HOME"
714
+ rm -rf "$fake_home"
715
+ }
716
+
717
+ test_find_install_dir_not_found
718
+
719
+ test_find_install_dir_openclaw_extensions() {
720
+ local fake_home
721
+ fake_home="$(mktemp -d)"
722
+ HOME="$fake_home"
723
+ CLAUDE_MEM_INSTALL_DIR=""
724
+
725
+ mkdir -p "${fake_home}/.openclaw/extensions/claude-mem/plugin/scripts"
726
+ touch "${fake_home}/.openclaw/extensions/claude-mem/plugin/scripts/worker-service.cjs"
727
+
728
+ if find_claude_mem_install_dir 2>/dev/null; then
729
+ test_pass "find_claude_mem_install_dir finds dir in ~/.openclaw/extensions/claude-mem/"
730
+ assert_eq "${fake_home}/.openclaw/extensions/claude-mem" "$CLAUDE_MEM_INSTALL_DIR" "CLAUDE_MEM_INSTALL_DIR set correctly for openclaw extensions"
731
+ else
732
+ test_fail "find_claude_mem_install_dir should find dir in ~/.openclaw/extensions/claude-mem/"
733
+ fi
734
+
735
+ HOME="$ORIGINAL_HOME"
736
+ rm -rf "$fake_home"
737
+ }
738
+
739
+ test_find_install_dir_openclaw_extensions
740
+
741
+ test_find_install_dir_marketplace() {
742
+ local fake_home
743
+ fake_home="$(mktemp -d)"
744
+ HOME="$fake_home"
745
+ CLAUDE_MEM_INSTALL_DIR=""
746
+
747
+ mkdir -p "${fake_home}/.claude/plugins/marketplaces/bjlee2024/plugin/scripts"
748
+ touch "${fake_home}/.claude/plugins/marketplaces/bjlee2024/plugin/scripts/worker-service.cjs"
749
+
750
+ if find_claude_mem_install_dir 2>/dev/null; then
751
+ test_pass "find_claude_mem_install_dir finds dir in marketplace path"
752
+ assert_eq "${fake_home}/.claude/plugins/marketplaces/bjlee2024" "$CLAUDE_MEM_INSTALL_DIR" "CLAUDE_MEM_INSTALL_DIR set correctly for marketplace"
753
+ else
754
+ test_fail "find_claude_mem_install_dir should find dir in marketplace path"
755
+ fi
756
+
757
+ HOME="$ORIGINAL_HOME"
758
+ rm -rf "$fake_home"
759
+ }
760
+
761
+ test_find_install_dir_marketplace
762
+
763
+ echo ""
764
+ echo "=== start_worker() ==="
765
+
766
+ test_start_worker_no_install_dir() {
767
+ local fake_home
768
+ fake_home="$(mktemp -d)"
769
+ HOME="$fake_home"
770
+ CLAUDE_MEM_INSTALL_DIR=""
771
+
772
+ local output
773
+ if output="$(start_worker 2>&1)"; then
774
+ test_fail "start_worker should fail when install dir not found"
775
+ else
776
+ test_pass "start_worker returns error when install dir not found"
777
+ fi
778
+
779
+ assert_contains "$output" "Cannot find claude-mem plugin installation directory" "start_worker error message mentions install dir"
780
+
781
+ HOME="$ORIGINAL_HOME"
782
+ rm -rf "$fake_home"
783
+ }
784
+
785
+ test_start_worker_no_install_dir
786
+
787
+ echo ""
788
+ echo "=== verify_health() ==="
789
+
790
+ test_verify_health_no_server() {
791
+ local result
792
+ result="$(bash -c '
793
+ set -euo pipefail
794
+ TERM=dumb
795
+ tmp=$(mktemp)
796
+ sed "$ d" "'"${INSTALL_SCRIPT}"'" > "$tmp"
797
+ echo "main() { :; }" >> "$tmp"
798
+ source "$tmp"
799
+ rm -f "$tmp"
800
+ verify_health 2>/dev/null && echo "PASS" || echo "FAIL"
801
+ ' 2>/dev/null)" || true
802
+
803
+ if [[ "$result" == *"FAIL"* ]]; then
804
+ test_pass "verify_health returns failure when no server is running"
805
+ else
806
+ test_pass "verify_health returned success (worker may already be running on 37777)"
807
+ fi
808
+ }
809
+
810
+ if command -v curl &>/dev/null; then
811
+ test_verify_health_no_server
812
+ else
813
+ test_pass "verify_health test skipped (curl not available)"
814
+ fi
815
+
816
+ echo ""
817
+ echo "=== print_completion_summary() ==="
818
+
819
+ test_print_completion_summary() {
820
+ AI_PROVIDER="claude"
821
+ WORKER_PID=""
822
+ FEED_CONFIGURED=false
823
+ FEED_CHANNEL=""
824
+ FEED_TARGET_ID=""
825
+
826
+ local output
827
+ output="$(print_completion_summary 2>&1)"
828
+
829
+ assert_contains "$output" "Installation Complete" "Completion summary shows 'Installation Complete'"
830
+ assert_contains "$output" "Claude Max Plan" "Completion summary shows correct provider"
831
+ assert_contains "$output" "not configured" "Completion summary shows feed 'not configured' when skipped"
832
+ assert_contains "$output" "What's next" "Completion summary shows What's next section"
833
+ assert_contains "$output" "/claude-mem-status" "Completion summary mentions status command"
834
+ assert_contains "$output" "localhost:37777" "Completion summary mentions viewer URL"
835
+ assert_contains "$output" "re-run this installer" "Completion summary shows re-run instructions"
836
+ }
837
+
838
+ test_print_completion_summary
839
+
840
+ test_print_completion_summary_gemini() {
841
+ AI_PROVIDER="gemini"
842
+ WORKER_PID=""
843
+ FEED_CONFIGURED=false
844
+
845
+ local output
846
+ output="$(print_completion_summary 2>&1)"
847
+
848
+ assert_contains "$output" "Gemini" "Gemini provider shown in completion summary"
849
+ }
850
+
851
+ test_print_completion_summary_gemini
852
+
853
+ test_print_completion_summary_openrouter() {
854
+ AI_PROVIDER="openrouter"
855
+ WORKER_PID=""
856
+ FEED_CONFIGURED=false
857
+
858
+ local output
859
+ output="$(print_completion_summary 2>&1)"
860
+
861
+ assert_contains "$output" "OpenRouter" "OpenRouter provider shown in completion summary"
862
+ }
863
+
864
+ test_print_completion_summary_openrouter
865
+
866
+ echo ""
867
+ echo "=== New function existence ==="
868
+
869
+ for fn in find_claude_mem_install_dir start_worker verify_health print_completion_summary; do
870
+ if declare -f "$fn" &>/dev/null; then
871
+ test_pass "Function ${fn}() is defined"
872
+ else
873
+ test_fail "Function ${fn}() should be defined"
874
+ fi
875
+ done
876
+
877
+ echo ""
878
+ echo "=== main() function structure ==="
879
+
880
+ test_main_calls_start_worker() {
881
+ if grep -q 'start_worker' "$INSTALL_SCRIPT"; then
882
+ test_pass "main() calls start_worker"
883
+ else
884
+ test_fail "main() should call start_worker"
885
+ fi
886
+ }
887
+
888
+ test_main_calls_start_worker
889
+
890
+ test_main_calls_verify_health() {
891
+ if grep -q 'verify_health' "$INSTALL_SCRIPT"; then
892
+ test_pass "main() calls verify_health"
893
+ else
894
+ test_fail "main() should call verify_health"
895
+ fi
896
+ }
897
+
898
+ test_main_calls_verify_health
899
+
900
+ test_main_calls_completion_summary() {
901
+ if grep -q 'print_completion_summary' "$INSTALL_SCRIPT"; then
902
+ test_pass "main() calls print_completion_summary"
903
+ else
904
+ test_fail "main() should call print_completion_summary"
905
+ fi
906
+ }
907
+
908
+ test_main_calls_completion_summary
909
+
910
+ test_main_has_progress_indicators() {
911
+ if grep -q '\[1/8\]' "$INSTALL_SCRIPT" && grep -q '\[8/8\]' "$INSTALL_SCRIPT"; then
912
+ test_pass "main() has progress indicators [1/8] through [8/8]"
913
+ else
914
+ test_fail "main() should have progress indicators [1/8] through [8/8]"
915
+ fi
916
+ }
917
+
918
+ test_main_has_progress_indicators
919
+
920
+ test_main_calls_setup_observation_feed() {
921
+ if grep -q 'setup_observation_feed' "$INSTALL_SCRIPT"; then
922
+ test_pass "main() calls setup_observation_feed"
923
+ else
924
+ test_fail "main() should call setup_observation_feed"
925
+ fi
926
+ }
927
+
928
+ test_main_calls_setup_observation_feed
929
+
930
+ test_main_calls_write_observation_feed_config() {
931
+ if grep -q 'write_observation_feed_config' "$INSTALL_SCRIPT"; then
932
+ test_pass "main() calls write_observation_feed_config"
933
+ else
934
+ test_fail "main() should call write_observation_feed_config"
935
+ fi
936
+ }
937
+
938
+ test_main_calls_write_observation_feed_config
939
+
940
+ echo ""
941
+ echo "=== setup_observation_feed() ==="
942
+
943
+ for fn in setup_observation_feed write_observation_feed_config; do
944
+ if declare -f "$fn" &>/dev/null; then
945
+ test_pass "Function ${fn}() is defined"
946
+ else
947
+ test_fail "Function ${fn}() should be defined"
948
+ fi
949
+ done
950
+
951
+ test_setup_observation_feed_non_interactive() {
952
+ local feed_result
953
+ feed_result="$(bash -c '
954
+ set -euo pipefail
955
+ TERM=dumb
956
+ tmp=$(mktemp)
957
+ sed "$ d" "'"${INSTALL_SCRIPT}"'" > "$tmp"
958
+ echo "main() { :; }" >> "$tmp"
959
+ set -- "--non-interactive"
960
+ source "$tmp"
961
+ rm -f "$tmp"
962
+ setup_observation_feed 2>/dev/null
963
+ echo "CHANNEL=$FEED_CHANNEL"
964
+ echo "CONFIGURED=$FEED_CONFIGURED"
965
+ ' 2>/dev/null)" || true
966
+
967
+ assert_contains "$feed_result" "CHANNEL=" "Non-interactive mode: FEED_CHANNEL is empty"
968
+ assert_contains "$feed_result" "CONFIGURED=false" "Non-interactive mode: FEED_CONFIGURED is false"
969
+ }
970
+
971
+ test_setup_observation_feed_non_interactive
972
+
973
+ echo ""
974
+ echo "=== write_observation_feed_config() ==="
975
+
976
+ test_write_observation_feed_config_writes_json() {
977
+ local fake_home
978
+ fake_home="$(mktemp -d)"
979
+ HOME="$fake_home"
980
+
981
+ mkdir -p "${fake_home}/.openclaw"
982
+ local config_file="${fake_home}/.openclaw/openclaw.json"
983
+ node -e "
984
+ const config = {
985
+ plugins: {
986
+ slots: { memory: 'claude-mem' },
987
+ entries: {
988
+ 'claude-mem': {
989
+ enabled: true,
990
+ config: { workerPort: 37777, syncMemoryFile: true }
991
+ }
992
+ }
993
+ }
994
+ };
995
+ require('fs').writeFileSync('${config_file}', JSON.stringify(config, null, 2));
996
+ "
997
+
998
+ FEED_CHANNEL="telegram"
999
+ FEED_TARGET_ID="123456789"
1000
+ FEED_CONFIGURED="true"
1001
+
1002
+ write_observation_feed_config >/dev/null 2>&1
1003
+
1004
+ local feed_enabled
1005
+ feed_enabled="$(node -e "const c = JSON.parse(require('fs').readFileSync('${config_file}','utf8')); console.log(c.plugins.entries['claude-mem'].config.observationFeed.enabled);")"
1006
+ assert_eq "true" "$feed_enabled" "observationFeed.enabled is true"
1007
+
1008
+ local feed_channel
1009
+ feed_channel="$(node -e "const c = JSON.parse(require('fs').readFileSync('${config_file}','utf8')); console.log(c.plugins.entries['claude-mem'].config.observationFeed.channel);")"
1010
+ assert_eq "telegram" "$feed_channel" "observationFeed.channel is telegram"
1011
+
1012
+ local feed_to
1013
+ feed_to="$(node -e "const c = JSON.parse(require('fs').readFileSync('${config_file}','utf8')); console.log(c.plugins.entries['claude-mem'].config.observationFeed.to);")"
1014
+ assert_eq "123456789" "$feed_to" "observationFeed.to is 123456789"
1015
+
1016
+ local worker_port
1017
+ worker_port="$(node -e "const c = JSON.parse(require('fs').readFileSync('${config_file}','utf8')); console.log(c.plugins.entries['claude-mem'].config.workerPort);")"
1018
+ assert_eq "37777" "$worker_port" "Existing workerPort preserved after feed config write"
1019
+
1020
+ HOME="$ORIGINAL_HOME"
1021
+ FEED_CHANNEL=""
1022
+ FEED_TARGET_ID=""
1023
+ FEED_CONFIGURED=false
1024
+ rm -rf "$fake_home"
1025
+ }
1026
+
1027
+ test_write_observation_feed_config_writes_json
1028
+
1029
+ test_write_observation_feed_config_skips_when_not_configured() {
1030
+ local fake_home
1031
+ fake_home="$(mktemp -d)"
1032
+ HOME="$fake_home"
1033
+
1034
+ mkdir -p "${fake_home}/.openclaw"
1035
+ local config_file="${fake_home}/.openclaw/openclaw.json"
1036
+ node -e "
1037
+ require('fs').writeFileSync('${config_file}', JSON.stringify({ plugins: {} }, null, 2));
1038
+ "
1039
+
1040
+ FEED_CONFIGURED="false"
1041
+
1042
+ write_observation_feed_config >/dev/null 2>&1
1043
+
1044
+ local has_feed
1045
+ has_feed="$(node -e "const c = JSON.parse(require('fs').readFileSync('${config_file}','utf8')); console.log(c.plugins.entries !== undefined);")"
1046
+ assert_eq "false" "$has_feed" "Config unchanged when FEED_CONFIGURED is false"
1047
+
1048
+ HOME="$ORIGINAL_HOME"
1049
+ rm -rf "$fake_home"
1050
+ }
1051
+
1052
+ test_write_observation_feed_config_skips_when_not_configured
1053
+
1054
+ test_write_observation_feed_config_discord() {
1055
+ local fake_home
1056
+ fake_home="$(mktemp -d)"
1057
+ HOME="$fake_home"
1058
+
1059
+ mkdir -p "${fake_home}/.openclaw"
1060
+ local config_file="${fake_home}/.openclaw/openclaw.json"
1061
+ node -e "
1062
+ const config = {
1063
+ plugins: {
1064
+ entries: {
1065
+ 'claude-mem': { enabled: true, config: {} }
1066
+ }
1067
+ }
1068
+ };
1069
+ require('fs').writeFileSync('${config_file}', JSON.stringify(config, null, 2));
1070
+ "
1071
+
1072
+ FEED_CHANNEL="discord"
1073
+ FEED_TARGET_ID="1234567890123456789"
1074
+ FEED_CONFIGURED="true"
1075
+
1076
+ write_observation_feed_config >/dev/null 2>&1
1077
+
1078
+ local feed_channel
1079
+ feed_channel="$(node -e "const c = JSON.parse(require('fs').readFileSync('${config_file}','utf8')); console.log(c.plugins.entries['claude-mem'].config.observationFeed.channel);")"
1080
+ assert_eq "discord" "$feed_channel" "Discord channel type written correctly"
1081
+
1082
+ local feed_to
1083
+ feed_to="$(node -e "const c = JSON.parse(require('fs').readFileSync('${config_file}','utf8')); console.log(c.plugins.entries['claude-mem'].config.observationFeed.to);")"
1084
+ assert_eq "1234567890123456789" "$feed_to" "Discord channel ID written correctly"
1085
+
1086
+ HOME="$ORIGINAL_HOME"
1087
+ FEED_CHANNEL=""
1088
+ FEED_TARGET_ID=""
1089
+ FEED_CONFIGURED=false
1090
+ rm -rf "$fake_home"
1091
+ }
1092
+
1093
+ test_write_observation_feed_config_discord
1094
+
1095
+ echo ""
1096
+ echo "=== write_observation_feed_config() — fallback paths ==="
1097
+
1098
+ verify_feed_config_json() {
1099
+ local config_file="$1" expected_channel="$2" expected_target="$3" label="$4"
1100
+
1101
+ local feed_enabled
1102
+ feed_enabled="$(node -e "const c = JSON.parse(require('fs').readFileSync('${config_file}','utf8')); console.log(c.plugins.entries['claude-mem'].config.observationFeed.enabled);")"
1103
+ assert_eq "true" "$feed_enabled" "${label}: observationFeed.enabled is true"
1104
+
1105
+ local feed_channel
1106
+ feed_channel="$(node -e "const c = JSON.parse(require('fs').readFileSync('${config_file}','utf8')); console.log(c.plugins.entries['claude-mem'].config.observationFeed.channel);")"
1107
+ assert_eq "$expected_channel" "$feed_channel" "${label}: observationFeed.channel correct"
1108
+
1109
+ local feed_to
1110
+ feed_to="$(node -e "const c = JSON.parse(require('fs').readFileSync('${config_file}','utf8')); console.log(c.plugins.entries['claude-mem'].config.observationFeed.to);")"
1111
+ assert_eq "$expected_target" "$feed_to" "${label}: observationFeed.to correct"
1112
+
1113
+ local worker_port
1114
+ worker_port="$(node -e "const c = JSON.parse(require('fs').readFileSync('${config_file}','utf8')); console.log(c.plugins.entries['claude-mem'].config.workerPort);")"
1115
+ assert_eq "37777" "$worker_port" "${label}: existing workerPort preserved"
1116
+ }
1117
+
1118
+ create_seed_config() {
1119
+ local config_file="$1"
1120
+ mkdir -p "$(dirname "$config_file")"
1121
+ node -e "
1122
+ const config = {
1123
+ plugins: {
1124
+ slots: { memory: 'claude-mem' },
1125
+ entries: {
1126
+ 'claude-mem': {
1127
+ enabled: true,
1128
+ config: { workerPort: 37777, syncMemoryFile: true }
1129
+ }
1130
+ }
1131
+ }
1132
+ };
1133
+ require('fs').writeFileSync('${config_file}', JSON.stringify(config, null, 2));
1134
+ "
1135
+ }
1136
+
1137
+ test_write_feed_config_jq_path() {
1138
+ if ! command -v jq &>/dev/null; then
1139
+ test_pass "jq path: skipped (jq not installed)"
1140
+ return 0
1141
+ fi
1142
+
1143
+ local fake_home
1144
+ fake_home="$(mktemp -d)"
1145
+ HOME="$fake_home"
1146
+ local config_file="${fake_home}/.openclaw/openclaw.json"
1147
+ create_seed_config "$config_file"
1148
+
1149
+ FEED_CHANNEL="slack"
1150
+ FEED_TARGET_ID="C01ABC2DEFG"
1151
+ FEED_CONFIGURED="true"
1152
+
1153
+ write_observation_feed_config >/dev/null 2>&1
1154
+
1155
+ verify_feed_config_json "$config_file" "slack" "C01ABC2DEFG" "jq path"
1156
+
1157
+ HOME="$ORIGINAL_HOME"
1158
+ FEED_CHANNEL=""
1159
+ FEED_TARGET_ID=""
1160
+ FEED_CONFIGURED=false
1161
+ rm -rf "$fake_home"
1162
+ }
1163
+
1164
+ test_write_feed_config_jq_path
1165
+
1166
+ test_write_feed_config_python3_path() {
1167
+ if ! command -v python3 &>/dev/null; then
1168
+ test_pass "python3 path: skipped (python3 not installed)"
1169
+ return 0
1170
+ fi
1171
+
1172
+ local fake_home
1173
+ fake_home="$(mktemp -d)"
1174
+
1175
+ local result
1176
+ result="$(bash -c '
1177
+ set -euo pipefail
1178
+ TERM=dumb
1179
+ export HOME="'"$fake_home"'"
1180
+
1181
+ mkdir -p "'"${fake_home}"'/.openclaw"
1182
+ node -e "
1183
+ const config = {
1184
+ plugins: {
1185
+ slots: { memory: \"claude-mem\" },
1186
+ entries: {
1187
+ \"claude-mem\": {
1188
+ enabled: true,
1189
+ config: { workerPort: 37777, syncMemoryFile: true }
1190
+ }
1191
+ }
1192
+ }
1193
+ };
1194
+ require(\"fs\").writeFileSync(\"'"${fake_home}"'/.openclaw/openclaw.json\", JSON.stringify(config, null, 2));
1195
+ "
1196
+
1197
+ tmp=$(mktemp)
1198
+ sed "$ d" "'"${INSTALL_SCRIPT}"'" > "$tmp"
1199
+ echo "main() { :; }" >> "$tmp"
1200
+ source "$tmp"
1201
+ rm -f "$tmp"
1202
+
1203
+ SAFE_PATH=""
1204
+ IFS=":" read -ra path_parts <<< "$PATH"
1205
+ for p in "${path_parts[@]}"; do
1206
+ if [[ ! -x "${p}/jq" ]]; then
1207
+ SAFE_PATH="${SAFE_PATH:+${SAFE_PATH}:}${p}"
1208
+ fi
1209
+ done
1210
+ export PATH="$SAFE_PATH"
1211
+
1212
+ FEED_CHANNEL="signal"
1213
+ FEED_TARGET_ID="+15551234567"
1214
+ FEED_CONFIGURED="true"
1215
+ write_observation_feed_config >/dev/null 2>&1
1216
+ echo "DONE"
1217
+ ' 2>/dev/null)" || true
1218
+
1219
+ if [[ "$result" == *"DONE"* ]]; then
1220
+ local config_file="${fake_home}/.openclaw/openclaw.json"
1221
+ verify_feed_config_json "$config_file" "signal" "+15551234567" "python3 path"
1222
+ else
1223
+ test_fail "python3 path: write_observation_feed_config failed"
1224
+ fi
1225
+
1226
+ rm -rf "$fake_home"
1227
+ }
1228
+
1229
+ test_write_feed_config_python3_path
1230
+
1231
+ test_write_feed_config_node_path() {
1232
+ local fake_home
1233
+ fake_home="$(mktemp -d)"
1234
+
1235
+ local result
1236
+ result="$(bash -c '
1237
+ set -euo pipefail
1238
+ TERM=dumb
1239
+ export HOME="'"$fake_home"'"
1240
+
1241
+ mkdir -p "'"${fake_home}"'/.openclaw"
1242
+ node -e "
1243
+ const config = {
1244
+ plugins: {
1245
+ slots: { memory: \"claude-mem\" },
1246
+ entries: {
1247
+ \"claude-mem\": {
1248
+ enabled: true,
1249
+ config: { workerPort: 37777, syncMemoryFile: true }
1250
+ }
1251
+ }
1252
+ }
1253
+ };
1254
+ require(\"fs\").writeFileSync(\"'"${fake_home}"'/.openclaw/openclaw.json\", JSON.stringify(config, null, 2));
1255
+ "
1256
+
1257
+ tmp=$(mktemp)
1258
+ sed "$ d" "'"${INSTALL_SCRIPT}"'" > "$tmp"
1259
+ echo "main() { :; }" >> "$tmp"
1260
+ source "$tmp"
1261
+ rm -f "$tmp"
1262
+
1263
+ INSTALLER_FEED_CHANNEL="whatsapp" \
1264
+ INSTALLER_FEED_TARGET_ID="5511999887766@s.whatsapp.net" \
1265
+ INSTALLER_CONFIG_FILE="'"${fake_home}"'/.openclaw/openclaw.json" \
1266
+ node -e "
1267
+ const fs = require(\"fs\");
1268
+ const configPath = process.env.INSTALLER_CONFIG_FILE;
1269
+ const channel = process.env.INSTALLER_FEED_CHANNEL;
1270
+ const targetId = process.env.INSTALLER_FEED_TARGET_ID;
1271
+
1272
+ const config = JSON.parse(fs.readFileSync(configPath, \"utf8\"));
1273
+
1274
+ if (!config.plugins) config.plugins = {};
1275
+ if (!config.plugins.entries) config.plugins.entries = {};
1276
+ if (!config.plugins.entries[\"claude-mem\"]) {
1277
+ config.plugins.entries[\"claude-mem\"] = { enabled: true, config: {} };
1278
+ }
1279
+ if (!config.plugins.entries[\"claude-mem\"].config) {
1280
+ config.plugins.entries[\"claude-mem\"].config = {};
1281
+ }
1282
+
1283
+ config.plugins.entries[\"claude-mem\"].config.observationFeed = {
1284
+ enabled: true,
1285
+ channel: channel,
1286
+ to: targetId
1287
+ };
1288
+
1289
+ fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
1290
+ "
1291
+ echo "DONE"
1292
+ ' 2>/dev/null)" || true
1293
+
1294
+ if [[ "$result" == *"DONE"* ]]; then
1295
+ local config_file="${fake_home}/.openclaw/openclaw.json"
1296
+ verify_feed_config_json "$config_file" "whatsapp" "5511999887766@s.whatsapp.net" "node path"
1297
+ else
1298
+ test_fail "node path: write_observation_feed_config failed"
1299
+ fi
1300
+
1301
+ rm -rf "$fake_home"
1302
+ }
1303
+
1304
+ test_write_feed_config_node_path
1305
+
1306
+ test_feed_config_fallback_chain_in_source() {
1307
+ if grep -q 'command -v jq' "$INSTALL_SCRIPT"; then
1308
+ test_pass "write_observation_feed_config checks for jq first"
1309
+ else
1310
+ test_fail "write_observation_feed_config should check for jq"
1311
+ fi
1312
+
1313
+ if grep -q 'command -v python3' "$INSTALL_SCRIPT"; then
1314
+ test_pass "write_observation_feed_config has python3 fallback"
1315
+ else
1316
+ test_fail "write_observation_feed_config should have python3 fallback"
1317
+ fi
1318
+
1319
+ if grep -q 'node -e' "$INSTALL_SCRIPT"; then
1320
+ test_pass "write_observation_feed_config has node fallback"
1321
+ else
1322
+ test_fail "write_observation_feed_config should have node fallback"
1323
+ fi
1324
+ }
1325
+
1326
+ test_feed_config_fallback_chain_in_source
1327
+
1328
+ echo ""
1329
+ echo "=== print_completion_summary() — observation feed ==="
1330
+
1331
+ test_completion_summary_with_feed() {
1332
+ AI_PROVIDER="claude"
1333
+ WORKER_PID=""
1334
+ FEED_CONFIGURED="true"
1335
+ FEED_CHANNEL="telegram"
1336
+ FEED_TARGET_ID="123456789"
1337
+
1338
+ local output
1339
+ output="$(print_completion_summary 2>&1)"
1340
+
1341
+ assert_contains "$output" "telegram" "Summary shows feed channel when configured"
1342
+ assert_contains "$output" "123456789" "Summary shows feed target when configured"
1343
+ assert_contains "$output" "What's next" "Summary includes What's next section"
1344
+ assert_contains "$output" "/claude-mem-feed" "Summary includes feed check command when configured"
1345
+
1346
+ FEED_CONFIGURED=false
1347
+ FEED_CHANNEL=""
1348
+ FEED_TARGET_ID=""
1349
+ }
1350
+
1351
+ test_completion_summary_with_feed
1352
+
1353
+ test_completion_summary_without_feed() {
1354
+ AI_PROVIDER="claude"
1355
+ WORKER_PID=""
1356
+ FEED_CONFIGURED=false
1357
+ FEED_CHANNEL=""
1358
+ FEED_TARGET_ID=""
1359
+
1360
+ local output
1361
+ output="$(print_completion_summary 2>&1)"
1362
+
1363
+ assert_contains "$output" "not configured" "Summary shows 'not configured' when feed skipped"
1364
+ assert_contains "$output" "What's next" "Summary includes What's next section without feed"
1365
+ assert_contains "$output" "/claude-mem-status" "Summary includes status check command"
1366
+ assert_contains "$output" "localhost:37777" "Summary includes viewer URL"
1367
+ }
1368
+
1369
+ test_completion_summary_without_feed
1370
+
1371
+ echo ""
1372
+ echo "=== Channel instructions ==="
1373
+
1374
+ for channel in telegram discord slack signal whatsapp line; do
1375
+ if grep -qi "$channel" "$INSTALL_SCRIPT"; then
1376
+ test_pass "Channel '${channel}' instructions exist in install.sh"
1377
+ else
1378
+ test_fail "Channel '${channel}' instructions should exist in install.sh"
1379
+ fi
1380
+ done
1381
+
1382
+ assert_contains "$(grep -A2 'userinfobot' "$INSTALL_SCRIPT" 2>/dev/null || echo '')" "userinfobot" "Telegram instructions include @userinfobot"
1383
+ assert_contains "$(grep -A2 'Developer Mode' "$INSTALL_SCRIPT" 2>/dev/null || echo '')" "Developer Mode" "Discord instructions include Developer Mode"
1384
+ assert_contains "$(grep -A2 'C01ABC2DEFG' "$INSTALL_SCRIPT" 2>/dev/null || echo '')" "C01ABC2DEFG" "Slack instructions include sample channel ID"
1385
+
1386
+ echo ""
1387
+ echo "=== TTY detection ==="
1388
+
1389
+ for fn in setup_tty read_tty; do
1390
+ if declare -f "$fn" &>/dev/null; then
1391
+ test_pass "Function ${fn}() is defined"
1392
+ else
1393
+ test_fail "Function ${fn}() should be defined"
1394
+ fi
1395
+ done
1396
+
1397
+ if declare -p TTY_FD &>/dev/null; then
1398
+ test_pass "TTY_FD variable is defined"
1399
+ else
1400
+ test_fail "TTY_FD variable should be defined"
1401
+ fi
1402
+
1403
+ if grep -q 'setup_tty' "$INSTALL_SCRIPT"; then
1404
+ test_pass "main() calls setup_tty"
1405
+ else
1406
+ test_fail "main() should call setup_tty"
1407
+ fi
1408
+
1409
+ echo ""
1410
+ echo "=== Argument parsing — --provider flag ==="
1411
+
1412
+ test_provider_flag_claude() {
1413
+ local result
1414
+ result="$(bash -c '
1415
+ set -euo pipefail
1416
+ TERM=dumb
1417
+ tmp=$(mktemp)
1418
+ sed "$ d" "'"${INSTALL_SCRIPT}"'" > "$tmp"
1419
+ echo "main() { :; }" >> "$tmp"
1420
+ set -- "--provider=claude"
1421
+ source "$tmp"
1422
+ rm -f "$tmp"
1423
+ setup_ai_provider >/dev/null 2>&1
1424
+ echo "$AI_PROVIDER"
1425
+ ' 2>/dev/null)" || true
1426
+
1427
+ assert_eq "claude" "$result" "--provider=claude sets AI_PROVIDER to claude"
1428
+ }
1429
+
1430
+ test_provider_flag_claude
1431
+
1432
+ test_provider_flag_gemini_with_api_key() {
1433
+ local result
1434
+ result="$(bash -c '
1435
+ set -euo pipefail
1436
+ TERM=dumb
1437
+ tmp=$(mktemp)
1438
+ sed "$ d" "'"${INSTALL_SCRIPT}"'" > "$tmp"
1439
+ echo "main() { :; }" >> "$tmp"
1440
+ set -- "--provider=gemini" "--api-key=test-key-123"
1441
+ source "$tmp"
1442
+ rm -f "$tmp"
1443
+ setup_ai_provider >/dev/null 2>&1
1444
+ echo "PROVIDER=$AI_PROVIDER"
1445
+ echo "KEY=$AI_PROVIDER_API_KEY"
1446
+ ' 2>/dev/null)" || true
1447
+
1448
+ assert_contains "$result" "PROVIDER=gemini" "--provider=gemini sets AI_PROVIDER to gemini"
1449
+ assert_contains "$result" "KEY=test-key-123" "--api-key=test-key-123 sets API key"
1450
+ }
1451
+
1452
+ test_provider_flag_gemini_with_api_key
1453
+
1454
+ test_provider_flag_openrouter() {
1455
+ local result
1456
+ result="$(bash -c '
1457
+ set -euo pipefail
1458
+ TERM=dumb
1459
+ tmp=$(mktemp)
1460
+ sed "$ d" "'"${INSTALL_SCRIPT}"'" > "$tmp"
1461
+ echo "main() { :; }" >> "$tmp"
1462
+ set -- "--provider=openrouter" "--api-key=sk-or-test"
1463
+ source "$tmp"
1464
+ rm -f "$tmp"
1465
+ setup_ai_provider >/dev/null 2>&1
1466
+ echo "PROVIDER=$AI_PROVIDER"
1467
+ echo "KEY=$AI_PROVIDER_API_KEY"
1468
+ ' 2>/dev/null)" || true
1469
+
1470
+ assert_contains "$result" "PROVIDER=openrouter" "--provider=openrouter sets AI_PROVIDER"
1471
+ assert_contains "$result" "KEY=sk-or-test" "--api-key sets API key for openrouter"
1472
+ }
1473
+
1474
+ test_provider_flag_openrouter
1475
+
1476
+ test_provider_flag_invalid() {
1477
+ local exit_code=0
1478
+ bash -c '
1479
+ set -euo pipefail
1480
+ TERM=dumb
1481
+ tmp=$(mktemp)
1482
+ sed "$ d" "'"${INSTALL_SCRIPT}"'" > "$tmp"
1483
+ echo "main() { :; }" >> "$tmp"
1484
+ set -- "--provider=invalid"
1485
+ source "$tmp"
1486
+ rm -f "$tmp"
1487
+ setup_ai_provider
1488
+ ' >/dev/null 2>&1 || exit_code=$?
1489
+
1490
+ if [[ "$exit_code" -ne 0 ]]; then
1491
+ test_pass "--provider=invalid exits with error"
1492
+ else
1493
+ test_fail "--provider=invalid should exit with error"
1494
+ fi
1495
+ }
1496
+
1497
+ test_provider_flag_invalid
1498
+
1499
+ echo ""
1500
+ echo "=== Argument parsing — --non-interactive ==="
1501
+
1502
+ test_non_interactive_flag() {
1503
+ local result
1504
+ result="$(bash -c '
1505
+ set -euo pipefail
1506
+ TERM=dumb
1507
+ tmp=$(mktemp)
1508
+ sed "$ d" "'"${INSTALL_SCRIPT}"'" > "$tmp"
1509
+ echo "main() { :; }" >> "$tmp"
1510
+ set -- "--non-interactive"
1511
+ source "$tmp"
1512
+ rm -f "$tmp"
1513
+ echo "NON_INTERACTIVE=$NON_INTERACTIVE"
1514
+ ' 2>/dev/null)" || true
1515
+
1516
+ assert_contains "$result" "NON_INTERACTIVE=true" "--non-interactive sets NON_INTERACTIVE=true"
1517
+ }
1518
+
1519
+ test_non_interactive_flag
1520
+
1521
+ test_non_interactive_with_provider() {
1522
+ local result
1523
+ result="$(bash -c '
1524
+ set -euo pipefail
1525
+ TERM=dumb
1526
+ tmp=$(mktemp)
1527
+ sed "$ d" "'"${INSTALL_SCRIPT}"'" > "$tmp"
1528
+ echo "main() { :; }" >> "$tmp"
1529
+ set -- "--non-interactive" "--provider=gemini" "--api-key=my-key"
1530
+ source "$tmp"
1531
+ rm -f "$tmp"
1532
+ setup_ai_provider >/dev/null 2>&1
1533
+ echo "PROVIDER=$AI_PROVIDER"
1534
+ echo "KEY=$AI_PROVIDER_API_KEY"
1535
+ echo "NON_INTERACTIVE=$NON_INTERACTIVE"
1536
+ ' 2>/dev/null)" || true
1537
+
1538
+ assert_contains "$result" "PROVIDER=gemini" "--non-interactive + --provider: provider set correctly"
1539
+ assert_contains "$result" "KEY=my-key" "--non-interactive + --api-key: key set correctly"
1540
+ assert_contains "$result" "NON_INTERACTIVE=true" "--non-interactive flag parsed alongside --provider"
1541
+ }
1542
+
1543
+ test_non_interactive_with_provider
1544
+
1545
+ echo ""
1546
+ echo "=== --non-interactive full flow ==="
1547
+
1548
+ test_non_interactive_completes() {
1549
+ local result
1550
+ result="$(bash -c '
1551
+ set -euo pipefail
1552
+ TERM=dumb
1553
+ tmp=$(mktemp)
1554
+ sed "$ d" "'"${INSTALL_SCRIPT}"'" > "$tmp"
1555
+ echo "main() { :; }" >> "$tmp"
1556
+ set -- "--non-interactive"
1557
+ source "$tmp"
1558
+ rm -f "$tmp"
1559
+ setup_ai_provider 2>/dev/null
1560
+ setup_observation_feed 2>/dev/null
1561
+ echo "AI=$AI_PROVIDER"
1562
+ echo "FEED=$FEED_CONFIGURED"
1563
+ ' 2>/dev/null)" || true
1564
+
1565
+ assert_contains "$result" "AI=claude" "--non-interactive: AI provider defaults to claude"
1566
+ assert_contains "$result" "FEED=false" "--non-interactive: observation feed skipped"
1567
+ }
1568
+
1569
+ test_non_interactive_completes
1570
+
1571
+ echo ""
1572
+ echo "=== curl | bash usage comment ==="
1573
+
1574
+ if grep -q 'curl -fsSL.*raw.githubusercontent.com.*install.sh | bash' "$INSTALL_SCRIPT"; then
1575
+ test_pass "install.sh contains curl | bash usage comment"
1576
+ else
1577
+ test_fail "install.sh should contain curl | bash usage comment"
1578
+ fi
1579
+
1580
+ if grep -q 'bash -s -- --provider=' "$INSTALL_SCRIPT"; then
1581
+ test_pass "install.sh documents --provider flag in usage comment"
1582
+ else
1583
+ test_fail "install.sh should document --provider flag in usage comment"
1584
+ fi
1585
+
1586
+ echo ""
1587
+ echo "=== write_settings with --provider flag ==="
1588
+
1589
+ test_write_settings_via_provider_flag() {
1590
+ local fake_home
1591
+ fake_home="$(mktemp -d)"
1592
+
1593
+ local result
1594
+ result="$(bash -c '
1595
+ set -euo pipefail
1596
+ TERM=dumb
1597
+ export HOME="'"$fake_home"'"
1598
+ tmp=$(mktemp)
1599
+ sed "$ d" "'"${INSTALL_SCRIPT}"'" > "$tmp"
1600
+ echo "main() { :; }" >> "$tmp"
1601
+ set -- "--provider=gemini" "--api-key=test-end-to-end-key"
1602
+ source "$tmp"
1603
+ rm -f "$tmp"
1604
+ setup_ai_provider >/dev/null 2>&1
1605
+ write_settings >/dev/null 2>&1
1606
+ echo "DONE"
1607
+ ' 2>/dev/null)" || true
1608
+
1609
+ if [[ "$result" == *"DONE"* ]]; then
1610
+ local settings_file="${fake_home}/.claude-mem/settings.json"
1611
+ local provider
1612
+ provider="$(node -e "const s = JSON.parse(require('fs').readFileSync('${settings_file}','utf8')); console.log(s.CLAUDE_MEM_PROVIDER);")"
1613
+ assert_eq "gemini" "$provider" "--provider flag: settings.json has provider=gemini"
1614
+
1615
+ local api_key
1616
+ api_key="$(node -e "const s = JSON.parse(require('fs').readFileSync('${settings_file}','utf8')); console.log(s.CLAUDE_MEM_GEMINI_API_KEY);")"
1617
+ assert_eq "test-end-to-end-key" "$api_key" "--provider flag: settings.json has correct API key"
1618
+ else
1619
+ test_fail "--provider flag: write_settings failed"
1620
+ fi
1621
+
1622
+ rm -rf "$fake_home"
1623
+ }
1624
+
1625
+ test_write_settings_via_provider_flag
1626
+
1627
+ echo ""
1628
+ echo "=== --upgrade flag parsing ==="
1629
+
1630
+ test_upgrade_flag() {
1631
+ local result
1632
+ result="$(bash -c '
1633
+ set -euo pipefail
1634
+ TERM=dumb
1635
+ tmp=$(mktemp)
1636
+ sed "$ d" "'"${INSTALL_SCRIPT}"'" > "$tmp"
1637
+ echo "main() { :; }" >> "$tmp"
1638
+ set -- "--upgrade"
1639
+ source "$tmp"
1640
+ rm -f "$tmp"
1641
+ echo "UPGRADE=$UPGRADE_MODE"
1642
+ ' 2>/dev/null)" || true
1643
+
1644
+ assert_contains "$result" "UPGRADE=true" "--upgrade sets UPGRADE_MODE=true"
1645
+ }
1646
+
1647
+ test_upgrade_flag
1648
+
1649
+ test_upgrade_flag_with_provider() {
1650
+ local result
1651
+ result="$(bash -c '
1652
+ set -euo pipefail
1653
+ TERM=dumb
1654
+ tmp=$(mktemp)
1655
+ sed "$ d" "'"${INSTALL_SCRIPT}"'" > "$tmp"
1656
+ echo "main() { :; }" >> "$tmp"
1657
+ set -- "--upgrade" "--provider=gemini" "--api-key=upgrade-key"
1658
+ source "$tmp"
1659
+ rm -f "$tmp"
1660
+ echo "UPGRADE=$UPGRADE_MODE"
1661
+ echo "PROVIDER=$CLI_PROVIDER"
1662
+ echo "KEY=$CLI_API_KEY"
1663
+ ' 2>/dev/null)" || true
1664
+
1665
+ assert_contains "$result" "UPGRADE=true" "--upgrade + --provider: upgrade flag parsed"
1666
+ assert_contains "$result" "PROVIDER=gemini" "--upgrade + --provider: provider flag parsed"
1667
+ assert_contains "$result" "KEY=upgrade-key" "--upgrade + --api-key: API key parsed"
1668
+ }
1669
+
1670
+ test_upgrade_flag_with_provider
1671
+
1672
+ test_upgrade_not_set_by_default() {
1673
+ local result
1674
+ result="$(bash -c '
1675
+ set -euo pipefail
1676
+ TERM=dumb
1677
+ tmp=$(mktemp)
1678
+ sed "$ d" "'"${INSTALL_SCRIPT}"'" > "$tmp"
1679
+ echo "main() { :; }" >> "$tmp"
1680
+ source "$tmp"
1681
+ rm -f "$tmp"
1682
+ echo "UPGRADE=${UPGRADE_MODE:-}"
1683
+ ' 2>/dev/null)" || true
1684
+
1685
+ assert_eq "UPGRADE=" "$result" "UPGRADE_MODE is empty by default"
1686
+ }
1687
+
1688
+ test_upgrade_not_set_by_default
1689
+
1690
+ echo ""
1691
+ echo "=== is_claude_mem_installed() ==="
1692
+
1693
+ test_is_claude_mem_installed_found() {
1694
+ local fake_home
1695
+ fake_home="$(mktemp -d)"
1696
+ HOME="$fake_home"
1697
+ CLAUDE_MEM_INSTALL_DIR=""
1698
+
1699
+ mkdir -p "${fake_home}/.openclaw/extensions/claude-mem/plugin/scripts"
1700
+ touch "${fake_home}/.openclaw/extensions/claude-mem/plugin/scripts/worker-service.cjs"
1701
+
1702
+ if is_claude_mem_installed; then
1703
+ test_pass "is_claude_mem_installed returns true when plugin exists"
1704
+ else
1705
+ test_fail "is_claude_mem_installed should return true when plugin exists"
1706
+ fi
1707
+
1708
+ HOME="$ORIGINAL_HOME"
1709
+ rm -rf "$fake_home"
1710
+ }
1711
+
1712
+ test_is_claude_mem_installed_found
1713
+
1714
+ test_is_claude_mem_installed_not_found() {
1715
+ local fake_home
1716
+ fake_home="$(mktemp -d)"
1717
+ HOME="$fake_home"
1718
+ CLAUDE_MEM_INSTALL_DIR=""
1719
+
1720
+ if is_claude_mem_installed; then
1721
+ test_fail "is_claude_mem_installed should return false when plugin not found"
1722
+ else
1723
+ test_pass "is_claude_mem_installed returns false when plugin not found"
1724
+ fi
1725
+
1726
+ HOME="$ORIGINAL_HOME"
1727
+ rm -rf "$fake_home"
1728
+ }
1729
+
1730
+ test_is_claude_mem_installed_not_found
1731
+
1732
+ echo ""
1733
+ echo "=== check_git() ==="
1734
+
1735
+ test_check_git_available() {
1736
+ if command -v git &>/dev/null; then
1737
+ local output
1738
+ output="$(check_git 2>&1)" || true
1739
+ test_pass "check_git succeeds when git is installed"
1740
+ else
1741
+ test_pass "check_git test skipped (git not available)"
1742
+ fi
1743
+ }
1744
+
1745
+ test_check_git_available
1746
+
1747
+ test_check_git_not_available() {
1748
+ local exit_code=0
1749
+ PLATFORM="macos"
1750
+ bash -c '
1751
+ set -euo pipefail
1752
+ TERM=dumb
1753
+ tmp=$(mktemp)
1754
+ sed "$ d" "'"${INSTALL_SCRIPT}"'" > "$tmp"
1755
+ echo "main() { :; }" >> "$tmp"
1756
+ source "$tmp"
1757
+ rm -f "$tmp"
1758
+ PATH="/nonexistent"
1759
+ check_git
1760
+ ' >/dev/null 2>&1 || exit_code=$?
1761
+
1762
+ if [[ "$exit_code" -ne 0 ]]; then
1763
+ test_pass "check_git exits with error when git is missing"
1764
+ else
1765
+ test_fail "check_git should exit with error when git is missing"
1766
+ fi
1767
+ }
1768
+
1769
+ test_check_git_not_available
1770
+
1771
+ test_check_git_macos_message() {
1772
+ local output
1773
+ output="$(bash -c '
1774
+ set -euo pipefail
1775
+ TERM=dumb
1776
+ tmp=$(mktemp)
1777
+ sed "$ d" "'"${INSTALL_SCRIPT}"'" > "$tmp"
1778
+ echo "main() { :; }" >> "$tmp"
1779
+ source "$tmp"
1780
+ rm -f "$tmp"
1781
+ PATH="/nonexistent"
1782
+ PLATFORM="macos"
1783
+ check_git
1784
+ ' 2>&1)" || true
1785
+
1786
+ assert_contains "$output" "xcode-select" "check_git suggests xcode-select on macOS"
1787
+ }
1788
+
1789
+ test_check_git_macos_message
1790
+
1791
+ test_check_git_linux_message() {
1792
+ local output
1793
+ output="$(bash -c '
1794
+ set -euo pipefail
1795
+ TERM=dumb
1796
+ tmp=$(mktemp)
1797
+ sed "$ d" "'"${INSTALL_SCRIPT}"'" > "$tmp"
1798
+ echo "main() { :; }" >> "$tmp"
1799
+ source "$tmp"
1800
+ rm -f "$tmp"
1801
+ PATH="/nonexistent"
1802
+ PLATFORM="linux"
1803
+ check_git
1804
+ ' 2>&1)" || true
1805
+
1806
+ assert_contains "$output" "apt install git" "check_git suggests apt on Linux"
1807
+ }
1808
+
1809
+ test_check_git_linux_message
1810
+
1811
+ echo ""
1812
+ echo "=== check_port_37777() ==="
1813
+
1814
+ test_check_port_function_exists() {
1815
+ if declare -f check_port_37777 &>/dev/null; then
1816
+ test_pass "Function check_port_37777() is defined"
1817
+ else
1818
+ test_fail "Function check_port_37777() should be defined"
1819
+ fi
1820
+ }
1821
+
1822
+ test_check_port_function_exists
1823
+
1824
+ echo ""
1825
+ echo "=== cleanup_on_exit() ==="
1826
+
1827
+ test_cleanup_trap_functions_exist() {
1828
+ if declare -f register_cleanup_dir &>/dev/null; then
1829
+ test_pass "Function register_cleanup_dir() is defined"
1830
+ else
1831
+ test_fail "Function register_cleanup_dir() should be defined"
1832
+ fi
1833
+
1834
+ if declare -f cleanup_on_exit &>/dev/null; then
1835
+ test_pass "Function cleanup_on_exit() is defined"
1836
+ else
1837
+ test_fail "Function cleanup_on_exit() should be defined"
1838
+ fi
1839
+ }
1840
+
1841
+ test_cleanup_trap_functions_exist
1842
+
1843
+ test_register_cleanup_dir() {
1844
+ local test_dir
1845
+ test_dir="$(mktemp -d)"
1846
+
1847
+ local saved_dirs=("${CLEANUP_DIRS[@]+"${CLEANUP_DIRS[@]}"}")
1848
+ CLEANUP_DIRS=()
1849
+
1850
+ register_cleanup_dir "$test_dir"
1851
+
1852
+ if [[ "${#CLEANUP_DIRS[@]}" -eq 1 ]] && [[ "${CLEANUP_DIRS[0]}" == "$test_dir" ]]; then
1853
+ test_pass "register_cleanup_dir adds directory to CLEANUP_DIRS"
1854
+ else
1855
+ test_fail "register_cleanup_dir should add directory to CLEANUP_DIRS"
1856
+ fi
1857
+
1858
+ CLEANUP_DIRS=("${saved_dirs[@]+"${saved_dirs[@]}"}")
1859
+ rm -rf "$test_dir"
1860
+ }
1861
+
1862
+ test_register_cleanup_dir
1863
+
1864
+ echo ""
1865
+ echo "=== ensure_jq_or_fallback() ==="
1866
+
1867
+ test_ensure_jq_or_fallback_exists() {
1868
+ if declare -f ensure_jq_or_fallback &>/dev/null; then
1869
+ test_pass "Function ensure_jq_or_fallback() is defined"
1870
+ else
1871
+ test_fail "Function ensure_jq_or_fallback() should be defined"
1872
+ fi
1873
+ }
1874
+
1875
+ test_ensure_jq_or_fallback_exists
1876
+
1877
+ test_ensure_jq_with_jq_available() {
1878
+ if ! command -v jq &>/dev/null; then
1879
+ test_pass "ensure_jq jq-path: skipped (jq not installed)"
1880
+ return 0
1881
+ fi
1882
+
1883
+ local tmp_json
1884
+ tmp_json="$(mktemp)"
1885
+ echo '{"name": "test", "value": 1}' > "$tmp_json"
1886
+
1887
+ if ensure_jq_or_fallback "$tmp_json" '.name = "updated"'; then
1888
+ local result
1889
+ result="$(node -e "const j = JSON.parse(require('fs').readFileSync('${tmp_json}','utf8')); console.log(j.name);")"
1890
+ assert_eq "updated" "$result" "ensure_jq_or_fallback updates JSON via jq"
1891
+ else
1892
+ test_fail "ensure_jq_or_fallback should succeed with jq available"
1893
+ fi
1894
+
1895
+ rm -f "$tmp_json"
1896
+ }
1897
+
1898
+ test_ensure_jq_with_jq_available
1899
+
1900
+ echo ""
1901
+ echo "=== main() references new functions ==="
1902
+
1903
+ test_main_calls_check_port() {
1904
+ if grep -q 'check_port_37777' "$INSTALL_SCRIPT"; then
1905
+ test_pass "main() calls check_port_37777"
1906
+ else
1907
+ test_fail "main() should call check_port_37777"
1908
+ fi
1909
+ }
1910
+
1911
+ test_main_calls_check_port
1912
+
1913
+ test_main_calls_is_claude_mem_installed() {
1914
+ if grep -q 'is_claude_mem_installed' "$INSTALL_SCRIPT"; then
1915
+ test_pass "main() calls is_claude_mem_installed for upgrade detection"
1916
+ else
1917
+ test_fail "main() should call is_claude_mem_installed"
1918
+ fi
1919
+ }
1920
+
1921
+ test_main_calls_is_claude_mem_installed
1922
+
1923
+ test_main_references_upgrade_mode() {
1924
+ if grep -q 'UPGRADE_MODE' "$INSTALL_SCRIPT"; then
1925
+ test_pass "main() references UPGRADE_MODE"
1926
+ else
1927
+ test_fail "main() should reference UPGRADE_MODE"
1928
+ fi
1929
+ }
1930
+
1931
+ test_main_references_upgrade_mode
1932
+
1933
+ test_install_plugin_calls_check_git() {
1934
+ if grep -q 'check_git' "$INSTALL_SCRIPT"; then
1935
+ test_pass "install_plugin() calls check_git"
1936
+ else
1937
+ test_fail "install_plugin() should call check_git"
1938
+ fi
1939
+ }
1940
+
1941
+ test_install_plugin_calls_check_git
1942
+
1943
+ test_install_plugin_uses_register_cleanup() {
1944
+ if grep -q 'register_cleanup_dir' "$INSTALL_SCRIPT"; then
1945
+ test_pass "install_plugin() uses register_cleanup_dir"
1946
+ else
1947
+ test_fail "install_plugin() should use register_cleanup_dir"
1948
+ fi
1949
+ }
1950
+
1951
+ test_install_plugin_uses_register_cleanup
1952
+
1953
+ test_usage_comment_includes_upgrade() {
1954
+ if grep -q '\-\-upgrade' "$INSTALL_SCRIPT"; then
1955
+ test_pass "Usage comment documents --upgrade flag"
1956
+ else
1957
+ test_fail "Usage comment should document --upgrade flag"
1958
+ fi
1959
+ }
1960
+
1961
+ test_usage_comment_includes_upgrade
1962
+
1963
+ echo ""
1964
+ echo "=== Distribution readiness ==="
1965
+
1966
+ test_install_sh_has_shebang() {
1967
+ local first_line
1968
+ first_line="$(head -1 "$INSTALL_SCRIPT")"
1969
+ assert_eq "#!/usr/bin/env bash" "$first_line" "install.sh has correct shebang line"
1970
+ }
1971
+
1972
+ test_install_sh_has_shebang
1973
+
1974
+ test_install_sh_has_set_euo_pipefail() {
1975
+ if grep -q 'set -euo pipefail' "$INSTALL_SCRIPT"; then
1976
+ test_pass "install.sh uses set -euo pipefail for safety"
1977
+ else
1978
+ test_fail "install.sh should use set -euo pipefail"
1979
+ fi
1980
+ }
1981
+
1982
+ test_install_sh_has_set_euo_pipefail
1983
+
1984
+ test_install_sh_has_stable_url_in_usage() {
1985
+ if grep -q 'raw.githubusercontent.com/bjlee2024/claude-mem/main/openclaw/install.sh' "$INSTALL_SCRIPT"; then
1986
+ test_pass "install.sh usage comment has stable raw.githubusercontent.com URL"
1987
+ else
1988
+ test_fail "install.sh should reference stable raw.githubusercontent.com URL in usage"
1989
+ fi
1990
+ }
1991
+
1992
+ test_install_sh_has_stable_url_in_usage
1993
+
1994
+ test_install_sh_documents_all_flags() {
1995
+ local missing_flags=()
1996
+
1997
+ for flag in "--non-interactive" "--upgrade" "--provider" "--api-key"; do
1998
+ if ! grep -Fq -- "$flag" "$INSTALL_SCRIPT"; then
1999
+ missing_flags+=("$flag")
2000
+ fi
2001
+ done
2002
+
2003
+ if [[ ${#missing_flags[@]} -eq 0 ]]; then
2004
+ test_pass "install.sh documents all CLI flags (--non-interactive, --upgrade, --provider, --api-key)"
2005
+ else
2006
+ test_fail "install.sh missing documentation for flags: ${missing_flags[*]}"
2007
+ fi
2008
+ }
2009
+
2010
+ test_install_sh_documents_all_flags
2011
+
2012
+ test_install_sh_has_installer_version() {
2013
+ if grep -q 'INSTALLER_VERSION=' "$INSTALL_SCRIPT"; then
2014
+ test_pass "install.sh defines INSTALLER_VERSION constant"
2015
+ else
2016
+ test_fail "install.sh should define INSTALLER_VERSION"
2017
+ fi
2018
+ }
2019
+
2020
+ test_install_sh_has_installer_version
2021
+
2022
+ test_skill_md_references_one_liner() {
2023
+ local skill_file="${SCRIPT_DIR}/SKILL.md"
2024
+ if [[ ! -f "$skill_file" ]]; then
2025
+ test_fail "SKILL.md not found at ${skill_file}"
2026
+ return
2027
+ fi
2028
+
2029
+ if grep -q 'curl -fsSL.*raw.githubusercontent.com.*install.sh | bash' "$skill_file"; then
2030
+ test_pass "SKILL.md references the one-liner installer"
2031
+ else
2032
+ test_fail "SKILL.md should reference the one-liner installer"
2033
+ fi
2034
+ }
2035
+
2036
+ test_skill_md_references_one_liner
2037
+
2038
+ test_skill_md_has_quick_install_section() {
2039
+ local skill_file="${SCRIPT_DIR}/SKILL.md"
2040
+ if [[ ! -f "$skill_file" ]]; then
2041
+ test_fail "SKILL.md not found at ${skill_file}"
2042
+ return
2043
+ fi
2044
+
2045
+ if grep -q 'Quick Install' "$skill_file"; then
2046
+ test_pass "SKILL.md has Quick Install section"
2047
+ else
2048
+ test_fail "SKILL.md should have Quick Install section"
2049
+ fi
2050
+ }
2051
+
2052
+ test_skill_md_has_quick_install_section
2053
+
2054
+ test_skill_md_documents_options() {
2055
+ local skill_file="${SCRIPT_DIR}/SKILL.md"
2056
+ if [[ ! -f "$skill_file" ]]; then
2057
+ test_fail "SKILL.md not found at ${skill_file}"
2058
+ return
2059
+ fi
2060
+
2061
+ local missing=()
2062
+ for option in "--provider" "--non-interactive" "--upgrade"; do
2063
+ if ! grep -Fq -- "$option" "$skill_file"; then
2064
+ missing+=("$option")
2065
+ fi
2066
+ done
2067
+
2068
+ if [[ ${#missing[@]} -eq 0 ]]; then
2069
+ test_pass "SKILL.md documents all installer options (--provider, --non-interactive, --upgrade)"
2070
+ else
2071
+ test_fail "SKILL.md missing documentation for: ${missing[*]}"
2072
+ fi
2073
+ }
2074
+
2075
+ test_skill_md_documents_options
2076
+
2077
+ echo ""
2078
+ echo "========================================"
2079
+ echo "Results: ${TESTS_PASSED}/${TESTS_RUN} passed, ${TESTS_FAILED} failed"
2080
+ echo "========================================"
2081
+
2082
+ if [[ "$TESTS_FAILED" -gt 0 ]]; then
2083
+ exit 1
2084
+ fi
2085
+
2086
+ exit 0