methodproof 0.7.35__tar.gz → 0.7.37__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.
- {methodproof-0.7.35 → methodproof-0.7.37}/CHANGELOG.md +20 -0
- {methodproof-0.7.35 → methodproof-0.7.37}/PKG-INFO +6 -4
- {methodproof-0.7.35 → methodproof-0.7.37}/README.md +5 -3
- {methodproof-0.7.35 → methodproof-0.7.37}/methodproof/agents/base.py +0 -9
- {methodproof-0.7.35 → methodproof-0.7.37}/methodproof/cli.py +78 -11
- {methodproof-0.7.35 → methodproof-0.7.37}/methodproof/config.py +4 -14
- {methodproof-0.7.35 → methodproof-0.7.37}/methodproof/hooks/claude_code.py +6 -0
- {methodproof-0.7.35 → methodproof-0.7.37}/methodproof/hooks/claude_code.sh +54 -3
- {methodproof-0.7.35 → methodproof-0.7.37}/methodproof/tui/start.py +82 -16
- {methodproof-0.7.35 → methodproof-0.7.37}/pyproject.toml +1 -1
- {methodproof-0.7.35 → methodproof-0.7.37}/tests/test_cli_start.py +5 -2
- {methodproof-0.7.35 → methodproof-0.7.37}/.github/workflows/ci.yml +0 -0
- {methodproof-0.7.35 → methodproof-0.7.37}/.gitignore +0 -0
- {methodproof-0.7.35 → methodproof-0.7.37}/LICENSE +0 -0
- {methodproof-0.7.35 → methodproof-0.7.37}/methodproof/__init__.py +0 -0
- {methodproof-0.7.35 → methodproof-0.7.37}/methodproof/__main__.py +0 -0
- {methodproof-0.7.35 → methodproof-0.7.37}/methodproof/_daemon.py +0 -0
- {methodproof-0.7.35 → methodproof-0.7.37}/methodproof/agents/__init__.py +0 -0
- {methodproof-0.7.35 → methodproof-0.7.37}/methodproof/agents/music.py +0 -0
- {methodproof-0.7.35 → methodproof-0.7.37}/methodproof/agents/terminal.py +0 -0
- {methodproof-0.7.35 → methodproof-0.7.37}/methodproof/agents/watcher.py +0 -0
- {methodproof-0.7.35 → methodproof-0.7.37}/methodproof/analysis.py +0 -0
- {methodproof-0.7.35 → methodproof-0.7.37}/methodproof/binding.py +0 -0
- {methodproof-0.7.35 → methodproof-0.7.37}/methodproof/bip39.py +0 -0
- {methodproof-0.7.35 → methodproof-0.7.37}/methodproof/bridge.py +0 -0
- {methodproof-0.7.35 → methodproof-0.7.37}/methodproof/crypto.py +0 -0
- {methodproof-0.7.35 → methodproof-0.7.37}/methodproof/e2e.py +0 -0
- {methodproof-0.7.35 → methodproof-0.7.37}/methodproof/graph.py +0 -0
- {methodproof-0.7.35 → methodproof-0.7.37}/methodproof/hook.py +0 -0
- {methodproof-0.7.35 → methodproof-0.7.37}/methodproof/hooks/__init__.py +0 -0
- {methodproof-0.7.35 → methodproof-0.7.37}/methodproof/hooks/cline_hook.sh +0 -0
- {methodproof-0.7.35 → methodproof-0.7.37}/methodproof/hooks/codex_hook.sh +0 -0
- {methodproof-0.7.35 → methodproof-0.7.37}/methodproof/hooks/gemini_hook.sh +0 -0
- {methodproof-0.7.35 → methodproof-0.7.37}/methodproof/hooks/install.py +0 -0
- {methodproof-0.7.35 → methodproof-0.7.37}/methodproof/hooks/kiro_hook.sh +0 -0
- {methodproof-0.7.35 → methodproof-0.7.37}/methodproof/hooks/mcp_register.py +0 -0
- {methodproof-0.7.35 → methodproof-0.7.37}/methodproof/hooks/openclaw/HOOK.md +0 -0
- {methodproof-0.7.35 → methodproof-0.7.37}/methodproof/hooks/openclaw/handler.ts +0 -0
- {methodproof-0.7.35 → methodproof-0.7.37}/methodproof/hooks/openclaw_install.py +0 -0
- {methodproof-0.7.35 → methodproof-0.7.37}/methodproof/hooks/opencode_plugin.js +0 -0
- {methodproof-0.7.35 → methodproof-0.7.37}/methodproof/hooks/wrappers.py +0 -0
- {methodproof-0.7.35 → methodproof-0.7.37}/methodproof/integrity.py +0 -0
- {methodproof-0.7.35 → methodproof-0.7.37}/methodproof/kdf.py +0 -0
- {methodproof-0.7.35 → methodproof-0.7.37}/methodproof/keychain.py +0 -0
- {methodproof-0.7.35 → methodproof-0.7.37}/methodproof/live.py +0 -0
- {methodproof-0.7.35 → methodproof-0.7.37}/methodproof/lock.py +0 -0
- {methodproof-0.7.35 → methodproof-0.7.37}/methodproof/mcp.py +0 -0
- {methodproof-0.7.35 → methodproof-0.7.37}/methodproof/migrate_db.py +0 -0
- {methodproof-0.7.35 → methodproof-0.7.37}/methodproof/proxy.py +0 -0
- {methodproof-0.7.35 → methodproof-0.7.37}/methodproof/proxy_daemon.py +0 -0
- {methodproof-0.7.35 → methodproof-0.7.37}/methodproof/repos.py +0 -0
- {methodproof-0.7.35 → methodproof-0.7.37}/methodproof/skills/methodproof/SKILL.md +0 -0
- {methodproof-0.7.35 → methodproof-0.7.37}/methodproof/store.py +0 -0
- {methodproof-0.7.35 → methodproof-0.7.37}/methodproof/sync.py +0 -0
- {methodproof-0.7.35 → methodproof-0.7.37}/methodproof/tui/__init__.py +0 -0
- {methodproof-0.7.35 → methodproof-0.7.37}/methodproof/tui/consent.py +0 -0
- {methodproof-0.7.35 → methodproof-0.7.37}/methodproof/tui/init.py +0 -0
- {methodproof-0.7.35 → methodproof-0.7.37}/methodproof/tui/log.py +0 -0
- {methodproof-0.7.35 → methodproof-0.7.37}/methodproof/tui/login_success.py +0 -0
- {methodproof-0.7.35 → methodproof-0.7.37}/methodproof/tui/review.py +0 -0
- {methodproof-0.7.35 → methodproof-0.7.37}/methodproof/tui/status.py +0 -0
- {methodproof-0.7.35 → methodproof-0.7.37}/methodproof/tui/theme.py +0 -0
- {methodproof-0.7.35 → methodproof-0.7.37}/methodproof/viewer.py +0 -0
- {methodproof-0.7.35 → methodproof-0.7.37}/methodproof/wordlist.py +0 -0
- {methodproof-0.7.35 → methodproof-0.7.37}/test_windows_compat.py +0 -0
- {methodproof-0.7.35 → methodproof-0.7.37}/tests/__init__.py +0 -0
- {methodproof-0.7.35 → methodproof-0.7.37}/tests/conftest.py +0 -0
- {methodproof-0.7.35 → methodproof-0.7.37}/tests/test_analysis.py +0 -0
- {methodproof-0.7.35 → methodproof-0.7.37}/tests/test_cli_auth.py +0 -0
- {methodproof-0.7.35 → methodproof-0.7.37}/tests/test_cli_config.py +0 -0
- {methodproof-0.7.35 → methodproof-0.7.37}/tests/test_cli_helpers.py +0 -0
- {methodproof-0.7.35 → methodproof-0.7.37}/tests/test_cli_session.py +0 -0
- {methodproof-0.7.35 → methodproof-0.7.37}/tests/test_cli_share.py +0 -0
- {methodproof-0.7.35 → methodproof-0.7.37}/tests/test_cli_update.py +0 -0
- {methodproof-0.7.35 → methodproof-0.7.37}/tests/test_e2e_integration.py +0 -0
- {methodproof-0.7.35 → methodproof-0.7.37}/tests/test_graph.py +0 -0
- {methodproof-0.7.35 → methodproof-0.7.37}/tests/test_hooks.py +0 -0
- {methodproof-0.7.35 → methodproof-0.7.37}/tests/test_live.py +0 -0
- {methodproof-0.7.35 → methodproof-0.7.37}/tests/test_openclaw_hooks.py +0 -0
- {methodproof-0.7.35 → methodproof-0.7.37}/tests/test_profiles.py +0 -0
- {methodproof-0.7.35 → methodproof-0.7.37}/tests/test_security.py +0 -0
- {methodproof-0.7.35 → methodproof-0.7.37}/tests/test_store.py +0 -0
- {methodproof-0.7.35 → methodproof-0.7.37}/tests/test_sync.py +0 -0
- {methodproof-0.7.35 → methodproof-0.7.37}/tests/test_viewer.py +0 -0
- {methodproof-0.7.35 → methodproof-0.7.37}/tests/test_wrappers.py +0 -0
- {methodproof-0.7.35 → methodproof-0.7.37}/uv.lock +0 -0
|
@@ -1,5 +1,25 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.7.37] — 2026-04-12
|
|
4
|
+
|
|
5
|
+
### Changed
|
|
6
|
+
- **Full tool metadata capture** — shell hook now stores complete `tool_input` and `tool_response` objects alongside display previews. Previously only extracted preview strings; raw data was discarded. Enables upstream graph analysis, causal edge building, and artifact tracking from tool events.
|
|
7
|
+
- **Enriched shell hook** — `PreToolUse` captures `tool_input_preview` per tool type (Bash→command, Read/Edit/Write→file_path, Grep→pattern+path, Glob→pattern, Agent→description). `PostToolUse` captures `result_preview` (Bash→stdout/stderr, Read→line count, Grep→file+line count, Glob→file count) plus `tool_input_preview` so results show what was done.
|
|
8
|
+
- **Journal gating removed** — all captured metadata is now persisted regardless of journal mode. No more field stripping in `emit()`. `JOURNAL_CONTENT_FIELDS` retained as a reference for TUI enrichment display.
|
|
9
|
+
- **`tool_result` TUI shows input + result** — e.g. `Grep ✓ def main /src 3 files, 15 lines` instead of just `Grep ✓`.
|
|
10
|
+
|
|
11
|
+
## [0.7.36] — 2026-04-12
|
|
12
|
+
|
|
13
|
+
### Added
|
|
14
|
+
- **`mp connect [session_id]`** — attach TUI to an active recording session. Defaults to the current active session. `mp start` with an active session now auto-connects instead of erroring.
|
|
15
|
+
- **Source tracking in TUI** — events display which AI tool session generated them (e.g. `claude`, `codex`). Session bar shows source name and conversation number (`claude #2`). Tracks across multiple AI sessions within one `mp start`.
|
|
16
|
+
- **End session from TUI (`x`)** — confirmation prompt (`y`/`n`) to stop the session and finalize from within the TUI. `q` now exits the TUI without stopping the daemon — reconnect later with `mp connect`.
|
|
17
|
+
|
|
18
|
+
### Fixed
|
|
19
|
+
- **Tree indentation invisible** — `gold_ember` (`#3d3118`) was near-invisible on dark background; switched to `gold_aged` (`#9a7b3a`).
|
|
20
|
+
- **Orphan tool_call events showed no tree** — `tool_call` after `agent_turn_end`/`agent_complete` fell through to flat rendering. Now implicitly opens a new chain.
|
|
21
|
+
- **`tool_name` field empty in TUI** — hook events store the tool name in `tool` not `tool_name`; formatters now check both fields.
|
|
22
|
+
|
|
3
23
|
## [0.7.35] — 2026-04-12
|
|
4
24
|
|
|
5
25
|
### Added
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: methodproof
|
|
3
|
-
Version: 0.7.
|
|
3
|
+
Version: 0.7.37
|
|
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
|
|
@@ -88,9 +88,10 @@ methodproof view # explore your session in the browser
|
|
|
88
88
|
| `enter` | Event detail | Modal overlay with all metadata fields |
|
|
89
89
|
| `m` | Quiet mode | Hide dim events (music, environment, MCP, context compaction) |
|
|
90
90
|
| `p` | Pause | Pause/resume event polling |
|
|
91
|
-
| `q` |
|
|
91
|
+
| `q` | Exit TUI | Detach — daemon keeps recording. Reconnect with `mp connect` |
|
|
92
|
+
| `x` | End session | Stop recording with confirmation prompt |
|
|
92
93
|
|
|
93
|
-
Active modes display as badges in the session bar: `J` (journal), `⏸` (scroll locked), `Q` (quiet), `T` (tree collapsed), filter name when not "all".
|
|
94
|
+
Active modes display as badges in the session bar: `J` (journal), `⏸` (scroll locked), `Q` (quiet), `T` (tree collapsed), filter name when not "all". Source tracking shows which AI tool session (`claude #1`, `codex #2`) generated each event.
|
|
94
95
|
|
|
95
96
|
## Security Architecture
|
|
96
97
|
|
|
@@ -178,8 +179,9 @@ flowchart TB
|
|
|
178
179
|
| Command | What it does |
|
|
179
180
|
|---------|-------------|
|
|
180
181
|
| `init` | Interactive consent selector, install hooks, create data directory |
|
|
181
|
-
| `start [--dir .] [--tags t1,t2] [--public] [--live] [--journal] [--e2e]` | Start recording |
|
|
182
|
+
| `start [--dir .] [--tags t1,t2] [--public] [--live] [--journal] [--e2e]` | Start recording (auto-connects if session already active) |
|
|
182
183
|
| `stop` | Stop recording, build process graph |
|
|
184
|
+
| `connect [session_id]` | Attach TUI to an active session (defaults to current) |
|
|
183
185
|
| `view [session_id]` | Open session graph in browser |
|
|
184
186
|
| `log` | List sessions with sync status, visibility, tags |
|
|
185
187
|
| `login [--api-url URL]` | Authenticate with the platform |
|
|
@@ -71,9 +71,10 @@ methodproof view # explore your session in the browser
|
|
|
71
71
|
| `enter` | Event detail | Modal overlay with all metadata fields |
|
|
72
72
|
| `m` | Quiet mode | Hide dim events (music, environment, MCP, context compaction) |
|
|
73
73
|
| `p` | Pause | Pause/resume event polling |
|
|
74
|
-
| `q` |
|
|
74
|
+
| `q` | Exit TUI | Detach — daemon keeps recording. Reconnect with `mp connect` |
|
|
75
|
+
| `x` | End session | Stop recording with confirmation prompt |
|
|
75
76
|
|
|
76
|
-
Active modes display as badges in the session bar: `J` (journal), `⏸` (scroll locked), `Q` (quiet), `T` (tree collapsed), filter name when not "all".
|
|
77
|
+
Active modes display as badges in the session bar: `J` (journal), `⏸` (scroll locked), `Q` (quiet), `T` (tree collapsed), filter name when not "all". Source tracking shows which AI tool session (`claude #1`, `codex #2`) generated each event.
|
|
77
78
|
|
|
78
79
|
## Security Architecture
|
|
79
80
|
|
|
@@ -161,8 +162,9 @@ flowchart TB
|
|
|
161
162
|
| Command | What it does |
|
|
162
163
|
|---------|-------------|
|
|
163
164
|
| `init` | Interactive consent selector, install hooks, create data directory |
|
|
164
|
-
| `start [--dir .] [--tags t1,t2] [--public] [--live] [--journal] [--e2e]` | Start recording |
|
|
165
|
+
| `start [--dir .] [--tags t1,t2] [--public] [--live] [--journal] [--e2e]` | Start recording (auto-connects if session already active) |
|
|
165
166
|
| `stop` | Stop recording, build process graph |
|
|
167
|
+
| `connect [session_id]` | Attach TUI to an active session (defaults to current) |
|
|
166
168
|
| `view [session_id]` | Open session graph in browser |
|
|
167
169
|
| `log` | List sessions with sync status, visibility, tags |
|
|
168
170
|
| `login [--api-url URL]` | Authenticate with the platform |
|
|
@@ -140,15 +140,6 @@ def emit(event_type: str, metadata: dict[str, Any]) -> None:
|
|
|
140
140
|
if event_type == etype and not _capture.get(category, True):
|
|
141
141
|
metadata.pop(field, None)
|
|
142
142
|
|
|
143
|
-
# Journal mode gate — strip content fields when journal is OFF (default).
|
|
144
|
-
# Structural equivalents (prompt_length, etc.) are always kept.
|
|
145
|
-
# Journal ON = complete explicit record. Journal OFF = structural only.
|
|
146
|
-
if not _journal_mode:
|
|
147
|
-
from methodproof.config import JOURNAL_CONTENT_FIELDS
|
|
148
|
-
for etype, field in JOURNAL_CONTENT_FIELDS:
|
|
149
|
-
if event_type == etype and field in metadata:
|
|
150
|
-
metadata.pop(field, None)
|
|
151
|
-
|
|
152
143
|
entry = {
|
|
153
144
|
"id": uuid.uuid4().hex,
|
|
154
145
|
"session_id": _session_id,
|
|
@@ -1098,9 +1098,11 @@ def cmd_start(args: argparse.Namespace) -> None:
|
|
|
1098
1098
|
|
|
1099
1099
|
if cfg.get("active_session"):
|
|
1100
1100
|
if _is_daemon_alive():
|
|
1101
|
-
|
|
1102
|
-
print("
|
|
1103
|
-
|
|
1101
|
+
sid = cfg["active_session"]
|
|
1102
|
+
print(f" Session {sid[:8]} already active. Connecting...")
|
|
1103
|
+
args.session_id = None # connect resolves from config
|
|
1104
|
+
cmd_connect(args)
|
|
1105
|
+
return
|
|
1104
1106
|
stale_sid = cfg["active_session"]
|
|
1105
1107
|
_log_step(f"Cleaning stale session {stale_sid[:8]}")
|
|
1106
1108
|
store.complete_session(stale_sid)
|
|
@@ -1301,7 +1303,9 @@ def cmd_start(args: argparse.Namespace) -> None:
|
|
|
1301
1303
|
if _resolve_ui(args, cfg):
|
|
1302
1304
|
session = store.get_session(sid)
|
|
1303
1305
|
from methodproof.tui.start import run as tui_start
|
|
1304
|
-
tui_start(sid, session)
|
|
1306
|
+
result = tui_start(sid, session)
|
|
1307
|
+
if result == "end_session":
|
|
1308
|
+
cmd_stop(args)
|
|
1305
1309
|
return
|
|
1306
1310
|
print("Run `mp stop` to finish.")
|
|
1307
1311
|
return
|
|
@@ -1444,6 +1448,33 @@ def cmd_stop(args: argparse.Namespace) -> None:
|
|
|
1444
1448
|
_print_summary(session, stats, cfg)
|
|
1445
1449
|
|
|
1446
1450
|
|
|
1451
|
+
def cmd_connect(args: argparse.Namespace) -> None:
|
|
1452
|
+
"""Attach TUI to an active recording session."""
|
|
1453
|
+
cfg = config.load()
|
|
1454
|
+
sid = getattr(args, "session_id", None)
|
|
1455
|
+
if sid:
|
|
1456
|
+
# Resolve prefix
|
|
1457
|
+
sessions = store.list_sessions()
|
|
1458
|
+
match = [s for s in sessions if s["id"].startswith(sid)]
|
|
1459
|
+
if not match:
|
|
1460
|
+
print(f"No session matching '{sid}'")
|
|
1461
|
+
sys.exit(1)
|
|
1462
|
+
sid = match[0]["id"]
|
|
1463
|
+
else:
|
|
1464
|
+
sid = cfg.get("active_session")
|
|
1465
|
+
if not sid:
|
|
1466
|
+
print("No active session. Run `mp start` first.")
|
|
1467
|
+
sys.exit(1)
|
|
1468
|
+
session = store.get_session(sid)
|
|
1469
|
+
if not session:
|
|
1470
|
+
print(f"Session {sid[:8]} not found.")
|
|
1471
|
+
sys.exit(1)
|
|
1472
|
+
from methodproof.tui.start import run as tui_start
|
|
1473
|
+
result = tui_start(sid, session)
|
|
1474
|
+
if result == "end_session":
|
|
1475
|
+
cmd_stop(args)
|
|
1476
|
+
|
|
1477
|
+
|
|
1447
1478
|
def cmd_view(args: argparse.Namespace) -> None:
|
|
1448
1479
|
session = _resolve_session(args.session_id)
|
|
1449
1480
|
from methodproof.viewer import view
|
|
@@ -1782,18 +1813,51 @@ def cmd_push(args: argparse.Namespace) -> None:
|
|
|
1782
1813
|
local = getattr(args, "local", False)
|
|
1783
1814
|
cfg = config.load(local=local)
|
|
1784
1815
|
if not cfg.get("token"):
|
|
1785
|
-
target = "local API" if local else "platform"
|
|
1786
1816
|
print(f"Run `methodproof login{' --api-url http://localhost:8000' if local else ''}` first.")
|
|
1787
1817
|
sys.exit(1)
|
|
1788
1818
|
from methodproof.sync import sync_research_consent
|
|
1789
1819
|
sync_research_consent(cfg["token"], cfg["api_url"])
|
|
1790
1820
|
cfg = config.load(local=local)
|
|
1791
|
-
sid = args.session_id or _latest()
|
|
1792
|
-
if not sid:
|
|
1793
|
-
print("No sessions to push.")
|
|
1794
|
-
sys.exit(1)
|
|
1795
|
-
from methodproof.sync import push
|
|
1796
1821
|
force = getattr(args, "force", False)
|
|
1822
|
+
|
|
1823
|
+
if args.session_id:
|
|
1824
|
+
_push_one(args.session_id, cfg, force=force)
|
|
1825
|
+
return
|
|
1826
|
+
|
|
1827
|
+
# No session_id — check for unsynced completed sessions
|
|
1828
|
+
unsynced = [s for s in store.list_sessions()
|
|
1829
|
+
if not s["synced"] and s.get("completed_at")]
|
|
1830
|
+
if not unsynced:
|
|
1831
|
+
# Fall back to latest session
|
|
1832
|
+
sid = _latest()
|
|
1833
|
+
if not sid:
|
|
1834
|
+
print("No sessions to push.")
|
|
1835
|
+
sys.exit(1)
|
|
1836
|
+
_push_one(sid, cfg, force=force)
|
|
1837
|
+
return
|
|
1838
|
+
|
|
1839
|
+
if len(unsynced) == 1:
|
|
1840
|
+
_push_one(unsynced[0]["id"], cfg, force=force)
|
|
1841
|
+
return
|
|
1842
|
+
|
|
1843
|
+
# Multiple unsynced — offer to push all
|
|
1844
|
+
print(f"Found {len(unsynced)} unsynced sessions:\n")
|
|
1845
|
+
for s in unsynced:
|
|
1846
|
+
events = len(store.get_events(s["id"]))
|
|
1847
|
+
date = s["created_at"][:10] if s.get("created_at") else "?"
|
|
1848
|
+
print(f" {s['id'][:8]} {date} {events} events")
|
|
1849
|
+
print()
|
|
1850
|
+
answer = input(f"Push all {len(unsynced)}? [Y/n] ").strip().lower()
|
|
1851
|
+
if answer in ("", "y", "yes"):
|
|
1852
|
+
for s in unsynced:
|
|
1853
|
+
_push_one(s["id"], cfg, force=force)
|
|
1854
|
+
print()
|
|
1855
|
+
else:
|
|
1856
|
+
print("Cancelled. Push individually with `mp push <session_id>`.")
|
|
1857
|
+
|
|
1858
|
+
|
|
1859
|
+
def _push_one(sid: str, cfg: dict, force: bool = False) -> None:
|
|
1860
|
+
from methodproof.sync import push
|
|
1797
1861
|
remote_id = push(sid, cfg["token"], cfg["api_url"], force=force)
|
|
1798
1862
|
app = _app_url(cfg["api_url"])
|
|
1799
1863
|
print(f"Pushed {sid[:8]} → {cfg['api_url']} (private).")
|
|
@@ -2205,6 +2269,9 @@ def main() -> None:
|
|
|
2205
2269
|
s.add_argument("--streaming", action="store_true", help="Blocking foreground — stream every captured event to stdout")
|
|
2206
2270
|
_add_ui_flags(s)
|
|
2207
2271
|
sub.add_parser("stop", help="Stop recording")
|
|
2272
|
+
cn = sub.add_parser("connect", help="Attach TUI to active session")
|
|
2273
|
+
cn.add_argument("session_id", nargs="?", help="Session ID prefix (defaults to active)")
|
|
2274
|
+
_add_ui_flags(cn)
|
|
2208
2275
|
v = sub.add_parser("view", help="Inspect captured session data")
|
|
2209
2276
|
v.add_argument("session_id", nargs="?")
|
|
2210
2277
|
l_log = sub.add_parser("log", help="List sessions")
|
|
@@ -2287,7 +2354,7 @@ def main() -> None:
|
|
|
2287
2354
|
|
|
2288
2355
|
args = p.parse_args()
|
|
2289
2356
|
cmds = {
|
|
2290
|
-
"init": cmd_init, "start": cmd_start, "stop": cmd_stop,
|
|
2357
|
+
"init": cmd_init, "start": cmd_start, "stop": cmd_stop, "connect": cmd_connect,
|
|
2291
2358
|
"view": cmd_view, "log": cmd_log, "status": cmd_status,
|
|
2292
2359
|
"login": cmd_login, "logout": cmd_logout, "accounts": cmd_accounts, "switch": cmd_switch,
|
|
2293
2360
|
"push": cmd_push, "tag": cmd_tag, "publish": cmd_publish,
|
|
@@ -79,47 +79,37 @@ CAPTURE_DESCRIPTIONS: dict[str, str] = {
|
|
|
79
79
|
"code_capture": "Full file diffs and git patches (Pro only, encrypted, private by default)",
|
|
80
80
|
}
|
|
81
81
|
|
|
82
|
-
#
|
|
83
|
-
#
|
|
84
|
-
#
|
|
85
|
-
# Journal = the complete, explicit record of the session.
|
|
82
|
+
# Journal content fields — no longer gated. All captured metadata is persisted.
|
|
83
|
+
# This list is retained as a reference for the TUI journal enrichment layer,
|
|
84
|
+
# which uses it to identify fields worth displaying as secondary content lines.
|
|
86
85
|
JOURNAL_CONTENT_FIELDS: list[tuple[str, str]] = [
|
|
87
|
-
# AI prompts — full prompt text
|
|
88
86
|
("llm_prompt", "prompt_text"),
|
|
89
87
|
("agent_prompt", "prompt_preview"),
|
|
90
|
-
# AI responses — full completion text
|
|
91
88
|
("llm_completion", "response_text"),
|
|
92
89
|
("agent_completion", "response_preview"),
|
|
93
90
|
("agent_tool_dispatch", "tool_input_preview"),
|
|
94
91
|
("agent_tool_result", "result_preview"),
|
|
95
92
|
("agent_skill_invoke", "skill_input_preview"),
|
|
96
|
-
# Terminal — full command output (command itself is structural, not gated)
|
|
97
93
|
("terminal_cmd", "output_snippet"),
|
|
98
|
-
# Code — full diffs and commit messages
|
|
99
94
|
("file_edit", "diff"),
|
|
100
95
|
("git_commit", "diff"),
|
|
101
96
|
("git_commit", "message"),
|
|
102
|
-
|
|
97
|
+
("git_commit", "body"),
|
|
103
98
|
("web_search", "query"),
|
|
104
99
|
("web_search", "clicked_results"),
|
|
105
100
|
("web_visit", "url"),
|
|
106
101
|
("web_visit", "title"),
|
|
107
|
-
# Browser — full search queries, URLs, copy content, AI chat input
|
|
108
102
|
("browser_search", "query"),
|
|
109
103
|
("browser_visit", "url"),
|
|
110
104
|
("browser_visit", "title"),
|
|
111
105
|
("browser_copy", "text_snippet"),
|
|
112
106
|
("browser_ai_chat", "detected_input"),
|
|
113
107
|
("browser_ai_chat", "url"),
|
|
114
|
-
# Tasks — subject reveals intent
|
|
115
108
|
("task_created", "subject"),
|
|
116
|
-
# Claude Code hooks — tool input/output and raw user prompt
|
|
117
109
|
("user_prompt", "prompt_text"),
|
|
118
110
|
("tool_call", "tool_input_preview"),
|
|
119
111
|
("tool_result", "result_preview"),
|
|
120
|
-
# Agent final message and commit body reveal content
|
|
121
112
|
("agent_complete", "last_message_preview"),
|
|
122
|
-
("git_commit", "body"),
|
|
123
113
|
]
|
|
124
114
|
|
|
125
115
|
|
|
@@ -99,20 +99,26 @@ _META_EXTRACTORS = {
|
|
|
99
99
|
},
|
|
100
100
|
"PreToolUse": lambda d: {
|
|
101
101
|
"tool": _TOOL, "tool_name": d.get("tool_name", "unknown"),
|
|
102
|
+
"tool_input": d.get("tool_input") or {},
|
|
102
103
|
"tool_input_preview": _tool_input_preview(d),
|
|
103
104
|
},
|
|
104
105
|
"PostToolUse": lambda d: {
|
|
105
106
|
"tool": _TOOL, "tool_name": d.get("tool_name", "unknown"), "success": True,
|
|
107
|
+
"tool_input": d.get("tool_input") or {},
|
|
108
|
+
"tool_response": d.get("tool_response") or {},
|
|
109
|
+
"tool_input_preview": _tool_input_preview(d),
|
|
106
110
|
"result_preview": _extract_result_text(d.get("tool_response")),
|
|
107
111
|
},
|
|
108
112
|
"PostToolUseFailure": lambda d: {
|
|
109
113
|
"tool": _TOOL, "tool_name": d.get("tool_name", "unknown"),
|
|
110
114
|
"success": False, "is_interrupt": d.get("is_interrupt", False),
|
|
115
|
+
"tool_input": d.get("tool_input") or {},
|
|
111
116
|
"error": str(d.get("error", ""))[:200],
|
|
112
117
|
},
|
|
113
118
|
"SubagentStart": lambda d: {"tool": _TOOL, "agent_type": d.get("agent_type", "unknown"), "agent_id": d.get("agent_id", "")},
|
|
114
119
|
"SubagentStop": lambda d: {
|
|
115
120
|
"tool": _TOOL, "agent_type": d.get("agent_type", "unknown"), "agent_id": d.get("agent_id", ""),
|
|
121
|
+
"last_assistant_message": d.get("last_assistant_message", ""),
|
|
116
122
|
"last_message_preview": str(d.get("last_assistant_message", ""))[:200],
|
|
117
123
|
},
|
|
118
124
|
"TaskCreated": lambda d: {"tool": _TOOL, "task_id": d.get("task_id", ""), "subject": d.get("task_subject", "")},
|
|
@@ -37,11 +37,62 @@ if command -v jq >/dev/null 2>&1; then
|
|
|
37
37
|
;;
|
|
38
38
|
PreToolUse)
|
|
39
39
|
TYPE="tool_call"
|
|
40
|
-
META=$(echo "$INPUT" | jq -c '{
|
|
40
|
+
META=$(echo "$INPUT" | jq -c '{
|
|
41
|
+
tool: (.tool_name // "unknown"),
|
|
42
|
+
tool_use_id: (.tool_use_id // ""),
|
|
43
|
+
tool_input: (.tool_input // {}),
|
|
44
|
+
tool_input_preview: (
|
|
45
|
+
(.tool_input // {}) as $ti |
|
|
46
|
+
(.tool_name // "unknown") as $tn |
|
|
47
|
+
(if $tn == "Bash" then ($ti.command // "")
|
|
48
|
+
elif $tn == "Read" then ($ti.file_path // "")
|
|
49
|
+
elif $tn == "Write" then ($ti.file_path // "")
|
|
50
|
+
elif $tn == "Edit" then ($ti.file_path // "")
|
|
51
|
+
elif $tn == "Grep" then (($ti.pattern // "") + " " + ($ti.path // ""))
|
|
52
|
+
elif $tn == "Glob" then ($ti.pattern // "")
|
|
53
|
+
elif $tn == "Agent" then ($ti.description // $ti.prompt // "" | .[0:200])
|
|
54
|
+
else ($ti.command // $ti.file_path // $ti.path // $ti.query // $ti.pattern // $ti.url // $ti.description // $ti.prompt // ($ti | tostring)) end
|
|
55
|
+
) | tostring | .[0:200]
|
|
56
|
+
)
|
|
57
|
+
}' 2>/dev/null || echo '{}')
|
|
41
58
|
;;
|
|
42
59
|
PostToolUse)
|
|
43
60
|
TYPE="tool_result"
|
|
44
|
-
META=$(echo "$INPUT" | jq -c '{
|
|
61
|
+
META=$(echo "$INPUT" | jq -c '{
|
|
62
|
+
tool: (.tool_name // "unknown"),
|
|
63
|
+
tool_use_id: (.tool_use_id // ""),
|
|
64
|
+
success: true,
|
|
65
|
+
tool_input: (.tool_input // {}),
|
|
66
|
+
tool_response: (.tool_response // {}),
|
|
67
|
+
tool_input_preview: (
|
|
68
|
+
(.tool_input // {}) as $ti |
|
|
69
|
+
(.tool_name // "unknown") as $tn |
|
|
70
|
+
(if $tn == "Bash" then ($ti.command // "")
|
|
71
|
+
elif $tn == "Read" then ($ti.file_path // "")
|
|
72
|
+
elif $tn == "Write" then ($ti.file_path // "")
|
|
73
|
+
elif $tn == "Edit" then ($ti.file_path // "")
|
|
74
|
+
elif $tn == "Grep" then (($ti.pattern // "") + " " + ($ti.path // ""))
|
|
75
|
+
elif $tn == "Glob" then ($ti.pattern // "")
|
|
76
|
+
elif $tn == "Agent" then ($ti.description // $ti.prompt // "" | .[0:200])
|
|
77
|
+
else ($ti.command // $ti.file_path // $ti.path // $ti.query // $ti.pattern // $ti.url // $ti.description // $ti.prompt // ($ti | tostring)) end
|
|
78
|
+
) | tostring | .[0:200]
|
|
79
|
+
),
|
|
80
|
+
result_preview: (
|
|
81
|
+
(.tool_response // {}) as $tr |
|
|
82
|
+
(.tool_name // "unknown") as $tn |
|
|
83
|
+
(if $tn == "Bash" then
|
|
84
|
+
(if ($tr.stderr // "") != "" then ("stderr: " + $tr.stderr) else ($tr.stdout // "") end | .[0:200])
|
|
85
|
+
elif $tn == "Read" then
|
|
86
|
+
(($tr.file.numLines // $tr.numLines // 0) | tostring) + " lines"
|
|
87
|
+
elif $tn == "Grep" then
|
|
88
|
+
(($tr.numFiles // 0) | tostring) + " files, " + (($tr.numLines // 0) | tostring) + " lines"
|
|
89
|
+
elif $tn == "Glob" then
|
|
90
|
+
(($tr.numFiles // 0) | tostring) + " files"
|
|
91
|
+
elif ($tr | type) == "string" then ($tr | .[0:200])
|
|
92
|
+
else ($tr | tostring | .[0:200]) end
|
|
93
|
+
)
|
|
94
|
+
)
|
|
95
|
+
}' 2>/dev/null || echo '{}')
|
|
45
96
|
;;
|
|
46
97
|
SubagentStart)
|
|
47
98
|
TYPE="agent_launch"
|
|
@@ -65,7 +116,7 @@ if command -v jq >/dev/null 2>&1; then
|
|
|
65
116
|
;;
|
|
66
117
|
PostToolUseFailure)
|
|
67
118
|
TYPE="tool_failure"
|
|
68
|
-
META=$(echo "$INPUT" | jq -c '{
|
|
119
|
+
META=$(echo "$INPUT" | jq -c '{tool: (.tool_name // "unknown"), is_interrupt: (.is_interrupt // false), success: false, error: (.error // "" | .[0:200])}' 2>/dev/null || echo '{}')
|
|
69
120
|
;;
|
|
70
121
|
SessionEnd)
|
|
71
122
|
TYPE="claude_session_end"
|
|
@@ -197,6 +197,11 @@ class _TreeTracker:
|
|
|
197
197
|
return "├ ", True
|
|
198
198
|
self._in_chain = False
|
|
199
199
|
return " ", True
|
|
200
|
+
# Not in chain — tool_call starts one implicitly (orphan after closer)
|
|
201
|
+
if etype in self._INNERS:
|
|
202
|
+
self._in_chain = True
|
|
203
|
+
self._collapse_count = 0
|
|
204
|
+
return "├ ", True
|
|
200
205
|
return " ", True
|
|
201
206
|
|
|
202
207
|
|
|
@@ -255,15 +260,17 @@ def _fmt_meta(ev: dict) -> str:
|
|
|
255
260
|
|
|
256
261
|
# Hook lifecycle — tool call / result
|
|
257
262
|
if etype == "tool_call":
|
|
258
|
-
name = meta.get("tool_name", "")
|
|
263
|
+
name = meta.get("tool_name") or meta.get("tool", "")
|
|
259
264
|
preview = (meta.get("tool_input_preview") or "")[:60]
|
|
260
265
|
return f"{name} {preview}".strip()
|
|
261
266
|
if etype == "tool_result":
|
|
262
|
-
name = meta.get("tool_name", "")
|
|
267
|
+
name = meta.get("tool_name") or meta.get("tool", "")
|
|
263
268
|
ok = "✓" if meta.get("success", True) else "✗"
|
|
264
|
-
|
|
269
|
+
inp = (meta.get("tool_input_preview") or "")[:40]
|
|
270
|
+
result = (meta.get("result_preview") or "")[:40]
|
|
271
|
+
return f"{name} {ok} {inp} {result}".strip()
|
|
265
272
|
if etype == "tool_failure":
|
|
266
|
-
name = meta.get("tool_name", "")
|
|
273
|
+
name = meta.get("tool_name") or meta.get("tool", "")
|
|
267
274
|
err = (meta.get("error") or "")[:40]
|
|
268
275
|
return f"{name} ✗ {err}".strip()
|
|
269
276
|
|
|
@@ -392,14 +399,40 @@ class _DetailScreen(Screen[None]):
|
|
|
392
399
|
self.app.pop_screen()
|
|
393
400
|
|
|
394
401
|
|
|
402
|
+
class _ConfirmEndScreen(Screen[bool]):
|
|
403
|
+
"""Confirm session end."""
|
|
404
|
+
|
|
405
|
+
BINDINGS = [
|
|
406
|
+
Binding("y", "confirm", "yes"),
|
|
407
|
+
Binding("n", "cancel", "no"),
|
|
408
|
+
Binding("escape", "cancel", "cancel"),
|
|
409
|
+
]
|
|
410
|
+
|
|
411
|
+
def compose(self) -> ComposeResult:
|
|
412
|
+
P = ACTIVE
|
|
413
|
+
yield Static(
|
|
414
|
+
f"\n [{P.red}]End this session?[/{P.red}]\n\n"
|
|
415
|
+
f" [{P.dim}]This will stop all capture agents and finalize the session.[/{P.dim}]\n\n"
|
|
416
|
+
f" [{P.gold}]y[/{P.gold}] [{P.dim}]confirm[/{P.dim}] "
|
|
417
|
+
f"[{P.gold}]n[/{P.gold}] [{P.dim}]cancel[/{P.dim}]",
|
|
418
|
+
)
|
|
419
|
+
|
|
420
|
+
def action_confirm(self) -> None:
|
|
421
|
+
self.dismiss(True)
|
|
422
|
+
|
|
423
|
+
def action_cancel(self) -> None:
|
|
424
|
+
self.dismiss(False)
|
|
425
|
+
|
|
426
|
+
|
|
395
427
|
# ── App ──────────────────────────────────────────────────────────────
|
|
396
|
-
class StartApp(App[None]):
|
|
428
|
+
class StartApp(App[str | None]):
|
|
397
429
|
"""Live session view — tails the active session's events."""
|
|
398
430
|
|
|
399
431
|
TITLE = "methodproof — mp start"
|
|
400
432
|
CSS = _CSS
|
|
401
433
|
BINDINGS = [
|
|
402
|
-
Binding("q", "
|
|
434
|
+
Binding("q", "exit_tui", "exit"),
|
|
435
|
+
Binding("x", "end_session", "end session"),
|
|
403
436
|
Binding("p", "pause", "pause"),
|
|
404
437
|
Binding("j", "toggle_journal", "journal"),
|
|
405
438
|
Binding("f", "cycle_filter", "filter"),
|
|
@@ -438,6 +471,10 @@ class StartApp(App[None]):
|
|
|
438
471
|
self._last_event_ts: float = 0.0
|
|
439
472
|
self._recent_files: list[str] = []
|
|
440
473
|
self._recent_tools: list[str] = []
|
|
474
|
+
# Source tracking: which AI tool session is active
|
|
475
|
+
self._active_source: str = "" # e.g. "claude", "codex", "gemini"
|
|
476
|
+
self._source_session_id: str = "" # short id of the AI session
|
|
477
|
+
self._source_count: int = 0 # how many AI sessions so far
|
|
441
478
|
|
|
442
479
|
def compose(self) -> ComposeResult:
|
|
443
480
|
yield Header(show_clock=False)
|
|
@@ -518,10 +555,13 @@ class StartApp(App[None]):
|
|
|
518
555
|
badge_str = f" {badge_str}"
|
|
519
556
|
|
|
520
557
|
ev = f" {self._event_count} ev" if self._event_count else ""
|
|
558
|
+
src = ""
|
|
559
|
+
if self._active_source:
|
|
560
|
+
src = f" · [{P.purple_muted}]{self._active_source} #{self._source_count}[/{P.purple_muted}]"
|
|
521
561
|
self.query_one("#session-bar", Static).update(
|
|
522
562
|
f" session: [{P.gold}]{sid}[/{P.gold}] · {watch_dir}"
|
|
523
563
|
f" · [{P.green}]●[/{P.green}] {h:02d}:{m:02d}:{s:02d}"
|
|
524
|
-
f"{ev}{badge_str} · [{P.purple}]{self._account_type}[/{P.purple}]"
|
|
564
|
+
f"{ev}{src}{badge_str} · [{P.purple}]{self._account_type}[/{P.purple}]"
|
|
525
565
|
)
|
|
526
566
|
|
|
527
567
|
# ── Event poll: Layers B + E + A ─────────────────────────────
|
|
@@ -544,6 +584,17 @@ class StartApp(App[None]):
|
|
|
544
584
|
self._last_event = ev
|
|
545
585
|
ev_ts = ev.get("ts", time.time())
|
|
546
586
|
|
|
587
|
+
# Source tracking: detect AI session boundaries
|
|
588
|
+
if etype.endswith("_session_start"):
|
|
589
|
+
self._active_source = etype.replace("_session_start", "")
|
|
590
|
+
ev_meta_src = ev.get("metadata") or {}
|
|
591
|
+
sid_field = ev_meta_src.get("session_id") or ev_meta_src.get("claude_session_id", "")
|
|
592
|
+
self._source_session_id = str(sid_field)[:8]
|
|
593
|
+
self._source_count += 1
|
|
594
|
+
elif etype.endswith("_session_end"):
|
|
595
|
+
self._active_source = ""
|
|
596
|
+
self._source_session_id = ""
|
|
597
|
+
|
|
547
598
|
# Quiet mode: skip dim events
|
|
548
599
|
if self._quiet_mode and etype in _DIM_TYPES:
|
|
549
600
|
self._stats[etype] = self._stats.get(etype, 0) + 1
|
|
@@ -572,7 +623,7 @@ class StartApp(App[None]):
|
|
|
572
623
|
self._recent_files.append(path)
|
|
573
624
|
if len(self._recent_files) > 10:
|
|
574
625
|
self._recent_files.pop(0)
|
|
575
|
-
tool = ev_meta.get("tool_name")
|
|
626
|
+
tool = ev_meta.get("tool_name") or ev_meta.get("tool")
|
|
576
627
|
if tool and etype in ("tool_call", "tool_result"):
|
|
577
628
|
if tool not in self._recent_tools:
|
|
578
629
|
self._recent_tools.append(tool)
|
|
@@ -585,16 +636,20 @@ class StartApp(App[None]):
|
|
|
585
636
|
self._event_count += 1
|
|
586
637
|
continue
|
|
587
638
|
|
|
639
|
+
# Source tag
|
|
640
|
+
src_tag = ""
|
|
641
|
+
if self._active_source:
|
|
642
|
+
src_tag = f"[{P.purple_muted}]{self._active_source}[/{P.purple_muted}] "
|
|
643
|
+
|
|
588
644
|
# Search highlight
|
|
589
645
|
line_plain = f"{etype} {meta}"
|
|
590
646
|
if self._search_query and self._search_query.lower() not in line_plain.lower():
|
|
591
|
-
# Still render but dimmed
|
|
592
647
|
feed.write(f"[{P.dim}]{ts} {prefix}{etype:<18} {meta}[/{P.dim}]")
|
|
593
648
|
else:
|
|
594
|
-
# Layer B + E: structural line with tree prefix
|
|
595
649
|
feed.write(
|
|
596
650
|
f"[{P.dim}]{ts}[/{P.dim}] "
|
|
597
|
-
f"
|
|
651
|
+
f"{src_tag}"
|
|
652
|
+
f"[{P.gold_aged}]{prefix}[/{P.gold_aged}]"
|
|
598
653
|
f"[{color}]{etype:<18}[/{color}] "
|
|
599
654
|
f"[{P.dim}]{meta}[/{P.dim}]"
|
|
600
655
|
)
|
|
@@ -602,7 +657,7 @@ class StartApp(App[None]):
|
|
|
602
657
|
# Layer A: journal content enrichment
|
|
603
658
|
for jline in _journal_lines(ev, self._journal_mode):
|
|
604
659
|
feed.write(
|
|
605
|
-
f" [{P.
|
|
660
|
+
f" [{P.gold_aged}]│[/{P.gold_aged}] "
|
|
606
661
|
f"[{P.dim}]{jline}[/{P.dim}]"
|
|
607
662
|
)
|
|
608
663
|
|
|
@@ -655,9 +710,19 @@ class StartApp(App[None]):
|
|
|
655
710
|
self.set_timer(4.0, lambda: alert.remove_class("visible"))
|
|
656
711
|
|
|
657
712
|
# ── Actions ──────────────────────────────────────────────────
|
|
658
|
-
def
|
|
713
|
+
def action_exit_tui(self) -> None:
|
|
714
|
+
"""Detach TUI — daemon keeps recording."""
|
|
659
715
|
self.exit(None)
|
|
660
716
|
|
|
717
|
+
def action_end_session(self) -> None:
|
|
718
|
+
"""End the session (with confirmation)."""
|
|
719
|
+
self.push_screen(_ConfirmEndScreen(), self._on_confirm_end)
|
|
720
|
+
|
|
721
|
+
def _on_confirm_end(self, confirmed: bool) -> None:
|
|
722
|
+
if not confirmed:
|
|
723
|
+
return
|
|
724
|
+
self.exit("end_session")
|
|
725
|
+
|
|
661
726
|
def action_pause(self) -> None:
|
|
662
727
|
self._paused = not self._paused
|
|
663
728
|
|
|
@@ -743,6 +808,7 @@ class StartApp(App[None]):
|
|
|
743
808
|
self._tick_timer()
|
|
744
809
|
|
|
745
810
|
|
|
746
|
-
def run(session_id: str, session: dict) -> None:
|
|
747
|
-
"""Launch the live session view
|
|
748
|
-
StartApp(session_id, session)
|
|
811
|
+
def run(session_id: str, session: dict) -> str | None:
|
|
812
|
+
"""Launch the live session view. Returns 'end_session' if user chose to end."""
|
|
813
|
+
app = StartApp(session_id, session)
|
|
814
|
+
return app.run()
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "methodproof"
|
|
3
|
-
version = "0.7.
|
|
3
|
+
version = "0.7.37"
|
|
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"]
|
|
@@ -38,14 +38,17 @@ def test_start_auth_fails(mock_auth, mock_hook, cli_args):
|
|
|
38
38
|
|
|
39
39
|
@patch("methodproof.cli._is_daemon_alive", return_value=True)
|
|
40
40
|
@patch("methodproof.hook.is_installed", return_value=True)
|
|
41
|
-
def test_start_session_already_active(mock_hook, mock_alive, logged_in_cfg, cli_args, make_session):
|
|
41
|
+
def test_start_session_already_active(mock_hook, mock_alive, logged_in_cfg, cli_args, make_session, capsys):
|
|
42
42
|
logged_in_cfg()
|
|
43
43
|
sid, _ = make_session()
|
|
44
44
|
cfg = config.load()
|
|
45
45
|
cfg["active_session"] = sid
|
|
46
46
|
config.save(cfg)
|
|
47
|
-
with
|
|
47
|
+
with patch("methodproof.cli.cmd_connect") as mock_connect:
|
|
48
48
|
cli.cmd_start(cli_args())
|
|
49
|
+
mock_connect.assert_called_once()
|
|
50
|
+
out = capsys.readouterr().out
|
|
51
|
+
assert "already active" in out
|
|
49
52
|
|
|
50
53
|
|
|
51
54
|
@patch("methodproof.cli._is_daemon_alive", return_value=False)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|