claude-memory-agent 2.0.1 → 2.2.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 (97) hide show
  1. package/README.md +206 -206
  2. package/agent_card.py +186 -0
  3. package/bin/cli.js +327 -185
  4. package/bin/lib/banner.js +39 -0
  5. package/bin/lib/environment.js +166 -0
  6. package/bin/lib/installer.js +291 -0
  7. package/bin/lib/models.js +95 -0
  8. package/bin/lib/steps/advanced.js +101 -0
  9. package/bin/lib/steps/confirm.js +87 -0
  10. package/bin/lib/steps/model.js +57 -0
  11. package/bin/lib/steps/provider.js +65 -0
  12. package/bin/lib/steps/scope.js +59 -0
  13. package/bin/lib/steps/server.js +74 -0
  14. package/bin/lib/ui.js +75 -0
  15. package/bin/onboarding.js +164 -0
  16. package/bin/postinstall.js +35 -270
  17. package/config.py +103 -4
  18. package/dashboard.html +4902 -2689
  19. package/hooks/extract_memories.py +439 -0
  20. package/hooks/grounding-hook.py +422 -348
  21. package/hooks/pre_compact_hook.py +76 -0
  22. package/hooks/session_end.py +293 -192
  23. package/hooks/session_end_hook.py +149 -0
  24. package/hooks/session_start.py +227 -227
  25. package/hooks/stop_hook.py +372 -0
  26. package/install.py +972 -902
  27. package/main.py +5240 -2859
  28. package/mcp_server.py +451 -0
  29. package/package.json +58 -47
  30. package/requirements.txt +12 -8
  31. package/services/__init__.py +50 -50
  32. package/services/adaptive_ranker.py +272 -0
  33. package/services/agent_catalog.json +153 -0
  34. package/services/agent_registry.py +245 -730
  35. package/services/claude_md_sync.py +320 -4
  36. package/services/consolidation.py +417 -0
  37. package/services/curator.py +1606 -0
  38. package/services/database.py +4118 -2485
  39. package/services/embedding_pipeline.py +262 -0
  40. package/services/embeddings.py +493 -85
  41. package/services/memory_decay.py +408 -0
  42. package/services/native_memory_paths.py +86 -0
  43. package/services/native_memory_sync.py +496 -0
  44. package/services/response_manager.py +183 -0
  45. package/services/terminal_ui.py +199 -0
  46. package/services/tier_manager.py +235 -0
  47. package/services/websocket.py +26 -6
  48. package/skills/__init__.py +21 -1
  49. package/skills/confidence_tracker.py +441 -0
  50. package/skills/context.py +675 -0
  51. package/skills/curator.py +348 -0
  52. package/skills/search.py +444 -213
  53. package/skills/session_review.py +605 -0
  54. package/skills/store.py +484 -179
  55. package/terminal_dashboard.py +474 -0
  56. package/update_system.py +829 -817
  57. package/hooks/__pycache__/auto-detect-response.cpython-312.pyc +0 -0
  58. package/hooks/__pycache__/auto_capture.cpython-312.pyc +0 -0
  59. package/hooks/__pycache__/session_end.cpython-312.pyc +0 -0
  60. package/hooks/__pycache__/session_start.cpython-312.pyc +0 -0
  61. package/services/__pycache__/__init__.cpython-312.pyc +0 -0
  62. package/services/__pycache__/agent_registry.cpython-312.pyc +0 -0
  63. package/services/__pycache__/auth.cpython-312.pyc +0 -0
  64. package/services/__pycache__/auto_inject.cpython-312.pyc +0 -0
  65. package/services/__pycache__/claude_md_sync.cpython-312.pyc +0 -0
  66. package/services/__pycache__/cleanup.cpython-312.pyc +0 -0
  67. package/services/__pycache__/compaction_flush.cpython-312.pyc +0 -0
  68. package/services/__pycache__/confidence.cpython-312.pyc +0 -0
  69. package/services/__pycache__/daily_log.cpython-312.pyc +0 -0
  70. package/services/__pycache__/database.cpython-312.pyc +0 -0
  71. package/services/__pycache__/embeddings.cpython-312.pyc +0 -0
  72. package/services/__pycache__/insights.cpython-312.pyc +0 -0
  73. package/services/__pycache__/llm_analyzer.cpython-312.pyc +0 -0
  74. package/services/__pycache__/memory_md_sync.cpython-312.pyc +0 -0
  75. package/services/__pycache__/retry_queue.cpython-312.pyc +0 -0
  76. package/services/__pycache__/timeline.cpython-312.pyc +0 -0
  77. package/services/__pycache__/vector_index.cpython-312.pyc +0 -0
  78. package/services/__pycache__/websocket.cpython-312.pyc +0 -0
  79. package/skills/__pycache__/__init__.cpython-312.pyc +0 -0
  80. package/skills/__pycache__/admin.cpython-312.pyc +0 -0
  81. package/skills/__pycache__/checkpoint.cpython-312.pyc +0 -0
  82. package/skills/__pycache__/claude_md.cpython-312.pyc +0 -0
  83. package/skills/__pycache__/cleanup.cpython-312.pyc +0 -0
  84. package/skills/__pycache__/grounding.cpython-312.pyc +0 -0
  85. package/skills/__pycache__/insights.cpython-312.pyc +0 -0
  86. package/skills/__pycache__/natural_language.cpython-312.pyc +0 -0
  87. package/skills/__pycache__/retrieve.cpython-312.pyc +0 -0
  88. package/skills/__pycache__/search.cpython-312.pyc +0 -0
  89. package/skills/__pycache__/state.cpython-312.pyc +0 -0
  90. package/skills/__pycache__/store.cpython-312.pyc +0 -0
  91. package/skills/__pycache__/summarize.cpython-312.pyc +0 -0
  92. package/skills/__pycache__/timeline.cpython-312.pyc +0 -0
  93. package/skills/__pycache__/verification.cpython-312.pyc +0 -0
  94. package/test_automation.py +0 -221
  95. package/test_complete.py +0 -338
  96. package/test_full.py +0 -322
  97. package/verify_db.py +0 -134
package/install.py CHANGED
@@ -1,902 +1,972 @@
1
- #!/usr/bin/env python3
2
- """
3
- Claude Memory Agent - Installation Script
4
-
5
- This script sets up the Claude Memory Agent for first-time use:
6
- 1. Creates .env file with auto-detected paths
7
- 2. Configures Claude Code MCP settings
8
- 3. Sets up hooks for auto-start and context injection
9
- 4. Creates platform-specific startup scripts
10
- 5. Installs Python dependencies
11
-
12
- Usage:
13
- python install.py # Interactive installation
14
- python install.py --auto # Auto-install with defaults
15
- python install.py --uninstall # Remove Claude Code integration
16
- """
17
- import os
18
- import sys
19
- import json
20
- import shutil
21
- import argparse
22
- import subprocess
23
- from pathlib import Path
24
- from typing import Optional, Dict, Any
25
-
26
- # =============================================================================
27
- # CONFIGURATION
28
- # =============================================================================
29
-
30
- # Agent directory (where this script lives)
31
- AGENT_DIR = Path(__file__).parent.resolve()
32
-
33
- # Default configuration
34
- DEFAULT_CONFIG = {
35
- "PORT": "8102",
36
- "HOST": "0.0.0.0",
37
- "MEMORY_AGENT_URL": "http://localhost:8102",
38
- "OLLAMA_HOST": "http://localhost:11434",
39
- "EMBEDDING_MODEL": "nomic-embed-text",
40
- "LOG_LEVEL": "INFO",
41
- "USE_VECTOR_INDEX": "true",
42
- "DB_POOL_SIZE": "5",
43
- "DB_TIMEOUT": "30.0",
44
- "AUTH_ENABLED": "false",
45
- }
46
-
47
- # Claude Code settings paths
48
- def get_claude_settings_dir() -> Path:
49
- """Get the Claude Code settings directory."""
50
- if sys.platform == "win32":
51
- return Path.home() / ".claude"
52
- elif sys.platform == "darwin":
53
- return Path.home() / ".claude"
54
- else: # Linux
55
- return Path.home() / ".claude"
56
-
57
- def get_claude_settings_file() -> Path:
58
- """Get the Claude Code settings.json file path."""
59
- return get_claude_settings_dir() / "settings.json"
60
-
61
- def get_hooks_dir() -> Path:
62
- """Get the Claude Code hooks directory."""
63
- return get_claude_settings_dir() / "hooks"
64
-
65
-
66
- # =============================================================================
67
- # UTILITY FUNCTIONS
68
- # =============================================================================
69
-
70
- def print_header(text: str):
71
- """Print a formatted header."""
72
- print(f"\n{'='*60}")
73
- print(f" {text}")
74
- print(f"{'='*60}\n")
75
-
76
-
77
- def print_step(step: int, total: int, text: str):
78
- """Print a step indicator."""
79
- print(f"[{step}/{total}] {text}")
80
-
81
-
82
- def print_success(text: str):
83
- """Print a success message."""
84
- print(f" [OK] {text}")
85
-
86
-
87
- def print_warning(text: str):
88
- """Print a warning message."""
89
- print(f" [!!] {text}")
90
-
91
-
92
- def print_error(text: str):
93
- """Print an error message."""
94
- print(f" [ERROR] {text}")
95
-
96
-
97
- def prompt_yes_no(question: str, default: bool = True) -> bool:
98
- """Prompt for yes/no answer."""
99
- suffix = " [Y/n]: " if default else " [y/N]: "
100
- while True:
101
- answer = input(question + suffix).strip().lower()
102
- if not answer:
103
- return default
104
- if answer in ("y", "yes"):
105
- return True
106
- if answer in ("n", "no"):
107
- return False
108
- print("Please answer 'y' or 'n'")
109
-
110
-
111
- def prompt_value(question: str, default: str) -> str:
112
- """Prompt for a value with a default."""
113
- answer = input(f"{question} [{default}]: ").strip()
114
- return answer if answer else default
115
-
116
-
117
- def check_python_version() -> bool:
118
- """Check if Python version is compatible."""
119
- major, minor = sys.version_info[:2]
120
- if major < 3 or (major == 3 and minor < 9):
121
- print_error(f"Python 3.9+ required, found {major}.{minor}")
122
- return False
123
- print_success(f"Python {major}.{minor} detected")
124
- return True
125
-
126
-
127
- def check_nodejs() -> tuple[bool, Optional[str]]:
128
- """Check if Node.js is installed and get version."""
129
- try:
130
- result = subprocess.run(
131
- ["node", "--version"],
132
- capture_output=True,
133
- text=True,
134
- timeout=5
135
- )
136
- if result.returncode == 0:
137
- version = result.stdout.strip()
138
- print_success(f"Node.js {version} detected")
139
- return True, version
140
- except FileNotFoundError:
141
- pass
142
- except subprocess.TimeoutExpired:
143
- pass
144
- except Exception:
145
- pass
146
-
147
- print_warning("Node.js not found")
148
- return False, None
149
-
150
-
151
- def check_npm() -> bool:
152
- """Check if npm is available."""
153
- # On Windows, npm is a .cmd file
154
- commands_to_try = ["npm", "npm.cmd"] if sys.platform == "win32" else ["npm"]
155
-
156
- for cmd in commands_to_try:
157
- try:
158
- result = subprocess.run(
159
- [cmd, "--version"],
160
- capture_output=True,
161
- text=True,
162
- timeout=5,
163
- shell=(sys.platform == "win32") # Use shell on Windows
164
- )
165
- if result.returncode == 0:
166
- print_success(f"npm {result.stdout.strip()} detected")
167
- return True
168
- except Exception:
169
- continue
170
- return False
171
-
172
-
173
- def check_claude_code() -> tuple[bool, Optional[str]]:
174
- """Check if Claude Code CLI is installed."""
175
- # Try different possible command names
176
- # On Windows, these may be .cmd files
177
- if sys.platform == "win32":
178
- commands_to_try = ["claude", "claude.cmd", "claude-code", "claude-code.cmd"]
179
- else:
180
- commands_to_try = ["claude", "claude-code"]
181
-
182
- for cmd in commands_to_try:
183
- try:
184
- result = subprocess.run(
185
- [cmd, "--version"],
186
- capture_output=True,
187
- text=True,
188
- timeout=10,
189
- shell=(sys.platform == "win32") # Use shell on Windows
190
- )
191
- if result.returncode == 0:
192
- version = result.stdout.strip().split('\n')[0]
193
- print_success(f"Claude Code detected: {version}")
194
- return True, version
195
- except FileNotFoundError:
196
- continue
197
- except subprocess.TimeoutExpired:
198
- continue
199
- except Exception:
200
- continue
201
-
202
- # Check if .claude directory exists (indicates Claude Code was used)
203
- claude_dir = get_claude_settings_dir()
204
- if claude_dir.exists():
205
- print_success("Claude Code settings directory found (~/.claude)")
206
- return True, "directory exists"
207
-
208
- print_warning("Claude Code not detected")
209
- return False, None
210
-
211
-
212
- def install_claude_code() -> bool:
213
- """Attempt to install Claude Code via npm."""
214
- print("\nClaude Code is not installed. Installing via npm...")
215
-
216
- # On Windows, use npm.cmd via shell
217
- npm_cmd = "npm" if sys.platform != "win32" else "npm.cmd"
218
-
219
- try:
220
- result = subprocess.run(
221
- [npm_cmd, "install", "-g", "@anthropic-ai/claude-code"],
222
- capture_output=True,
223
- text=True,
224
- timeout=120,
225
- shell=(sys.platform == "win32")
226
- )
227
- if result.returncode == 0:
228
- print_success("Claude Code installed successfully!")
229
- print(" Run 'claude' to start Claude Code")
230
- return True
231
- else:
232
- print_error(f"npm install failed: {result.stderr}")
233
- return False
234
- except subprocess.TimeoutExpired:
235
- print_error("Installation timed out")
236
- return False
237
- except Exception as e:
238
- print_error(f"Installation failed: {e}")
239
- return False
240
-
241
-
242
- def print_installation_instructions():
243
- """Print manual installation instructions for missing dependencies."""
244
- print("\n" + "="*60)
245
- print(" INSTALLATION REQUIRED")
246
- print("="*60)
247
- print("""
248
- To use Claude Memory Agent, you need:
249
-
250
- 1. NODE.JS (required for Claude Code)
251
- Download from: https://nodejs.org/
252
- - Windows: Download and run the installer
253
- - Mac: brew install node
254
- - Linux: sudo apt install nodejs npm
255
-
256
- 2. CLAUDE CODE (the AI coding assistant)
257
- After installing Node.js, run:
258
- npm install -g @anthropic-ai/claude-code
259
-
260
- 3. OLLAMA (for embeddings - optional but recommended)
261
- Download from: https://ollama.ai/
262
- Then run: ollama pull nomic-embed-text
263
-
264
- After installing the prerequisites, run this installer again:
265
- python install.py
266
- """)
267
-
268
-
269
- def check_ollama() -> bool:
270
- """Check if Ollama is installed and running."""
271
- try:
272
- import requests
273
- r = requests.get("http://localhost:11434/api/tags", timeout=2)
274
- if r.status_code == 200:
275
- print_success("Ollama is running")
276
- return True
277
- except Exception:
278
- pass
279
-
280
- print_warning("Ollama not detected")
281
- print("")
282
- print(" " + "="*56)
283
- print(" OLLAMA REQUIRED FOR SEMANTIC SEARCH")
284
- print(" " + "="*56)
285
- print("")
286
- print(" The memory agent needs Ollama for embeddings.")
287
- print(" Without it, semantic search will not work.")
288
- print("")
289
- print(" To install Ollama:")
290
- print(" 1. Download from: https://ollama.ai/download")
291
- print(" 2. Install and run: ollama pull nomic-embed-text")
292
- print(" 3. Start Ollama: ollama serve")
293
- print(" 4. Re-run this installer")
294
- print("")
295
- return False
296
-
297
-
298
- def check_ollama_model(model: str = "nomic-embed-text") -> bool:
299
- """Check if the embedding model is available in Ollama."""
300
- try:
301
- import requests
302
- r = requests.get("http://localhost:11434/api/tags", timeout=2)
303
- if r.status_code == 200:
304
- models = r.json().get("models", [])
305
- model_names = [m.get("name", "").split(":")[0] for m in models]
306
- if model in model_names:
307
- print_success(f"Embedding model '{model}' is available")
308
- return True
309
- print_warning(f"Model '{model}' not found. Run: ollama pull {model}")
310
- except Exception:
311
- pass
312
- return False
313
-
314
-
315
- # =============================================================================
316
- # INSTALLATION STEPS
317
- # =============================================================================
318
-
319
- def install_dependencies() -> bool:
320
- """Install Python dependencies from requirements.txt."""
321
- requirements_file = AGENT_DIR / "requirements.txt"
322
- if not requirements_file.exists():
323
- print_warning("requirements.txt not found, skipping dependency installation")
324
- return True
325
-
326
- print("Installing Python dependencies...")
327
- try:
328
- subprocess.run(
329
- [sys.executable, "-m", "pip", "install", "-r", str(requirements_file), "-q"],
330
- check=True,
331
- capture_output=True
332
- )
333
- print_success("Dependencies installed")
334
- return True
335
- except subprocess.CalledProcessError as e:
336
- print_error(f"Failed to install dependencies: {e.stderr.decode()}")
337
- return False
338
-
339
-
340
- def create_env_file(config: Dict[str, str], force: bool = False) -> bool:
341
- """Create the .env configuration file."""
342
- env_file = AGENT_DIR / ".env"
343
-
344
- if env_file.exists() and not force:
345
- if not prompt_yes_no(".env file already exists. Overwrite?", default=False):
346
- print_success("Keeping existing .env file")
347
- return True
348
-
349
- # Build .env content
350
- lines = [
351
- "# Claude Memory Agent Configuration",
352
- "# Generated by install.py",
353
- f"# Installation date: {__import__('datetime').datetime.now().isoformat()}",
354
- "",
355
- "# Server Configuration",
356
- f"HOST={config['HOST']}",
357
- f"PORT={config['PORT']}",
358
- f"MEMORY_AGENT_URL={config['MEMORY_AGENT_URL']}",
359
- "",
360
- "# Ollama Configuration",
361
- f"OLLAMA_HOST={config['OLLAMA_HOST']}",
362
- f"EMBEDDING_MODEL={config['EMBEDDING_MODEL']}",
363
- "",
364
- "# Database Configuration",
365
- f"DATABASE_PATH={AGENT_DIR / 'memories.db'}",
366
- f"USE_VECTOR_INDEX={config['USE_VECTOR_INDEX']}",
367
- f"DB_POOL_SIZE={config['DB_POOL_SIZE']}",
368
- f"DB_TIMEOUT={config['DB_TIMEOUT']}",
369
- "",
370
- "# Logging",
371
- f"LOG_LEVEL={config['LOG_LEVEL']}",
372
- "",
373
- "# Authentication (disabled by default for local use)",
374
- f"AUTH_ENABLED={config['AUTH_ENABLED']}",
375
- ]
376
-
377
- try:
378
- env_file.write_text("\n".join(lines) + "\n")
379
- print_success(f"Created .env file at {env_file}")
380
- return True
381
- except Exception as e:
382
- print_error(f"Failed to create .env file: {e}")
383
- return False
384
-
385
-
386
- def create_startup_script() -> bool:
387
- """Create platform-specific startup script."""
388
- if sys.platform == "win32":
389
- return create_windows_startup_script()
390
- else:
391
- return create_unix_startup_script()
392
-
393
-
394
- def create_windows_startup_script() -> bool:
395
- """Create Windows VBS startup script with auto-detected paths."""
396
- vbs_file = AGENT_DIR / "start-memory-agent.vbs"
397
-
398
- # Use forward slashes for VBS string, then replace
399
- agent_dir_str = str(AGENT_DIR).replace("\\", "\\\\")
400
-
401
- content = f'''' Start Memory Agent silently in background
402
- ' Auto-generated by install.py
403
-
404
- Set WshShell = CreateObject("WScript.Shell")
405
- Set fso = CreateObject("Scripting.FileSystemObject")
406
-
407
- ' Get the directory where this script is located
408
- scriptPath = WScript.ScriptFullName
409
- agentDir = fso.GetParentFolderName(scriptPath)
410
-
411
- ' Alternatively, use the configured path (uncomment if needed):
412
- ' agentDir = "{agent_dir_str}"
413
-
414
- pythonCmd = "python """ & agentDir & "\\main.py"""
415
-
416
- WshShell.CurrentDirectory = agentDir
417
- WshShell.Run "cmd /c " & pythonCmd, 0, False
418
- '''
419
-
420
- try:
421
- vbs_file.write_text(content)
422
- print_success(f"Created Windows startup script: {vbs_file}")
423
- return True
424
- except Exception as e:
425
- print_error(f"Failed to create startup script: {e}")
426
- return False
427
-
428
-
429
- def create_unix_startup_script() -> bool:
430
- """Create Unix/Mac startup script."""
431
- sh_file = AGENT_DIR / "start-memory-agent.sh"
432
-
433
- content = f'''#!/bin/bash
434
- # Start Memory Agent in background
435
- # Auto-generated by install.py
436
-
437
- # Get the directory where this script is located
438
- SCRIPT_DIR="$(cd "$(dirname "${{BASH_SOURCE[0]}}")" && pwd)"
439
-
440
- cd "$SCRIPT_DIR"
441
- nohup python main.py > memory-agent.log 2>&1 &
442
- echo "Memory Agent started (PID: $!)"
443
- '''
444
-
445
- try:
446
- sh_file.write_text(content)
447
- sh_file.chmod(0o755)
448
- print_success(f"Created Unix startup script: {sh_file}")
449
- return True
450
- except Exception as e:
451
- print_error(f"Failed to create startup script: {e}")
452
- return False
453
-
454
-
455
- def configure_claude_mcp(config: Dict[str, str]) -> bool:
456
- """Configure Claude Code MCP settings."""
457
- settings_file = get_claude_settings_file()
458
- settings_dir = get_claude_settings_dir()
459
-
460
- # Ensure settings directory exists
461
- settings_dir.mkdir(parents=True, exist_ok=True)
462
-
463
- # Load existing settings or create new
464
- if settings_file.exists():
465
- try:
466
- settings = json.loads(settings_file.read_text())
467
- except json.JSONDecodeError:
468
- print_warning("Existing settings.json is invalid, creating backup")
469
- shutil.copy(settings_file, settings_file.with_suffix(".json.bak"))
470
- settings = {}
471
- else:
472
- settings = {}
473
-
474
- # Ensure mcpServers section exists
475
- if "mcpServers" not in settings:
476
- settings["mcpServers"] = {}
477
-
478
- # Add/update claude-memory server configuration
479
- settings["mcpServers"]["claude-memory"] = {
480
- "command": sys.executable,
481
- "args": [str(AGENT_DIR / "main.py")],
482
- "env": {
483
- "MEMORY_AGENT_URL": config["MEMORY_AGENT_URL"],
484
- "PORT": config["PORT"],
485
- }
486
- }
487
-
488
- try:
489
- settings_file.write_text(json.dumps(settings, indent=2))
490
- print_success(f"Configured Claude Code MCP settings: {settings_file}")
491
- return True
492
- except Exception as e:
493
- print_error(f"Failed to configure MCP settings: {e}")
494
- return False
495
-
496
-
497
- def setup_hooks(config: Dict[str, str]) -> bool:
498
- """Set up Claude Code hooks for auto-start and context injection."""
499
- hooks_dir = get_hooks_dir()
500
- hooks_dir.mkdir(parents=True, exist_ok=True)
501
-
502
- source_hooks_dir = AGENT_DIR / "hooks"
503
- if not source_hooks_dir.exists():
504
- print_warning("Hooks directory not found in agent, skipping hook setup")
505
- return True
506
-
507
- # Hooks to install
508
- hooks_to_install = [
509
- "session_start.py",
510
- "session_end.py",
511
- "grounding-hook.py",
512
- ]
513
-
514
- installed = 0
515
- for hook_name in hooks_to_install:
516
- source = source_hooks_dir / hook_name
517
- if not source.exists():
518
- continue
519
-
520
- dest = hooks_dir / hook_name
521
-
522
- # Read source and update MEMORY_AGENT_URL default
523
- content = source.read_text()
524
-
525
- # Copy to hooks directory
526
- try:
527
- dest.write_text(content)
528
- installed += 1
529
- except Exception as e:
530
- print_warning(f"Failed to install hook {hook_name}: {e}")
531
-
532
- if installed > 0:
533
- print_success(f"Installed {installed} hooks to {hooks_dir}")
534
-
535
- return True
536
-
537
-
538
- def configure_hooks_json() -> bool:
539
- """Configure hooks.json to enable the hooks."""
540
- hooks_file = get_claude_settings_dir() / "hooks.json"
541
-
542
- # Default hooks configuration
543
- hooks_config = {
544
- "hooks": {
545
- "UserPromptSubmit": [
546
- {
547
- "command": f"{sys.executable} {get_hooks_dir() / 'session_start.py'}",
548
- "description": "Initialize memory session",
549
- "timeout": 5000
550
- },
551
- {
552
- "command": f"{sys.executable} {get_hooks_dir() / 'grounding-hook.py'}",
553
- "description": "Inject grounding context",
554
- "timeout": 3000
555
- }
556
- ],
557
- "SessionEnd": [
558
- {
559
- "command": f"{sys.executable} {get_hooks_dir() / 'session_end.py'}",
560
- "description": "Save session summary",
561
- "timeout": 10000
562
- }
563
- ]
564
- }
565
- }
566
-
567
- # Merge with existing if present
568
- if hooks_file.exists():
569
- try:
570
- existing = json.loads(hooks_file.read_text())
571
- # Don't overwrite if user has customized
572
- if prompt_yes_no("hooks.json exists. Update with memory agent hooks?", default=True):
573
- if "hooks" not in existing:
574
- existing["hooks"] = {}
575
- existing["hooks"].update(hooks_config["hooks"])
576
- hooks_config = existing
577
- else:
578
- print_success("Keeping existing hooks.json")
579
- return True
580
- except json.JSONDecodeError:
581
- pass
582
-
583
- try:
584
- hooks_file.write_text(json.dumps(hooks_config, indent=2))
585
- print_success(f"Configured hooks: {hooks_file}")
586
- return True
587
- except Exception as e:
588
- print_error(f"Failed to configure hooks: {e}")
589
- return False
590
-
591
-
592
- def fix_agent_card_port() -> bool:
593
- """Fix the port in agent_card.py from 8100 to 8102."""
594
- agent_card_file = AGENT_DIR / "agent_card.py"
595
-
596
- if not agent_card_file.exists():
597
- return True
598
-
599
- content = agent_card_file.read_text()
600
- if '"url": "http://localhost:8100"' in content:
601
- content = content.replace(
602
- '"url": "http://localhost:8100"',
603
- '"url": "http://localhost:8102"'
604
- )
605
- agent_card_file.write_text(content)
606
- print_success("Fixed agent_card.py port (8100 -> 8102)")
607
-
608
- return True
609
-
610
-
611
- def fix_dashboard_urls() -> bool:
612
- """Make dashboard.html use dynamic URLs."""
613
- dashboard_file = AGENT_DIR / "dashboard.html"
614
-
615
- if not dashboard_file.exists():
616
- return True
617
-
618
- content = dashboard_file.read_text()
619
-
620
- # Replace hardcoded URLs with dynamic detection
621
- old_js = "const API_URL = 'http://localhost:8102';\n const WS_URL = 'ws://localhost:8102/ws';"
622
- new_js = """// Auto-detect server URL from current location
623
- const API_URL = window.location.origin || 'http://localhost:8102';
624
- const WS_URL = (window.location.protocol === 'https:' ? 'wss:' : 'ws:') + '//' + (window.location.host || 'localhost:8102') + '/ws';"""
625
-
626
- if old_js in content:
627
- content = content.replace(old_js, new_js)
628
- dashboard_file.write_text(content)
629
- print_success("Updated dashboard.html to use dynamic URLs")
630
-
631
- return True
632
-
633
-
634
- def fix_start_daemon_url(config: Dict[str, str]) -> bool:
635
- """Fix hardcoded URL in start_daemon.py."""
636
- daemon_file = AGENT_DIR / "start_daemon.py"
637
-
638
- if not daemon_file.exists():
639
- return True
640
-
641
- content = daemon_file.read_text()
642
-
643
- # Replace hardcoded health check URL with environment variable
644
- old_line = 'r = requests.get("http://localhost:8102/health", timeout=2)'
645
- new_line = f'r = requests.get(os.getenv("MEMORY_AGENT_URL", "http://localhost:8102") + "/health", timeout=2)'
646
-
647
- if old_line in content:
648
- content = content.replace(old_line, new_line)
649
- daemon_file.write_text(content)
650
- print_success("Updated start_daemon.py to use environment variable")
651
-
652
- return True
653
-
654
-
655
- def verify_installation() -> bool:
656
- """Verify the installation is working."""
657
- print("\nVerifying installation...")
658
-
659
- # Check .env exists
660
- if not (AGENT_DIR / ".env").exists():
661
- print_warning(".env file not created")
662
- return False
663
-
664
- # Try to import main module
665
- try:
666
- sys.path.insert(0, str(AGENT_DIR))
667
- from dotenv import load_dotenv
668
- load_dotenv(AGENT_DIR / ".env")
669
- print_success("Configuration loaded successfully")
670
- except Exception as e:
671
- print_warning(f"Could not load configuration: {e}")
672
-
673
- return True
674
-
675
-
676
- def print_post_install_instructions(config: Dict[str, str]):
677
- """Print instructions for after installation."""
678
- print_header("Installation Complete!")
679
-
680
- print("Next steps:")
681
- print("")
682
- print("1. Make sure Ollama is running with the embedding model:")
683
- print(f" ollama pull {config['EMBEDDING_MODEL']}")
684
- print(f" ollama serve")
685
- print("")
686
- print("2. Start the Memory Agent:")
687
- print(f" cd \"{AGENT_DIR}\"")
688
- print(f" python main.py")
689
- print("")
690
- print("3. Or use the startup script:")
691
- if sys.platform == "win32":
692
- print(f" Double-click: start-memory-agent.vbs")
693
- else:
694
- print(f" ./start-memory-agent.sh")
695
- print("")
696
- print("4. Open the dashboard in your browser:")
697
- print(f" {config['MEMORY_AGENT_URL']}/dashboard")
698
- print("")
699
- print("5. Restart Claude Code to load the MCP configuration")
700
- print("")
701
- print(f"Configuration file: {AGENT_DIR / '.env'}")
702
- print(f"Claude settings: {get_claude_settings_file()}")
703
-
704
-
705
- # =============================================================================
706
- # UNINSTALL
707
- # =============================================================================
708
-
709
- def uninstall() -> bool:
710
- """Remove Claude Code integration."""
711
- print_header("Uninstalling Claude Memory Agent Integration")
712
-
713
- # Remove MCP configuration
714
- settings_file = get_claude_settings_file()
715
- if settings_file.exists():
716
- try:
717
- settings = json.loads(settings_file.read_text())
718
- if "mcpServers" in settings and "claude-memory" in settings["mcpServers"]:
719
- del settings["mcpServers"]["claude-memory"]
720
- settings_file.write_text(json.dumps(settings, indent=2))
721
- print_success("Removed MCP configuration")
722
- except Exception as e:
723
- print_warning(f"Could not update settings: {e}")
724
-
725
- # Remove hooks
726
- hooks_dir = get_hooks_dir()
727
- hooks_to_remove = ["session_start.py", "session_end.py", "grounding-hook.py"]
728
- for hook in hooks_to_remove:
729
- hook_file = hooks_dir / hook
730
- if hook_file.exists():
731
- hook_file.unlink()
732
- print_success(f"Removed hook: {hook}")
733
-
734
- print("\nUninstall complete. The .env file and database are preserved.")
735
- print("To fully remove, delete the memory-agent directory.")
736
- return True
737
-
738
-
739
- # =============================================================================
740
- # MAIN
741
- # =============================================================================
742
-
743
- def main():
744
- parser = argparse.ArgumentParser(
745
- description="Install and configure Claude Memory Agent"
746
- )
747
- parser.add_argument(
748
- "--auto",
749
- action="store_true",
750
- help="Auto-install with default settings"
751
- )
752
- parser.add_argument(
753
- "--uninstall",
754
- action="store_true",
755
- help="Remove Claude Code integration"
756
- )
757
- parser.add_argument(
758
- "--port",
759
- type=int,
760
- default=8102,
761
- help="Port for the memory agent (default: 8102)"
762
- )
763
- parser.add_argument(
764
- "--skip-deps",
765
- action="store_true",
766
- help="Skip installing Python dependencies"
767
- )
768
- parser.add_argument(
769
- "--skip-claude-check",
770
- action="store_true",
771
- help="Skip Claude Code installation check (for standalone use)"
772
- )
773
-
774
- args = parser.parse_args()
775
-
776
- if args.uninstall:
777
- return 0 if uninstall() else 1
778
-
779
- print_header("Claude Memory Agent Installation")
780
- print(f"Agent directory: {AGENT_DIR}")
781
- print(f"Platform: {sys.platform}")
782
-
783
- # Step 1: Check prerequisites
784
- total_steps = 9
785
- print_step(1, total_steps, "Checking prerequisites...")
786
-
787
- if not check_python_version():
788
- return 1
789
-
790
- # Check Node.js and Claude Code (unless skipped)
791
- claude_ok = False
792
- if not args.skip_claude_check:
793
- nodejs_ok, nodejs_version = check_nodejs()
794
- npm_ok = False
795
- if nodejs_ok:
796
- npm_ok = check_npm()
797
-
798
- # Check Claude Code
799
- claude_ok, claude_version = check_claude_code()
800
-
801
- # If Claude Code not found, check if we can install it
802
- if not claude_ok:
803
- if not nodejs_ok:
804
- # Neither Node.js nor Claude Code - show instructions and exit
805
- print_installation_instructions()
806
- return 1
807
- elif npm_ok:
808
- # Node.js available but Claude Code not installed
809
- if args.auto or prompt_yes_no("Claude Code not found. Install it now?"):
810
- if not install_claude_code():
811
- print_error("Could not install Claude Code automatically.")
812
- print("Please install manually: npm install -g @anthropic-ai/claude-code")
813
- if not prompt_yes_no("Continue anyway (memory agent only)?", default=False):
814
- return 1
815
- else:
816
- claude_ok = True
817
- else:
818
- print_warning("Skipping Claude Code installation")
819
- print(" The memory agent will work, but Claude Code integration requires Claude Code")
820
- else:
821
- print_warning("npm not found - cannot auto-install Claude Code")
822
- print(" Install Claude Code manually: npm install -g @anthropic-ai/claude-code")
823
- else:
824
- print_success("Skipping Claude Code check (--skip-claude-check)")
825
- claude_ok = True # Assume it's OK for standalone mode
826
-
827
- # Check Ollama
828
- ollama_ok = check_ollama()
829
-
830
- # Step 2: Configure settings
831
- print_step(2, total_steps, "Configuring settings...")
832
-
833
- config = DEFAULT_CONFIG.copy()
834
- config["PORT"] = str(args.port)
835
- config["MEMORY_AGENT_URL"] = f"http://localhost:{args.port}"
836
-
837
- if not args.auto:
838
- if not ollama_ok:
839
- config["OLLAMA_HOST"] = prompt_value(
840
- "Ollama host URL",
841
- config["OLLAMA_HOST"]
842
- )
843
-
844
- if prompt_yes_no("Use default embedding model (nomic-embed-text)?"):
845
- pass
846
- else:
847
- config["EMBEDDING_MODEL"] = prompt_value(
848
- "Embedding model name",
849
- config["EMBEDDING_MODEL"]
850
- )
851
-
852
- if ollama_ok:
853
- check_ollama_model(config["EMBEDDING_MODEL"])
854
-
855
- # Step 3: Install dependencies
856
- print_step(3, total_steps, "Installing dependencies...")
857
- if not args.skip_deps:
858
- install_dependencies()
859
- else:
860
- print_success("Skipped dependency installation")
861
-
862
- # Step 4: Create .env file
863
- print_step(4, total_steps, "Creating configuration file...")
864
- if not create_env_file(config, force=args.auto):
865
- return 1
866
-
867
- # Step 5: Fix hardcoded values
868
- print_step(5, total_steps, "Fixing hardcoded values...")
869
- fix_agent_card_port()
870
- fix_dashboard_urls()
871
- fix_start_daemon_url(config)
872
-
873
- # Step 6: Create startup script
874
- print_step(6, total_steps, "Creating startup script...")
875
- create_startup_script()
876
-
877
- # Step 7: Configure Claude Code (only if Claude Code is available)
878
- print_step(7, total_steps, "Configuring Claude Code integration...")
879
-
880
- if claude_ok:
881
- if args.auto or prompt_yes_no("Configure Claude Code MCP settings?"):
882
- configure_claude_mcp(config)
883
-
884
- if args.auto or prompt_yes_no("Install Claude Code hooks?"):
885
- setup_hooks(config)
886
- configure_hooks_json()
887
- else:
888
- print_warning("Skipping Claude Code configuration (Claude Code not installed)")
889
- print(" Run 'python install.py' again after installing Claude Code")
890
-
891
- # Step 8: Verify
892
- print_step(8, total_steps, "Verifying installation...")
893
- verify_installation()
894
-
895
- # Done!
896
- print_post_install_instructions(config)
897
-
898
- return 0
899
-
900
-
901
- if __name__ == "__main__":
902
- sys.exit(main())
1
+ #!/usr/bin/env python3
2
+ """
3
+ Claude Memory Agent - Installation Script
4
+
5
+ This script sets up the Claude Memory Agent for first-time use:
6
+ 1. Creates .env file with auto-detected paths
7
+ 2. Configures Claude Code MCP settings
8
+ 3. Sets up hooks for auto-start and context injection
9
+ 4. Creates platform-specific startup scripts
10
+ 5. Installs Python dependencies
11
+
12
+ Usage:
13
+ python install.py # Interactive installation
14
+ python install.py --auto # Auto-install with defaults
15
+ python install.py --uninstall # Remove Claude Code integration
16
+ """
17
+ import os
18
+ import sys
19
+ import json
20
+ import shutil
21
+ import argparse
22
+ import subprocess
23
+ from pathlib import Path
24
+ from typing import Optional, Dict, Any
25
+
26
+ # =============================================================================
27
+ # CONFIGURATION
28
+ # =============================================================================
29
+
30
+ # Agent directory (where this script lives)
31
+ AGENT_DIR = Path(__file__).parent.resolve()
32
+
33
+ # Default configuration
34
+ DEFAULT_CONFIG = {
35
+ "PORT": "8102",
36
+ "HOST": "0.0.0.0",
37
+ "MEMORY_AGENT_URL": "http://localhost:8102",
38
+ "OLLAMA_HOST": "http://localhost:11434",
39
+ "EMBEDDING_MODEL": "Alibaba-NLP/gte-large-en-v1.5",
40
+ "EMBEDDING_PROVIDER": "sentence-transformers",
41
+ "LOG_LEVEL": "INFO",
42
+ "USE_VECTOR_INDEX": "true",
43
+ "DB_POOL_SIZE": "5",
44
+ "DB_TIMEOUT": "30.0",
45
+ "AUTH_ENABLED": "false",
46
+ }
47
+
48
+ # Claude Code settings paths
49
+ def get_claude_settings_dir() -> Path:
50
+ """Get the Claude Code settings directory."""
51
+ return Path.home() / ".claude"
52
+
53
+ def get_claude_settings_file() -> Path:
54
+ """Get the Claude Code settings.json file path."""
55
+ return get_claude_settings_dir() / "settings.json"
56
+
57
+ def get_hooks_dir() -> Path:
58
+ """Get the Claude Code hooks directory."""
59
+ return get_claude_settings_dir() / "hooks"
60
+
61
+
62
+ # =============================================================================
63
+ # UTILITY FUNCTIONS
64
+ # =============================================================================
65
+
66
+ def print_header(text: str):
67
+ """Print a formatted header."""
68
+ print(f"\n{'='*60}")
69
+ print(f" {text}")
70
+ print(f"{'='*60}\n")
71
+
72
+
73
+ def print_step(step: int, total: int, text: str):
74
+ """Print a step indicator."""
75
+ print(f"[{step}/{total}] {text}")
76
+
77
+
78
+ def print_success(text: str):
79
+ """Print a success message."""
80
+ print(f" [OK] {text}")
81
+
82
+
83
+ def print_warning(text: str):
84
+ """Print a warning message."""
85
+ print(f" [!!] {text}")
86
+
87
+
88
+ def print_error(text: str):
89
+ """Print an error message."""
90
+ print(f" [ERROR] {text}")
91
+
92
+
93
+ def prompt_yes_no(question: str, default: bool = True) -> bool:
94
+ """Prompt for yes/no answer."""
95
+ suffix = " [Y/n]: " if default else " [y/N]: "
96
+ while True:
97
+ answer = input(question + suffix).strip().lower()
98
+ if not answer:
99
+ return default
100
+ if answer in ("y", "yes"):
101
+ return True
102
+ if answer in ("n", "no"):
103
+ return False
104
+ print("Please answer 'y' or 'n'")
105
+
106
+
107
+ def prompt_value(question: str, default: str) -> str:
108
+ """Prompt for a value with a default."""
109
+ answer = input(f"{question} [{default}]: ").strip()
110
+ return answer if answer else default
111
+
112
+
113
+ def check_python_version() -> bool:
114
+ """Check if Python version is compatible."""
115
+ major, minor = sys.version_info[:2]
116
+ if major < 3 or (major == 3 and minor < 9):
117
+ print_error(f"Python 3.9+ required, found {major}.{minor}")
118
+ return False
119
+ print_success(f"Python {major}.{minor} detected")
120
+ return True
121
+
122
+
123
+ def check_nodejs() -> tuple[bool, Optional[str]]:
124
+ """Check if Node.js is installed and get version."""
125
+ try:
126
+ result = subprocess.run(
127
+ ["node", "--version"],
128
+ capture_output=True,
129
+ text=True,
130
+ timeout=5
131
+ )
132
+ if result.returncode == 0:
133
+ version = result.stdout.strip()
134
+ print_success(f"Node.js {version} detected")
135
+ return True, version
136
+ except FileNotFoundError:
137
+ pass
138
+ except subprocess.TimeoutExpired:
139
+ pass
140
+ except Exception:
141
+ pass
142
+
143
+ print_warning("Node.js not found")
144
+ return False, None
145
+
146
+
147
+ def check_npm() -> bool:
148
+ """Check if npm is available."""
149
+ # On Windows, npm is a .cmd file
150
+ commands_to_try = ["npm", "npm.cmd"] if sys.platform == "win32" else ["npm"]
151
+
152
+ for cmd in commands_to_try:
153
+ try:
154
+ result = subprocess.run(
155
+ [cmd, "--version"],
156
+ capture_output=True,
157
+ text=True,
158
+ timeout=5,
159
+ shell=(sys.platform == "win32") # Use shell on Windows
160
+ )
161
+ if result.returncode == 0:
162
+ print_success(f"npm {result.stdout.strip()} detected")
163
+ return True
164
+ except Exception:
165
+ continue
166
+ return False
167
+
168
+
169
+ def check_claude_code() -> tuple[bool, Optional[str]]:
170
+ """Check if Claude Code CLI is installed."""
171
+ # Try different possible command names
172
+ # On Windows, these may be .cmd files
173
+ if sys.platform == "win32":
174
+ commands_to_try = ["claude", "claude.cmd", "claude-code", "claude-code.cmd"]
175
+ else:
176
+ commands_to_try = ["claude", "claude-code"]
177
+
178
+ for cmd in commands_to_try:
179
+ try:
180
+ result = subprocess.run(
181
+ [cmd, "--version"],
182
+ capture_output=True,
183
+ text=True,
184
+ timeout=10,
185
+ shell=(sys.platform == "win32") # Use shell on Windows
186
+ )
187
+ if result.returncode == 0:
188
+ version = result.stdout.strip().split('\n')[0]
189
+ print_success(f"Claude Code detected: {version}")
190
+ return True, version
191
+ except FileNotFoundError:
192
+ continue
193
+ except subprocess.TimeoutExpired:
194
+ continue
195
+ except Exception:
196
+ continue
197
+
198
+ # Check if .claude directory exists (indicates Claude Code was used)
199
+ claude_dir = get_claude_settings_dir()
200
+ if claude_dir.exists():
201
+ print_success("Claude Code settings directory found (~/.claude)")
202
+ return True, "directory exists"
203
+
204
+ print_warning("Claude Code not detected")
205
+ return False, None
206
+
207
+
208
+ def install_claude_code() -> bool:
209
+ """Attempt to install Claude Code via npm."""
210
+ print("\nClaude Code is not installed. Installing via npm...")
211
+
212
+ # On Windows, use npm.cmd via shell
213
+ npm_cmd = "npm" if sys.platform != "win32" else "npm.cmd"
214
+
215
+ try:
216
+ result = subprocess.run(
217
+ [npm_cmd, "install", "-g", "@anthropic-ai/claude-code"],
218
+ capture_output=True,
219
+ text=True,
220
+ timeout=120,
221
+ shell=(sys.platform == "win32")
222
+ )
223
+ if result.returncode == 0:
224
+ print_success("Claude Code installed successfully!")
225
+ print(" Run 'claude' to start Claude Code")
226
+ return True
227
+ else:
228
+ print_error(f"npm install failed: {result.stderr}")
229
+ return False
230
+ except subprocess.TimeoutExpired:
231
+ print_error("Installation timed out")
232
+ return False
233
+ except Exception as e:
234
+ print_error(f"Installation failed: {e}")
235
+ return False
236
+
237
+
238
+ def print_installation_instructions():
239
+ """Print manual installation instructions for missing dependencies."""
240
+ print("\n" + "="*60)
241
+ print(" INSTALLATION REQUIRED")
242
+ print("="*60)
243
+ print("""
244
+ To use Claude Memory Agent, you need:
245
+
246
+ 1. NODE.JS (required for Claude Code)
247
+ Download from: https://nodejs.org/
248
+ - Windows: Download and run the installer
249
+ - Mac: brew install node
250
+ - Linux: sudo apt install nodejs npm
251
+
252
+ 2. CLAUDE CODE (the AI coding assistant)
253
+ After installing Node.js, run:
254
+ npm install -g @anthropic-ai/claude-code
255
+
256
+ 3. OLLAMA (for embeddings - optional but recommended)
257
+ Download from: https://ollama.ai/
258
+ Then run: ollama pull nomic-embed-text
259
+
260
+ After installing the prerequisites, run this installer again:
261
+ python install.py
262
+ """)
263
+
264
+
265
+ def check_ollama() -> bool:
266
+ """Check if Ollama is installed and running."""
267
+ try:
268
+ import requests
269
+ r = requests.get("http://localhost:11434/api/tags", timeout=2)
270
+ if r.status_code == 200:
271
+ print_success("Ollama is running")
272
+ return True
273
+ except Exception:
274
+ pass
275
+
276
+ print_warning("Ollama not detected")
277
+ print("")
278
+ print(" " + "="*56)
279
+ print(" OLLAMA (OPTIONAL)")
280
+ print(" " + "="*56)
281
+ print("")
282
+ print(" Ollama is optional. The default provider (sentence-transformers)")
283
+ print(" runs locally without Ollama. Install Ollama only if you prefer")
284
+ print(" the Ollama provider.")
285
+ print("")
286
+ print(" To install Ollama (if desired):")
287
+ print(" 1. Download from: https://ollama.ai/download")
288
+ print(" 2. Install and run: ollama pull nomic-embed-text")
289
+ print(" 3. Start Ollama: ollama serve")
290
+ print(" 4. Set EMBEDDING_PROVIDER=ollama in .env")
291
+ print("")
292
+ return False
293
+
294
+
295
+ def check_ollama_model(model: str = "nomic-embed-text") -> bool:
296
+ """Check if the embedding model is available in Ollama."""
297
+ try:
298
+ import requests
299
+ r = requests.get("http://localhost:11434/api/tags", timeout=2)
300
+ if r.status_code == 200:
301
+ models = r.json().get("models", [])
302
+ model_names = [m.get("name", "").split(":")[0] for m in models]
303
+ if model in model_names:
304
+ print_success(f"Embedding model '{model}' is available")
305
+ return True
306
+ print_warning(f"Model '{model}' not found. Run: ollama pull {model}")
307
+ except Exception:
308
+ pass
309
+ return False
310
+
311
+
312
+ # =============================================================================
313
+ # INSTALLATION STEPS
314
+ # =============================================================================
315
+
316
+ def install_dependencies() -> bool:
317
+ """Install Python dependencies from requirements.txt."""
318
+ requirements_file = AGENT_DIR / "requirements.txt"
319
+ if not requirements_file.exists():
320
+ print_warning("requirements.txt not found, skipping dependency installation")
321
+ return True
322
+
323
+ print("Installing Python dependencies...")
324
+ try:
325
+ subprocess.run(
326
+ [sys.executable, "-m", "pip", "install", "-r", str(requirements_file), "-q"],
327
+ check=True,
328
+ capture_output=True
329
+ )
330
+ print_success("Dependencies installed")
331
+ return True
332
+ except subprocess.CalledProcessError as e:
333
+ print_error(f"Failed to install dependencies: {e.stderr.decode()}")
334
+ return False
335
+
336
+
337
+ def create_env_file(config: Dict[str, str], force: bool = False) -> bool:
338
+ """Create the .env configuration file."""
339
+ env_file = AGENT_DIR / ".env"
340
+
341
+ if env_file.exists() and not force:
342
+ if not prompt_yes_no(".env file already exists. Overwrite?", default=False):
343
+ print_success("Keeping existing .env file")
344
+ return True
345
+
346
+ # Build .env content
347
+ lines = [
348
+ "# Claude Memory Agent Configuration",
349
+ "# Generated by install.py",
350
+ f"# Installation date: {__import__('datetime').datetime.now().isoformat()}",
351
+ "",
352
+ "# Server Configuration",
353
+ f"HOST={config['HOST']}",
354
+ f"PORT={config['PORT']}",
355
+ f"MEMORY_AGENT_URL={config['MEMORY_AGENT_URL']}",
356
+ "",
357
+ "# Embedding Configuration",
358
+ f"EMBEDDING_PROVIDER={config.get('EMBEDDING_PROVIDER', 'sentence-transformers')}",
359
+ f"EMBEDDING_MODEL={config['EMBEDDING_MODEL']}",
360
+ "",
361
+ "# Ollama Configuration (only needed if EMBEDDING_PROVIDER=ollama)",
362
+ f"OLLAMA_HOST={config['OLLAMA_HOST']}",
363
+ "",
364
+ "# Database Configuration",
365
+ f"DATABASE_PATH={AGENT_DIR / 'memories.db'}",
366
+ f"USE_VECTOR_INDEX={config['USE_VECTOR_INDEX']}",
367
+ f"DB_POOL_SIZE={config['DB_POOL_SIZE']}",
368
+ f"DB_TIMEOUT={config['DB_TIMEOUT']}",
369
+ "",
370
+ "# Logging",
371
+ f"LOG_LEVEL={config['LOG_LEVEL']}",
372
+ "",
373
+ "# Authentication (disabled by default for local use)",
374
+ f"AUTH_ENABLED={config['AUTH_ENABLED']}",
375
+ ]
376
+
377
+ try:
378
+ env_file.write_text("\n".join(lines) + "\n")
379
+ print_success(f"Created .env file at {env_file}")
380
+ return True
381
+ except Exception as e:
382
+ print_error(f"Failed to create .env file: {e}")
383
+ return False
384
+
385
+
386
+ def create_startup_script() -> bool:
387
+ """Create platform-specific startup script."""
388
+ if sys.platform == "win32":
389
+ return create_windows_startup_script()
390
+ else:
391
+ return create_unix_startup_script()
392
+
393
+
394
+ def create_windows_startup_script() -> bool:
395
+ """Create Windows VBS startup script with auto-detected paths."""
396
+ vbs_file = AGENT_DIR / "start-memory-agent.vbs"
397
+
398
+ # Use forward slashes for VBS string, then replace
399
+ agent_dir_str = str(AGENT_DIR).replace("\\", "\\\\")
400
+
401
+ content = f'''' Start Memory Agent silently in background
402
+ ' Auto-generated by install.py
403
+
404
+ Set WshShell = CreateObject("WScript.Shell")
405
+ Set fso = CreateObject("Scripting.FileSystemObject")
406
+
407
+ ' Get the directory where this script is located
408
+ scriptPath = WScript.ScriptFullName
409
+ agentDir = fso.GetParentFolderName(scriptPath)
410
+
411
+ ' Alternatively, use the configured path (uncomment if needed):
412
+ ' agentDir = "{agent_dir_str}"
413
+
414
+ pythonCmd = "python """ & agentDir & "\\main.py"""
415
+
416
+ WshShell.CurrentDirectory = agentDir
417
+ WshShell.Run "cmd /c " & pythonCmd, 0, False
418
+ '''
419
+
420
+ try:
421
+ vbs_file.write_text(content)
422
+ print_success(f"Created Windows startup script: {vbs_file}")
423
+ return True
424
+ except Exception as e:
425
+ print_error(f"Failed to create startup script: {e}")
426
+ return False
427
+
428
+
429
+ def create_unix_startup_script() -> bool:
430
+ """Create Unix/Mac startup script."""
431
+ sh_file = AGENT_DIR / "start-memory-agent.sh"
432
+
433
+ content = f'''#!/bin/bash
434
+ # Start Memory Agent in background
435
+ # Auto-generated by install.py
436
+
437
+ # Get the directory where this script is located
438
+ SCRIPT_DIR="$(cd "$(dirname "${{BASH_SOURCE[0]}}")" && pwd)"
439
+
440
+ cd "$SCRIPT_DIR"
441
+ nohup python main.py > memory-agent.log 2>&1 &
442
+ echo "Memory Agent started (PID: $!)"
443
+ '''
444
+
445
+ try:
446
+ sh_file.write_text(content)
447
+ sh_file.chmod(0o755)
448
+ print_success(f"Created Unix startup script: {sh_file}")
449
+ return True
450
+ except Exception as e:
451
+ print_error(f"Failed to create startup script: {e}")
452
+ return False
453
+
454
+
455
+ def _write_mcp_settings(settings_file: Path, config: Dict[str, str]) -> bool:
456
+ """Write MCP settings to a given settings file."""
457
+ settings_dir = settings_file.parent
458
+
459
+ # Ensure settings directory exists
460
+ settings_dir.mkdir(parents=True, exist_ok=True)
461
+
462
+ # Load existing settings or create new
463
+ if settings_file.exists():
464
+ try:
465
+ settings = json.loads(settings_file.read_text())
466
+ except json.JSONDecodeError:
467
+ print_warning(f"Existing {settings_file.name} is invalid, creating backup")
468
+ shutil.copy(settings_file, settings_file.with_suffix(".json.bak"))
469
+ settings = {}
470
+ else:
471
+ settings = {}
472
+
473
+ # Ensure mcpServers section exists
474
+ if "mcpServers" not in settings:
475
+ settings["mcpServers"] = {}
476
+
477
+ # Add/update claude-memory server configuration
478
+ settings["mcpServers"]["claude-memory"] = {
479
+ "command": sys.executable,
480
+ "args": [str(AGENT_DIR / "mcp_server.py")],
481
+ "env": {
482
+ "MEMORY_AGENT_URL": config["MEMORY_AGENT_URL"],
483
+ "PORT": config["PORT"],
484
+ }
485
+ }
486
+
487
+ try:
488
+ settings_file.write_text(json.dumps(settings, indent=2))
489
+ print_success(f"Configured Claude Code MCP settings: {settings_file}")
490
+ return True
491
+ except Exception as e:
492
+ print_error(f"Failed to configure MCP settings: {e}")
493
+ return False
494
+
495
+
496
+ def configure_claude_mcp(config: Dict[str, str], scope: str = "global", project_path: Optional[str] = None) -> bool:
497
+ """Configure Claude Code MCP settings.
498
+
499
+ Args:
500
+ config: Configuration dictionary with PORT, MEMORY_AGENT_URL, etc.
501
+ scope: Installation scope - 'global', 'project', or 'both'.
502
+ project_path: Project directory path for project-specific installation.
503
+ """
504
+ success = True
505
+
506
+ if scope in ("global", "both"):
507
+ settings_file = get_claude_settings_file()
508
+ if not _write_mcp_settings(settings_file, config):
509
+ success = False
510
+
511
+ if scope in ("project", "both"):
512
+ if project_path:
513
+ project_settings_dir = Path(project_path) / ".claude"
514
+ project_settings_file = project_settings_dir / "settings.local.json"
515
+ if not _write_mcp_settings(project_settings_file, config):
516
+ success = False
517
+ else:
518
+ print_warning("Project path not specified, skipping project-level MCP settings")
519
+ if scope == "project":
520
+ success = False
521
+
522
+ return success
523
+
524
+
525
+ def setup_hooks(config: Dict[str, str]) -> bool:
526
+ """Set up Claude Code hooks for auto-start and context injection."""
527
+ hooks_dir = get_hooks_dir()
528
+ hooks_dir.mkdir(parents=True, exist_ok=True)
529
+
530
+ source_hooks_dir = AGENT_DIR / "hooks"
531
+ if not source_hooks_dir.exists():
532
+ print_warning("Hooks directory not found in agent, skipping hook setup")
533
+ return True
534
+
535
+ # Hooks to install
536
+ hooks_to_install = [
537
+ "session_start.py",
538
+ "session_end.py",
539
+ "grounding-hook.py",
540
+ ]
541
+
542
+ installed = 0
543
+ for hook_name in hooks_to_install:
544
+ source = source_hooks_dir / hook_name
545
+ if not source.exists():
546
+ continue
547
+
548
+ dest = hooks_dir / hook_name
549
+
550
+ # Read source and update MEMORY_AGENT_URL default
551
+ content = source.read_text()
552
+
553
+ # Copy to hooks directory
554
+ try:
555
+ dest.write_text(content)
556
+ installed += 1
557
+ except Exception as e:
558
+ print_warning(f"Failed to install hook {hook_name}: {e}")
559
+
560
+ if installed > 0:
561
+ print_success(f"Installed {installed} hooks to {hooks_dir}")
562
+
563
+ return True
564
+
565
+
566
+ def configure_hooks_json() -> bool:
567
+ """Configure hooks.json to enable the hooks."""
568
+ hooks_file = get_claude_settings_dir() / "hooks.json"
569
+
570
+ # Default hooks configuration
571
+ hooks_config = {
572
+ "hooks": {
573
+ "UserPromptSubmit": [
574
+ {
575
+ "command": f"{sys.executable} {get_hooks_dir() / 'session_start.py'}",
576
+ "description": "Initialize memory session",
577
+ "timeout": 5000
578
+ },
579
+ {
580
+ "command": f"{sys.executable} {get_hooks_dir() / 'grounding-hook.py'}",
581
+ "description": "Inject grounding context",
582
+ "timeout": 3000
583
+ }
584
+ ],
585
+ "SessionEnd": [
586
+ {
587
+ "command": f"{sys.executable} {get_hooks_dir() / 'session_end.py'}",
588
+ "description": "Save session summary",
589
+ "timeout": 10000
590
+ }
591
+ ]
592
+ }
593
+ }
594
+
595
+ # Merge with existing if present
596
+ if hooks_file.exists():
597
+ try:
598
+ existing = json.loads(hooks_file.read_text())
599
+ # Don't overwrite if user has customized
600
+ if prompt_yes_no("hooks.json exists. Update with memory agent hooks?", default=True):
601
+ if "hooks" not in existing:
602
+ existing["hooks"] = {}
603
+ existing["hooks"].update(hooks_config["hooks"])
604
+ hooks_config = existing
605
+ else:
606
+ print_success("Keeping existing hooks.json")
607
+ return True
608
+ except json.JSONDecodeError:
609
+ pass
610
+
611
+ try:
612
+ hooks_file.write_text(json.dumps(hooks_config, indent=2))
613
+ print_success(f"Configured hooks: {hooks_file}")
614
+ return True
615
+ except Exception as e:
616
+ print_error(f"Failed to configure hooks: {e}")
617
+ return False
618
+
619
+
620
+ def fix_agent_card_port() -> bool:
621
+ """Fix the port in agent_card.py from 8100 to 8102."""
622
+ agent_card_file = AGENT_DIR / "agent_card.py"
623
+
624
+ if not agent_card_file.exists():
625
+ return True
626
+
627
+ content = agent_card_file.read_text()
628
+ if '"url": "http://localhost:8100"' in content:
629
+ content = content.replace(
630
+ '"url": "http://localhost:8100"',
631
+ '"url": "http://localhost:8102"'
632
+ )
633
+ agent_card_file.write_text(content)
634
+ print_success("Fixed agent_card.py port (8100 -> 8102)")
635
+
636
+ return True
637
+
638
+
639
+ def fix_dashboard_urls() -> bool:
640
+ """Make dashboard.html use dynamic URLs."""
641
+ dashboard_file = AGENT_DIR / "dashboard.html"
642
+
643
+ if not dashboard_file.exists():
644
+ return True
645
+
646
+ content = dashboard_file.read_text()
647
+
648
+ # Replace hardcoded URLs with dynamic detection
649
+ old_js = "const API_URL = 'http://localhost:8102';\n const WS_URL = 'ws://localhost:8102/ws';"
650
+ new_js = """// Auto-detect server URL from current location
651
+ const API_URL = window.location.origin || 'http://localhost:8102';
652
+ const WS_URL = (window.location.protocol === 'https:' ? 'wss:' : 'ws:') + '//' + (window.location.host || 'localhost:8102') + '/ws';"""
653
+
654
+ if old_js in content:
655
+ content = content.replace(old_js, new_js)
656
+ dashboard_file.write_text(content)
657
+ print_success("Updated dashboard.html to use dynamic URLs")
658
+
659
+ return True
660
+
661
+
662
+ def fix_start_daemon_url(config: Dict[str, str]) -> bool:
663
+ """Fix hardcoded URL in start_daemon.py."""
664
+ daemon_file = AGENT_DIR / "start_daemon.py"
665
+
666
+ if not daemon_file.exists():
667
+ return True
668
+
669
+ content = daemon_file.read_text()
670
+
671
+ # Replace hardcoded health check URL with environment variable
672
+ old_line = 'r = requests.get("http://localhost:8102/health", timeout=2)'
673
+ new_line = f'r = requests.get(os.getenv("MEMORY_AGENT_URL", "http://localhost:8102") + "/health", timeout=2)'
674
+
675
+ if old_line in content:
676
+ content = content.replace(old_line, new_line)
677
+ daemon_file.write_text(content)
678
+ print_success("Updated start_daemon.py to use environment variable")
679
+
680
+ return True
681
+
682
+
683
+ def verify_installation() -> bool:
684
+ """Verify the installation is working."""
685
+ print("\nVerifying installation...")
686
+
687
+ # Check .env exists
688
+ if not (AGENT_DIR / ".env").exists():
689
+ print_warning(".env file not created")
690
+ return False
691
+
692
+ # Try to import main module
693
+ try:
694
+ sys.path.insert(0, str(AGENT_DIR))
695
+ from dotenv import load_dotenv
696
+ load_dotenv(AGENT_DIR / ".env")
697
+ print_success("Configuration loaded successfully")
698
+ except Exception as e:
699
+ print_warning(f"Could not load configuration: {e}")
700
+
701
+ return True
702
+
703
+
704
+ def print_post_install_instructions(config: Dict[str, str]):
705
+ """Print instructions for after installation."""
706
+ print_header("Installation Complete!")
707
+
708
+ print("Next steps:")
709
+ print("")
710
+ print("1. (Optional) If using Ollama provider, make sure Ollama is running:")
711
+ print(f" ollama pull nomic-embed-text")
712
+ print(f" ollama serve")
713
+ print(f" Then set EMBEDDING_PROVIDER=ollama in .env")
714
+ print("")
715
+ print("2. Start the Memory Agent:")
716
+ print(f" cd \"{AGENT_DIR}\"")
717
+ print(f" python main.py")
718
+ print("")
719
+ print("3. Or use the startup script:")
720
+ if sys.platform == "win32":
721
+ print(f" Double-click: start-memory-agent.vbs")
722
+ else:
723
+ print(f" ./start-memory-agent.sh")
724
+ print("")
725
+ print("4. Open the dashboard in your browser:")
726
+ print(f" {config['MEMORY_AGENT_URL']}/dashboard")
727
+ print("")
728
+ print("5. Restart Claude Code to load the MCP configuration")
729
+ print("")
730
+ print(f"Configuration file: {AGENT_DIR / '.env'}")
731
+ print(f"Claude settings: {get_claude_settings_file()}")
732
+
733
+
734
+ # =============================================================================
735
+ # UNINSTALL
736
+ # =============================================================================
737
+
738
+ def uninstall() -> bool:
739
+ """Remove Claude Code integration."""
740
+ print_header("Uninstalling Claude Memory Agent Integration")
741
+
742
+ # Remove MCP configuration
743
+ settings_file = get_claude_settings_file()
744
+ if settings_file.exists():
745
+ try:
746
+ settings = json.loads(settings_file.read_text())
747
+ if "mcpServers" in settings and "claude-memory" in settings["mcpServers"]:
748
+ del settings["mcpServers"]["claude-memory"]
749
+ settings_file.write_text(json.dumps(settings, indent=2))
750
+ print_success("Removed MCP configuration")
751
+ except Exception as e:
752
+ print_warning(f"Could not update settings: {e}")
753
+
754
+ # Remove hooks
755
+ hooks_dir = get_hooks_dir()
756
+ hooks_to_remove = ["session_start.py", "session_end.py", "grounding-hook.py"]
757
+ for hook in hooks_to_remove:
758
+ hook_file = hooks_dir / hook
759
+ if hook_file.exists():
760
+ hook_file.unlink()
761
+ print_success(f"Removed hook: {hook}")
762
+
763
+ print("\nUninstall complete. The .env file and database are preserved.")
764
+ print("To fully remove, delete the memory-agent directory.")
765
+ return True
766
+
767
+
768
+ # =============================================================================
769
+ # MAIN
770
+ # =============================================================================
771
+
772
+ def main():
773
+ parser = argparse.ArgumentParser(
774
+ description="Install and configure Claude Memory Agent"
775
+ )
776
+ parser.add_argument(
777
+ "--auto",
778
+ action="store_true",
779
+ help="Auto-install with default settings"
780
+ )
781
+ parser.add_argument(
782
+ "--uninstall",
783
+ action="store_true",
784
+ help="Remove Claude Code integration"
785
+ )
786
+ parser.add_argument(
787
+ "--port",
788
+ type=int,
789
+ default=8102,
790
+ help="Port for the memory agent (default: 8102)"
791
+ )
792
+ parser.add_argument(
793
+ "--skip-deps",
794
+ action="store_true",
795
+ help="Skip installing Python dependencies"
796
+ )
797
+ parser.add_argument(
798
+ "--skip-claude-check",
799
+ action="store_true",
800
+ help="Skip Claude Code installation check (for standalone use)"
801
+ )
802
+ parser.add_argument(
803
+ "--skip-env",
804
+ action="store_true",
805
+ help="Skip .env file creation (already created by Node.js wizard)"
806
+ )
807
+ parser.add_argument(
808
+ "--scope",
809
+ choices=["global", "project", "both"],
810
+ default="global",
811
+ help="Installation scope for Claude Code settings"
812
+ )
813
+ parser.add_argument(
814
+ "--project-path",
815
+ type=str,
816
+ default=None,
817
+ help="Project path for project-specific installation"
818
+ )
819
+ parser.add_argument(
820
+ "--no-start",
821
+ action="store_true",
822
+ help="Don't auto-start the agent after installation"
823
+ )
824
+
825
+ args = parser.parse_args()
826
+
827
+ if args.uninstall:
828
+ return 0 if uninstall() else 1
829
+
830
+ print_header("Claude Memory Agent Installation")
831
+ print(f"Agent directory: {AGENT_DIR}")
832
+ print(f"Platform: {sys.platform}")
833
+
834
+ # Step 1: Check prerequisites
835
+ total_steps = 9
836
+ print_step(1, total_steps, "Checking prerequisites...")
837
+
838
+ if not check_python_version():
839
+ return 1
840
+
841
+ # Check Node.js and Claude Code (unless skipped)
842
+ claude_ok = False
843
+ if not args.skip_claude_check:
844
+ nodejs_ok, nodejs_version = check_nodejs()
845
+ npm_ok = False
846
+ if nodejs_ok:
847
+ npm_ok = check_npm()
848
+
849
+ # Check Claude Code
850
+ claude_ok, claude_version = check_claude_code()
851
+
852
+ # If Claude Code not found, check if we can install it
853
+ if not claude_ok:
854
+ if not nodejs_ok:
855
+ # Neither Node.js nor Claude Code - show instructions and exit
856
+ print_installation_instructions()
857
+ return 1
858
+ elif npm_ok:
859
+ # Node.js available but Claude Code not installed
860
+ if args.auto or prompt_yes_no("Claude Code not found. Install it now?"):
861
+ if not install_claude_code():
862
+ print_error("Could not install Claude Code automatically.")
863
+ print("Please install manually: npm install -g @anthropic-ai/claude-code")
864
+ if not prompt_yes_no("Continue anyway (memory agent only)?", default=False):
865
+ return 1
866
+ else:
867
+ claude_ok = True
868
+ else:
869
+ print_warning("Skipping Claude Code installation")
870
+ print(" The memory agent will work, but Claude Code integration requires Claude Code")
871
+ else:
872
+ print_warning("npm not found - cannot auto-install Claude Code")
873
+ print(" Install Claude Code manually: npm install -g @anthropic-ai/claude-code")
874
+ else:
875
+ print_success("Skipping Claude Code check (--skip-claude-check)")
876
+ claude_ok = True # Assume it's OK for standalone mode
877
+
878
+ # Check Ollama
879
+ ollama_ok = check_ollama()
880
+
881
+ # Step 2: Configure settings
882
+ print_step(2, total_steps, "Configuring settings...")
883
+
884
+ config = DEFAULT_CONFIG.copy()
885
+ config["PORT"] = str(args.port)
886
+ config["MEMORY_AGENT_URL"] = f"http://localhost:{args.port}"
887
+
888
+ if not args.auto:
889
+ if not ollama_ok:
890
+ config["OLLAMA_HOST"] = prompt_value(
891
+ "Ollama host URL",
892
+ config["OLLAMA_HOST"]
893
+ )
894
+
895
+ if prompt_yes_no("Use default embedding model (gte-large-en-v1.5 via sentence-transformers)?"):
896
+ pass
897
+ else:
898
+ config["EMBEDDING_MODEL"] = prompt_value(
899
+ "Embedding model name",
900
+ config["EMBEDDING_MODEL"]
901
+ )
902
+
903
+ if ollama_ok:
904
+ check_ollama_model(config["EMBEDDING_MODEL"])
905
+
906
+ # Step 3: Install dependencies
907
+ print_step(3, total_steps, "Installing dependencies...")
908
+ if not args.skip_deps:
909
+ install_dependencies()
910
+ else:
911
+ print_success("Skipped dependency installation")
912
+
913
+ # Step 4: Create .env file
914
+ print_step(4, total_steps, "Creating configuration file...")
915
+ if not args.skip_env:
916
+ if not create_env_file(config, force=args.auto):
917
+ return 1
918
+ else:
919
+ print_success("Skipped .env creation (--skip-env)")
920
+
921
+ # Step 5: Fix hardcoded values
922
+ print_step(5, total_steps, "Fixing hardcoded values...")
923
+ fix_agent_card_port()
924
+ fix_dashboard_urls()
925
+ fix_start_daemon_url(config)
926
+
927
+ # Step 6: Create startup script
928
+ print_step(6, total_steps, "Creating startup script...")
929
+ create_startup_script()
930
+
931
+ # Step 7: Configure Claude Code (only if Claude Code is available)
932
+ print_step(7, total_steps, "Configuring Claude Code integration...")
933
+
934
+ if claude_ok:
935
+ if args.auto or prompt_yes_no("Configure Claude Code MCP settings?"):
936
+ configure_claude_mcp(config, scope=args.scope, project_path=args.project_path)
937
+
938
+ if args.auto or prompt_yes_no("Install Claude Code hooks?"):
939
+ setup_hooks(config)
940
+ configure_hooks_json()
941
+ else:
942
+ print_warning("Skipping Claude Code configuration (Claude Code not installed)")
943
+ print(" Run 'python install.py' again after installing Claude Code")
944
+
945
+ # Step 8: Verify
946
+ print_step(8, total_steps, "Verifying installation...")
947
+ verify_installation()
948
+
949
+ # Step 9: Auto-start agent
950
+ print_step(9, total_steps, "Starting Memory Agent...")
951
+ if args.no_start:
952
+ print_success("Skipped auto-start (--no-start)")
953
+ else:
954
+ try:
955
+ subprocess.run(
956
+ [sys.executable, str(AGENT_DIR / "memory-agent"), "start"],
957
+ cwd=str(AGENT_DIR),
958
+ timeout=30
959
+ )
960
+ print_success("Memory Agent started!")
961
+ except Exception as e:
962
+ print_warning(f"Could not auto-start agent: {e}")
963
+ print(" Start manually with: python main.py")
964
+
965
+ # Done!
966
+ print_post_install_instructions(config)
967
+
968
+ return 0
969
+
970
+
971
+ if __name__ == "__main__":
972
+ sys.exit(main())