runtime-memory 3.0.0__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.
- runtime_memory/__init__.py +28 -0
- runtime_memory/claude_code/__init__.py +48 -0
- runtime_memory/claude_code/commands.py +698 -0
- runtime_memory/claude_code/daemon.py +852 -0
- runtime_memory/claude_code/hooks.py +722 -0
- runtime_memory/cli/__init__.py +8 -0
- runtime_memory/cli/main.py +1936 -0
- runtime_memory/core/__init__.py +216 -0
- runtime_memory/core/config.py +473 -0
- runtime_memory/core/embeddings.py +908 -0
- runtime_memory/core/engine.py +1007 -0
- runtime_memory/core/exceptions.py +547 -0
- runtime_memory/core/legacy_env.py +39 -0
- runtime_memory/core/logging.py +160 -0
- runtime_memory/core/models.py +1051 -0
- runtime_memory/core/observability.py +725 -0
- runtime_memory/core/paths.py +30 -0
- runtime_memory/core/resilience.py +511 -0
- runtime_memory/core/retrieval.py +819 -0
- runtime_memory/core/storage.py +1105 -0
- runtime_memory/extraction/__init__.py +36 -0
- runtime_memory/extraction/extractor.py +1143 -0
- runtime_memory/hermes/__init__.py +39 -0
- runtime_memory/hermes/_base.py +154 -0
- runtime_memory/hermes/bridge.py +119 -0
- runtime_memory/hermes/plugin.yaml +13 -0
- runtime_memory/hermes/provider.py +536 -0
- runtime_memory/hermes/tools.py +230 -0
- runtime_memory/hermes/trace.py +177 -0
- runtime_memory/plugin/__init__.py +646 -0
- runtime_memory/sdk/__init__.py +97 -0
- runtime_memory/sdk/client.py +1577 -0
- runtime_memory/server/__init__.py +75 -0
- runtime_memory/server/api.py +1665 -0
- runtime_memory/server/mcp.py +1574 -0
- runtime_memory/server/static/css/styles.css +1110 -0
- runtime_memory/server/static/index.html +264 -0
- runtime_memory/server/static/js/api.js +294 -0
- runtime_memory/server/static/js/app.js +771 -0
- runtime_memory/tasks/__init__.py +114 -0
- runtime_memory/tasks/adapter.py +501 -0
- runtime_memory/tasks/claude_code_adapter.py +495 -0
- runtime_memory/tasks/claude_code_parser.py +339 -0
- runtime_memory/tasks/cli_bridge.py +415 -0
- runtime_memory/tasks/linking.py +397 -0
- runtime_memory/tasks/models.py +520 -0
- runtime_memory/tasks/outcomes.py +320 -0
- runtime_memory/tasks/parser.py +305 -0
- runtime_memory/tasks/unified_adapter.py +661 -0
- runtime_memory-3.0.0.dist-info/METADATA +497 -0
- runtime_memory-3.0.0.dist-info/RECORD +54 -0
- runtime_memory-3.0.0.dist-info/WHEEL +4 -0
- runtime_memory-3.0.0.dist-info/entry_points.txt +6 -0
- runtime_memory-3.0.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,698 @@
|
|
|
1
|
+
"""Custom commands for Claude Code integration.
|
|
2
|
+
|
|
3
|
+
This module implements slash commands that can be used within Claude Code
|
|
4
|
+
to interact with the memory layer.
|
|
5
|
+
|
|
6
|
+
Commands:
|
|
7
|
+
- /remember: Store a new memory
|
|
8
|
+
- /recall: Search for memories
|
|
9
|
+
- /forget: Delete a memory
|
|
10
|
+
- /outcome: Record outcome feedback
|
|
11
|
+
- /context: Get formatted context
|
|
12
|
+
- /memories: List memories
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import json
|
|
18
|
+
import re
|
|
19
|
+
from dataclasses import dataclass, field
|
|
20
|
+
from enum import Enum
|
|
21
|
+
from pathlib import Path
|
|
22
|
+
from typing import Any
|
|
23
|
+
|
|
24
|
+
from runtime_memory.core.engine import MemoryEngine
|
|
25
|
+
from runtime_memory.core.models import (
|
|
26
|
+
Memory,
|
|
27
|
+
MemoryCategory,
|
|
28
|
+
MemoryScope,
|
|
29
|
+
MemorySource,
|
|
30
|
+
Outcome,
|
|
31
|
+
SearchResult,
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class CommandType(str, Enum):
|
|
36
|
+
"""Types of custom commands."""
|
|
37
|
+
|
|
38
|
+
REMEMBER = "remember"
|
|
39
|
+
RECALL = "recall"
|
|
40
|
+
FORGET = "forget"
|
|
41
|
+
OUTCOME = "outcome"
|
|
42
|
+
CONTEXT = "context"
|
|
43
|
+
MEMORIES = "memories"
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@dataclass
|
|
47
|
+
class CommandResult:
|
|
48
|
+
"""Result from executing a command."""
|
|
49
|
+
|
|
50
|
+
success: bool
|
|
51
|
+
command: CommandType
|
|
52
|
+
message: str
|
|
53
|
+
data: dict[str, Any] = field(default_factory=dict)
|
|
54
|
+
error: str | None = None
|
|
55
|
+
|
|
56
|
+
def to_dict(self) -> dict[str, Any]:
|
|
57
|
+
"""Convert to dictionary."""
|
|
58
|
+
result = {
|
|
59
|
+
"success": self.success,
|
|
60
|
+
"command": self.command.value,
|
|
61
|
+
"message": self.message,
|
|
62
|
+
}
|
|
63
|
+
if self.data:
|
|
64
|
+
result["data"] = self.data
|
|
65
|
+
if self.error:
|
|
66
|
+
result["error"] = self.error
|
|
67
|
+
return result
|
|
68
|
+
|
|
69
|
+
def to_json(self) -> str:
|
|
70
|
+
"""Convert to JSON string."""
|
|
71
|
+
return json.dumps(self.to_dict(), indent=2)
|
|
72
|
+
|
|
73
|
+
def to_markdown(self) -> str:
|
|
74
|
+
"""Convert to markdown format for display."""
|
|
75
|
+
if not self.success:
|
|
76
|
+
return f"❌ **Error**: {self.error or self.message}"
|
|
77
|
+
|
|
78
|
+
lines = [f"✅ **{self.command.value.title()}**: {self.message}"]
|
|
79
|
+
|
|
80
|
+
if self.data:
|
|
81
|
+
if "memories" in self.data:
|
|
82
|
+
lines.append("")
|
|
83
|
+
for mem in self.data["memories"]:
|
|
84
|
+
score_str = f" (score: {mem.get('score', 0):.2f})" if "score" in mem else ""
|
|
85
|
+
lines.append(f"- **{mem.get('category', 'unknown')}**{score_str}: {mem.get('content', '')[:100]}...")
|
|
86
|
+
elif "memory" in self.data:
|
|
87
|
+
mem = self.data["memory"]
|
|
88
|
+
lines.append(f"\n**ID**: `{mem.get('id', 'unknown')}`")
|
|
89
|
+
lines.append(f"**Category**: {mem.get('category', 'unknown')}")
|
|
90
|
+
lines.append(f"**Content**: {mem.get('content', '')}")
|
|
91
|
+
elif "context" in self.data:
|
|
92
|
+
lines.append("")
|
|
93
|
+
lines.append(self.data["context"])
|
|
94
|
+
elif "stats" in self.data:
|
|
95
|
+
stats = self.data["stats"]
|
|
96
|
+
lines.append(f"\n**Total**: {stats.get('total', 0)} memories")
|
|
97
|
+
lines.append(f"**Active**: {stats.get('active', 0)}")
|
|
98
|
+
lines.append(f"**Archived**: {stats.get('archived', 0)}")
|
|
99
|
+
|
|
100
|
+
return "\n".join(lines)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
@dataclass
|
|
104
|
+
class CommandConfig:
|
|
105
|
+
"""Configuration for command handler."""
|
|
106
|
+
|
|
107
|
+
default_project: str | None = None
|
|
108
|
+
default_scope: MemoryScope = MemoryScope.PROJECT
|
|
109
|
+
max_results: int = 10
|
|
110
|
+
include_archived: bool = False
|
|
111
|
+
output_format: str = "markdown" # markdown, json, plain
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
class CommandHandler:
|
|
115
|
+
"""Handler for custom slash commands.
|
|
116
|
+
|
|
117
|
+
This class processes slash commands and interacts with the memory engine
|
|
118
|
+
to perform operations like storing, searching, and managing memories.
|
|
119
|
+
"""
|
|
120
|
+
|
|
121
|
+
def __init__(
|
|
122
|
+
self,
|
|
123
|
+
engine: MemoryEngine,
|
|
124
|
+
config: CommandConfig | None = None,
|
|
125
|
+
) -> None:
|
|
126
|
+
"""Initialize command handler.
|
|
127
|
+
|
|
128
|
+
Args:
|
|
129
|
+
engine: Memory engine instance
|
|
130
|
+
config: Command configuration
|
|
131
|
+
"""
|
|
132
|
+
self.engine = engine
|
|
133
|
+
self.config = config or CommandConfig()
|
|
134
|
+
|
|
135
|
+
async def execute(self, command: str, args: str = "", project: str | None = None) -> CommandResult:
|
|
136
|
+
"""Execute a slash command.
|
|
137
|
+
|
|
138
|
+
Args:
|
|
139
|
+
command: Command name (with or without leading slash)
|
|
140
|
+
args: Command arguments as string
|
|
141
|
+
project: Project context (optional)
|
|
142
|
+
|
|
143
|
+
Returns:
|
|
144
|
+
CommandResult with operation outcome
|
|
145
|
+
"""
|
|
146
|
+
# Normalize command name
|
|
147
|
+
cmd = command.lstrip("/").lower().strip()
|
|
148
|
+
|
|
149
|
+
# Use project from config if not specified
|
|
150
|
+
project = project or self.config.default_project
|
|
151
|
+
|
|
152
|
+
try:
|
|
153
|
+
if cmd == CommandType.REMEMBER.value:
|
|
154
|
+
return await self._remember(args, project)
|
|
155
|
+
elif cmd == CommandType.RECALL.value:
|
|
156
|
+
return await self._recall(args, project)
|
|
157
|
+
elif cmd == CommandType.FORGET.value:
|
|
158
|
+
return await self._forget(args, project)
|
|
159
|
+
elif cmd == CommandType.OUTCOME.value:
|
|
160
|
+
return await self._outcome(args, project)
|
|
161
|
+
elif cmd == CommandType.CONTEXT.value:
|
|
162
|
+
return await self._context(args, project)
|
|
163
|
+
elif cmd == CommandType.MEMORIES.value:
|
|
164
|
+
return await self._memories(args, project)
|
|
165
|
+
else:
|
|
166
|
+
return CommandResult(
|
|
167
|
+
success=False,
|
|
168
|
+
command=CommandType.REMEMBER, # Default
|
|
169
|
+
message="Unknown command",
|
|
170
|
+
error=f"Unknown command: /{cmd}. Available: /remember, /recall, /forget, /outcome, /context, /memories",
|
|
171
|
+
)
|
|
172
|
+
except Exception as e:
|
|
173
|
+
return CommandResult(
|
|
174
|
+
success=False,
|
|
175
|
+
command=CommandType(cmd) if cmd in [c.value for c in CommandType] else CommandType.REMEMBER,
|
|
176
|
+
message="Command execution failed",
|
|
177
|
+
error=str(e),
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
async def _remember(self, args: str, project: str | None) -> CommandResult:
|
|
181
|
+
"""Store a new memory.
|
|
182
|
+
|
|
183
|
+
Format: /remember [category:CATEGORY] content
|
|
184
|
+
Example: /remember category:gotcha Always use --no-cache with pip install
|
|
185
|
+
"""
|
|
186
|
+
# Parse category from args
|
|
187
|
+
category = MemoryCategory.PATTERN
|
|
188
|
+
content = args.strip()
|
|
189
|
+
|
|
190
|
+
# Check for category prefix
|
|
191
|
+
category_match = re.match(r"category:(\w+)\s+(.+)", content, re.IGNORECASE | re.DOTALL)
|
|
192
|
+
if category_match:
|
|
193
|
+
cat_str = category_match.group(1).upper()
|
|
194
|
+
content = category_match.group(2).strip()
|
|
195
|
+
try:
|
|
196
|
+
category = MemoryCategory(cat_str.lower())
|
|
197
|
+
except ValueError:
|
|
198
|
+
# Try to match partial category name
|
|
199
|
+
for cat in MemoryCategory:
|
|
200
|
+
if cat.value.upper().startswith(cat_str):
|
|
201
|
+
category = cat
|
|
202
|
+
break
|
|
203
|
+
|
|
204
|
+
if not content:
|
|
205
|
+
return CommandResult(
|
|
206
|
+
success=False,
|
|
207
|
+
command=CommandType.REMEMBER,
|
|
208
|
+
message="No content provided",
|
|
209
|
+
error="Usage: /remember [category:CATEGORY] content",
|
|
210
|
+
)
|
|
211
|
+
|
|
212
|
+
# Add the memory
|
|
213
|
+
memory = await self.engine.add(
|
|
214
|
+
content=content,
|
|
215
|
+
category=category,
|
|
216
|
+
scope=self.config.default_scope,
|
|
217
|
+
source=MemorySource.EXPLICIT,
|
|
218
|
+
project=project,
|
|
219
|
+
)
|
|
220
|
+
|
|
221
|
+
return CommandResult(
|
|
222
|
+
success=True,
|
|
223
|
+
command=CommandType.REMEMBER,
|
|
224
|
+
message=f"Memory stored with ID: {memory.id}",
|
|
225
|
+
data={
|
|
226
|
+
"memory": {
|
|
227
|
+
"id": memory.id,
|
|
228
|
+
"content": memory.content,
|
|
229
|
+
"category": memory.category.value,
|
|
230
|
+
"scope": memory.scope.value,
|
|
231
|
+
"project": memory.project,
|
|
232
|
+
}
|
|
233
|
+
},
|
|
234
|
+
)
|
|
235
|
+
|
|
236
|
+
async def _recall(self, args: str, project: str | None) -> CommandResult:
|
|
237
|
+
"""Search for memories.
|
|
238
|
+
|
|
239
|
+
Format: /recall query [limit:N]
|
|
240
|
+
Example: /recall docker memory issues limit:5
|
|
241
|
+
"""
|
|
242
|
+
query = args.strip()
|
|
243
|
+
limit = self.config.max_results
|
|
244
|
+
|
|
245
|
+
# Check for limit suffix
|
|
246
|
+
limit_match = re.search(r"\s+limit:(\d+)$", query)
|
|
247
|
+
if limit_match:
|
|
248
|
+
limit = min(int(limit_match.group(1)), 50)
|
|
249
|
+
query = query[: limit_match.start()].strip()
|
|
250
|
+
|
|
251
|
+
if not query:
|
|
252
|
+
return CommandResult(
|
|
253
|
+
success=False,
|
|
254
|
+
command=CommandType.RECALL,
|
|
255
|
+
message="No search query provided",
|
|
256
|
+
error="Usage: /recall query [limit:N]",
|
|
257
|
+
)
|
|
258
|
+
|
|
259
|
+
# Search memories
|
|
260
|
+
results = await self.engine.search(
|
|
261
|
+
query=query,
|
|
262
|
+
project=project,
|
|
263
|
+
limit=limit,
|
|
264
|
+
)
|
|
265
|
+
|
|
266
|
+
if not results:
|
|
267
|
+
return CommandResult(
|
|
268
|
+
success=True,
|
|
269
|
+
command=CommandType.RECALL,
|
|
270
|
+
message="No memories found matching your query",
|
|
271
|
+
data={"memories": [], "query": query},
|
|
272
|
+
)
|
|
273
|
+
|
|
274
|
+
memories_data = []
|
|
275
|
+
for result in results:
|
|
276
|
+
memories_data.append({
|
|
277
|
+
"id": result.memory.id,
|
|
278
|
+
"content": result.memory.content,
|
|
279
|
+
"category": result.memory.category.value,
|
|
280
|
+
"score": result.score,
|
|
281
|
+
"project": result.memory.project,
|
|
282
|
+
})
|
|
283
|
+
|
|
284
|
+
return CommandResult(
|
|
285
|
+
success=True,
|
|
286
|
+
command=CommandType.RECALL,
|
|
287
|
+
message=f"Found {len(results)} memories",
|
|
288
|
+
data={"memories": memories_data, "query": query},
|
|
289
|
+
)
|
|
290
|
+
|
|
291
|
+
async def _forget(self, args: str, project: str | None) -> CommandResult:
|
|
292
|
+
"""Delete/archive a memory.
|
|
293
|
+
|
|
294
|
+
Format: /forget memory_id [--permanent]
|
|
295
|
+
Example: /forget abc123
|
|
296
|
+
"""
|
|
297
|
+
args = args.strip()
|
|
298
|
+
permanent = False
|
|
299
|
+
|
|
300
|
+
if "--permanent" in args:
|
|
301
|
+
permanent = True
|
|
302
|
+
args = args.replace("--permanent", "").strip()
|
|
303
|
+
|
|
304
|
+
memory_id = args.strip()
|
|
305
|
+
|
|
306
|
+
if not memory_id:
|
|
307
|
+
return CommandResult(
|
|
308
|
+
success=False,
|
|
309
|
+
command=CommandType.FORGET,
|
|
310
|
+
message="No memory ID provided",
|
|
311
|
+
error="Usage: /forget memory_id [--permanent]",
|
|
312
|
+
)
|
|
313
|
+
|
|
314
|
+
# Check if memory exists
|
|
315
|
+
memory = await self.engine.get(memory_id)
|
|
316
|
+
if not memory:
|
|
317
|
+
return CommandResult(
|
|
318
|
+
success=False,
|
|
319
|
+
command=CommandType.FORGET,
|
|
320
|
+
message="Memory not found",
|
|
321
|
+
error=f"No memory found with ID: {memory_id}",
|
|
322
|
+
)
|
|
323
|
+
|
|
324
|
+
# Delete/archive the memory
|
|
325
|
+
await self.engine.delete(memory_id, permanent=permanent)
|
|
326
|
+
|
|
327
|
+
action = "deleted" if permanent else "archived"
|
|
328
|
+
return CommandResult(
|
|
329
|
+
success=True,
|
|
330
|
+
command=CommandType.FORGET,
|
|
331
|
+
message=f"Memory {action}: {memory_id}",
|
|
332
|
+
data={"memory_id": memory_id, "permanent": permanent},
|
|
333
|
+
)
|
|
334
|
+
|
|
335
|
+
async def _outcome(self, args: str, project: str | None) -> CommandResult:
|
|
336
|
+
"""Record outcome feedback for a memory.
|
|
337
|
+
|
|
338
|
+
Format: /outcome memory_id worked|failed|partial [notes]
|
|
339
|
+
Example: /outcome abc123 worked The solution fixed the issue
|
|
340
|
+
"""
|
|
341
|
+
parts = args.strip().split(None, 2)
|
|
342
|
+
|
|
343
|
+
if len(parts) < 2:
|
|
344
|
+
return CommandResult(
|
|
345
|
+
success=False,
|
|
346
|
+
command=CommandType.OUTCOME,
|
|
347
|
+
message="Invalid arguments",
|
|
348
|
+
error="Usage: /outcome memory_id worked|failed|partial [notes]",
|
|
349
|
+
)
|
|
350
|
+
|
|
351
|
+
memory_id = parts[0]
|
|
352
|
+
outcome_str = parts[1].lower()
|
|
353
|
+
notes = parts[2] if len(parts) > 2 else None
|
|
354
|
+
|
|
355
|
+
# Parse outcome
|
|
356
|
+
try:
|
|
357
|
+
outcome = Outcome(outcome_str)
|
|
358
|
+
except ValueError:
|
|
359
|
+
return CommandResult(
|
|
360
|
+
success=False,
|
|
361
|
+
command=CommandType.OUTCOME,
|
|
362
|
+
message="Invalid outcome value",
|
|
363
|
+
error=f"Outcome must be one of: worked, failed, partial. Got: {outcome_str}",
|
|
364
|
+
)
|
|
365
|
+
|
|
366
|
+
# Check if memory exists
|
|
367
|
+
memory = await self.engine.get(memory_id)
|
|
368
|
+
if not memory:
|
|
369
|
+
return CommandResult(
|
|
370
|
+
success=False,
|
|
371
|
+
command=CommandType.OUTCOME,
|
|
372
|
+
message="Memory not found",
|
|
373
|
+
error=f"No memory found with ID: {memory_id}",
|
|
374
|
+
)
|
|
375
|
+
|
|
376
|
+
# Record the outcome
|
|
377
|
+
old_score = memory.outcome_score
|
|
378
|
+
updated_memory = await self.engine.record_outcome(
|
|
379
|
+
memory_id=memory_id,
|
|
380
|
+
outcome=outcome,
|
|
381
|
+
context=notes,
|
|
382
|
+
)
|
|
383
|
+
|
|
384
|
+
return CommandResult(
|
|
385
|
+
success=True,
|
|
386
|
+
command=CommandType.OUTCOME,
|
|
387
|
+
message=f"Outcome recorded for memory {memory_id}",
|
|
388
|
+
data={
|
|
389
|
+
"memory_id": memory_id,
|
|
390
|
+
"outcome": outcome.value,
|
|
391
|
+
"old_score": old_score,
|
|
392
|
+
"new_score": updated_memory.outcome_score,
|
|
393
|
+
"notes": notes,
|
|
394
|
+
},
|
|
395
|
+
)
|
|
396
|
+
|
|
397
|
+
async def _context(self, args: str, project: str | None) -> CommandResult:
|
|
398
|
+
"""Get formatted context for current project.
|
|
399
|
+
|
|
400
|
+
Format: /context [project_name] [limit:N]
|
|
401
|
+
Example: /context my-project limit:10
|
|
402
|
+
"""
|
|
403
|
+
args = args.strip()
|
|
404
|
+
limit = self.config.max_results
|
|
405
|
+
|
|
406
|
+
# Check for limit suffix
|
|
407
|
+
limit_match = re.search(r"\s+limit:(\d+)$", args)
|
|
408
|
+
if limit_match:
|
|
409
|
+
limit = min(int(limit_match.group(1)), 50)
|
|
410
|
+
args = args[: limit_match.start()].strip()
|
|
411
|
+
|
|
412
|
+
# Use args as project name if provided
|
|
413
|
+
target_project = args or project
|
|
414
|
+
|
|
415
|
+
# Get context
|
|
416
|
+
context = await self.engine.get_context(
|
|
417
|
+
project=target_project,
|
|
418
|
+
limit=limit,
|
|
419
|
+
)
|
|
420
|
+
|
|
421
|
+
return CommandResult(
|
|
422
|
+
success=True,
|
|
423
|
+
command=CommandType.CONTEXT,
|
|
424
|
+
message=f"Context for project: {target_project or 'all'}",
|
|
425
|
+
data={
|
|
426
|
+
"context": context.formatted,
|
|
427
|
+
"memory_count": context.memory_count,
|
|
428
|
+
"token_estimate": context.token_estimate,
|
|
429
|
+
"project": target_project,
|
|
430
|
+
},
|
|
431
|
+
)
|
|
432
|
+
|
|
433
|
+
async def _memories(self, args: str, project: str | None) -> CommandResult:
|
|
434
|
+
"""List memories with optional filtering.
|
|
435
|
+
|
|
436
|
+
Format: /memories [category:CATEGORY] [scope:SCOPE] [limit:N]
|
|
437
|
+
Example: /memories category:gotcha limit:20
|
|
438
|
+
"""
|
|
439
|
+
category: MemoryCategory | None = None
|
|
440
|
+
scope: MemoryScope | None = None
|
|
441
|
+
limit = self.config.max_results
|
|
442
|
+
include_archived = self.config.include_archived
|
|
443
|
+
|
|
444
|
+
# Parse arguments
|
|
445
|
+
args_str = args.strip()
|
|
446
|
+
|
|
447
|
+
# Category filter
|
|
448
|
+
cat_match = re.search(r"category:(\w+)", args_str, re.IGNORECASE)
|
|
449
|
+
if cat_match:
|
|
450
|
+
cat_str = cat_match.group(1).lower()
|
|
451
|
+
try:
|
|
452
|
+
category = MemoryCategory(cat_str)
|
|
453
|
+
except ValueError:
|
|
454
|
+
for cat in MemoryCategory:
|
|
455
|
+
if cat.value.startswith(cat_str):
|
|
456
|
+
category = cat
|
|
457
|
+
break
|
|
458
|
+
|
|
459
|
+
# Scope filter
|
|
460
|
+
scope_match = re.search(r"scope:(\w+)", args_str, re.IGNORECASE)
|
|
461
|
+
if scope_match:
|
|
462
|
+
scope_str = scope_match.group(1).lower()
|
|
463
|
+
try:
|
|
464
|
+
scope = MemoryScope(scope_str)
|
|
465
|
+
except ValueError:
|
|
466
|
+
pass
|
|
467
|
+
|
|
468
|
+
# Limit
|
|
469
|
+
limit_match = re.search(r"limit:(\d+)", args_str)
|
|
470
|
+
if limit_match:
|
|
471
|
+
limit = min(int(limit_match.group(1)), 100)
|
|
472
|
+
|
|
473
|
+
# Include archived flag
|
|
474
|
+
if "--archived" in args_str or "-a" in args_str:
|
|
475
|
+
include_archived = True
|
|
476
|
+
|
|
477
|
+
# List memories
|
|
478
|
+
memories = await self.engine.list(
|
|
479
|
+
project=project,
|
|
480
|
+
category=category,
|
|
481
|
+
scope=scope,
|
|
482
|
+
include_archived=include_archived,
|
|
483
|
+
limit=limit,
|
|
484
|
+
)
|
|
485
|
+
|
|
486
|
+
# Get stats
|
|
487
|
+
stats = await self.engine.stats(project=project)
|
|
488
|
+
|
|
489
|
+
memories_data = []
|
|
490
|
+
for memory in memories:
|
|
491
|
+
memories_data.append({
|
|
492
|
+
"id": memory.id,
|
|
493
|
+
"content": memory.content[:100] + ("..." if len(memory.content) > 100 else ""),
|
|
494
|
+
"category": memory.category.value,
|
|
495
|
+
"scope": memory.scope.value,
|
|
496
|
+
"outcome_score": memory.outcome_score,
|
|
497
|
+
"use_count": memory.use_count,
|
|
498
|
+
"archived": memory.archived,
|
|
499
|
+
})
|
|
500
|
+
|
|
501
|
+
return CommandResult(
|
|
502
|
+
success=True,
|
|
503
|
+
command=CommandType.MEMORIES,
|
|
504
|
+
message=f"Listing {len(memories)} memories",
|
|
505
|
+
data={
|
|
506
|
+
"memories": memories_data,
|
|
507
|
+
"stats": {
|
|
508
|
+
"total": stats.total_memories,
|
|
509
|
+
"active": stats.active_memories,
|
|
510
|
+
"archived": stats.archived_memories,
|
|
511
|
+
"by_category": {k.value: v for k, v in stats.by_category.items()},
|
|
512
|
+
},
|
|
513
|
+
"filters": {
|
|
514
|
+
"category": category.value if category else None,
|
|
515
|
+
"scope": scope.value if scope else None,
|
|
516
|
+
"project": project,
|
|
517
|
+
"include_archived": include_archived,
|
|
518
|
+
},
|
|
519
|
+
},
|
|
520
|
+
)
|
|
521
|
+
|
|
522
|
+
|
|
523
|
+
def get_command_schemas() -> dict[str, dict[str, Any]]:
|
|
524
|
+
"""Generate JSON schemas for all commands.
|
|
525
|
+
|
|
526
|
+
Returns:
|
|
527
|
+
Dictionary mapping command names to their JSON schemas
|
|
528
|
+
"""
|
|
529
|
+
schemas = {
|
|
530
|
+
"remember": {
|
|
531
|
+
"name": "remember",
|
|
532
|
+
"description": "Store a new memory in the memory layer",
|
|
533
|
+
"parameters": {
|
|
534
|
+
"type": "object",
|
|
535
|
+
"properties": {
|
|
536
|
+
"content": {
|
|
537
|
+
"type": "string",
|
|
538
|
+
"description": "The content to remember",
|
|
539
|
+
},
|
|
540
|
+
"category": {
|
|
541
|
+
"type": "string",
|
|
542
|
+
"enum": [c.value for c in MemoryCategory],
|
|
543
|
+
"description": "Category of the memory",
|
|
544
|
+
"default": "pattern",
|
|
545
|
+
},
|
|
546
|
+
},
|
|
547
|
+
"required": ["content"],
|
|
548
|
+
},
|
|
549
|
+
"examples": [
|
|
550
|
+
"/remember Always run migrations before deploying",
|
|
551
|
+
"/remember category:gotcha Docker needs --shm-size for PyTorch",
|
|
552
|
+
],
|
|
553
|
+
},
|
|
554
|
+
"recall": {
|
|
555
|
+
"name": "recall",
|
|
556
|
+
"description": "Search for memories matching a query",
|
|
557
|
+
"parameters": {
|
|
558
|
+
"type": "object",
|
|
559
|
+
"properties": {
|
|
560
|
+
"query": {
|
|
561
|
+
"type": "string",
|
|
562
|
+
"description": "Search query",
|
|
563
|
+
},
|
|
564
|
+
"limit": {
|
|
565
|
+
"type": "integer",
|
|
566
|
+
"description": "Maximum number of results",
|
|
567
|
+
"default": 10,
|
|
568
|
+
},
|
|
569
|
+
},
|
|
570
|
+
"required": ["query"],
|
|
571
|
+
},
|
|
572
|
+
"examples": [
|
|
573
|
+
"/recall docker memory issues",
|
|
574
|
+
"/recall database connection limit:5",
|
|
575
|
+
],
|
|
576
|
+
},
|
|
577
|
+
"forget": {
|
|
578
|
+
"name": "forget",
|
|
579
|
+
"description": "Delete or archive a memory",
|
|
580
|
+
"parameters": {
|
|
581
|
+
"type": "object",
|
|
582
|
+
"properties": {
|
|
583
|
+
"memory_id": {
|
|
584
|
+
"type": "string",
|
|
585
|
+
"description": "ID of the memory to forget",
|
|
586
|
+
},
|
|
587
|
+
"permanent": {
|
|
588
|
+
"type": "boolean",
|
|
589
|
+
"description": "Permanently delete instead of archive",
|
|
590
|
+
"default": False,
|
|
591
|
+
},
|
|
592
|
+
},
|
|
593
|
+
"required": ["memory_id"],
|
|
594
|
+
},
|
|
595
|
+
"examples": [
|
|
596
|
+
"/forget abc123",
|
|
597
|
+
"/forget abc123 --permanent",
|
|
598
|
+
],
|
|
599
|
+
},
|
|
600
|
+
"outcome": {
|
|
601
|
+
"name": "outcome",
|
|
602
|
+
"description": "Record whether a memory was helpful",
|
|
603
|
+
"parameters": {
|
|
604
|
+
"type": "object",
|
|
605
|
+
"properties": {
|
|
606
|
+
"memory_id": {
|
|
607
|
+
"type": "string",
|
|
608
|
+
"description": "ID of the memory",
|
|
609
|
+
},
|
|
610
|
+
"outcome": {
|
|
611
|
+
"type": "string",
|
|
612
|
+
"enum": ["worked", "failed", "partial"],
|
|
613
|
+
"description": "The outcome of using this memory",
|
|
614
|
+
},
|
|
615
|
+
"notes": {
|
|
616
|
+
"type": "string",
|
|
617
|
+
"description": "Optional notes about the outcome",
|
|
618
|
+
},
|
|
619
|
+
},
|
|
620
|
+
"required": ["memory_id", "outcome"],
|
|
621
|
+
},
|
|
622
|
+
"examples": [
|
|
623
|
+
"/outcome abc123 worked",
|
|
624
|
+
"/outcome abc123 failed The solution was outdated",
|
|
625
|
+
],
|
|
626
|
+
},
|
|
627
|
+
"context": {
|
|
628
|
+
"name": "context",
|
|
629
|
+
"description": "Get formatted context for the current project",
|
|
630
|
+
"parameters": {
|
|
631
|
+
"type": "object",
|
|
632
|
+
"properties": {
|
|
633
|
+
"project": {
|
|
634
|
+
"type": "string",
|
|
635
|
+
"description": "Project name (optional, uses current project)",
|
|
636
|
+
},
|
|
637
|
+
"limit": {
|
|
638
|
+
"type": "integer",
|
|
639
|
+
"description": "Maximum number of memories to include",
|
|
640
|
+
"default": 10,
|
|
641
|
+
},
|
|
642
|
+
},
|
|
643
|
+
"required": [],
|
|
644
|
+
},
|
|
645
|
+
"examples": [
|
|
646
|
+
"/context",
|
|
647
|
+
"/context my-project limit:20",
|
|
648
|
+
],
|
|
649
|
+
},
|
|
650
|
+
"memories": {
|
|
651
|
+
"name": "memories",
|
|
652
|
+
"description": "List memories with optional filtering",
|
|
653
|
+
"parameters": {
|
|
654
|
+
"type": "object",
|
|
655
|
+
"properties": {
|
|
656
|
+
"category": {
|
|
657
|
+
"type": "string",
|
|
658
|
+
"enum": [c.value for c in MemoryCategory],
|
|
659
|
+
"description": "Filter by category",
|
|
660
|
+
},
|
|
661
|
+
"scope": {
|
|
662
|
+
"type": "string",
|
|
663
|
+
"enum": [s.value for s in MemoryScope],
|
|
664
|
+
"description": "Filter by scope",
|
|
665
|
+
},
|
|
666
|
+
"limit": {
|
|
667
|
+
"type": "integer",
|
|
668
|
+
"description": "Maximum number of results",
|
|
669
|
+
"default": 10,
|
|
670
|
+
},
|
|
671
|
+
"include_archived": {
|
|
672
|
+
"type": "boolean",
|
|
673
|
+
"description": "Include archived memories",
|
|
674
|
+
"default": False,
|
|
675
|
+
},
|
|
676
|
+
},
|
|
677
|
+
"required": [],
|
|
678
|
+
},
|
|
679
|
+
"examples": [
|
|
680
|
+
"/memories",
|
|
681
|
+
"/memories category:gotcha limit:20",
|
|
682
|
+
"/memories scope:global --archived",
|
|
683
|
+
],
|
|
684
|
+
},
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
return schemas
|
|
688
|
+
|
|
689
|
+
|
|
690
|
+
def export_command_schemas(output_path: Path) -> None:
|
|
691
|
+
"""Export command schemas to a JSON file.
|
|
692
|
+
|
|
693
|
+
Args:
|
|
694
|
+
output_path: Path to write the JSON schema file
|
|
695
|
+
"""
|
|
696
|
+
schemas = get_command_schemas()
|
|
697
|
+
with open(output_path, "w") as f:
|
|
698
|
+
json.dump(schemas, f, indent=2)
|