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.
- roampal/__init__.py +29 -0
- roampal/__main__.py +6 -0
- roampal/backend/__init__.py +1 -0
- roampal/backend/modules/__init__.py +1 -0
- roampal/backend/modules/memory/__init__.py +43 -0
- roampal/backend/modules/memory/chromadb_adapter.py +623 -0
- roampal/backend/modules/memory/config.py +102 -0
- roampal/backend/modules/memory/content_graph.py +543 -0
- roampal/backend/modules/memory/context_service.py +455 -0
- roampal/backend/modules/memory/embedding_service.py +96 -0
- roampal/backend/modules/memory/knowledge_graph_service.py +1052 -0
- roampal/backend/modules/memory/memory_bank_service.py +433 -0
- roampal/backend/modules/memory/memory_types.py +296 -0
- roampal/backend/modules/memory/outcome_service.py +400 -0
- roampal/backend/modules/memory/promotion_service.py +473 -0
- roampal/backend/modules/memory/routing_service.py +444 -0
- roampal/backend/modules/memory/scoring_service.py +324 -0
- roampal/backend/modules/memory/search_service.py +646 -0
- roampal/backend/modules/memory/tests/__init__.py +1 -0
- roampal/backend/modules/memory/tests/conftest.py +12 -0
- roampal/backend/modules/memory/tests/unit/__init__.py +1 -0
- roampal/backend/modules/memory/tests/unit/conftest.py +7 -0
- roampal/backend/modules/memory/tests/unit/test_knowledge_graph_service.py +517 -0
- roampal/backend/modules/memory/tests/unit/test_memory_bank_service.py +504 -0
- roampal/backend/modules/memory/tests/unit/test_outcome_service.py +485 -0
- roampal/backend/modules/memory/tests/unit/test_scoring_service.py +255 -0
- roampal/backend/modules/memory/tests/unit/test_search_service.py +413 -0
- roampal/backend/modules/memory/tests/unit/test_unified_memory_system.py +418 -0
- roampal/backend/modules/memory/unified_memory_system.py +1277 -0
- roampal/cli.py +638 -0
- roampal/hooks/__init__.py +16 -0
- roampal/hooks/session_manager.py +587 -0
- roampal/hooks/stop_hook.py +176 -0
- roampal/hooks/user_prompt_submit_hook.py +103 -0
- roampal/mcp/__init__.py +7 -0
- roampal/mcp/server.py +611 -0
- roampal/server/__init__.py +7 -0
- roampal/server/main.py +744 -0
- roampal-0.1.4.dist-info/METADATA +179 -0
- roampal-0.1.4.dist-info/RECORD +44 -0
- roampal-0.1.4.dist-info/WHEEL +5 -0
- roampal-0.1.4.dist-info/entry_points.txt +2 -0
- roampal-0.1.4.dist-info/licenses/LICENSE +190 -0
- 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"]
|