claude-memory-agent 2.0.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 (100) hide show
  1. package/.env.example +107 -0
  2. package/README.md +200 -0
  3. package/agent_card.py +512 -0
  4. package/bin/cli.js +181 -0
  5. package/bin/postinstall.js +216 -0
  6. package/config.py +104 -0
  7. package/dashboard.html +2689 -0
  8. package/hooks/README.md +196 -0
  9. package/hooks/__pycache__/auto-detect-response.cpython-312.pyc +0 -0
  10. package/hooks/__pycache__/auto_capture.cpython-312.pyc +0 -0
  11. package/hooks/__pycache__/session_end.cpython-312.pyc +0 -0
  12. package/hooks/__pycache__/session_start.cpython-312.pyc +0 -0
  13. package/hooks/auto-detect-response.py +348 -0
  14. package/hooks/auto_capture.py +255 -0
  15. package/hooks/detect-correction.py +173 -0
  16. package/hooks/grounding-hook.py +348 -0
  17. package/hooks/log-tool-use.py +234 -0
  18. package/hooks/log-user-request.py +208 -0
  19. package/hooks/pre-tool-decision.py +218 -0
  20. package/hooks/problem-detector.py +343 -0
  21. package/hooks/session_end.py +192 -0
  22. package/hooks/session_start.py +227 -0
  23. package/install.py +887 -0
  24. package/main.py +2859 -0
  25. package/manager.py +997 -0
  26. package/package.json +55 -0
  27. package/requirements.txt +8 -0
  28. package/run_server.py +136 -0
  29. package/services/__init__.py +50 -0
  30. package/services/__pycache__/__init__.cpython-312.pyc +0 -0
  31. package/services/__pycache__/agent_registry.cpython-312.pyc +0 -0
  32. package/services/__pycache__/auth.cpython-312.pyc +0 -0
  33. package/services/__pycache__/auto_inject.cpython-312.pyc +0 -0
  34. package/services/__pycache__/claude_md_sync.cpython-312.pyc +0 -0
  35. package/services/__pycache__/cleanup.cpython-312.pyc +0 -0
  36. package/services/__pycache__/compaction_flush.cpython-312.pyc +0 -0
  37. package/services/__pycache__/confidence.cpython-312.pyc +0 -0
  38. package/services/__pycache__/daily_log.cpython-312.pyc +0 -0
  39. package/services/__pycache__/database.cpython-312.pyc +0 -0
  40. package/services/__pycache__/embeddings.cpython-312.pyc +0 -0
  41. package/services/__pycache__/insights.cpython-312.pyc +0 -0
  42. package/services/__pycache__/llm_analyzer.cpython-312.pyc +0 -0
  43. package/services/__pycache__/memory_md_sync.cpython-312.pyc +0 -0
  44. package/services/__pycache__/retry_queue.cpython-312.pyc +0 -0
  45. package/services/__pycache__/timeline.cpython-312.pyc +0 -0
  46. package/services/__pycache__/vector_index.cpython-312.pyc +0 -0
  47. package/services/__pycache__/websocket.cpython-312.pyc +0 -0
  48. package/services/agent_registry.py +753 -0
  49. package/services/auth.py +331 -0
  50. package/services/auto_inject.py +250 -0
  51. package/services/claude_md_sync.py +275 -0
  52. package/services/cleanup.py +667 -0
  53. package/services/compaction_flush.py +447 -0
  54. package/services/confidence.py +301 -0
  55. package/services/daily_log.py +333 -0
  56. package/services/database.py +2485 -0
  57. package/services/embeddings.py +358 -0
  58. package/services/insights.py +632 -0
  59. package/services/llm_analyzer.py +595 -0
  60. package/services/memory_md_sync.py +409 -0
  61. package/services/retry_queue.py +453 -0
  62. package/services/timeline.py +579 -0
  63. package/services/vector_index.py +398 -0
  64. package/services/websocket.py +257 -0
  65. package/skills/__init__.py +6 -0
  66. package/skills/__pycache__/__init__.cpython-312.pyc +0 -0
  67. package/skills/__pycache__/admin.cpython-312.pyc +0 -0
  68. package/skills/__pycache__/checkpoint.cpython-312.pyc +0 -0
  69. package/skills/__pycache__/claude_md.cpython-312.pyc +0 -0
  70. package/skills/__pycache__/cleanup.cpython-312.pyc +0 -0
  71. package/skills/__pycache__/grounding.cpython-312.pyc +0 -0
  72. package/skills/__pycache__/insights.cpython-312.pyc +0 -0
  73. package/skills/__pycache__/natural_language.cpython-312.pyc +0 -0
  74. package/skills/__pycache__/retrieve.cpython-312.pyc +0 -0
  75. package/skills/__pycache__/search.cpython-312.pyc +0 -0
  76. package/skills/__pycache__/state.cpython-312.pyc +0 -0
  77. package/skills/__pycache__/store.cpython-312.pyc +0 -0
  78. package/skills/__pycache__/summarize.cpython-312.pyc +0 -0
  79. package/skills/__pycache__/timeline.cpython-312.pyc +0 -0
  80. package/skills/__pycache__/verification.cpython-312.pyc +0 -0
  81. package/skills/admin.py +469 -0
  82. package/skills/checkpoint.py +198 -0
  83. package/skills/claude_md.py +363 -0
  84. package/skills/cleanup.py +241 -0
  85. package/skills/grounding.py +801 -0
  86. package/skills/insights.py +231 -0
  87. package/skills/natural_language.py +277 -0
  88. package/skills/retrieve.py +67 -0
  89. package/skills/search.py +213 -0
  90. package/skills/state.py +182 -0
  91. package/skills/store.py +179 -0
  92. package/skills/summarize.py +588 -0
  93. package/skills/timeline.py +387 -0
  94. package/skills/verification.py +391 -0
  95. package/start_daemon.py +155 -0
  96. package/test_automation.py +221 -0
  97. package/test_complete.py +338 -0
  98. package/test_full.py +322 -0
  99. package/update_system.py +817 -0
  100. package/verify_db.py +134 -0
package/install.py ADDED
@@ -0,0 +1,887 @@
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
+ print_warning("Ollama not detected - embeddings will be disabled until Ollama is running")
280
+ return False
281
+
282
+
283
+ def check_ollama_model(model: str = "nomic-embed-text") -> bool:
284
+ """Check if the embedding model is available in Ollama."""
285
+ try:
286
+ import requests
287
+ r = requests.get("http://localhost:11434/api/tags", timeout=2)
288
+ if r.status_code == 200:
289
+ models = r.json().get("models", [])
290
+ model_names = [m.get("name", "").split(":")[0] for m in models]
291
+ if model in model_names:
292
+ print_success(f"Embedding model '{model}' is available")
293
+ return True
294
+ print_warning(f"Model '{model}' not found. Run: ollama pull {model}")
295
+ except Exception:
296
+ pass
297
+ return False
298
+
299
+
300
+ # =============================================================================
301
+ # INSTALLATION STEPS
302
+ # =============================================================================
303
+
304
+ def install_dependencies() -> bool:
305
+ """Install Python dependencies from requirements.txt."""
306
+ requirements_file = AGENT_DIR / "requirements.txt"
307
+ if not requirements_file.exists():
308
+ print_warning("requirements.txt not found, skipping dependency installation")
309
+ return True
310
+
311
+ print("Installing Python dependencies...")
312
+ try:
313
+ subprocess.run(
314
+ [sys.executable, "-m", "pip", "install", "-r", str(requirements_file), "-q"],
315
+ check=True,
316
+ capture_output=True
317
+ )
318
+ print_success("Dependencies installed")
319
+ return True
320
+ except subprocess.CalledProcessError as e:
321
+ print_error(f"Failed to install dependencies: {e.stderr.decode()}")
322
+ return False
323
+
324
+
325
+ def create_env_file(config: Dict[str, str], force: bool = False) -> bool:
326
+ """Create the .env configuration file."""
327
+ env_file = AGENT_DIR / ".env"
328
+
329
+ if env_file.exists() and not force:
330
+ if not prompt_yes_no(".env file already exists. Overwrite?", default=False):
331
+ print_success("Keeping existing .env file")
332
+ return True
333
+
334
+ # Build .env content
335
+ lines = [
336
+ "# Claude Memory Agent Configuration",
337
+ "# Generated by install.py",
338
+ f"# Installation date: {__import__('datetime').datetime.now().isoformat()}",
339
+ "",
340
+ "# Server Configuration",
341
+ f"HOST={config['HOST']}",
342
+ f"PORT={config['PORT']}",
343
+ f"MEMORY_AGENT_URL={config['MEMORY_AGENT_URL']}",
344
+ "",
345
+ "# Ollama Configuration",
346
+ f"OLLAMA_HOST={config['OLLAMA_HOST']}",
347
+ f"EMBEDDING_MODEL={config['EMBEDDING_MODEL']}",
348
+ "",
349
+ "# Database Configuration",
350
+ f"DATABASE_PATH={AGENT_DIR / 'memories.db'}",
351
+ f"USE_VECTOR_INDEX={config['USE_VECTOR_INDEX']}",
352
+ f"DB_POOL_SIZE={config['DB_POOL_SIZE']}",
353
+ f"DB_TIMEOUT={config['DB_TIMEOUT']}",
354
+ "",
355
+ "# Logging",
356
+ f"LOG_LEVEL={config['LOG_LEVEL']}",
357
+ "",
358
+ "# Authentication (disabled by default for local use)",
359
+ f"AUTH_ENABLED={config['AUTH_ENABLED']}",
360
+ ]
361
+
362
+ try:
363
+ env_file.write_text("\n".join(lines) + "\n")
364
+ print_success(f"Created .env file at {env_file}")
365
+ return True
366
+ except Exception as e:
367
+ print_error(f"Failed to create .env file: {e}")
368
+ return False
369
+
370
+
371
+ def create_startup_script() -> bool:
372
+ """Create platform-specific startup script."""
373
+ if sys.platform == "win32":
374
+ return create_windows_startup_script()
375
+ else:
376
+ return create_unix_startup_script()
377
+
378
+
379
+ def create_windows_startup_script() -> bool:
380
+ """Create Windows VBS startup script with auto-detected paths."""
381
+ vbs_file = AGENT_DIR / "start-memory-agent.vbs"
382
+
383
+ # Use forward slashes for VBS string, then replace
384
+ agent_dir_str = str(AGENT_DIR).replace("\\", "\\\\")
385
+
386
+ content = f'''' Start Memory Agent silently in background
387
+ ' Auto-generated by install.py
388
+
389
+ Set WshShell = CreateObject("WScript.Shell")
390
+ Set fso = CreateObject("Scripting.FileSystemObject")
391
+
392
+ ' Get the directory where this script is located
393
+ scriptPath = WScript.ScriptFullName
394
+ agentDir = fso.GetParentFolderName(scriptPath)
395
+
396
+ ' Alternatively, use the configured path (uncomment if needed):
397
+ ' agentDir = "{agent_dir_str}"
398
+
399
+ pythonCmd = "python """ & agentDir & "\\main.py"""
400
+
401
+ WshShell.CurrentDirectory = agentDir
402
+ WshShell.Run "cmd /c " & pythonCmd, 0, False
403
+ '''
404
+
405
+ try:
406
+ vbs_file.write_text(content)
407
+ print_success(f"Created Windows startup script: {vbs_file}")
408
+ return True
409
+ except Exception as e:
410
+ print_error(f"Failed to create startup script: {e}")
411
+ return False
412
+
413
+
414
+ def create_unix_startup_script() -> bool:
415
+ """Create Unix/Mac startup script."""
416
+ sh_file = AGENT_DIR / "start-memory-agent.sh"
417
+
418
+ content = f'''#!/bin/bash
419
+ # Start Memory Agent in background
420
+ # Auto-generated by install.py
421
+
422
+ # Get the directory where this script is located
423
+ SCRIPT_DIR="$(cd "$(dirname "${{BASH_SOURCE[0]}}")" && pwd)"
424
+
425
+ cd "$SCRIPT_DIR"
426
+ nohup python main.py > memory-agent.log 2>&1 &
427
+ echo "Memory Agent started (PID: $!)"
428
+ '''
429
+
430
+ try:
431
+ sh_file.write_text(content)
432
+ sh_file.chmod(0o755)
433
+ print_success(f"Created Unix startup script: {sh_file}")
434
+ return True
435
+ except Exception as e:
436
+ print_error(f"Failed to create startup script: {e}")
437
+ return False
438
+
439
+
440
+ def configure_claude_mcp(config: Dict[str, str]) -> bool:
441
+ """Configure Claude Code MCP settings."""
442
+ settings_file = get_claude_settings_file()
443
+ settings_dir = get_claude_settings_dir()
444
+
445
+ # Ensure settings directory exists
446
+ settings_dir.mkdir(parents=True, exist_ok=True)
447
+
448
+ # Load existing settings or create new
449
+ if settings_file.exists():
450
+ try:
451
+ settings = json.loads(settings_file.read_text())
452
+ except json.JSONDecodeError:
453
+ print_warning("Existing settings.json is invalid, creating backup")
454
+ shutil.copy(settings_file, settings_file.with_suffix(".json.bak"))
455
+ settings = {}
456
+ else:
457
+ settings = {}
458
+
459
+ # Ensure mcpServers section exists
460
+ if "mcpServers" not in settings:
461
+ settings["mcpServers"] = {}
462
+
463
+ # Add/update claude-memory server configuration
464
+ settings["mcpServers"]["claude-memory"] = {
465
+ "command": sys.executable,
466
+ "args": [str(AGENT_DIR / "main.py")],
467
+ "env": {
468
+ "MEMORY_AGENT_URL": config["MEMORY_AGENT_URL"],
469
+ "PORT": config["PORT"],
470
+ }
471
+ }
472
+
473
+ try:
474
+ settings_file.write_text(json.dumps(settings, indent=2))
475
+ print_success(f"Configured Claude Code MCP settings: {settings_file}")
476
+ return True
477
+ except Exception as e:
478
+ print_error(f"Failed to configure MCP settings: {e}")
479
+ return False
480
+
481
+
482
+ def setup_hooks(config: Dict[str, str]) -> bool:
483
+ """Set up Claude Code hooks for auto-start and context injection."""
484
+ hooks_dir = get_hooks_dir()
485
+ hooks_dir.mkdir(parents=True, exist_ok=True)
486
+
487
+ source_hooks_dir = AGENT_DIR / "hooks"
488
+ if not source_hooks_dir.exists():
489
+ print_warning("Hooks directory not found in agent, skipping hook setup")
490
+ return True
491
+
492
+ # Hooks to install
493
+ hooks_to_install = [
494
+ "session_start.py",
495
+ "session_end.py",
496
+ "grounding-hook.py",
497
+ ]
498
+
499
+ installed = 0
500
+ for hook_name in hooks_to_install:
501
+ source = source_hooks_dir / hook_name
502
+ if not source.exists():
503
+ continue
504
+
505
+ dest = hooks_dir / hook_name
506
+
507
+ # Read source and update MEMORY_AGENT_URL default
508
+ content = source.read_text()
509
+
510
+ # Copy to hooks directory
511
+ try:
512
+ dest.write_text(content)
513
+ installed += 1
514
+ except Exception as e:
515
+ print_warning(f"Failed to install hook {hook_name}: {e}")
516
+
517
+ if installed > 0:
518
+ print_success(f"Installed {installed} hooks to {hooks_dir}")
519
+
520
+ return True
521
+
522
+
523
+ def configure_hooks_json() -> bool:
524
+ """Configure hooks.json to enable the hooks."""
525
+ hooks_file = get_claude_settings_dir() / "hooks.json"
526
+
527
+ # Default hooks configuration
528
+ hooks_config = {
529
+ "hooks": {
530
+ "UserPromptSubmit": [
531
+ {
532
+ "command": f"{sys.executable} {get_hooks_dir() / 'session_start.py'}",
533
+ "description": "Initialize memory session",
534
+ "timeout": 5000
535
+ },
536
+ {
537
+ "command": f"{sys.executable} {get_hooks_dir() / 'grounding-hook.py'}",
538
+ "description": "Inject grounding context",
539
+ "timeout": 3000
540
+ }
541
+ ],
542
+ "SessionEnd": [
543
+ {
544
+ "command": f"{sys.executable} {get_hooks_dir() / 'session_end.py'}",
545
+ "description": "Save session summary",
546
+ "timeout": 10000
547
+ }
548
+ ]
549
+ }
550
+ }
551
+
552
+ # Merge with existing if present
553
+ if hooks_file.exists():
554
+ try:
555
+ existing = json.loads(hooks_file.read_text())
556
+ # Don't overwrite if user has customized
557
+ if prompt_yes_no("hooks.json exists. Update with memory agent hooks?", default=True):
558
+ if "hooks" not in existing:
559
+ existing["hooks"] = {}
560
+ existing["hooks"].update(hooks_config["hooks"])
561
+ hooks_config = existing
562
+ else:
563
+ print_success("Keeping existing hooks.json")
564
+ return True
565
+ except json.JSONDecodeError:
566
+ pass
567
+
568
+ try:
569
+ hooks_file.write_text(json.dumps(hooks_config, indent=2))
570
+ print_success(f"Configured hooks: {hooks_file}")
571
+ return True
572
+ except Exception as e:
573
+ print_error(f"Failed to configure hooks: {e}")
574
+ return False
575
+
576
+
577
+ def fix_agent_card_port() -> bool:
578
+ """Fix the port in agent_card.py from 8100 to 8102."""
579
+ agent_card_file = AGENT_DIR / "agent_card.py"
580
+
581
+ if not agent_card_file.exists():
582
+ return True
583
+
584
+ content = agent_card_file.read_text()
585
+ if '"url": "http://localhost:8100"' in content:
586
+ content = content.replace(
587
+ '"url": "http://localhost:8100"',
588
+ '"url": "http://localhost:8102"'
589
+ )
590
+ agent_card_file.write_text(content)
591
+ print_success("Fixed agent_card.py port (8100 -> 8102)")
592
+
593
+ return True
594
+
595
+
596
+ def fix_dashboard_urls() -> bool:
597
+ """Make dashboard.html use dynamic URLs."""
598
+ dashboard_file = AGENT_DIR / "dashboard.html"
599
+
600
+ if not dashboard_file.exists():
601
+ return True
602
+
603
+ content = dashboard_file.read_text()
604
+
605
+ # Replace hardcoded URLs with dynamic detection
606
+ old_js = "const API_URL = 'http://localhost:8102';\n const WS_URL = 'ws://localhost:8102/ws';"
607
+ new_js = """// Auto-detect server URL from current location
608
+ const API_URL = window.location.origin || 'http://localhost:8102';
609
+ const WS_URL = (window.location.protocol === 'https:' ? 'wss:' : 'ws:') + '//' + (window.location.host || 'localhost:8102') + '/ws';"""
610
+
611
+ if old_js in content:
612
+ content = content.replace(old_js, new_js)
613
+ dashboard_file.write_text(content)
614
+ print_success("Updated dashboard.html to use dynamic URLs")
615
+
616
+ return True
617
+
618
+
619
+ def fix_start_daemon_url(config: Dict[str, str]) -> bool:
620
+ """Fix hardcoded URL in start_daemon.py."""
621
+ daemon_file = AGENT_DIR / "start_daemon.py"
622
+
623
+ if not daemon_file.exists():
624
+ return True
625
+
626
+ content = daemon_file.read_text()
627
+
628
+ # Replace hardcoded health check URL with environment variable
629
+ old_line = 'r = requests.get("http://localhost:8102/health", timeout=2)'
630
+ new_line = f'r = requests.get(os.getenv("MEMORY_AGENT_URL", "http://localhost:8102") + "/health", timeout=2)'
631
+
632
+ if old_line in content:
633
+ content = content.replace(old_line, new_line)
634
+ daemon_file.write_text(content)
635
+ print_success("Updated start_daemon.py to use environment variable")
636
+
637
+ return True
638
+
639
+
640
+ def verify_installation() -> bool:
641
+ """Verify the installation is working."""
642
+ print("\nVerifying installation...")
643
+
644
+ # Check .env exists
645
+ if not (AGENT_DIR / ".env").exists():
646
+ print_warning(".env file not created")
647
+ return False
648
+
649
+ # Try to import main module
650
+ try:
651
+ sys.path.insert(0, str(AGENT_DIR))
652
+ from dotenv import load_dotenv
653
+ load_dotenv(AGENT_DIR / ".env")
654
+ print_success("Configuration loaded successfully")
655
+ except Exception as e:
656
+ print_warning(f"Could not load configuration: {e}")
657
+
658
+ return True
659
+
660
+
661
+ def print_post_install_instructions(config: Dict[str, str]):
662
+ """Print instructions for after installation."""
663
+ print_header("Installation Complete!")
664
+
665
+ print("Next steps:")
666
+ print("")
667
+ print("1. Make sure Ollama is running with the embedding model:")
668
+ print(f" ollama pull {config['EMBEDDING_MODEL']}")
669
+ print(f" ollama serve")
670
+ print("")
671
+ print("2. Start the Memory Agent:")
672
+ print(f" cd \"{AGENT_DIR}\"")
673
+ print(f" python main.py")
674
+ print("")
675
+ print("3. Or use the startup script:")
676
+ if sys.platform == "win32":
677
+ print(f" Double-click: start-memory-agent.vbs")
678
+ else:
679
+ print(f" ./start-memory-agent.sh")
680
+ print("")
681
+ print("4. Open the dashboard in your browser:")
682
+ print(f" {config['MEMORY_AGENT_URL']}/dashboard")
683
+ print("")
684
+ print("5. Restart Claude Code to load the MCP configuration")
685
+ print("")
686
+ print(f"Configuration file: {AGENT_DIR / '.env'}")
687
+ print(f"Claude settings: {get_claude_settings_file()}")
688
+
689
+
690
+ # =============================================================================
691
+ # UNINSTALL
692
+ # =============================================================================
693
+
694
+ def uninstall() -> bool:
695
+ """Remove Claude Code integration."""
696
+ print_header("Uninstalling Claude Memory Agent Integration")
697
+
698
+ # Remove MCP configuration
699
+ settings_file = get_claude_settings_file()
700
+ if settings_file.exists():
701
+ try:
702
+ settings = json.loads(settings_file.read_text())
703
+ if "mcpServers" in settings and "claude-memory" in settings["mcpServers"]:
704
+ del settings["mcpServers"]["claude-memory"]
705
+ settings_file.write_text(json.dumps(settings, indent=2))
706
+ print_success("Removed MCP configuration")
707
+ except Exception as e:
708
+ print_warning(f"Could not update settings: {e}")
709
+
710
+ # Remove hooks
711
+ hooks_dir = get_hooks_dir()
712
+ hooks_to_remove = ["session_start.py", "session_end.py", "grounding-hook.py"]
713
+ for hook in hooks_to_remove:
714
+ hook_file = hooks_dir / hook
715
+ if hook_file.exists():
716
+ hook_file.unlink()
717
+ print_success(f"Removed hook: {hook}")
718
+
719
+ print("\nUninstall complete. The .env file and database are preserved.")
720
+ print("To fully remove, delete the memory-agent directory.")
721
+ return True
722
+
723
+
724
+ # =============================================================================
725
+ # MAIN
726
+ # =============================================================================
727
+
728
+ def main():
729
+ parser = argparse.ArgumentParser(
730
+ description="Install and configure Claude Memory Agent"
731
+ )
732
+ parser.add_argument(
733
+ "--auto",
734
+ action="store_true",
735
+ help="Auto-install with default settings"
736
+ )
737
+ parser.add_argument(
738
+ "--uninstall",
739
+ action="store_true",
740
+ help="Remove Claude Code integration"
741
+ )
742
+ parser.add_argument(
743
+ "--port",
744
+ type=int,
745
+ default=8102,
746
+ help="Port for the memory agent (default: 8102)"
747
+ )
748
+ parser.add_argument(
749
+ "--skip-deps",
750
+ action="store_true",
751
+ help="Skip installing Python dependencies"
752
+ )
753
+ parser.add_argument(
754
+ "--skip-claude-check",
755
+ action="store_true",
756
+ help="Skip Claude Code installation check (for standalone use)"
757
+ )
758
+
759
+ args = parser.parse_args()
760
+
761
+ if args.uninstall:
762
+ return 0 if uninstall() else 1
763
+
764
+ print_header("Claude Memory Agent Installation")
765
+ print(f"Agent directory: {AGENT_DIR}")
766
+ print(f"Platform: {sys.platform}")
767
+
768
+ # Step 1: Check prerequisites
769
+ total_steps = 9
770
+ print_step(1, total_steps, "Checking prerequisites...")
771
+
772
+ if not check_python_version():
773
+ return 1
774
+
775
+ # Check Node.js and Claude Code (unless skipped)
776
+ claude_ok = False
777
+ if not args.skip_claude_check:
778
+ nodejs_ok, nodejs_version = check_nodejs()
779
+ npm_ok = False
780
+ if nodejs_ok:
781
+ npm_ok = check_npm()
782
+
783
+ # Check Claude Code
784
+ claude_ok, claude_version = check_claude_code()
785
+
786
+ # If Claude Code not found, check if we can install it
787
+ if not claude_ok:
788
+ if not nodejs_ok:
789
+ # Neither Node.js nor Claude Code - show instructions and exit
790
+ print_installation_instructions()
791
+ return 1
792
+ elif npm_ok:
793
+ # Node.js available but Claude Code not installed
794
+ if args.auto or prompt_yes_no("Claude Code not found. Install it now?"):
795
+ if not install_claude_code():
796
+ print_error("Could not install Claude Code automatically.")
797
+ print("Please install manually: npm install -g @anthropic-ai/claude-code")
798
+ if not prompt_yes_no("Continue anyway (memory agent only)?", default=False):
799
+ return 1
800
+ else:
801
+ claude_ok = True
802
+ else:
803
+ print_warning("Skipping Claude Code installation")
804
+ print(" The memory agent will work, but Claude Code integration requires Claude Code")
805
+ else:
806
+ print_warning("npm not found - cannot auto-install Claude Code")
807
+ print(" Install Claude Code manually: npm install -g @anthropic-ai/claude-code")
808
+ else:
809
+ print_success("Skipping Claude Code check (--skip-claude-check)")
810
+ claude_ok = True # Assume it's OK for standalone mode
811
+
812
+ # Check Ollama
813
+ ollama_ok = check_ollama()
814
+
815
+ # Step 2: Configure settings
816
+ print_step(2, total_steps, "Configuring settings...")
817
+
818
+ config = DEFAULT_CONFIG.copy()
819
+ config["PORT"] = str(args.port)
820
+ config["MEMORY_AGENT_URL"] = f"http://localhost:{args.port}"
821
+
822
+ if not args.auto:
823
+ if not ollama_ok:
824
+ config["OLLAMA_HOST"] = prompt_value(
825
+ "Ollama host URL",
826
+ config["OLLAMA_HOST"]
827
+ )
828
+
829
+ if prompt_yes_no("Use default embedding model (nomic-embed-text)?"):
830
+ pass
831
+ else:
832
+ config["EMBEDDING_MODEL"] = prompt_value(
833
+ "Embedding model name",
834
+ config["EMBEDDING_MODEL"]
835
+ )
836
+
837
+ if ollama_ok:
838
+ check_ollama_model(config["EMBEDDING_MODEL"])
839
+
840
+ # Step 3: Install dependencies
841
+ print_step(3, total_steps, "Installing dependencies...")
842
+ if not args.skip_deps:
843
+ install_dependencies()
844
+ else:
845
+ print_success("Skipped dependency installation")
846
+
847
+ # Step 4: Create .env file
848
+ print_step(4, total_steps, "Creating configuration file...")
849
+ if not create_env_file(config, force=args.auto):
850
+ return 1
851
+
852
+ # Step 5: Fix hardcoded values
853
+ print_step(5, total_steps, "Fixing hardcoded values...")
854
+ fix_agent_card_port()
855
+ fix_dashboard_urls()
856
+ fix_start_daemon_url(config)
857
+
858
+ # Step 6: Create startup script
859
+ print_step(6, total_steps, "Creating startup script...")
860
+ create_startup_script()
861
+
862
+ # Step 7: Configure Claude Code (only if Claude Code is available)
863
+ print_step(7, total_steps, "Configuring Claude Code integration...")
864
+
865
+ if claude_ok:
866
+ if args.auto or prompt_yes_no("Configure Claude Code MCP settings?"):
867
+ configure_claude_mcp(config)
868
+
869
+ if args.auto or prompt_yes_no("Install Claude Code hooks?"):
870
+ setup_hooks(config)
871
+ configure_hooks_json()
872
+ else:
873
+ print_warning("Skipping Claude Code configuration (Claude Code not installed)")
874
+ print(" Run 'python install.py' again after installing Claude Code")
875
+
876
+ # Step 8: Verify
877
+ print_step(8, total_steps, "Verifying installation...")
878
+ verify_installation()
879
+
880
+ # Done!
881
+ print_post_install_instructions(config)
882
+
883
+ return 0
884
+
885
+
886
+ if __name__ == "__main__":
887
+ sys.exit(main())