up-cli 0.1.1__py3-none-any.whl → 0.5.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.
- up/__init__.py +1 -1
- up/ai_cli.py +229 -0
- up/cli.py +75 -4
- up/commands/agent.py +521 -0
- up/commands/bisect.py +343 -0
- up/commands/branch.py +350 -0
- up/commands/dashboard.py +248 -0
- up/commands/init.py +195 -6
- up/commands/learn.py +1741 -0
- up/commands/memory.py +545 -0
- up/commands/new.py +108 -10
- up/commands/provenance.py +267 -0
- up/commands/review.py +239 -0
- up/commands/start.py +1124 -0
- up/commands/status.py +360 -0
- up/commands/summarize.py +122 -0
- up/commands/sync.py +317 -0
- up/commands/vibe.py +304 -0
- up/context.py +421 -0
- up/core/__init__.py +69 -0
- up/core/checkpoint.py +479 -0
- up/core/provenance.py +364 -0
- up/core/state.py +678 -0
- up/events.py +512 -0
- up/git/__init__.py +37 -0
- up/git/utils.py +270 -0
- up/git/worktree.py +331 -0
- up/learn/__init__.py +155 -0
- up/learn/analyzer.py +227 -0
- up/learn/plan.py +374 -0
- up/learn/research.py +511 -0
- up/learn/utils.py +117 -0
- up/memory.py +1096 -0
- up/parallel.py +551 -0
- up/summarizer.py +407 -0
- up/templates/__init__.py +70 -2
- up/templates/config/__init__.py +502 -20
- up/templates/docs/SKILL.md +28 -0
- up/templates/docs/__init__.py +341 -0
- up/templates/docs/standards/HEADERS.md +24 -0
- up/templates/docs/standards/STRUCTURE.md +18 -0
- up/templates/docs/standards/TEMPLATES.md +19 -0
- up/templates/learn/__init__.py +567 -14
- up/templates/loop/__init__.py +546 -27
- up/templates/mcp/__init__.py +474 -0
- up/templates/projects/__init__.py +786 -0
- up/ui/__init__.py +14 -0
- up/ui/loop_display.py +650 -0
- up/ui/theme.py +137 -0
- up_cli-0.5.0.dist-info/METADATA +519 -0
- up_cli-0.5.0.dist-info/RECORD +55 -0
- up_cli-0.1.1.dist-info/METADATA +0 -186
- up_cli-0.1.1.dist-info/RECORD +0 -14
- {up_cli-0.1.1.dist-info → up_cli-0.5.0.dist-info}/WHEEL +0 -0
- {up_cli-0.1.1.dist-info → up_cli-0.5.0.dist-info}/entry_points.txt +0 -0
up/commands/memory.py
ADDED
|
@@ -0,0 +1,545 @@
|
|
|
1
|
+
"""up memory - Long-term memory management."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
import click
|
|
7
|
+
from rich.console import Console
|
|
8
|
+
from rich.panel import Panel
|
|
9
|
+
from rich.table import Table
|
|
10
|
+
|
|
11
|
+
console = Console()
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@click.group()
|
|
15
|
+
def memory_cmd():
|
|
16
|
+
"""Long-term memory management.
|
|
17
|
+
|
|
18
|
+
Store and recall information across sessions using semantic search.
|
|
19
|
+
Automatically indexes git commits and file changes.
|
|
20
|
+
"""
|
|
21
|
+
pass
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@memory_cmd.command("search")
|
|
25
|
+
@click.argument("query")
|
|
26
|
+
@click.option("--limit", "-n", default=5, help="Number of results")
|
|
27
|
+
@click.option("--type", "-t", "entry_type", help="Filter by type (session, learning, decision, error, commit)")
|
|
28
|
+
@click.option("--branch", "-b", help="Filter by branch (use 'current' for current branch)")
|
|
29
|
+
def memory_search(query: str, limit: int, entry_type: str, branch: str):
|
|
30
|
+
"""Search memory for relevant information.
|
|
31
|
+
|
|
32
|
+
Uses semantic search (with ChromaDB) or keyword search (fallback).
|
|
33
|
+
Filter by branch to see knowledge from specific versions.
|
|
34
|
+
|
|
35
|
+
\b
|
|
36
|
+
Examples:
|
|
37
|
+
up memory search "authentication"
|
|
38
|
+
up memory search "database error" --type error
|
|
39
|
+
up memory search "api design" --branch main
|
|
40
|
+
up memory search "bug fix" --branch current
|
|
41
|
+
"""
|
|
42
|
+
from up.memory import MemoryManager
|
|
43
|
+
|
|
44
|
+
manager = MemoryManager()
|
|
45
|
+
|
|
46
|
+
# Handle 'current' branch shorthand
|
|
47
|
+
if branch == "current":
|
|
48
|
+
branch = manager._get_git_context()["branch"]
|
|
49
|
+
console.print(f"[dim]Searching on branch: {branch}[/]\n")
|
|
50
|
+
|
|
51
|
+
results = manager.search(query, limit=limit, entry_type=entry_type, branch=branch)
|
|
52
|
+
|
|
53
|
+
if not results:
|
|
54
|
+
branch_info = f" on branch '{branch}'" if branch else ""
|
|
55
|
+
console.print(f"[dim]No memories found for '{query}'{branch_info}[/]")
|
|
56
|
+
return
|
|
57
|
+
|
|
58
|
+
console.print(f"\n[bold]Found {len(results)} memories:[/]\n")
|
|
59
|
+
|
|
60
|
+
for entry in results:
|
|
61
|
+
# Color by type
|
|
62
|
+
type_colors = {
|
|
63
|
+
"session": "blue",
|
|
64
|
+
"learning": "green",
|
|
65
|
+
"decision": "yellow",
|
|
66
|
+
"error": "red",
|
|
67
|
+
"commit": "cyan",
|
|
68
|
+
"code": "magenta",
|
|
69
|
+
}
|
|
70
|
+
color = type_colors.get(entry.type, "white")
|
|
71
|
+
|
|
72
|
+
# Add branch info to title
|
|
73
|
+
branch_info = f" @{entry.branch}" if entry.branch else ""
|
|
74
|
+
|
|
75
|
+
console.print(Panel(
|
|
76
|
+
entry.content[:500] + ("..." if len(entry.content) > 500 else ""),
|
|
77
|
+
title=f"[{color}]{entry.type}[/]{branch_info} | {entry.timestamp[:10]}",
|
|
78
|
+
border_style=color
|
|
79
|
+
))
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
@memory_cmd.command("recall")
|
|
83
|
+
@click.argument("topic")
|
|
84
|
+
def memory_recall(topic: str):
|
|
85
|
+
"""Recall information about a topic.
|
|
86
|
+
|
|
87
|
+
Returns a formatted summary of relevant memories.
|
|
88
|
+
"""
|
|
89
|
+
from up.memory import MemoryManager
|
|
90
|
+
|
|
91
|
+
manager = MemoryManager()
|
|
92
|
+
result = manager.recall(topic)
|
|
93
|
+
console.print(result)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
@memory_cmd.command("stats")
|
|
97
|
+
def memory_stats():
|
|
98
|
+
"""Show memory statistics including branch info."""
|
|
99
|
+
from up.memory import MemoryManager, _check_chromadb
|
|
100
|
+
|
|
101
|
+
manager = MemoryManager()
|
|
102
|
+
stats = manager.get_stats()
|
|
103
|
+
|
|
104
|
+
console.print(Panel.fit(
|
|
105
|
+
"[bold blue]Memory Statistics[/]",
|
|
106
|
+
border_style="blue"
|
|
107
|
+
))
|
|
108
|
+
|
|
109
|
+
# Backend info
|
|
110
|
+
backend = stats.get("backend", "json")
|
|
111
|
+
if backend == "chromadb":
|
|
112
|
+
backend_desc = "ChromaDB (semantic search)"
|
|
113
|
+
else:
|
|
114
|
+
backend_desc = "JSON (keyword search)"
|
|
115
|
+
|
|
116
|
+
console.print(f"\nBackend: [cyan]{backend_desc}[/]")
|
|
117
|
+
console.print(f"Current: [cyan]{stats.get('current_branch', 'unknown')}[/] @ [dim]{stats.get('current_commit', 'unknown')}[/]")
|
|
118
|
+
|
|
119
|
+
if backend == "json" and not _check_chromadb():
|
|
120
|
+
console.print("[dim]Install chromadb for semantic search: pip install chromadb[/]")
|
|
121
|
+
|
|
122
|
+
# Stats table
|
|
123
|
+
table = Table(show_header=True)
|
|
124
|
+
table.add_column("Type", style="cyan")
|
|
125
|
+
table.add_column("Count", justify="right")
|
|
126
|
+
|
|
127
|
+
table.add_row("Sessions", str(stats.get("sessions", 0)))
|
|
128
|
+
table.add_row("Learnings", str(stats.get("learnings", 0)))
|
|
129
|
+
table.add_row("Decisions", str(stats.get("decisions", 0)))
|
|
130
|
+
table.add_row("Errors", str(stats.get("errors", 0)))
|
|
131
|
+
table.add_row("Commits", str(stats.get("commits", 0)))
|
|
132
|
+
table.add_row("Code Files", str(stats.get("code_files", 0)))
|
|
133
|
+
table.add_row("─" * 10, "─" * 5)
|
|
134
|
+
table.add_row("[bold]Total[/]", f"[bold]{stats.get('total', 0)}[/]")
|
|
135
|
+
|
|
136
|
+
console.print(table)
|
|
137
|
+
|
|
138
|
+
# Branch breakdown
|
|
139
|
+
branches = stats.get("branches", {})
|
|
140
|
+
if branches and len(branches) > 1:
|
|
141
|
+
console.print("\n[bold]Knowledge by Branch:[/]")
|
|
142
|
+
branch_table = Table(show_header=True)
|
|
143
|
+
branch_table.add_column("Branch", style="cyan")
|
|
144
|
+
branch_table.add_column("Entries", justify="right")
|
|
145
|
+
|
|
146
|
+
for branch, count in sorted(branches.items(), key=lambda x: x[1], reverse=True):
|
|
147
|
+
marker = " ←" if branch == stats.get("current_branch") else ""
|
|
148
|
+
branch_table.add_row(f"{branch}{marker}", str(count))
|
|
149
|
+
|
|
150
|
+
console.print(branch_table)
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
@memory_cmd.command("branch")
|
|
154
|
+
@click.argument("branch_name", required=False)
|
|
155
|
+
@click.option("--compare", "-c", help="Compare with another branch")
|
|
156
|
+
def memory_branch(branch_name: str, compare: str):
|
|
157
|
+
"""Show or compare knowledge by branch.
|
|
158
|
+
|
|
159
|
+
Shows what learnings, decisions, and errors were recorded on each branch.
|
|
160
|
+
Useful for reviewing what was learned during feature development.
|
|
161
|
+
|
|
162
|
+
\b
|
|
163
|
+
Examples:
|
|
164
|
+
up memory branch # Show current branch knowledge
|
|
165
|
+
up memory branch main # Show main branch knowledge
|
|
166
|
+
up memory branch feature-x --compare main # Compare branches
|
|
167
|
+
"""
|
|
168
|
+
from up.memory import MemoryManager
|
|
169
|
+
|
|
170
|
+
manager = MemoryManager()
|
|
171
|
+
git_ctx = manager._get_git_context()
|
|
172
|
+
|
|
173
|
+
# Default to current branch
|
|
174
|
+
if not branch_name:
|
|
175
|
+
branch_name = git_ctx["branch"]
|
|
176
|
+
|
|
177
|
+
if compare:
|
|
178
|
+
# Compare two branches
|
|
179
|
+
comparison = manager.compare_branches(branch_name, compare)
|
|
180
|
+
|
|
181
|
+
console.print(Panel.fit(
|
|
182
|
+
f"[bold]Comparing: {branch_name} vs {compare}[/]",
|
|
183
|
+
border_style="blue"
|
|
184
|
+
))
|
|
185
|
+
|
|
186
|
+
# Summary table
|
|
187
|
+
table = Table(show_header=True)
|
|
188
|
+
table.add_column("Metric", style="cyan")
|
|
189
|
+
table.add_column(branch_name, justify="right")
|
|
190
|
+
table.add_column(compare, justify="right")
|
|
191
|
+
|
|
192
|
+
b1 = comparison["branch1"]
|
|
193
|
+
b2 = comparison["branch2"]
|
|
194
|
+
|
|
195
|
+
table.add_row("Total Entries", str(b1["total"]), str(b2["total"]))
|
|
196
|
+
table.add_row("Learnings", str(b1["learnings"]), str(b2["learnings"]))
|
|
197
|
+
table.add_row("Decisions", str(b1["decisions"]), str(b2["decisions"]))
|
|
198
|
+
|
|
199
|
+
console.print(table)
|
|
200
|
+
|
|
201
|
+
# Unique knowledge
|
|
202
|
+
unique_b1 = comparison["unique_to_branch1"]
|
|
203
|
+
unique_b2 = comparison["unique_to_branch2"]
|
|
204
|
+
|
|
205
|
+
if unique_b1["learnings"] or unique_b1["decisions"]:
|
|
206
|
+
console.print(f"\n[bold green]Unique to {branch_name}:[/]")
|
|
207
|
+
for learning in unique_b1["learnings"][:3]:
|
|
208
|
+
console.print(f" 💡 {learning.content[:60]}...")
|
|
209
|
+
for decision in unique_b1["decisions"][:3]:
|
|
210
|
+
console.print(f" 🎯 {decision.content[:60]}...")
|
|
211
|
+
|
|
212
|
+
if unique_b2["learnings"] or unique_b2["decisions"]:
|
|
213
|
+
console.print(f"\n[bold yellow]Unique to {compare}:[/]")
|
|
214
|
+
for learning in unique_b2["learnings"][:3]:
|
|
215
|
+
console.print(f" 💡 {learning.content[:60]}...")
|
|
216
|
+
for decision in unique_b2["decisions"][:3]:
|
|
217
|
+
console.print(f" 🎯 {decision.content[:60]}...")
|
|
218
|
+
|
|
219
|
+
else:
|
|
220
|
+
# Show single branch knowledge
|
|
221
|
+
knowledge = manager.get_branch_knowledge(branch_name)
|
|
222
|
+
|
|
223
|
+
total = sum(len(v) for v in knowledge.values())
|
|
224
|
+
current_marker = " (current)" if branch_name == git_ctx["branch"] else ""
|
|
225
|
+
|
|
226
|
+
console.print(Panel.fit(
|
|
227
|
+
f"[bold]Knowledge on branch: {branch_name}{current_marker}[/]",
|
|
228
|
+
border_style="blue"
|
|
229
|
+
))
|
|
230
|
+
|
|
231
|
+
if total == 0:
|
|
232
|
+
console.print("[dim]No knowledge recorded on this branch yet.[/]")
|
|
233
|
+
console.print("\n[dim]Knowledge is recorded when you:[/]")
|
|
234
|
+
console.print(" • Use [cyan]up memory record --learning[/]")
|
|
235
|
+
console.print(" • Use [cyan]up memory record --decision[/]")
|
|
236
|
+
console.print(" • Errors occur during [cyan]up start[/]")
|
|
237
|
+
return
|
|
238
|
+
|
|
239
|
+
console.print(f"\n[bold]Total: {total} entries[/]\n")
|
|
240
|
+
|
|
241
|
+
for entry_type, entries in knowledge.items():
|
|
242
|
+
if entries:
|
|
243
|
+
icon = {"learnings": "💡", "decisions": "🎯", "errors": "❌", "commits": "📝"}.get(entry_type, "•")
|
|
244
|
+
console.print(f"[bold]{icon} {entry_type.title()} ({len(entries)}):[/]")
|
|
245
|
+
for entry in entries[:3]:
|
|
246
|
+
preview = entry.content[:70].replace("\n", " ")
|
|
247
|
+
console.print(f" • {preview}...")
|
|
248
|
+
if len(entries) > 3:
|
|
249
|
+
console.print(f" [dim]...and {len(entries) - 3} more[/]")
|
|
250
|
+
console.print()
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
@memory_cmd.command("sync")
|
|
254
|
+
def memory_sync():
|
|
255
|
+
"""Sync memory with current state.
|
|
256
|
+
|
|
257
|
+
Indexes recent git commits and file changes into memory.
|
|
258
|
+
All entries are tagged with the current branch for version tracking.
|
|
259
|
+
"""
|
|
260
|
+
import os
|
|
261
|
+
import warnings
|
|
262
|
+
from up.memory import MemoryManager
|
|
263
|
+
from tqdm import tqdm
|
|
264
|
+
|
|
265
|
+
# Suppress noisy warnings from tokenizers
|
|
266
|
+
os.environ["TOKENIZERS_PARALLELISM"] = "false"
|
|
267
|
+
warnings.filterwarnings("ignore", category=UserWarning)
|
|
268
|
+
|
|
269
|
+
manager = MemoryManager()
|
|
270
|
+
git_ctx = manager._get_git_context()
|
|
271
|
+
|
|
272
|
+
console.print(f"[bold]Syncing memory...[/]")
|
|
273
|
+
console.print(f"[dim]Branch: {git_ctx['branch']} @ {git_ctx['commit']}[/]\n")
|
|
274
|
+
|
|
275
|
+
with tqdm(total=2, desc="Syncing", bar_format="{l_bar}{bar}| {n_fmt}/{total_fmt}") as pbar:
|
|
276
|
+
pbar.set_description("Indexing commits")
|
|
277
|
+
commits = manager.index_recent_commits(count=20)
|
|
278
|
+
pbar.update(1)
|
|
279
|
+
|
|
280
|
+
pbar.set_description("Indexing files")
|
|
281
|
+
files = manager.index_file_changes()
|
|
282
|
+
pbar.update(1)
|
|
283
|
+
|
|
284
|
+
console.print(f"\n[green]✓[/] Indexed [cyan]{commits}[/] commits on [cyan]{git_ctx['branch']}[/]")
|
|
285
|
+
console.print(f"[green]✓[/] Indexed [cyan]{files}[/] files")
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
@memory_cmd.command("record")
|
|
289
|
+
@click.option("--learning", "-l", help="Record a learning")
|
|
290
|
+
@click.option("--decision", "-d", help="Record a decision")
|
|
291
|
+
@click.option("--error", "-e", help="Record an error")
|
|
292
|
+
@click.option("--solution", "-s", help="Solution for error (use with --error)")
|
|
293
|
+
def memory_record(learning: str, decision: str, error: str, solution: str):
|
|
294
|
+
"""Record information to memory.
|
|
295
|
+
|
|
296
|
+
\b
|
|
297
|
+
Examples:
|
|
298
|
+
up memory record --learning "Use dataclasses for config"
|
|
299
|
+
up memory record --decision "Use PostgreSQL for persistence"
|
|
300
|
+
up memory record --error "ImportError" --solution "pip install package"
|
|
301
|
+
"""
|
|
302
|
+
from up.memory import MemoryManager
|
|
303
|
+
|
|
304
|
+
manager = MemoryManager()
|
|
305
|
+
|
|
306
|
+
if learning:
|
|
307
|
+
manager.record_learning(learning)
|
|
308
|
+
console.print(f"[green]✓[/] Recorded learning: {learning}")
|
|
309
|
+
|
|
310
|
+
if decision:
|
|
311
|
+
manager.record_decision(decision)
|
|
312
|
+
console.print(f"[green]✓[/] Recorded decision: {decision}")
|
|
313
|
+
|
|
314
|
+
if error:
|
|
315
|
+
manager.record_error(error, solution)
|
|
316
|
+
console.print(f"[green]✓[/] Recorded error: {error}")
|
|
317
|
+
if solution:
|
|
318
|
+
console.print(f" Solution: {solution}")
|
|
319
|
+
|
|
320
|
+
if not any([learning, decision, error]):
|
|
321
|
+
console.print("[yellow]No input provided. Use --learning, --decision, or --error[/]")
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
@memory_cmd.command("session")
|
|
325
|
+
@click.option("--start", is_flag=True, help="Start a new session")
|
|
326
|
+
@click.option("--end", is_flag=True, help="End current session")
|
|
327
|
+
@click.option("--summary", "-s", help="Summary for session end")
|
|
328
|
+
def memory_session(start: bool, end: bool, summary: str):
|
|
329
|
+
"""Manage memory sessions.
|
|
330
|
+
|
|
331
|
+
\b
|
|
332
|
+
Examples:
|
|
333
|
+
up memory session --start
|
|
334
|
+
up memory session --end --summary "Implemented auth feature"
|
|
335
|
+
"""
|
|
336
|
+
from up.memory import MemoryManager
|
|
337
|
+
|
|
338
|
+
manager = MemoryManager()
|
|
339
|
+
|
|
340
|
+
if start:
|
|
341
|
+
session_id = manager.start_session()
|
|
342
|
+
console.print(f"[green]✓[/] Started session: [cyan]{session_id}[/]")
|
|
343
|
+
|
|
344
|
+
elif end:
|
|
345
|
+
manager.end_session(summary)
|
|
346
|
+
console.print("[green]✓[/] Session ended and saved to memory")
|
|
347
|
+
|
|
348
|
+
else:
|
|
349
|
+
# Show current session status
|
|
350
|
+
session_file = manager.workspace / ".up" / "current_session.json"
|
|
351
|
+
if session_file.exists():
|
|
352
|
+
data = json.loads(session_file.read_text())
|
|
353
|
+
console.print(f"Current session: [cyan]{data.get('session_id')}[/]")
|
|
354
|
+
console.print(f"Started: {data.get('started_at')}")
|
|
355
|
+
console.print(f"Tasks: {len(data.get('tasks', []))}")
|
|
356
|
+
console.print(f"Files: {len(data.get('files_modified', []))}")
|
|
357
|
+
else:
|
|
358
|
+
console.print("[dim]No active session. Use --start to begin.[/]")
|
|
359
|
+
|
|
360
|
+
|
|
361
|
+
@memory_cmd.command("list")
|
|
362
|
+
@click.option("--type", "-t", "entry_type",
|
|
363
|
+
type=click.Choice(["session", "learning", "decision", "error", "commit"]),
|
|
364
|
+
help="Filter by type")
|
|
365
|
+
@click.option("--limit", "-n", default=10, help="Number of entries")
|
|
366
|
+
def memory_list(entry_type: str, limit: int):
|
|
367
|
+
"""List recent memory entries.
|
|
368
|
+
|
|
369
|
+
\b
|
|
370
|
+
Examples:
|
|
371
|
+
up memory list --type learning
|
|
372
|
+
up memory list --type session --limit 5
|
|
373
|
+
"""
|
|
374
|
+
from up.memory import MemoryManager
|
|
375
|
+
|
|
376
|
+
manager = MemoryManager()
|
|
377
|
+
|
|
378
|
+
if entry_type:
|
|
379
|
+
if entry_type == "session":
|
|
380
|
+
entries = manager.get_recent_sessions(limit)
|
|
381
|
+
elif entry_type == "learning":
|
|
382
|
+
entries = manager.get_learnings(limit)
|
|
383
|
+
elif entry_type == "decision":
|
|
384
|
+
entries = manager.get_decisions(limit)
|
|
385
|
+
elif entry_type == "error":
|
|
386
|
+
entries = manager.get_errors(limit)
|
|
387
|
+
else:
|
|
388
|
+
entries = manager.store.get_by_type(entry_type, limit)
|
|
389
|
+
else:
|
|
390
|
+
# Get all types
|
|
391
|
+
entries = []
|
|
392
|
+
for t in ["session", "learning", "decision", "error", "commit"]:
|
|
393
|
+
entries.extend(manager.store.get_by_type(t, limit // 5 or 2))
|
|
394
|
+
entries.sort(key=lambda e: e.timestamp, reverse=True)
|
|
395
|
+
entries = entries[:limit]
|
|
396
|
+
|
|
397
|
+
if not entries:
|
|
398
|
+
console.print("[dim]No entries found[/]")
|
|
399
|
+
return
|
|
400
|
+
|
|
401
|
+
console.print(f"\n[bold]Memory Entries ({len(entries)}):[/]\n")
|
|
402
|
+
|
|
403
|
+
for entry in entries:
|
|
404
|
+
type_icons = {
|
|
405
|
+
"session": "📅",
|
|
406
|
+
"learning": "💡",
|
|
407
|
+
"decision": "🎯",
|
|
408
|
+
"error": "❌",
|
|
409
|
+
"commit": "📝",
|
|
410
|
+
"code": "💻",
|
|
411
|
+
}
|
|
412
|
+
icon = type_icons.get(entry.type, "•")
|
|
413
|
+
|
|
414
|
+
content_preview = entry.content[:80].replace("\n", " ")
|
|
415
|
+
if len(entry.content) > 80:
|
|
416
|
+
content_preview += "..."
|
|
417
|
+
|
|
418
|
+
console.print(f"{icon} [{entry.type}] {content_preview}")
|
|
419
|
+
console.print(f" [dim]{entry.timestamp[:16]}[/]")
|
|
420
|
+
console.print()
|
|
421
|
+
|
|
422
|
+
|
|
423
|
+
@memory_cmd.command("clear")
|
|
424
|
+
@click.confirmation_option(prompt="Are you sure you want to clear all memory?")
|
|
425
|
+
def memory_clear():
|
|
426
|
+
"""Clear all memory entries."""
|
|
427
|
+
from up.memory import MemoryManager
|
|
428
|
+
|
|
429
|
+
manager = MemoryManager()
|
|
430
|
+
manager.clear()
|
|
431
|
+
console.print("[green]✓[/] Memory cleared")
|
|
432
|
+
|
|
433
|
+
|
|
434
|
+
@memory_cmd.command("reset")
|
|
435
|
+
@click.confirmation_option(prompt="This will delete ALL memory data and re-initialize. Continue?")
|
|
436
|
+
def memory_reset():
|
|
437
|
+
"""Reset memory database completely.
|
|
438
|
+
|
|
439
|
+
Use this if you encounter database corruption errors like:
|
|
440
|
+
- "mismatched types"
|
|
441
|
+
- "InternalError"
|
|
442
|
+
- "Error reading from metadata segment"
|
|
443
|
+
|
|
444
|
+
This deletes the ChromaDB files and creates a fresh database.
|
|
445
|
+
"""
|
|
446
|
+
import shutil
|
|
447
|
+
|
|
448
|
+
cwd = Path.cwd()
|
|
449
|
+
chroma_dir = cwd / ".up" / "memory" / "chroma"
|
|
450
|
+
json_file = cwd / ".up" / "memory" / "index.json"
|
|
451
|
+
|
|
452
|
+
deleted = []
|
|
453
|
+
|
|
454
|
+
if chroma_dir.exists():
|
|
455
|
+
shutil.rmtree(chroma_dir)
|
|
456
|
+
deleted.append("ChromaDB")
|
|
457
|
+
|
|
458
|
+
if json_file.exists():
|
|
459
|
+
json_file.unlink()
|
|
460
|
+
deleted.append("JSON index")
|
|
461
|
+
|
|
462
|
+
if deleted:
|
|
463
|
+
console.print(f"[green]✓[/] Deleted: {', '.join(deleted)}")
|
|
464
|
+
console.print("\n[dim]Run 'up memory sync' to rebuild from git history[/]")
|
|
465
|
+
else:
|
|
466
|
+
console.print("[yellow]No memory files found to delete[/]")
|
|
467
|
+
|
|
468
|
+
|
|
469
|
+
@memory_cmd.command("migrate")
|
|
470
|
+
def memory_migrate():
|
|
471
|
+
"""Migrate JSON memory to ChromaDB.
|
|
472
|
+
|
|
473
|
+
If you have existing data in .up/memory/index.json from before
|
|
474
|
+
ChromaDB was enabled, this command migrates it to ChromaDB.
|
|
475
|
+
"""
|
|
476
|
+
from up.memory import MemoryManager, JSONMemoryStore, MemoryEntry, _check_chromadb
|
|
477
|
+
|
|
478
|
+
cwd = Path.cwd()
|
|
479
|
+
json_file = cwd / ".up" / "memory" / "index.json"
|
|
480
|
+
|
|
481
|
+
if not json_file.exists():
|
|
482
|
+
console.print("[yellow]No JSON memory file found. Nothing to migrate.[/]")
|
|
483
|
+
return
|
|
484
|
+
|
|
485
|
+
if not _check_chromadb():
|
|
486
|
+
console.print("[red]ChromaDB not installed. Install with: pip install chromadb[/]")
|
|
487
|
+
return
|
|
488
|
+
|
|
489
|
+
# Load JSON data
|
|
490
|
+
console.print("[dim]Loading JSON memory...[/]")
|
|
491
|
+
json_store = JSONMemoryStore(cwd)
|
|
492
|
+
entries = list(json_store.entries.values())
|
|
493
|
+
|
|
494
|
+
if not entries:
|
|
495
|
+
console.print("[yellow]No entries in JSON memory. Nothing to migrate.[/]")
|
|
496
|
+
return
|
|
497
|
+
|
|
498
|
+
console.print(f"Found [cyan]{len(entries)}[/] entries to migrate")
|
|
499
|
+
|
|
500
|
+
# Create ChromaDB manager and migrate
|
|
501
|
+
console.print("[dim]Migrating to ChromaDB (this may take a moment)...[/]")
|
|
502
|
+
manager = MemoryManager(cwd, use_vectors=True)
|
|
503
|
+
|
|
504
|
+
if manager._backend != "chromadb":
|
|
505
|
+
console.print("[red]Failed to initialize ChromaDB backend[/]")
|
|
506
|
+
return
|
|
507
|
+
|
|
508
|
+
migrated = 0
|
|
509
|
+
skipped = 0
|
|
510
|
+
for entry in entries:
|
|
511
|
+
try:
|
|
512
|
+
# Clean metadata - convert lists to strings for ChromaDB compatibility
|
|
513
|
+
clean_metadata = {}
|
|
514
|
+
for k, v in entry.metadata.items():
|
|
515
|
+
if isinstance(v, list):
|
|
516
|
+
clean_metadata[k] = ", ".join(str(x) for x in v) if v else ""
|
|
517
|
+
elif v is not None:
|
|
518
|
+
clean_metadata[k] = v
|
|
519
|
+
|
|
520
|
+
# Create clean entry
|
|
521
|
+
clean_entry = MemoryEntry(
|
|
522
|
+
id=entry.id,
|
|
523
|
+
type=entry.type,
|
|
524
|
+
content=entry.content,
|
|
525
|
+
metadata=clean_metadata,
|
|
526
|
+
timestamp=entry.timestamp,
|
|
527
|
+
branch=entry.branch,
|
|
528
|
+
commit=entry.commit,
|
|
529
|
+
)
|
|
530
|
+
|
|
531
|
+
manager.store.add(clean_entry)
|
|
532
|
+
migrated += 1
|
|
533
|
+
except Exception as e:
|
|
534
|
+
console.print(f"[yellow]Skip {entry.id}: {e}[/]")
|
|
535
|
+
skipped += 1
|
|
536
|
+
|
|
537
|
+
console.print(f"\n[green]✓[/] Migrated [cyan]{migrated}/{len(entries)}[/] entries to ChromaDB")
|
|
538
|
+
if skipped:
|
|
539
|
+
console.print(f"[yellow]Skipped {skipped} entries (see warnings above)[/]")
|
|
540
|
+
console.print(f"\n[dim]Old JSON file kept at: {json_file}[/]")
|
|
541
|
+
console.print("[dim]You can delete it manually if migration looks good.[/]")
|
|
542
|
+
|
|
543
|
+
|
|
544
|
+
if __name__ == "__main__":
|
|
545
|
+
memory_cmd()
|