roampal 0.1.4__py3-none-any.whl

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 (44) hide show
  1. roampal/__init__.py +29 -0
  2. roampal/__main__.py +6 -0
  3. roampal/backend/__init__.py +1 -0
  4. roampal/backend/modules/__init__.py +1 -0
  5. roampal/backend/modules/memory/__init__.py +43 -0
  6. roampal/backend/modules/memory/chromadb_adapter.py +623 -0
  7. roampal/backend/modules/memory/config.py +102 -0
  8. roampal/backend/modules/memory/content_graph.py +543 -0
  9. roampal/backend/modules/memory/context_service.py +455 -0
  10. roampal/backend/modules/memory/embedding_service.py +96 -0
  11. roampal/backend/modules/memory/knowledge_graph_service.py +1052 -0
  12. roampal/backend/modules/memory/memory_bank_service.py +433 -0
  13. roampal/backend/modules/memory/memory_types.py +296 -0
  14. roampal/backend/modules/memory/outcome_service.py +400 -0
  15. roampal/backend/modules/memory/promotion_service.py +473 -0
  16. roampal/backend/modules/memory/routing_service.py +444 -0
  17. roampal/backend/modules/memory/scoring_service.py +324 -0
  18. roampal/backend/modules/memory/search_service.py +646 -0
  19. roampal/backend/modules/memory/tests/__init__.py +1 -0
  20. roampal/backend/modules/memory/tests/conftest.py +12 -0
  21. roampal/backend/modules/memory/tests/unit/__init__.py +1 -0
  22. roampal/backend/modules/memory/tests/unit/conftest.py +7 -0
  23. roampal/backend/modules/memory/tests/unit/test_knowledge_graph_service.py +517 -0
  24. roampal/backend/modules/memory/tests/unit/test_memory_bank_service.py +504 -0
  25. roampal/backend/modules/memory/tests/unit/test_outcome_service.py +485 -0
  26. roampal/backend/modules/memory/tests/unit/test_scoring_service.py +255 -0
  27. roampal/backend/modules/memory/tests/unit/test_search_service.py +413 -0
  28. roampal/backend/modules/memory/tests/unit/test_unified_memory_system.py +418 -0
  29. roampal/backend/modules/memory/unified_memory_system.py +1277 -0
  30. roampal/cli.py +638 -0
  31. roampal/hooks/__init__.py +16 -0
  32. roampal/hooks/session_manager.py +587 -0
  33. roampal/hooks/stop_hook.py +176 -0
  34. roampal/hooks/user_prompt_submit_hook.py +103 -0
  35. roampal/mcp/__init__.py +7 -0
  36. roampal/mcp/server.py +611 -0
  37. roampal/server/__init__.py +7 -0
  38. roampal/server/main.py +744 -0
  39. roampal-0.1.4.dist-info/METADATA +179 -0
  40. roampal-0.1.4.dist-info/RECORD +44 -0
  41. roampal-0.1.4.dist-info/WHEEL +5 -0
  42. roampal-0.1.4.dist-info/entry_points.txt +2 -0
  43. roampal-0.1.4.dist-info/licenses/LICENSE +190 -0
  44. roampal-0.1.4.dist-info/top_level.txt +1 -0
roampal/cli.py ADDED
@@ -0,0 +1,638 @@
1
+ """
2
+ Roampal CLI - One command install for AI coding tools
3
+
4
+ Usage:
5
+ pip install roampal
6
+ roampal init # Configure Claude Code / Cursor
7
+ roampal start # Start the memory server
8
+ roampal status # Check server status
9
+ """
10
+
11
+ import argparse
12
+ import json
13
+ import os
14
+ import sys
15
+ import subprocess
16
+ from pathlib import Path
17
+
18
+ # ANSI colors
19
+ GREEN = "\033[92m"
20
+ YELLOW = "\033[93m"
21
+ RED = "\033[91m"
22
+ BLUE = "\033[94m"
23
+ RESET = "\033[0m"
24
+ BOLD = "\033[1m"
25
+
26
+
27
+ def get_data_dir() -> Path:
28
+ """Get the prod data directory path (matches Desktop)."""
29
+ if os.name == 'nt': # Windows
30
+ appdata = os.environ.get('APPDATA', str(Path.home()))
31
+ return Path(appdata) / "Roampal" / "data"
32
+ elif sys.platform == 'darwin': # macOS
33
+ return Path.home() / "Library" / "Application Support" / "Roampal" / "data"
34
+ else: # Linux
35
+ return Path.home() / ".local" / "share" / "roampal" / "data"
36
+
37
+
38
+ def print_banner():
39
+ """Print Roampal banner."""
40
+ print(f"""
41
+ {BLUE}{BOLD}+---------------------------------------------------+
42
+ | ROAMPAL |
43
+ | Persistent Memory for AI Coding Tools |
44
+ +---------------------------------------------------+{RESET}
45
+ """)
46
+
47
+
48
+ def cmd_init(args):
49
+ """Initialize Roampal for the current environment."""
50
+ print_banner()
51
+ print(f"{BOLD}Initializing Roampal...{RESET}\n")
52
+
53
+ # Detect environment
54
+ home = Path.home()
55
+ claude_code_dir = home / ".claude"
56
+ cursor_dir = home / ".cursor"
57
+
58
+ detected = []
59
+ if claude_code_dir.exists():
60
+ detected.append("claude-code")
61
+ if cursor_dir.exists():
62
+ detected.append("cursor")
63
+
64
+ if not detected:
65
+ print(f"{YELLOW}No AI coding tools detected.{RESET}")
66
+ print("Roampal works with:")
67
+ print(" - Claude Code (https://claude.com/claude-code)")
68
+ print(" - Cursor (https://cursor.sh)")
69
+ print("\nInstall one of these tools first, then run 'roampal init' again.")
70
+ return
71
+
72
+ print(f"{GREEN}Detected: {', '.join(detected)}{RESET}\n")
73
+
74
+ # Configure each detected tool
75
+ for tool in detected:
76
+ if tool == "claude-code":
77
+ configure_claude_code(claude_code_dir)
78
+ elif tool == "cursor":
79
+ configure_cursor(cursor_dir)
80
+
81
+ # Create data directory
82
+ data_dir = get_data_dir()
83
+ data_dir.mkdir(parents=True, exist_ok=True)
84
+ print(f"{GREEN}Created data directory: {data_dir}{RESET}")
85
+
86
+ print(f"""
87
+ {GREEN}{BOLD}Roampal initialized successfully!{RESET}
88
+
89
+ {BOLD}Next step:{RESET}
90
+ {BLUE}Restart Claude Code{RESET} and start chatting!
91
+ The MCP server auto-starts - no manual server needed.
92
+
93
+ {BOLD}How it works:{RESET}
94
+ - Hooks inject relevant memories into your context automatically
95
+ - The AI learns what works and what doesn't via outcome scoring
96
+ - You see your original message; the AI sees your message + context + scoring prompt
97
+
98
+ {BOLD}Optional commands:{RESET}
99
+ - {BLUE}roampal ingest myfile.pdf{RESET} - Add documents to memory
100
+ - {BLUE}roampal stats{RESET} - Show memory statistics
101
+ - {BLUE}roampal status{RESET} - Check server status
102
+ """)
103
+
104
+
105
+ def configure_claude_code(claude_dir: Path):
106
+ """Configure Claude Code hooks, MCP, and permissions."""
107
+ print(f"{BOLD}Configuring Claude Code...{RESET}")
108
+
109
+ # Create settings.json with hooks and permissions
110
+ settings_path = claude_dir / "settings.json"
111
+
112
+ # Load existing settings or create new
113
+ settings = {}
114
+ if settings_path.exists():
115
+ try:
116
+ settings = json.loads(settings_path.read_text())
117
+ except:
118
+ pass
119
+
120
+ # Configure hooks - Claude Code expects nested format with type/command
121
+ python_exe = sys.executable.replace("\\", "\\\\") # Escape backslashes for JSON
122
+ settings["hooks"] = {
123
+ "UserPromptSubmit": [
124
+ {
125
+ "hooks": [
126
+ {
127
+ "type": "command",
128
+ "command": f"{python_exe} -m roampal.hooks.user_prompt_submit_hook"
129
+ }
130
+ ]
131
+ }
132
+ ],
133
+ "Stop": [
134
+ {
135
+ "hooks": [
136
+ {
137
+ "type": "command",
138
+ "command": f"{python_exe} -m roampal.hooks.stop_hook"
139
+ }
140
+ ]
141
+ }
142
+ ]
143
+ }
144
+
145
+ # Configure permissions to auto-allow roampal MCP tools
146
+ # This prevents the user from being spammed with permission prompts
147
+ if "permissions" not in settings:
148
+ settings["permissions"] = {}
149
+ if "allow" not in settings["permissions"]:
150
+ settings["permissions"]["allow"] = []
151
+
152
+ # Add roampal MCP tools to allow list (using roampal-core server name)
153
+ roampal_perms = [
154
+ "mcp__roampal-core__search_memory",
155
+ "mcp__roampal-core__add_to_memory_bank",
156
+ "mcp__roampal-core__update_memory",
157
+ "mcp__roampal-core__archive_memory",
158
+ "mcp__roampal-core__get_context_insights",
159
+ "mcp__roampal-core__record_response",
160
+ "mcp__roampal-core__score_response"
161
+ ]
162
+
163
+ for perm in roampal_perms:
164
+ if perm not in settings["permissions"]["allow"]:
165
+ settings["permissions"]["allow"].append(perm)
166
+
167
+ settings_path.write_text(json.dumps(settings, indent=2))
168
+ print(f" {GREEN}Created settings: {settings_path}{RESET}")
169
+ print(f" {GREEN} - UserPromptSubmit hook (injects scoring + memories){RESET}")
170
+ print(f" {GREEN} - Stop hook (enforces record_response){RESET}")
171
+ print(f" {GREEN} - Auto-allowed MCP permissions{RESET}")
172
+
173
+ # Create MCP configuration (server name matches permission prefix)
174
+ mcp_config_path = claude_dir / ".mcp.json"
175
+ mcp_config = {
176
+ "mcpServers": {
177
+ "roampal-core": {
178
+ "command": sys.executable,
179
+ "args": ["-m", "roampal.mcp.server"],
180
+ "env": {}
181
+ }
182
+ }
183
+ }
184
+
185
+ # Merge with existing if present
186
+ if mcp_config_path.exists():
187
+ try:
188
+ existing = json.loads(mcp_config_path.read_text())
189
+ if "mcpServers" in existing:
190
+ existing["mcpServers"]["roampal-core"] = mcp_config["mcpServers"]["roampal-core"]
191
+ else:
192
+ existing["mcpServers"] = mcp_config["mcpServers"]
193
+ mcp_config = existing
194
+ except:
195
+ pass
196
+
197
+ mcp_config_path.write_text(json.dumps(mcp_config, indent=2))
198
+ print(f" {GREEN}Created MCP config: {mcp_config_path}{RESET}")
199
+
200
+ # Also create local .mcp.json in current working directory
201
+ # Some Claude Code setups look for project-level config
202
+ local_mcp_path = Path.cwd() / ".mcp.json"
203
+ local_mcp_config = {
204
+ "mcpServers": {
205
+ "roampal-core": {
206
+ "command": sys.executable,
207
+ "args": ["-m", "roampal.mcp.server"],
208
+ "env": {}
209
+ }
210
+ }
211
+ }
212
+
213
+ # Merge with existing local config if present
214
+ if local_mcp_path.exists():
215
+ try:
216
+ existing = json.loads(local_mcp_path.read_text())
217
+ if "mcpServers" in existing:
218
+ existing["mcpServers"]["roampal-core"] = local_mcp_config["mcpServers"]["roampal-core"]
219
+ else:
220
+ existing["mcpServers"] = local_mcp_config["mcpServers"]
221
+ local_mcp_config = existing
222
+ except:
223
+ pass
224
+
225
+ local_mcp_path.write_text(json.dumps(local_mcp_config, indent=2))
226
+ print(f" {GREEN}Created local MCP config: {local_mcp_path}{RESET}")
227
+
228
+ print(f" {GREEN}Claude Code configured!{RESET}\n")
229
+
230
+
231
+ def configure_cursor(cursor_dir: Path):
232
+ """Configure Cursor MCP."""
233
+ print(f"{BOLD}Configuring Cursor...{RESET}")
234
+
235
+ # Cursor uses a different MCP config location
236
+ mcp_config_path = cursor_dir / "mcp.json"
237
+ mcp_config = {
238
+ "mcpServers": {
239
+ "roampal": {
240
+ "command": "python",
241
+ "args": ["-m", "roampal.mcp.server"]
242
+ }
243
+ }
244
+ }
245
+
246
+ # Merge with existing if present
247
+ if mcp_config_path.exists():
248
+ try:
249
+ existing = json.loads(mcp_config_path.read_text())
250
+ if "mcpServers" in existing:
251
+ existing["mcpServers"]["roampal"] = mcp_config["mcpServers"]["roampal"]
252
+ else:
253
+ existing.update(mcp_config)
254
+ mcp_config = existing
255
+ except:
256
+ pass
257
+
258
+ mcp_config_path.write_text(json.dumps(mcp_config, indent=2))
259
+ print(f" {GREEN}Created MCP config: {mcp_config_path}{RESET}")
260
+
261
+ print(f" {GREEN}Cursor configured!{RESET}\n")
262
+ print(f" {YELLOW}Note: Cursor hooks coming in future version.{RESET}")
263
+ print(f" {YELLOW}For now, MCP tools provide memory access.{RESET}\n")
264
+
265
+
266
+ def cmd_start(args):
267
+ """Start the Roampal server."""
268
+ print_banner()
269
+
270
+ # Handle dev mode - uses Roampal_DEV folder to match Desktop
271
+ if args.dev:
272
+ os.environ["ROAMPAL_DEV"] = "1"
273
+ # Determine data path for display
274
+ if os.name == 'nt':
275
+ appdata = os.environ.get('APPDATA', str(Path.home()))
276
+ data_path = Path(appdata) / "Roampal_DEV" / "data"
277
+ else:
278
+ data_path = Path.home() / ".local" / "share" / "roampal_dev" / "data"
279
+ print(f"{YELLOW}DEV MODE{RESET} - Using Roampal_DEV data (matches Desktop)")
280
+ print(f" Data path: {data_path}\n")
281
+
282
+ print(f"{BOLD}Starting Roampal server...{RESET}\n")
283
+
284
+ host = args.host or "127.0.0.1"
285
+ port = args.port or 27182
286
+
287
+ print(f"Server: http://{host}:{port}")
288
+ print(f"Hooks endpoint: http://{host}:{port}/api/hooks/get-context")
289
+ print(f"Health check: http://{host}:{port}/api/health")
290
+ print(f"\nPress Ctrl+C to stop.\n")
291
+
292
+ # Import and start server
293
+ from roampal.server.main import start_server
294
+ start_server(host=host, port=port)
295
+
296
+
297
+ def cmd_status(args):
298
+ """Check Roampal server status."""
299
+ print_banner()
300
+
301
+ import httpx
302
+
303
+ host = args.host or "127.0.0.1"
304
+ port = args.port or 27182
305
+ url = f"http://{host}:{port}/api/health"
306
+
307
+ try:
308
+ response = httpx.get(url, timeout=2.0)
309
+ if response.status_code == 200:
310
+ data = response.json()
311
+ print(f"{GREEN}Server Status: RUNNING{RESET}")
312
+ print(f" Memory initialized: {data.get('memory_initialized', False)}")
313
+ print(f" Timestamp: {data.get('timestamp', 'N/A')}")
314
+ else:
315
+ print(f"{RED}Server returned error: {response.status_code}{RESET}")
316
+ except httpx.ConnectError:
317
+ print(f"{YELLOW}Server Status: NOT RUNNING{RESET}")
318
+ print(f"\nStart with: roampal start")
319
+ except Exception as e:
320
+ print(f"{RED}Error checking status: {e}{RESET}")
321
+
322
+
323
+ def cmd_stats(args):
324
+ """Show memory statistics."""
325
+ print_banner()
326
+
327
+ import httpx
328
+
329
+ host = args.host or "127.0.0.1"
330
+ port = args.port or 27182
331
+ url = f"http://{host}:{port}/api/stats"
332
+
333
+ try:
334
+ response = httpx.get(url, timeout=5.0)
335
+ if response.status_code == 200:
336
+ data = response.json()
337
+ print(f"{BOLD}Memory Statistics:{RESET}\n")
338
+ print(f"Data path: {data.get('data_path', 'N/A')}")
339
+ print(f"\nCollections:")
340
+ for name, info in data.get("collections", {}).items():
341
+ count = info.get("count", 0)
342
+ print(f" {name}: {count} items")
343
+ else:
344
+ print(f"{RED}Error getting stats: {response.status_code}{RESET}")
345
+ except httpx.ConnectError:
346
+ print(f"{YELLOW}Server not running. Start with: roampal start{RESET}")
347
+ except Exception as e:
348
+ print(f"{RED}Error: {e}{RESET}")
349
+
350
+
351
+ def cmd_ingest(args):
352
+ """Ingest a document into the books collection."""
353
+ import asyncio
354
+ import httpx
355
+
356
+ print_banner()
357
+
358
+ file_path = Path(args.file)
359
+ if not file_path.exists():
360
+ print(f"{RED}File not found: {file_path}{RESET}")
361
+ return
362
+
363
+ # Read file content
364
+ print(f"{BOLD}Ingesting:{RESET} {file_path.name}")
365
+
366
+ try:
367
+ # Detect file type and read content
368
+ suffix = file_path.suffix.lower()
369
+ content = None
370
+ title = args.title or file_path.stem
371
+
372
+ if suffix == '.txt':
373
+ content = file_path.read_text(encoding='utf-8')
374
+ elif suffix == '.md':
375
+ content = file_path.read_text(encoding='utf-8')
376
+ elif suffix == '.pdf':
377
+ # Try to use pypdf if available
378
+ try:
379
+ import pypdf
380
+ reader = pypdf.PdfReader(str(file_path))
381
+ content = ""
382
+ for page in reader.pages:
383
+ content += page.extract_text() + "\n"
384
+ print(f" Extracted {len(reader.pages)} pages from PDF")
385
+ except ImportError:
386
+ print(f"{RED}PDF support requires pypdf: pip install pypdf{RESET}")
387
+ return
388
+ else:
389
+ # Try to read as text
390
+ try:
391
+ content = file_path.read_text(encoding='utf-8')
392
+ except UnicodeDecodeError:
393
+ print(f"{RED}Cannot read file as text. Supported: .txt, .md, .pdf{RESET}")
394
+ return
395
+
396
+ if not content or len(content.strip()) == 0:
397
+ print(f"{YELLOW}File is empty or could not be read{RESET}")
398
+ return
399
+
400
+ print(f" Content length: {len(content):,} characters")
401
+
402
+ # Handle dev mode
403
+ data_path = None
404
+ if args.dev:
405
+ dev_data_dir = Path.home() / ".roampal" / "dev-data"
406
+ dev_data_dir.mkdir(parents=True, exist_ok=True)
407
+ data_path = str(dev_data_dir)
408
+ print(f" {YELLOW}DEV MODE{RESET} - Using: {dev_data_dir}")
409
+
410
+ # Try to use running server first (data immediately searchable)
411
+ host = "127.0.0.1"
412
+ port = 27182
413
+ server_url = f"http://{host}:{port}/api/ingest"
414
+
415
+ try:
416
+ response = httpx.post(
417
+ server_url,
418
+ json={
419
+ "content": content,
420
+ "title": title,
421
+ "source": str(file_path),
422
+ "chunk_size": args.chunk_size,
423
+ "chunk_overlap": args.chunk_overlap
424
+ },
425
+ timeout=60.0 # Long timeout for large files
426
+ )
427
+
428
+ if response.status_code == 200:
429
+ data = response.json()
430
+ print(f"\n{GREEN}Success!{RESET} Stored '{title}' in {data['chunks']} chunks")
431
+ print(f" (via running server - immediately searchable)")
432
+ print(f"\nThe document is now searchable via:")
433
+ print(f" - search_memory(query, collections=['books'])")
434
+ print(f" - Automatic context injection via hooks")
435
+ return
436
+ else:
437
+ print(f" {YELLOW}Server error, falling back to direct storage...{RESET}")
438
+
439
+ except httpx.ConnectError:
440
+ print(f" {YELLOW}Server not running, using direct storage...{RESET}")
441
+ print(f" {YELLOW}(Restart server for immediate searchability){RESET}")
442
+
443
+ # Fallback: Store directly (requires server restart to be searchable)
444
+ async def do_ingest():
445
+ from roampal.backend.modules.memory import UnifiedMemorySystem
446
+
447
+ mem = UnifiedMemorySystem(data_path=data_path)
448
+ await mem.initialize()
449
+
450
+ doc_ids = await mem.store_book(
451
+ content=content,
452
+ title=title,
453
+ source=str(file_path),
454
+ chunk_size=args.chunk_size,
455
+ chunk_overlap=args.chunk_overlap
456
+ )
457
+
458
+ return doc_ids
459
+
460
+ doc_ids = asyncio.run(do_ingest())
461
+
462
+ print(f"\n{GREEN}Success!{RESET} Stored '{title}' in {len(doc_ids)} chunks")
463
+ print(f"\nThe document is now searchable via:")
464
+ print(f" - search_memory(query, collections=['books'])")
465
+ print(f" - Automatic context injection via hooks")
466
+ print(f"\n{YELLOW}Note: Restart 'roampal start' for immediate searchability{RESET}")
467
+
468
+ except Exception as e:
469
+ print(f"{RED}Error ingesting file: {e}{RESET}")
470
+ raise
471
+
472
+
473
+ def cmd_remove(args):
474
+ """Remove a book from the books collection."""
475
+ import asyncio
476
+ import httpx
477
+
478
+ print_banner()
479
+ title = args.title
480
+ print(f"{BOLD}Removing book:{RESET} {title}\n")
481
+
482
+ # Try running server first
483
+ host = "127.0.0.1"
484
+ port = 27182
485
+ server_url = f"http://{host}:{port}/api/remove-book"
486
+
487
+ try:
488
+ response = httpx.post(
489
+ server_url,
490
+ json={"title": title},
491
+ timeout=30.0
492
+ )
493
+
494
+ if response.status_code == 200:
495
+ data = response.json()
496
+ if data.get("removed", 0) > 0:
497
+ print(f"{GREEN}Success!{RESET} Removed '{title}' ({data['removed']} chunks)")
498
+ if data.get("cleaned_kg_refs", 0) > 0:
499
+ print(f" Cleaned {data['cleaned_kg_refs']} Action KG references")
500
+ else:
501
+ print(f"{YELLOW}No book found with title '{title}'{RESET}")
502
+ return
503
+ else:
504
+ print(f" {YELLOW}Server error, falling back to direct removal...{RESET}")
505
+
506
+ except httpx.ConnectError:
507
+ print(f" {YELLOW}Server not running, using direct removal...{RESET}")
508
+
509
+ # Fallback: Remove directly
510
+ async def do_remove():
511
+ from roampal.backend.modules.memory import UnifiedMemorySystem
512
+
513
+ mem = UnifiedMemorySystem()
514
+ await mem.initialize()
515
+ return await mem.remove_book(title)
516
+
517
+ result = asyncio.run(do_remove())
518
+
519
+ if result.get("removed", 0) > 0:
520
+ print(f"\n{GREEN}Success!{RESET} Removed '{title}' ({result['removed']} chunks)")
521
+ if result.get("cleaned_kg_refs", 0) > 0:
522
+ print(f" Cleaned {result['cleaned_kg_refs']} Action KG references")
523
+ else:
524
+ print(f"{YELLOW}No book found with title '{title}'{RESET}")
525
+
526
+
527
+ def cmd_books(args):
528
+ """List all books in the books collection."""
529
+ import asyncio
530
+ import httpx
531
+
532
+ print_banner()
533
+ print(f"{BOLD}Books in memory:{RESET}\n")
534
+
535
+ # Try running server first
536
+ host = "127.0.0.1"
537
+ port = 27182
538
+ server_url = f"http://{host}:{port}/api/books"
539
+
540
+ books = None
541
+
542
+ try:
543
+ response = httpx.get(server_url, timeout=10.0)
544
+ if response.status_code == 200:
545
+ books = response.json().get("books", [])
546
+ except httpx.ConnectError:
547
+ pass
548
+
549
+ # Fallback: List directly
550
+ if books is None:
551
+ async def do_list():
552
+ from roampal.backend.modules.memory import UnifiedMemorySystem
553
+
554
+ mem = UnifiedMemorySystem()
555
+ await mem.initialize()
556
+ return await mem.list_books()
557
+
558
+ books = asyncio.run(do_list())
559
+
560
+ if not books:
561
+ print(f"{YELLOW}No books found.{RESET}")
562
+ print(f"\nAdd books with: roampal ingest <file>")
563
+ return
564
+
565
+ for book in books:
566
+ print(f" {GREEN}{book['title']}{RESET}")
567
+ print(f" Source: {book.get('source', 'unknown')}")
568
+ print(f" Chunks: {book.get('chunk_count', 0)}")
569
+ if book.get('created_at'):
570
+ print(f" Added: {book['created_at'][:10]}")
571
+ print()
572
+
573
+
574
+ def main():
575
+ """Main CLI entry point."""
576
+ parser = argparse.ArgumentParser(
577
+ description="Roampal - Persistent Memory for AI Coding Tools",
578
+ formatter_class=argparse.RawDescriptionHelpFormatter
579
+ )
580
+
581
+ subparsers = parser.add_subparsers(dest="command", help="Available commands")
582
+
583
+ # init command
584
+ init_parser = subparsers.add_parser("init", help="Initialize Roampal for Claude Code / Cursor")
585
+
586
+ # start command
587
+ start_parser = subparsers.add_parser("start", help="Start the memory server")
588
+ start_parser.add_argument("--host", default="127.0.0.1", help="Server host")
589
+ start_parser.add_argument("--port", type=int, default=27182, help="Server port")
590
+ start_parser.add_argument("--dev", action="store_true", help="Dev mode - use separate data directory")
591
+
592
+ # status command
593
+ status_parser = subparsers.add_parser("status", help="Check server status")
594
+ status_parser.add_argument("--host", default="127.0.0.1", help="Server host")
595
+ status_parser.add_argument("--port", type=int, default=27182, help="Server port")
596
+
597
+ # stats command
598
+ stats_parser = subparsers.add_parser("stats", help="Show memory statistics")
599
+ stats_parser.add_argument("--host", default="127.0.0.1", help="Server host")
600
+ stats_parser.add_argument("--port", type=int, default=27182, help="Server port")
601
+
602
+ # ingest command
603
+ ingest_parser = subparsers.add_parser("ingest", help="Ingest a document into the books collection")
604
+ ingest_parser.add_argument("file", help="File to ingest (.txt, .md, .pdf)")
605
+ ingest_parser.add_argument("--title", help="Document title (defaults to filename)")
606
+ ingest_parser.add_argument("--chunk-size", type=int, default=1000, help="Characters per chunk (default: 1000)")
607
+ ingest_parser.add_argument("--chunk-overlap", type=int, default=200, help="Overlap between chunks (default: 200)")
608
+ ingest_parser.add_argument("--dev", action="store_true", help="Dev mode - use separate data directory")
609
+
610
+ # remove command
611
+ remove_parser = subparsers.add_parser("remove", help="Remove a book from the books collection")
612
+ remove_parser.add_argument("title", help="Title of the book to remove")
613
+
614
+ # books command
615
+ books_parser = subparsers.add_parser("books", help="List all books in memory")
616
+
617
+ args = parser.parse_args()
618
+
619
+ if args.command == "init":
620
+ cmd_init(args)
621
+ elif args.command == "start":
622
+ cmd_start(args)
623
+ elif args.command == "status":
624
+ cmd_status(args)
625
+ elif args.command == "stats":
626
+ cmd_stats(args)
627
+ elif args.command == "ingest":
628
+ cmd_ingest(args)
629
+ elif args.command == "remove":
630
+ cmd_remove(args)
631
+ elif args.command == "books":
632
+ cmd_books(args)
633
+ else:
634
+ parser.print_help()
635
+
636
+
637
+ if __name__ == "__main__":
638
+ main()
@@ -0,0 +1,16 @@
1
+ """
2
+ Roampal Hooks Module
3
+
4
+ Manages session tracking and hook scripts for enforced outcome scoring.
5
+
6
+ Hook Scripts:
7
+ - user_prompt_submit_hook.py: Injects scoring prompt + memories BEFORE LLM
8
+ - stop_hook.py: Stores exchange, enforces record_response AFTER LLM
9
+
10
+ Usage:
11
+ roampal init # Configures hooks and permissions automatically
12
+ """
13
+
14
+ from .session_manager import SessionManager
15
+
16
+ __all__ = ["SessionManager"]