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.
Files changed (55) hide show
  1. up/__init__.py +1 -1
  2. up/ai_cli.py +229 -0
  3. up/cli.py +75 -4
  4. up/commands/agent.py +521 -0
  5. up/commands/bisect.py +343 -0
  6. up/commands/branch.py +350 -0
  7. up/commands/dashboard.py +248 -0
  8. up/commands/init.py +195 -6
  9. up/commands/learn.py +1741 -0
  10. up/commands/memory.py +545 -0
  11. up/commands/new.py +108 -10
  12. up/commands/provenance.py +267 -0
  13. up/commands/review.py +239 -0
  14. up/commands/start.py +1124 -0
  15. up/commands/status.py +360 -0
  16. up/commands/summarize.py +122 -0
  17. up/commands/sync.py +317 -0
  18. up/commands/vibe.py +304 -0
  19. up/context.py +421 -0
  20. up/core/__init__.py +69 -0
  21. up/core/checkpoint.py +479 -0
  22. up/core/provenance.py +364 -0
  23. up/core/state.py +678 -0
  24. up/events.py +512 -0
  25. up/git/__init__.py +37 -0
  26. up/git/utils.py +270 -0
  27. up/git/worktree.py +331 -0
  28. up/learn/__init__.py +155 -0
  29. up/learn/analyzer.py +227 -0
  30. up/learn/plan.py +374 -0
  31. up/learn/research.py +511 -0
  32. up/learn/utils.py +117 -0
  33. up/memory.py +1096 -0
  34. up/parallel.py +551 -0
  35. up/summarizer.py +407 -0
  36. up/templates/__init__.py +70 -2
  37. up/templates/config/__init__.py +502 -20
  38. up/templates/docs/SKILL.md +28 -0
  39. up/templates/docs/__init__.py +341 -0
  40. up/templates/docs/standards/HEADERS.md +24 -0
  41. up/templates/docs/standards/STRUCTURE.md +18 -0
  42. up/templates/docs/standards/TEMPLATES.md +19 -0
  43. up/templates/learn/__init__.py +567 -14
  44. up/templates/loop/__init__.py +546 -27
  45. up/templates/mcp/__init__.py +474 -0
  46. up/templates/projects/__init__.py +786 -0
  47. up/ui/__init__.py +14 -0
  48. up/ui/loop_display.py +650 -0
  49. up/ui/theme.py +137 -0
  50. up_cli-0.5.0.dist-info/METADATA +519 -0
  51. up_cli-0.5.0.dist-info/RECORD +55 -0
  52. up_cli-0.1.1.dist-info/METADATA +0 -186
  53. up_cli-0.1.1.dist-info/RECORD +0 -14
  54. {up_cli-0.1.1.dist-info → up_cli-0.5.0.dist-info}/WHEEL +0 -0
  55. {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()