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/status.py ADDED
@@ -0,0 +1,360 @@
1
+ """up status - Show system health and status."""
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
+ from rich.text import Text
11
+
12
+ console = Console()
13
+
14
+
15
+ @click.command()
16
+ @click.option(
17
+ "--json", "as_json",
18
+ is_flag=True,
19
+ help="Output as JSON",
20
+ )
21
+ def status_cmd(as_json: bool):
22
+ """Show current status of all up systems.
23
+
24
+ Displays health information for:
25
+ - Context budget (token usage)
26
+ - Circuit breaker states
27
+ - Product loop progress
28
+ - Learning system state
29
+ """
30
+ cwd = Path.cwd()
31
+
32
+ status = collect_status(cwd)
33
+
34
+ if as_json:
35
+ console.print(json.dumps(status, indent=2))
36
+ return
37
+
38
+ # Display rich formatted output
39
+ display_status(status)
40
+
41
+
42
+ def collect_status(workspace: Path) -> dict:
43
+ """Collect status from all systems."""
44
+ status = {
45
+ "workspace": str(workspace),
46
+ "initialized": False,
47
+ "state_version": None,
48
+ "context_budget": None,
49
+ "loop_state": None,
50
+ "circuit_breaker": None,
51
+ "checkpoints": None,
52
+ "agents": None,
53
+ "doom_loop": None,
54
+ "skills": [],
55
+ "hooks": None,
56
+ "memory": None,
57
+ }
58
+
59
+ # Check if initialized
60
+ claude_dir = workspace / ".claude"
61
+ cursor_dir = workspace / ".cursor"
62
+ up_dir = workspace / ".up"
63
+ status["initialized"] = claude_dir.exists() or cursor_dir.exists() or up_dir.exists()
64
+
65
+ if not status["initialized"]:
66
+ return status
67
+
68
+ # Try to load unified state first
69
+ try:
70
+ from up.core.state import get_state_manager
71
+ manager = get_state_manager(workspace)
72
+ state = manager.state
73
+ status["state_version"] = state.version
74
+
75
+ # Context budget from unified state
76
+ status["context_budget"] = {
77
+ "budget": state.context.budget,
78
+ "total_tokens": state.context.total_tokens,
79
+ "remaining_tokens": state.context.remaining_tokens,
80
+ "usage_percent": state.context.usage_percent,
81
+ "status": state.context.status,
82
+ }
83
+
84
+ # Loop state from unified state
85
+ status["loop_state"] = {
86
+ "iteration": state.loop.iteration,
87
+ "phase": state.loop.phase,
88
+ "current_task": state.loop.current_task,
89
+ "tasks_completed": len(state.loop.tasks_completed),
90
+ "tasks_failed": len(state.loop.tasks_failed),
91
+ "success_rate": state.metrics.success_rate,
92
+ "last_checkpoint": state.loop.last_checkpoint,
93
+ }
94
+
95
+ # Circuit breakers
96
+ status["circuit_breaker"] = {
97
+ name: {"state": cb.state, "failures": cb.failures}
98
+ for name, cb in state.circuit_breakers.items()
99
+ }
100
+
101
+ # Checkpoints
102
+ status["checkpoints"] = {
103
+ "total": len(state.checkpoints),
104
+ "last": state.loop.last_checkpoint,
105
+ "recent": state.checkpoints[-5:] if state.checkpoints else [],
106
+ }
107
+
108
+ # Agents
109
+ if state.agents:
110
+ status["agents"] = {
111
+ task_id: {
112
+ "status": agent.status,
113
+ "phase": agent.phase,
114
+ "worktree": agent.worktree_path,
115
+ }
116
+ for task_id, agent in state.agents.items()
117
+ }
118
+
119
+ # Doom loop detection
120
+ is_doom, doom_msg = manager.check_doom_loop()
121
+ if is_doom or state.loop.consecutive_failures > 0:
122
+ status["doom_loop"] = {
123
+ "triggered": is_doom,
124
+ "consecutive_failures": state.loop.consecutive_failures,
125
+ "threshold": state.loop.doom_loop_threshold,
126
+ "message": doom_msg if is_doom else None,
127
+ }
128
+
129
+ except ImportError:
130
+ # Fallback to old state files
131
+ _collect_legacy_status(workspace, status)
132
+
133
+ # Git hooks status
134
+ from up.commands.sync import check_hooks_installed
135
+ status["hooks"] = check_hooks_installed(workspace)
136
+
137
+ # Memory status
138
+ memory_dir = workspace / ".up" / "memory"
139
+ if memory_dir.exists():
140
+ try:
141
+ from up.memory import MemoryManager
142
+ manager = MemoryManager(workspace, use_vectors=False)
143
+ stats = manager.get_stats()
144
+ status["memory"] = {
145
+ "total": stats.get("total", 0),
146
+ "branch": stats.get("current_branch", "unknown"),
147
+ "commit": stats.get("current_commit", "unknown"),
148
+ }
149
+ except Exception:
150
+ status["memory"] = {"total": 0}
151
+
152
+ # Skills
153
+ skills_dirs = [
154
+ workspace / ".claude/skills",
155
+ workspace / ".cursor/skills",
156
+ ]
157
+ for skills_dir in skills_dirs:
158
+ if skills_dir.exists():
159
+ for skill_dir in skills_dir.iterdir():
160
+ if skill_dir.is_dir() and (skill_dir / "SKILL.md").exists():
161
+ status["skills"].append(skill_dir.name)
162
+
163
+ return status
164
+
165
+
166
+ def _collect_legacy_status(workspace: Path, status: dict) -> None:
167
+ """Collect status from legacy state files."""
168
+ # Context budget (old location)
169
+ context_file = workspace / ".claude/context_budget.json"
170
+ if context_file.exists():
171
+ try:
172
+ status["context_budget"] = json.loads(context_file.read_text())
173
+ except json.JSONDecodeError:
174
+ status["context_budget"] = {"error": "Invalid JSON"}
175
+
176
+ # Loop state (old location)
177
+ loop_file = workspace / ".loop_state.json"
178
+ if loop_file.exists():
179
+ try:
180
+ data = json.loads(loop_file.read_text())
181
+ status["loop_state"] = {
182
+ "iteration": data.get("iteration", 0),
183
+ "phase": data.get("phase", "UNKNOWN"),
184
+ "current_task": data.get("current_task"),
185
+ "tasks_completed": len(data.get("tasks_completed", [])),
186
+ "tasks_remaining": len(data.get("tasks_remaining", [])),
187
+ "success_rate": data.get("metrics", {}).get("success_rate", 1.0),
188
+ }
189
+ status["circuit_breaker"] = data.get("circuit_breaker", {})
190
+ except json.JSONDecodeError:
191
+ status["loop_state"] = {"error": "Invalid JSON"}
192
+
193
+
194
+ def display_status(status: dict) -> None:
195
+ """Display status in rich format."""
196
+
197
+ # Header
198
+ workspace_name = Path(status["workspace"]).name
199
+ if status["initialized"]:
200
+ header = f"[bold green]✓[/] {workspace_name} - up systems active"
201
+ else:
202
+ header = f"[bold yellow]○[/] {workspace_name} - not initialized"
203
+ console.print(Panel(header, border_style="yellow"))
204
+ console.print("\nRun [cyan]up init[/] to initialize up systems.")
205
+ return
206
+
207
+ console.print(Panel(header, border_style="green"))
208
+
209
+ # Context Budget
210
+ console.print("\n[bold]Context Budget[/]")
211
+ if status["context_budget"]:
212
+ budget = status["context_budget"]
213
+ if "error" in budget:
214
+ console.print(f" [red]Error: {budget['error']}[/]")
215
+ else:
216
+ usage = budget.get("usage_percent", 0)
217
+ remaining = budget.get("remaining_tokens", 0)
218
+ budget_status = budget.get("status", "OK")
219
+
220
+ # Color based on status
221
+ if budget_status == "CRITICAL":
222
+ color = "red"
223
+ icon = "🔴"
224
+ elif budget_status == "WARNING":
225
+ color = "yellow"
226
+ icon = "🟡"
227
+ else:
228
+ color = "green"
229
+ icon = "🟢"
230
+
231
+ console.print(f" {icon} Status: [{color}]{budget_status}[/]")
232
+ console.print(f" Usage: {usage:.1f}% ({remaining:,} tokens remaining)")
233
+ else:
234
+ console.print(" [dim]Not configured[/]")
235
+
236
+ # Circuit Breaker
237
+ console.print("\n[bold]Circuit Breaker[/]")
238
+ if status["circuit_breaker"]:
239
+ cb = status["circuit_breaker"]
240
+ for name, state in cb.items():
241
+ if isinstance(state, dict):
242
+ cb_state = state.get("state", "UNKNOWN")
243
+ failures = state.get("failures", 0)
244
+
245
+ if cb_state == "OPEN":
246
+ icon = "🔴"
247
+ color = "red"
248
+ elif cb_state == "HALF_OPEN":
249
+ icon = "🟡"
250
+ color = "yellow"
251
+ else:
252
+ icon = "🟢"
253
+ color = "green"
254
+
255
+ console.print(f" {icon} {name}: [{color}]{cb_state}[/] (failures: {failures})")
256
+ else:
257
+ console.print(" [dim]Not active[/]")
258
+
259
+ # Product Loop State
260
+ console.print("\n[bold]Product Loop[/]")
261
+ if status["loop_state"]:
262
+ loop = status["loop_state"]
263
+ if "error" in loop:
264
+ console.print(f" [red]Error: {loop['error']}[/]")
265
+ else:
266
+ console.print(f" Iteration: {loop.get('iteration', 0)}")
267
+ console.print(f" Phase: {loop.get('phase', 'UNKNOWN')}")
268
+
269
+ current = loop.get("current_task")
270
+ if current:
271
+ console.print(f" Current Task: [cyan]{current}[/]")
272
+
273
+ completed = loop.get("tasks_completed", 0)
274
+ failed = loop.get("tasks_failed", 0)
275
+ remaining = loop.get("tasks_remaining", 0)
276
+ total = completed + failed + remaining if remaining else completed + failed
277
+
278
+ if total > 0:
279
+ progress = completed / total * 100
280
+ bar_len = 20
281
+ filled = int(bar_len * completed / total)
282
+ bar = "█" * filled + "░" * (bar_len - filled)
283
+ console.print(f" Progress: [{bar}] {progress:.0f}% ({completed}/{total})")
284
+
285
+ success_rate = loop.get("success_rate", 1.0)
286
+ console.print(f" Success Rate: {success_rate * 100:.0f}%")
287
+
288
+ last_cp = loop.get("last_checkpoint")
289
+ if last_cp:
290
+ console.print(f" Last Checkpoint: [cyan]{last_cp}[/]")
291
+ else:
292
+ console.print(" [dim]Not active[/]")
293
+
294
+ # Doom Loop Detection
295
+ if status.get("doom_loop"):
296
+ doom = status["doom_loop"]
297
+ console.print("\n[bold]Doom Loop Detection[/]")
298
+ if doom["triggered"]:
299
+ console.print(f" [red]⚠ TRIGGERED[/] - {doom['consecutive_failures']}/{doom['threshold']} failures")
300
+ console.print(f" [dim]{doom.get('message', '')}[/]")
301
+ else:
302
+ console.print(f" Consecutive Failures: {doom['consecutive_failures']}/{doom['threshold']}")
303
+
304
+ # Checkpoints
305
+ if status.get("checkpoints"):
306
+ cp = status["checkpoints"]
307
+ console.print("\n[bold]Checkpoints[/]")
308
+ console.print(f" Total: {cp['total']}")
309
+ if cp.get("recent"):
310
+ console.print(f" Recent: {', '.join(cp['recent'][-3:])}")
311
+
312
+ # Active Agents
313
+ if status.get("agents"):
314
+ console.print("\n[bold]Active Agents[/]")
315
+ for task_id, agent in status["agents"].items():
316
+ status_icon = "🟢" if agent["status"] == "passed" else "🟡" if agent["status"] in ["executing", "verifying"] else "🔴"
317
+ console.print(f" {status_icon} {task_id}: {agent['status']} ({agent['phase']})")
318
+
319
+ # Memory
320
+ console.print("\n[bold]Memory[/]")
321
+ if status["memory"]:
322
+ mem = status["memory"]
323
+ total = mem.get("total", 0)
324
+ branch = mem.get("branch", "unknown")
325
+ commit = mem.get("commit", "unknown")
326
+ console.print(f" 📚 {total} entries | Branch: [cyan]{branch}[/] @ {commit}")
327
+ else:
328
+ console.print(" [dim]Not initialized - run [cyan]up memory sync[/][/]")
329
+
330
+ # Git Hooks
331
+ console.print("\n[bold]Auto-Sync (Git Hooks)[/]")
332
+ hooks = status.get("hooks", {})
333
+ if hooks.get("git"):
334
+ post_commit = hooks.get("post_commit", False)
335
+ post_checkout = hooks.get("post_checkout", False)
336
+
337
+ if post_commit and post_checkout:
338
+ console.print(" [green]✓ Enabled[/] - commits auto-indexed to memory")
339
+ else:
340
+ console.print(" [yellow]⚠ Partially installed[/]")
341
+ if not post_commit:
342
+ console.print(" • Missing: post-commit hook")
343
+ if not post_checkout:
344
+ console.print(" • Missing: post-checkout hook")
345
+ console.print("\n [dim]Run [cyan]up hooks[/] to install missing hooks[/]")
346
+ else:
347
+ console.print(" [yellow]✗ Not installed[/]")
348
+ console.print(" [dim]Run [cyan]up hooks[/] to enable auto-sync on commits[/]")
349
+
350
+ # Skills
351
+ console.print("\n[bold]Skills[/]")
352
+ if status["skills"]:
353
+ for skill in status["skills"]:
354
+ console.print(f" • {skill}")
355
+ else:
356
+ console.print(" [dim]No skills installed[/]")
357
+
358
+
359
+ if __name__ == "__main__":
360
+ status_cmd()
@@ -0,0 +1,122 @@
1
+ """up summarize - Summarize AI conversation history."""
2
+
3
+ import sys
4
+ from pathlib import Path
5
+
6
+ import click
7
+ from rich.console import Console
8
+ from rich.panel import Panel
9
+
10
+ console = Console()
11
+
12
+
13
+ @click.command()
14
+ @click.option(
15
+ "--format", "-f",
16
+ type=click.Choice(["markdown", "json"]),
17
+ default="markdown",
18
+ help="Output format",
19
+ )
20
+ @click.option(
21
+ "--output", "-o",
22
+ type=click.Path(),
23
+ help="Output file path",
24
+ )
25
+ @click.option(
26
+ "--project", "-p",
27
+ help="Filter by project path",
28
+ )
29
+ @click.option(
30
+ "--source",
31
+ type=click.Choice(["cursor", "claude", "all"]),
32
+ default="cursor",
33
+ help="Conversation source to analyze",
34
+ )
35
+ def summarize_cmd(format: str, output: str, project: str, source: str):
36
+ """Summarize AI conversation history.
37
+
38
+ Analyzes your Cursor or Claude chat history to extract:
39
+ - Common topics and patterns
40
+ - Frequent errors encountered
41
+ - Actionable insights
42
+ - Code snippets
43
+
44
+ \b
45
+ Examples:
46
+ up summarize # Markdown to stdout
47
+ up summarize -f json -o out.json # JSON to file
48
+ up summarize -p myproject # Filter by project
49
+ """
50
+ console.print(Panel.fit(
51
+ "[bold blue]Conversation Summarizer[/]",
52
+ border_style="blue"
53
+ ))
54
+
55
+ try:
56
+ if source in ("cursor", "all"):
57
+ result = _summarize_cursor(format, project)
58
+ _output_result(result, output, format)
59
+
60
+ if source == "claude":
61
+ console.print("[yellow]Claude history summarization not yet implemented.[/]")
62
+ console.print("Use [cyan]--source cursor[/] for Cursor history.")
63
+
64
+ except FileNotFoundError as e:
65
+ console.print(f"[red]Error:[/] {e}")
66
+ console.print("\nMake sure Cursor is installed and has chat history.")
67
+ sys.exit(1)
68
+ except Exception as e:
69
+ console.print(f"[red]Error:[/] {e}")
70
+ sys.exit(1)
71
+
72
+
73
+ def _summarize_cursor(format: str, project_filter: str = None) -> str:
74
+ """Summarize Cursor conversation history."""
75
+ # Add scripts to path
76
+ scripts_path = Path(__file__).parent.parent.parent.parent / "scripts"
77
+ if scripts_path.exists():
78
+ sys.path.insert(0, str(scripts_path))
79
+
80
+ try:
81
+ from export_cursor_history import load_all_data
82
+ except ImportError:
83
+ raise FileNotFoundError(
84
+ "Could not import export_cursor_history. "
85
+ "Make sure scripts/export_cursor_history.py exists."
86
+ )
87
+
88
+ from up.summarizer import ConversationSummarizer
89
+
90
+ console.print("Loading Cursor history...", style="dim")
91
+ conversations = load_all_data(project_filter=project_filter)
92
+
93
+ if not conversations:
94
+ raise ValueError("No conversations found in Cursor history.")
95
+
96
+ console.print(f"Found [cyan]{len(conversations)}[/] conversations")
97
+ console.print("Analyzing...", style="dim")
98
+
99
+ summarizer = ConversationSummarizer(conversations)
100
+
101
+ if format == "json":
102
+ return summarizer.to_json()
103
+ return summarizer.to_markdown()
104
+
105
+
106
+ def _output_result(result: str, output_path: str, format: str) -> None:
107
+ """Output result to file or stdout."""
108
+ if output_path:
109
+ Path(output_path).write_text(result)
110
+ console.print(f"\n[green]✓[/] Summary saved to [cyan]{output_path}[/]")
111
+ else:
112
+ console.print("\n")
113
+ if format == "markdown":
114
+ # Use rich markdown rendering
115
+ from rich.markdown import Markdown
116
+ console.print(Markdown(result))
117
+ else:
118
+ console.print(result)
119
+
120
+
121
+ if __name__ == "__main__":
122
+ summarize_cmd()