methodproof 0.7.31__tar.gz → 0.7.33__tar.gz

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 (88) hide show
  1. {methodproof-0.7.31 → methodproof-0.7.33}/CHANGELOG.md +5 -0
  2. {methodproof-0.7.31 → methodproof-0.7.33}/PKG-INFO +1 -1
  3. {methodproof-0.7.31 → methodproof-0.7.33}/methodproof/cli.py +4 -2
  4. methodproof-0.7.33/methodproof/tui/start.py +436 -0
  5. methodproof-0.7.33/methodproof/tui/theme.py +106 -0
  6. {methodproof-0.7.31 → methodproof-0.7.33}/pyproject.toml +1 -1
  7. {methodproof-0.7.31 → methodproof-0.7.33}/tests/test_cli_start.py +7 -1
  8. methodproof-0.7.31/methodproof/tui/start.py +0 -212
  9. methodproof-0.7.31/methodproof/tui/theme.py +0 -53
  10. {methodproof-0.7.31 → methodproof-0.7.33}/.github/workflows/ci.yml +0 -0
  11. {methodproof-0.7.31 → methodproof-0.7.33}/.gitignore +0 -0
  12. {methodproof-0.7.31 → methodproof-0.7.33}/LICENSE +0 -0
  13. {methodproof-0.7.31 → methodproof-0.7.33}/README.md +0 -0
  14. {methodproof-0.7.31 → methodproof-0.7.33}/methodproof/__init__.py +0 -0
  15. {methodproof-0.7.31 → methodproof-0.7.33}/methodproof/__main__.py +0 -0
  16. {methodproof-0.7.31 → methodproof-0.7.33}/methodproof/_daemon.py +0 -0
  17. {methodproof-0.7.31 → methodproof-0.7.33}/methodproof/agents/__init__.py +0 -0
  18. {methodproof-0.7.31 → methodproof-0.7.33}/methodproof/agents/base.py +0 -0
  19. {methodproof-0.7.31 → methodproof-0.7.33}/methodproof/agents/music.py +0 -0
  20. {methodproof-0.7.31 → methodproof-0.7.33}/methodproof/agents/terminal.py +0 -0
  21. {methodproof-0.7.31 → methodproof-0.7.33}/methodproof/agents/watcher.py +0 -0
  22. {methodproof-0.7.31 → methodproof-0.7.33}/methodproof/analysis.py +0 -0
  23. {methodproof-0.7.31 → methodproof-0.7.33}/methodproof/binding.py +0 -0
  24. {methodproof-0.7.31 → methodproof-0.7.33}/methodproof/bip39.py +0 -0
  25. {methodproof-0.7.31 → methodproof-0.7.33}/methodproof/bridge.py +0 -0
  26. {methodproof-0.7.31 → methodproof-0.7.33}/methodproof/config.py +0 -0
  27. {methodproof-0.7.31 → methodproof-0.7.33}/methodproof/crypto.py +0 -0
  28. {methodproof-0.7.31 → methodproof-0.7.33}/methodproof/e2e.py +0 -0
  29. {methodproof-0.7.31 → methodproof-0.7.33}/methodproof/graph.py +0 -0
  30. {methodproof-0.7.31 → methodproof-0.7.33}/methodproof/hook.py +0 -0
  31. {methodproof-0.7.31 → methodproof-0.7.33}/methodproof/hooks/__init__.py +0 -0
  32. {methodproof-0.7.31 → methodproof-0.7.33}/methodproof/hooks/claude_code.py +0 -0
  33. {methodproof-0.7.31 → methodproof-0.7.33}/methodproof/hooks/claude_code.sh +0 -0
  34. {methodproof-0.7.31 → methodproof-0.7.33}/methodproof/hooks/cline_hook.sh +0 -0
  35. {methodproof-0.7.31 → methodproof-0.7.33}/methodproof/hooks/codex_hook.sh +0 -0
  36. {methodproof-0.7.31 → methodproof-0.7.33}/methodproof/hooks/gemini_hook.sh +0 -0
  37. {methodproof-0.7.31 → methodproof-0.7.33}/methodproof/hooks/install.py +0 -0
  38. {methodproof-0.7.31 → methodproof-0.7.33}/methodproof/hooks/kiro_hook.sh +0 -0
  39. {methodproof-0.7.31 → methodproof-0.7.33}/methodproof/hooks/mcp_register.py +0 -0
  40. {methodproof-0.7.31 → methodproof-0.7.33}/methodproof/hooks/openclaw/HOOK.md +0 -0
  41. {methodproof-0.7.31 → methodproof-0.7.33}/methodproof/hooks/openclaw/handler.ts +0 -0
  42. {methodproof-0.7.31 → methodproof-0.7.33}/methodproof/hooks/openclaw_install.py +0 -0
  43. {methodproof-0.7.31 → methodproof-0.7.33}/methodproof/hooks/opencode_plugin.js +0 -0
  44. {methodproof-0.7.31 → methodproof-0.7.33}/methodproof/hooks/wrappers.py +0 -0
  45. {methodproof-0.7.31 → methodproof-0.7.33}/methodproof/integrity.py +0 -0
  46. {methodproof-0.7.31 → methodproof-0.7.33}/methodproof/kdf.py +0 -0
  47. {methodproof-0.7.31 → methodproof-0.7.33}/methodproof/keychain.py +0 -0
  48. {methodproof-0.7.31 → methodproof-0.7.33}/methodproof/live.py +0 -0
  49. {methodproof-0.7.31 → methodproof-0.7.33}/methodproof/lock.py +0 -0
  50. {methodproof-0.7.31 → methodproof-0.7.33}/methodproof/mcp.py +0 -0
  51. {methodproof-0.7.31 → methodproof-0.7.33}/methodproof/migrate_db.py +0 -0
  52. {methodproof-0.7.31 → methodproof-0.7.33}/methodproof/proxy.py +0 -0
  53. {methodproof-0.7.31 → methodproof-0.7.33}/methodproof/proxy_daemon.py +0 -0
  54. {methodproof-0.7.31 → methodproof-0.7.33}/methodproof/repos.py +0 -0
  55. {methodproof-0.7.31 → methodproof-0.7.33}/methodproof/skills/methodproof/SKILL.md +0 -0
  56. {methodproof-0.7.31 → methodproof-0.7.33}/methodproof/store.py +0 -0
  57. {methodproof-0.7.31 → methodproof-0.7.33}/methodproof/sync.py +0 -0
  58. {methodproof-0.7.31 → methodproof-0.7.33}/methodproof/tui/__init__.py +0 -0
  59. {methodproof-0.7.31 → methodproof-0.7.33}/methodproof/tui/consent.py +0 -0
  60. {methodproof-0.7.31 → methodproof-0.7.33}/methodproof/tui/init.py +0 -0
  61. {methodproof-0.7.31 → methodproof-0.7.33}/methodproof/tui/log.py +0 -0
  62. {methodproof-0.7.31 → methodproof-0.7.33}/methodproof/tui/login_success.py +0 -0
  63. {methodproof-0.7.31 → methodproof-0.7.33}/methodproof/tui/review.py +0 -0
  64. {methodproof-0.7.31 → methodproof-0.7.33}/methodproof/tui/status.py +0 -0
  65. {methodproof-0.7.31 → methodproof-0.7.33}/methodproof/viewer.py +0 -0
  66. {methodproof-0.7.31 → methodproof-0.7.33}/methodproof/wordlist.py +0 -0
  67. {methodproof-0.7.31 → methodproof-0.7.33}/test_windows_compat.py +0 -0
  68. {methodproof-0.7.31 → methodproof-0.7.33}/tests/__init__.py +0 -0
  69. {methodproof-0.7.31 → methodproof-0.7.33}/tests/conftest.py +0 -0
  70. {methodproof-0.7.31 → methodproof-0.7.33}/tests/test_analysis.py +0 -0
  71. {methodproof-0.7.31 → methodproof-0.7.33}/tests/test_cli_auth.py +0 -0
  72. {methodproof-0.7.31 → methodproof-0.7.33}/tests/test_cli_config.py +0 -0
  73. {methodproof-0.7.31 → methodproof-0.7.33}/tests/test_cli_helpers.py +0 -0
  74. {methodproof-0.7.31 → methodproof-0.7.33}/tests/test_cli_session.py +0 -0
  75. {methodproof-0.7.31 → methodproof-0.7.33}/tests/test_cli_share.py +0 -0
  76. {methodproof-0.7.31 → methodproof-0.7.33}/tests/test_cli_update.py +0 -0
  77. {methodproof-0.7.31 → methodproof-0.7.33}/tests/test_e2e_integration.py +0 -0
  78. {methodproof-0.7.31 → methodproof-0.7.33}/tests/test_graph.py +0 -0
  79. {methodproof-0.7.31 → methodproof-0.7.33}/tests/test_hooks.py +0 -0
  80. {methodproof-0.7.31 → methodproof-0.7.33}/tests/test_live.py +0 -0
  81. {methodproof-0.7.31 → methodproof-0.7.33}/tests/test_openclaw_hooks.py +0 -0
  82. {methodproof-0.7.31 → methodproof-0.7.33}/tests/test_profiles.py +0 -0
  83. {methodproof-0.7.31 → methodproof-0.7.33}/tests/test_security.py +0 -0
  84. {methodproof-0.7.31 → methodproof-0.7.33}/tests/test_store.py +0 -0
  85. {methodproof-0.7.31 → methodproof-0.7.33}/tests/test_sync.py +0 -0
  86. {methodproof-0.7.31 → methodproof-0.7.33}/tests/test_viewer.py +0 -0
  87. {methodproof-0.7.31 → methodproof-0.7.33}/tests/test_wrappers.py +0 -0
  88. {methodproof-0.7.31 → methodproof-0.7.33}/uv.lock +0 -0
@@ -1,5 +1,10 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.7.32] — 2026-04-12
4
+
5
+ ### Fixed
6
+ - **`mp start` no longer hard-fails on missing shell hook** — instead of `ERROR: Run methodproof init first` + exit, `mp start` now auto-installs the hook and continues. Prints a note that terminal capture won't work until the shell is restarted. Prevents the frustrating dead-end where auto-update + missing hook blocks the entire session.
7
+
3
8
  ## [0.7.31] — 2026-04-12
4
9
 
5
10
  ### Fixed
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: methodproof
3
- Version: 0.7.31
3
+ Version: 0.7.33
4
4
  Summary: See how you code. Capture and visualize your engineering process.
5
5
  License-Expression: Apache-2.0
6
6
  License-File: LICENSE
@@ -1111,8 +1111,10 @@ def cmd_start(args: argparse.Namespace) -> None:
1111
1111
 
1112
1112
  _log_step("Checking hooks")
1113
1113
  if not hook.is_installed():
1114
- print("ERROR: Run `methodproof init` first.")
1115
- sys.exit(1)
1114
+ rc = hook.install()
1115
+ print(f" Shell hook installed ({rc}). Restart your shell or run:")
1116
+ print(f" eval \"$(methodproof shell-hook)\"")
1117
+ print(f" Terminal command capture won't work until the hook is active.")
1116
1118
 
1117
1119
  _log_step("Authenticating")
1118
1120
  account_id = _require_auth(cfg)
@@ -0,0 +1,436 @@
1
+ """Textual TUI for mp start — live session event feed.
2
+
3
+ Four display layers:
4
+ B — Rich structural formatting for all 42 event types
5
+ E — Causal chain tree indentation (prompt→tool→result)
6
+ A — Journal mode content enrichment (second dim line)
7
+ D — Enriched session bar (badges, event count)
8
+ """
9
+ from __future__ import annotations
10
+
11
+ import json
12
+ import time
13
+ from datetime import datetime, UTC
14
+
15
+ from textual.app import App, ComposeResult
16
+ from textual.binding import Binding
17
+ from textual.containers import Horizontal, Vertical
18
+ from textual.reactive import reactive
19
+ from textual.widgets import Footer, Header, RichLog, Static
20
+ from methodproof import config, store
21
+ from methodproof.agents.base import log
22
+ from methodproof.tui.theme import ACTIVE, BASE_CSS
23
+
24
+ # ── CSS ──────────────────────────────────────────────────────────────
25
+ _CSS = BASE_CSS + f"""
26
+ #session-bar {{
27
+ background: {ACTIVE.surface};
28
+ height: 1;
29
+ padding: 0 2;
30
+ color: {ACTIVE.dim};
31
+ }}
32
+ #feed {{
33
+ width: 3fr;
34
+ border-right: solid {ACTIVE.border};
35
+ padding: 0 1;
36
+ }}
37
+ #sidebar {{
38
+ width: 22;
39
+ padding: 1 2;
40
+ background: {ACTIVE.sidebar_bg};
41
+ }}
42
+ .sidebar-title {{
43
+ color: {ACTIVE.gold};
44
+ text-style: bold;
45
+ margin: 0 0 1 0;
46
+ }}
47
+ .stat-row {{
48
+ color: {ACTIVE.dim};
49
+ height: 1;
50
+ }}
51
+ #moment-alert {{
52
+ background: {ACTIVE.gold_ember};
53
+ border: solid {ACTIVE.gold_deep};
54
+ margin: 1 1;
55
+ padding: 0 1;
56
+ height: 3;
57
+ display: none;
58
+ }}
59
+ #moment-alert.visible {{
60
+ display: block;
61
+ }}
62
+ """
63
+
64
+ _POLL_INTERVAL = 0.5
65
+
66
+ # ── Layer B: Semantic color roles ──────��─────────────────────────────
67
+ _EVENT_ROLE: dict[str, str] = {
68
+ # AI input (purple)
69
+ "llm_prompt": "ai_input", "agent_prompt": "ai_input",
70
+ "user_prompt": "ai_input", "tool_call": "ai_input",
71
+ "agent_launch": "ai_input", "task_start": "ai_input",
72
+ "permission_request": "ai_input",
73
+ "agent_tool_dispatch": "ai_input", "agent_skill_invoke": "ai_input",
74
+ "claude_session_start": "ai_input", "codex_session_start": "ai_input",
75
+ "gemini_session_start": "ai_input", "kiro_session_start": "ai_input",
76
+ # AI output (gold)
77
+ "llm_completion": "ai_output", "agent_completion": "ai_output",
78
+ "tool_result": "ai_output", "tool_failure": "ai_output",
79
+ "agent_complete": "ai_output", "task_end": "ai_output",
80
+ "agent_tool_result": "ai_output",
81
+ "agent_turn_end": "ai_output", "agent_turn_error": "ai_output",
82
+ "claude_session_end": "ai_output", "codex_session_end": "ai_output",
83
+ "gemini_session_end": "ai_output", "kiro_session_end": "ai_output",
84
+ # Human structural (cream/ink)
85
+ "file_edit": "human", "file_create": "human", "file_delete": "human",
86
+ "terminal_cmd": "human", "git_commit": "human",
87
+ "cwd_changed": "human",
88
+ "worktree_create": "human", "worktree_remove": "human",
89
+ # Verification (green)
90
+ "test_run": "verify",
91
+ "browser_visit": "verify", "browser_search": "verify",
92
+ "web_search": "verify", "web_visit": "verify",
93
+ }
94
+ # Everything else falls to "dim"
95
+
96
+ _MOMENT_TYPES = {
97
+ "rapid_iteration", "test_driven", "git_discipline",
98
+ "focused_session", "breakthrough", "approach_pivot",
99
+ }
100
+
101
+
102
+ def _event_color(etype: str) -> str:
103
+ role = _EVENT_ROLE.get(etype, "dim")
104
+ return getattr(ACTIVE, role, ACTIVE.dim)
105
+
106
+
107
+ # ── Layer E: Causal chain tree tracker ───��───────────────────────────
108
+ class _TreeTracker:
109
+ """Track prompt→tool_call→tool_result causal chains."""
110
+
111
+ _OPENERS = {
112
+ "user_prompt", "agent_launch", "task_start",
113
+ "claude_session_start", "codex_session_start",
114
+ "gemini_session_start", "kiro_session_start",
115
+ }
116
+ _INNERS = {
117
+ "tool_call", "tool_result", "tool_failure",
118
+ "permission_request", "permission_denied",
119
+ "agent_tool_dispatch", "agent_tool_result", "agent_skill_invoke",
120
+ "context_compact_start", "context_compact_end",
121
+ "cwd_changed", "mcp_elicitation", "mcp_elicitation_result",
122
+ "worktree_create", "worktree_remove",
123
+ }
124
+ _CLOSERS = {
125
+ "agent_turn_end", "agent_turn_error", "agent_complete", "task_end",
126
+ "claude_session_end", "codex_session_end",
127
+ "gemini_session_end", "kiro_session_end",
128
+ }
129
+
130
+ def __init__(self) -> None:
131
+ self._in_chain = False
132
+
133
+ def feed(self, etype: str) -> str:
134
+ if etype in self._OPENERS:
135
+ self._in_chain = True
136
+ return " "
137
+ if self._in_chain:
138
+ if etype in self._CLOSERS:
139
+ self._in_chain = False
140
+ return "└ "
141
+ if etype in self._INNERS:
142
+ return "├ "
143
+ # Non-chain event while in chain — implicit close
144
+ self._in_chain = False
145
+ return " "
146
+ return " "
147
+
148
+
149
+ # ── Layer B: Structural metadata formatters ──────────────────────────
150
+ def _fmt_meta(ev: dict) -> str:
151
+ etype = ev.get("type", "")
152
+ meta = ev.get("metadata") or {}
153
+ if not isinstance(meta, dict):
154
+ meta = {}
155
+
156
+ # File events
157
+ if etype in ("file_edit", "file_create", "file_delete"):
158
+ path = meta.get("path") or meta.get("file_path", "")
159
+ added = meta.get("lines_added") or meta.get("line_delta", "")
160
+ removed = meta.get("lines_removed", "")
161
+ delta = f"+{added}" if added else ""
162
+ if removed:
163
+ delta += f" -{removed}"
164
+ return f"{path} {delta}".strip()
165
+
166
+ # Terminal
167
+ if etype == "terminal_cmd":
168
+ cmd = (meta.get("command") or "")[:50]
169
+ ec = meta.get("exit_code", 0)
170
+ return f"{cmd} {'✓' if ec == 0 else f'✗ exit {ec}'}"
171
+
172
+ # Git
173
+ if etype == "git_commit":
174
+ h = (meta.get("hash") or "")[:7]
175
+ msg = (meta.get("message") or "")[:40]
176
+ return f"{h} {msg}".strip()
177
+
178
+ # Tests
179
+ if etype == "test_run":
180
+ fw = meta.get("framework", "")
181
+ p, f = meta.get("passed", 0), meta.get("failed", 0)
182
+ result = f"{p}✓ {f}✗" if f else f"{p}✓"
183
+ return f"{fw} {result}".strip()
184
+
185
+ # LLM / agent prompts
186
+ if etype in ("llm_prompt", "agent_prompt"):
187
+ tokens = meta.get("prompt_tokens") or meta.get("input_length", "")
188
+ return f"{tokens} tokens" if tokens else ""
189
+ if etype in ("llm_completion", "agent_completion"):
190
+ tokens = meta.get("completion_tokens") or meta.get("output_length", "")
191
+ dur = ev.get("duration_ms", "")
192
+ return f"{tokens} tokens {dur}ms" if tokens else ""
193
+
194
+ # Hook lifecycle — user prompt
195
+ if etype == "user_prompt":
196
+ length = meta.get("prompt_length", "")
197
+ return f"{length} chars" if length else ""
198
+
199
+ # Hook lifecycle — tool call / result
200
+ if etype == "tool_call":
201
+ name = meta.get("tool_name", "")
202
+ preview = (meta.get("tool_input_preview") or "")[:60]
203
+ return f"{name} {preview}".strip()
204
+ if etype == "tool_result":
205
+ name = meta.get("tool_name", "")
206
+ ok = "✓" if meta.get("success", True) else "✗"
207
+ return f"{name} {ok}"
208
+ if etype == "tool_failure":
209
+ name = meta.get("tool_name", "")
210
+ err = (meta.get("error") or "")[:40]
211
+ return f"{name} ✗ {err}".strip()
212
+
213
+ # Hook lifecycle — agents
214
+ if etype == "agent_launch":
215
+ return f"{meta.get('agent_type', '')} spawned"
216
+ if etype == "agent_complete":
217
+ return f"{meta.get('agent_type', '')} done"
218
+
219
+ # Hook lifecycle — tasks
220
+ if etype == "task_start":
221
+ return f"task {(meta.get('task_id') or '')[:8]}"
222
+ if etype == "task_end":
223
+ return f"task {(meta.get('task_id') or '')[:8]} done"
224
+
225
+ # Hook lifecycle — permissions
226
+ if etype == "permission_request":
227
+ return f"{meta.get('tool_name', '')} awaiting"
228
+ if etype == "permission_denied":
229
+ return f"{meta.get('tool_name', '')} denied"
230
+
231
+ # Session lifecycle
232
+ if etype.endswith("_session_start"):
233
+ return "session opened"
234
+ if etype.endswith("_session_end"):
235
+ return "session ended"
236
+
237
+ # Context / system
238
+ if etype == "context_compact_start":
239
+ return "compacting..."
240
+ if etype == "context_compact_end":
241
+ return "compacted"
242
+ if etype in ("agent_turn_end", "agent_turn_error"):
243
+ err = (meta.get("error") or "")[:40]
244
+ return err if err else ""
245
+ if etype in ("cwd_changed",):
246
+ return meta.get("cwd", "")
247
+ if etype in ("worktree_create", "worktree_remove"):
248
+ return meta.get("worktree_path", "")
249
+
250
+ # Music
251
+ if etype == "music_playing":
252
+ artist = meta.get("artist", "")
253
+ track = meta.get("track", "")
254
+ return f"{artist} — {track}" if artist else track
255
+
256
+ # Browser
257
+ if etype.startswith("browser_") or etype.startswith("web_"):
258
+ return (meta.get("url") or meta.get("query") or "")[:50]
259
+
260
+ # Environment
261
+ if etype == "environment_profile":
262
+ return f"{meta.get('tool_count', '?')} tools"
263
+
264
+ # Fallback: first 3 metadata keys
265
+ keys = list(meta.keys())[:3]
266
+ return ", ".join(f"{k}={meta[k]}" for k in keys) if keys else ""
267
+
268
+
269
+ # ── Layer A: Journal content enrichment ──���───────────────────────────
270
+ def _journal_line(ev: dict, journal_mode: bool) -> str | None:
271
+ """Return truncated content preview when journal mode is ON."""
272
+ if not journal_mode:
273
+ return None
274
+ meta = ev.get("metadata") or {}
275
+ if not isinstance(meta, dict):
276
+ return None
277
+ etype = ev.get("type", "")
278
+ for jetype, field in config.JOURNAL_CONTENT_FIELDS:
279
+ if jetype == etype and field in meta:
280
+ content = str(meta[field]).replace("\n", " ")[:120]
281
+ return content if content else None
282
+ return None
283
+
284
+
285
+ # ── App ──────────────────────────────────��───────────────────────────
286
+ class StartApp(App[None]):
287
+ """Live session view — tails the active session's events."""
288
+
289
+ TITLE = "methodproof — mp start"
290
+ CSS = _CSS
291
+ BINDINGS = [
292
+ Binding("q", "stop_session", "stop"),
293
+ Binding("p", "pause", "pause"),
294
+ Binding("l", "toggle_live", "toggle live"),
295
+ Binding("escape", "quit", "quit"),
296
+ ]
297
+
298
+ _elapsed: reactive[int] = reactive(0)
299
+ _paused: reactive[bool] = reactive(False)
300
+ _event_count: int = 0
301
+ _last_seen_id: str = ""
302
+
303
+ def __init__(self, session_id: str, session: dict) -> None:
304
+ super().__init__()
305
+ self._session_id = session_id
306
+ self._session = session
307
+ self._stats: dict[str, int] = {}
308
+ self._start_time = session.get("created_at", time.time())
309
+ self._tree = _TreeTracker()
310
+ self._journal_mode = False
311
+
312
+ def compose(self) -> ComposeResult:
313
+ yield Header(show_clock=False)
314
+ yield Static("", id="session-bar", markup=True)
315
+ with Horizontal():
316
+ with Vertical(id="feed-col"):
317
+ yield RichLog(id="feed", highlight=True, markup=True, wrap=False)
318
+ yield Static("", id="moment-alert", markup=True)
319
+ with Vertical(id="sidebar"):
320
+ yield Static("Stats", classes="sidebar-title")
321
+ yield Static("", id="stats-content", markup=True)
322
+ yield Footer()
323
+
324
+ def on_mount(self) -> None:
325
+ import base64
326
+ cfg = config.load()
327
+ self._journal_mode = cfg.get("journal_mode", False)
328
+ token = cfg.get("token", "")
329
+ try:
330
+ payload = token.split(".")[1] + "=="
331
+ claims = json.loads(base64.urlsafe_b64decode(payload))
332
+ except Exception as exc:
333
+ log("warning", "tui.jwt_decode.failed", error=str(exc))
334
+ claims = {}
335
+ self._account_type = (claims.get("account_type") or "free").capitalize()
336
+ self._tick_timer()
337
+ self.set_interval(_POLL_INTERVAL, self._poll_events)
338
+ self.set_interval(1.0, self._tick_timer)
339
+
340
+ # ── Layer D: Session bar ─────────────────────────────────────
341
+ def _tick_timer(self) -> None:
342
+ if self._paused:
343
+ return
344
+ P = ACTIVE
345
+ elapsed = int(time.time() - self._start_time)
346
+ h, m, s = elapsed // 3600, (elapsed % 3600) // 60, elapsed % 60
347
+ sid = self._session_id[:8]
348
+ watch_dir = self._session.get("watch_dir", "?")
349
+ journal = f" [{P.gold}]J[/{P.gold}]" if self._journal_mode else ""
350
+ ev = f" {self._event_count} ev" if self._event_count else ""
351
+ self.query_one("#session-bar", Static).update(
352
+ f" session: [{P.gold}]{sid}[/{P.gold}] · {watch_dir}"
353
+ f" · [{P.green}]●[/{P.green}] {h:02d}:{m:02d}:{s:02d}"
354
+ f"{ev}{journal} · [{P.purple}]{self._account_type}[/{P.purple}]"
355
+ )
356
+
357
+ # ── Event poll: Layers B + E + A ��────────────────────────────
358
+ def _poll_events(self) -> None:
359
+ if self._paused:
360
+ return
361
+ try:
362
+ events = store.get_session_events(
363
+ self._session_id, after_id=self._last_seen_id,
364
+ )
365
+ except Exception as exc:
366
+ log("warning", "tui.poll_events.failed", error=str(exc))
367
+ return
368
+
369
+ P = ACTIVE
370
+ feed = self.query_one(RichLog)
371
+ for ev in events:
372
+ self._last_seen_id = ev.get("id", self._last_seen_id)
373
+ etype = ev.get("type", "event")
374
+ color = _event_color(etype)
375
+ ts = datetime.fromtimestamp(
376
+ ev.get("ts", time.time()), tz=UTC,
377
+ ).strftime("%H:%M:%S")
378
+ prefix = self._tree.feed(etype)
379
+ meta = _fmt_meta(ev)
380
+
381
+ # Layer B + E: structural line with tree prefix
382
+ feed.write(
383
+ f"[{P.dim}]{ts}[/{P.dim}] "
384
+ f"[{P.gold_ember}]{prefix}[/{P.gold_ember}]"
385
+ f"[{color}]{etype:<18}[/{color}] "
386
+ f"[{P.dim}]{meta}[/{P.dim}]"
387
+ )
388
+ # Layer A: journal content enrichment
389
+ jline = _journal_line(ev, self._journal_mode)
390
+ if jline:
391
+ feed.write(
392
+ f" [{P.gold_ember}]│[/{P.gold_ember}] "
393
+ f"[{P.dim}]\"{jline}\"[/{P.dim}]"
394
+ )
395
+
396
+ self._stats[etype] = self._stats.get(etype, 0) + 1
397
+ self._event_count += 1
398
+
399
+ # Moment detection
400
+ if etype in _MOMENT_TYPES:
401
+ m = ev.get("metadata") or {}
402
+ detail = m.get("detail", m.get("description", etype))
403
+ self._show_moment(etype, str(detail)[:60])
404
+
405
+ if events:
406
+ self._refresh_stats()
407
+
408
+ def _refresh_stats(self) -> None:
409
+ lines = []
410
+ for etype, count in sorted(self._stats.items(), key=lambda x: -x[1])[:10]:
411
+ color = _event_color(etype)
412
+ lines.append(
413
+ f"[{ACTIVE.dim}]{etype:<14}[/{ACTIVE.dim}] [{color}]{count}[/{color}]"
414
+ )
415
+ self.query_one("#stats-content", Static).update("\n".join(lines))
416
+
417
+ def _show_moment(self, mtype: str, detail: str) -> None:
418
+ P = ACTIVE
419
+ alert = self.query_one("#moment-alert", Static)
420
+ alert.update(f" [{P.moment}]⚡ {mtype}[/{P.moment}] [{P.dim}]{detail}[/{P.dim}]")
421
+ alert.add_class("visible")
422
+ self.set_timer(4.0, lambda: alert.remove_class("visible"))
423
+
424
+ def action_stop_session(self) -> None:
425
+ self.exit(None)
426
+
427
+ def action_pause(self) -> None:
428
+ self._paused = not self._paused
429
+
430
+ def action_toggle_live(self) -> None:
431
+ pass # handled by caller in cli.py
432
+
433
+
434
+ def run(session_id: str, session: dict) -> None:
435
+ """Launch the live session view for the given session."""
436
+ StartApp(session_id, session).run()
@@ -0,0 +1,106 @@
1
+ """Theme palette for MethodProof TUI — KINMYAKU (dark) mode.
2
+
3
+ Colors derived from THEMES/METHODPROOF.md spec. Semantic roles encode
4
+ meaning (ai_input, ai_output, human, verify, moment) so TUI code reads
5
+ intent, not hex values.
6
+ """
7
+ from dataclasses import dataclass
8
+
9
+
10
+ @dataclass(frozen=True)
11
+ class Palette:
12
+ # Brand accents
13
+ gold: str
14
+ gold_aged: str
15
+ gold_deep: str
16
+ gold_ember: str
17
+ green: str
18
+ green_muted: str
19
+ red: str
20
+ purple: str
21
+ purple_muted: str
22
+ # Surfaces
23
+ bg: str
24
+ surface: str
25
+ panel_bg: str
26
+ border: str
27
+ sidebar_bg: str
28
+ deep_bg: str
29
+ purple_bg: str
30
+ # Text
31
+ text: str
32
+ dim: str
33
+ # Semantic roles (aliases for intent-driven lookup)
34
+ ai_input: str
35
+ ai_output: str
36
+ human: str
37
+ verify: str
38
+ moment: str
39
+
40
+
41
+ KINMYAKU = Palette(
42
+ gold="#c9a84c", gold_aged="#9a7b3a", gold_deep="#6b5528", gold_ember="#3d3118",
43
+ green="#40d98c", green_muted="#2a6b45",
44
+ red="#e85445",
45
+ purple="#9b59b6", purple_muted="#6b2f7d",
46
+ bg="#12110f", surface="#1c1a18", panel_bg="#0f0d0b", border="#2e2c29",
47
+ sidebar_bg="#0a0908", deep_bg="#050403", purple_bg="#200a26",
48
+ text="#e8e4de", dim="#8b8171",
49
+ ai_input="#9b59b6", ai_output="#c9a84c", human="#e8e4de",
50
+ verify="#40d98c", moment="#e85445",
51
+ )
52
+
53
+ ACTIVE = KINMYAKU
54
+
55
+ # Backward-compatible re-exports — all existing imports keep working.
56
+ GOLD = ACTIVE.gold
57
+ GREEN = ACTIVE.green
58
+ RED = ACTIVE.red
59
+ PURPLE = ACTIVE.purple
60
+ NAVY = "#192a56" # brand-only, not in palette
61
+ BG = ACTIVE.bg
62
+ BAR = ACTIVE.surface
63
+ PANEL_BG = ACTIVE.panel_bg
64
+ BORDER = ACTIVE.border
65
+ TEXT = ACTIVE.text
66
+ DIM = ACTIVE.dim
67
+ SIDEBAR_BG = ACTIVE.sidebar_bg
68
+ DEEP_BG = ACTIVE.deep_bg
69
+ PURPLE_BG = ACTIVE.purple_bg
70
+
71
+ BASE_CSS = f"""
72
+ Screen {{
73
+ background: {ACTIVE.bg};
74
+ }}
75
+ Header {{
76
+ background: {ACTIVE.surface};
77
+ color: {ACTIVE.gold};
78
+ text-style: bold;
79
+ }}
80
+ Footer {{
81
+ background: {ACTIVE.surface};
82
+ color: {ACTIVE.dim};
83
+ }}
84
+ .panel {{
85
+ border: solid {ACTIVE.border};
86
+ background: {ACTIVE.panel_bg};
87
+ margin: 0 0 1 0;
88
+ padding: 1 2;
89
+ }}
90
+ .section-title {{
91
+ color: {ACTIVE.gold};
92
+ text-style: bold;
93
+ margin: 0 0 1 0;
94
+ }}
95
+ .row-label {{
96
+ width: 24;
97
+ color: {ACTIVE.text};
98
+ }}
99
+ .row-desc {{
100
+ color: {ACTIVE.dim};
101
+ }}
102
+ Rule {{
103
+ color: {ACTIVE.border};
104
+ margin: 1 0;
105
+ }}
106
+ """
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "methodproof"
3
- version = "0.7.31"
3
+ version = "0.7.33"
4
4
  description = "See how you code. Capture and visualize your engineering process."
5
5
  requires-python = ">=3.11"
6
6
  dependencies = ["watchdog>=4.0", "websocket-client>=1.7", "cryptography>=43.0", "keyring>=25.0", "textual>=0.59", "rich>=13.7"]
@@ -17,10 +17,16 @@ from methodproof import cli, config, graph, store
17
17
 
18
18
 
19
19
  @patch("methodproof.hook.is_installed", return_value=False)
20
- def test_start_hooks_not_installed(mock_hook, logged_in_cfg, cli_args):
20
+ @patch("methodproof.hook.install", return_value="zsh: ~/.zshrc")
21
+ @patch("methodproof.cli._require_auth", side_effect=SystemExit("not logged in"))
22
+ def test_start_hooks_not_installed_auto_installs(mock_auth, mock_install, mock_hook, logged_in_cfg, cli_args, capsys):
23
+ """Missing hook auto-installs and continues (no longer hard-fails)."""
21
24
  logged_in_cfg()
22
25
  with pytest.raises(SystemExit):
23
26
  cli.cmd_start(cli_args())
27
+ out = capsys.readouterr().out
28
+ assert "Shell hook installed" in out
29
+ mock_install.assert_called_once()
24
30
 
25
31
 
26
32
  @patch("methodproof.hook.is_installed", return_value=True)
@@ -1,212 +0,0 @@
1
- """Textual TUI for mp start — live session event feed."""
2
- from __future__ import annotations
3
-
4
- import json
5
- import time
6
- from datetime import datetime, UTC
7
-
8
- from textual.app import App, ComposeResult
9
- from textual.binding import Binding
10
- from textual.containers import Horizontal, Vertical
11
- from textual.reactive import reactive
12
- from textual.widgets import Footer, Header, RichLog, Static
13
- from methodproof import config, store
14
- from methodproof.tui.theme import BASE_CSS, BORDER, DIM, GOLD, GREEN, PURPLE, RED, TEXT
15
-
16
- _CSS = BASE_CSS + f"""
17
- #session-bar {{
18
- background: #1c1a18;
19
- height: 1;
20
- padding: 0 2;
21
- color: {DIM};
22
- }}
23
- #feed {{
24
- width: 3fr;
25
- border-right: solid {BORDER};
26
- padding: 0 1;
27
- }}
28
- #sidebar {{
29
- width: 22;
30
- padding: 1 2;
31
- background: #0a0908;
32
- }}
33
- .sidebar-title {{
34
- color: {GOLD};
35
- text-style: bold;
36
- margin: 0 0 1 0;
37
- }}
38
- .stat-row {{
39
- color: {DIM};
40
- height: 1;
41
- }}
42
- #moment-alert {{
43
- background: #1a1408;
44
- border: solid #3a2e10;
45
- margin: 1 1;
46
- padding: 0 1;
47
- height: 3;
48
- display: none;
49
- }}
50
- #moment-alert.visible {{
51
- display: block;
52
- }}
53
- """
54
-
55
- _EVENT_COLORS = {
56
- "file_edit": GREEN, "file_create": GREEN, "file_delete": RED,
57
- "terminal_cmd": TEXT, "test_run": GOLD, "git_commit": GOLD,
58
- "llm_prompt": PURPLE, "llm_completion": PURPLE,
59
- "agent_prompt": PURPLE, "agent_completion": PURPLE,
60
- "browser_visit": DIM, "browser_search": DIM,
61
- "music_playing": DIM,
62
- }
63
-
64
- _POLL_INTERVAL = 0.5 # seconds between store polls
65
-
66
-
67
- class StartApp(App[None]):
68
- """Live session view — tails the active session's events."""
69
-
70
- TITLE = "methodproof — mp start"
71
- CSS = _CSS
72
- BINDINGS = [
73
- Binding("q", "stop_session", "stop"),
74
- Binding("p", "pause", "pause"),
75
- Binding("l", "toggle_live", "toggle live"),
76
- Binding("escape", "quit", "quit"),
77
- ]
78
-
79
- _elapsed: reactive[int] = reactive(0)
80
- _paused: reactive[bool] = reactive(False)
81
- _event_count: int = 0
82
- _last_seen_id: str = ""
83
-
84
- def __init__(self, session_id: str, session: dict) -> None:
85
- super().__init__()
86
- self._session_id = session_id
87
- self._session = session
88
- self._stats: dict[str, int] = {}
89
- self._start_time = session.get("created_at", time.time())
90
-
91
- def compose(self) -> ComposeResult:
92
- yield Header(show_clock=False)
93
- watch_dir = self._session.get("watch_dir", "?")
94
- sid = self._session_id[:8]
95
- yield Static(
96
- f" session: [{GOLD}]{sid}[/{GOLD}] · {watch_dir} · [{GREEN}]●[/{GREEN}] 00:00:00",
97
- id="session-bar",
98
- markup=True,
99
- )
100
- with Horizontal():
101
- with Vertical(id="feed-col"):
102
- yield RichLog(id="feed", highlight=True, markup=True, wrap=False)
103
- yield Static("", id="moment-alert", markup=True)
104
- with Vertical(id="sidebar"):
105
- yield Static("Stats", classes="sidebar-title")
106
- yield Static("", id="stats-content", markup=True)
107
- yield Footer()
108
-
109
- def on_mount(self) -> None:
110
- import base64
111
- cfg = config.load()
112
- token = cfg.get("token", "")
113
- try:
114
- payload = token.split(".")[1] + "=="
115
- claims = json.loads(base64.urlsafe_b64decode(payload))
116
- except Exception:
117
- claims = {}
118
- self._account_type = (claims.get("account_type") or "free").capitalize()
119
- self.set_interval(_POLL_INTERVAL, self._poll_events)
120
- self.set_interval(1.0, self._tick_timer)
121
-
122
- def _tick_timer(self) -> None:
123
- if self._paused:
124
- return
125
- elapsed = int(time.time() - self._start_time)
126
- h, m, s = elapsed // 3600, (elapsed % 3600) // 60, elapsed % 60
127
- sid = self._session_id[:8]
128
- watch_dir = self._session.get("watch_dir", "?")
129
- self.query_one("#session-bar", Static).update(
130
- f" session: [{GOLD}]{sid}[/{GOLD}] · {watch_dir} · [{GREEN}]●[/{GREEN}]"
131
- f" {h:02d}:{m:02d}:{s:02d} · [{PURPLE}]{self._account_type}[/{PURPLE}]"
132
- )
133
-
134
- def _poll_events(self) -> None:
135
- if self._paused:
136
- return
137
- try:
138
- events = store.get_session_events(self._session_id, after_id=self._last_seen_id)
139
- except Exception as exc:
140
- self.log.warning(f"poll_events failed: {exc}")
141
- return
142
-
143
- feed = self.query_one(RichLog)
144
- for ev in events:
145
- self._last_seen_id = ev.get("id", self._last_seen_id)
146
- etype = ev.get("type", "event")
147
- color = _EVENT_COLORS.get(etype, DIM)
148
- ts = datetime.fromtimestamp(ev.get("ts", time.time()), tz=UTC).strftime("%H:%M:%S")
149
- meta = _fmt_meta(ev)
150
- feed.write(
151
- f"[{DIM}]{ts}[/{DIM}] [{color}]{etype:<18}[/{color}] [{DIM}]{meta}[/{DIM}]"
152
- )
153
- self._stats[etype] = self._stats.get(etype, 0) + 1
154
- self._event_count += 1
155
-
156
- if events:
157
- self._refresh_stats()
158
-
159
- def _refresh_stats(self) -> None:
160
- lines = []
161
- for etype, count in sorted(self._stats.items(), key=lambda x: -x[1])[:10]:
162
- color = _EVENT_COLORS.get(etype, DIM)
163
- lines.append(f"[{DIM}]{etype:<14}[/{DIM}] [{color}]{count}[/{color}]")
164
- self.query_one("#stats-content", Static).update("\n".join(lines))
165
-
166
- def _show_moment(self, mtype: str, detail: str) -> None:
167
- alert = self.query_one("#moment-alert", Static)
168
- alert.update(f" [{GOLD}]⚡ {mtype}[/{GOLD}] [{DIM}]{detail}[/{DIM}]")
169
- alert.add_class("visible")
170
- self.set_timer(4.0, lambda: alert.remove_class("visible"))
171
-
172
- def action_stop_session(self) -> None:
173
- self.exit(None)
174
-
175
- def action_pause(self) -> None:
176
- self._paused = not self._paused
177
-
178
- def action_toggle_live(self) -> None:
179
- pass # handled by caller in cli.py
180
-
181
-
182
- def _fmt_meta(ev: dict) -> str:
183
- etype = ev.get("type", "")
184
- meta = ev.get("metadata") or {}
185
- if not isinstance(meta, dict):
186
- meta = {}
187
- if etype in ("file_edit", "file_create", "file_delete"):
188
- path = meta.get("path") or meta.get("file_path", "")
189
- delta = meta.get("line_delta") or meta.get("lines_added", "")
190
- return f"{path} {f'+{delta}' if delta else ''}".strip()
191
- if etype == "terminal_cmd":
192
- cmd = (meta.get("command") or "")[:40]
193
- ec = meta.get("exit_code", 0)
194
- return f"{cmd} {'✓' if ec == 0 else f'✗{ec}'}"
195
- if etype == "git_commit":
196
- return (meta.get("message") or "")[:40]
197
- if etype in ("llm_prompt", "agent_prompt"):
198
- tokens = meta.get("prompt_tokens") or meta.get("input_length", "")
199
- return f"{tokens} tokens" if tokens else ""
200
- if etype in ("llm_completion", "agent_completion"):
201
- tokens = meta.get("completion_tokens") or meta.get("output_length", "")
202
- dur = ev.get("duration_ms", "")
203
- return f"{tokens} tokens {dur}ms" if tokens else ""
204
- if etype == "test_run":
205
- p, f = meta.get("passed", 0), meta.get("failed", 0)
206
- return f"{p} passed {f} failed" if f else f"{p} passed"
207
- return ""
208
-
209
-
210
- def run(session_id: str, session: dict) -> None:
211
- """Launch the live session view for the given session."""
212
- StartApp(session_id, session).run()
@@ -1,53 +0,0 @@
1
- """Shared brand colors and base CSS for all MethodProof TUI screens."""
2
-
3
- # Brand palette
4
- GOLD = "#c9a84c"
5
- GREEN = "#109446"
6
- RED = "#d93326"
7
- PURPLE = "#803794"
8
- NAVY = "#192a56"
9
-
10
- # Surface palette
11
- BG = "#12110f"
12
- BAR = "#1c1a18"
13
- PANEL_BG = "#0f0d0b"
14
- BORDER = "#2e2c29"
15
- TEXT = "#e8e4de"
16
- DIM = "#6b6560"
17
-
18
- BASE_CSS = f"""
19
- Screen {{
20
- background: {BG};
21
- }}
22
- Header {{
23
- background: {BAR};
24
- color: {GOLD};
25
- text-style: bold;
26
- }}
27
- Footer {{
28
- background: {BAR};
29
- color: {DIM};
30
- }}
31
- .panel {{
32
- border: solid {BORDER};
33
- background: {PANEL_BG};
34
- margin: 0 0 1 0;
35
- padding: 1 2;
36
- }}
37
- .section-title {{
38
- color: {GOLD};
39
- text-style: bold;
40
- margin: 0 0 1 0;
41
- }}
42
- .row-label {{
43
- width: 24;
44
- color: {TEXT};
45
- }}
46
- .row-desc {{
47
- color: {DIM};
48
- }}
49
- Rule {{
50
- color: {BORDER};
51
- margin: 1 0;
52
- }}
53
- """
File without changes
File without changes
File without changes
File without changes