methodproof 0.3.3__tar.gz → 0.4.0__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.3.3 → methodproof-0.4.0}/CHANGELOG.md +6 -0
- {methodproof-0.3.3 → methodproof-0.4.0}/PKG-INFO +3 -1
- {methodproof-0.3.3 → methodproof-0.4.0}/methodproof/__init__.py +1 -1
- {methodproof-0.3.3 → methodproof-0.4.0}/methodproof/agents/base.py +30 -1
- {methodproof-0.3.3 → methodproof-0.4.0}/methodproof/analysis.py +65 -0
- {methodproof-0.3.3 → methodproof-0.4.0}/methodproof/cli.py +260 -23
- {methodproof-0.3.3 → methodproof-0.4.0}/methodproof/config.py +42 -0
- {methodproof-0.3.3 → methodproof-0.4.0}/methodproof/hooks/claude_code.py +10 -6
- methodproof-0.4.0/methodproof/hooks/cline_hook.sh +47 -0
- methodproof-0.4.0/methodproof/hooks/codex_hook.sh +62 -0
- methodproof-0.4.0/methodproof/hooks/gemini_hook.sh +58 -0
- methodproof-0.4.0/methodproof/hooks/install.py +246 -0
- methodproof-0.4.0/methodproof/hooks/kiro_hook.sh +70 -0
- methodproof-0.4.0/methodproof/hooks/mcp_register.py +92 -0
- methodproof-0.4.0/methodproof/hooks/opencode_plugin.js +33 -0
- {methodproof-0.3.3 → methodproof-0.4.0}/methodproof/hooks/wrappers.py +20 -0
- {methodproof-0.3.3 → methodproof-0.4.0}/methodproof/live.py +52 -3
- methodproof-0.4.0/methodproof/proxy.py +162 -0
- methodproof-0.4.0/methodproof/proxy_daemon.py +161 -0
- {methodproof-0.3.3 → methodproof-0.4.0}/methodproof/sync.py +3 -2
- {methodproof-0.3.3 → methodproof-0.4.0}/pyproject.toml +2 -1
- methodproof-0.3.3/methodproof/hooks/install.py +0 -87
- {methodproof-0.3.3 → methodproof-0.4.0}/.github/workflows/ci.yml +0 -0
- {methodproof-0.3.3 → methodproof-0.4.0}/.gitignore +0 -0
- {methodproof-0.3.3 → methodproof-0.4.0}/LICENSE +0 -0
- {methodproof-0.3.3 → methodproof-0.4.0}/README.md +0 -0
- {methodproof-0.3.3 → methodproof-0.4.0}/methodproof/__main__.py +0 -0
- {methodproof-0.3.3 → methodproof-0.4.0}/methodproof/agents/__init__.py +0 -0
- {methodproof-0.3.3 → methodproof-0.4.0}/methodproof/agents/music.py +0 -0
- {methodproof-0.3.3 → methodproof-0.4.0}/methodproof/agents/terminal.py +0 -0
- {methodproof-0.3.3 → methodproof-0.4.0}/methodproof/agents/watcher.py +0 -0
- {methodproof-0.3.3 → methodproof-0.4.0}/methodproof/bridge.py +0 -0
- {methodproof-0.3.3 → methodproof-0.4.0}/methodproof/crypto.py +0 -0
- {methodproof-0.3.3 → methodproof-0.4.0}/methodproof/graph.py +0 -0
- {methodproof-0.3.3 → methodproof-0.4.0}/methodproof/hook.py +0 -0
- {methodproof-0.3.3 → methodproof-0.4.0}/methodproof/hooks/__init__.py +0 -0
- {methodproof-0.3.3 → methodproof-0.4.0}/methodproof/hooks/claude_code.sh +0 -0
- {methodproof-0.3.3 → methodproof-0.4.0}/methodproof/hooks/openclaw/HOOK.md +0 -0
- {methodproof-0.3.3 → methodproof-0.4.0}/methodproof/hooks/openclaw/handler.ts +0 -0
- {methodproof-0.3.3 → methodproof-0.4.0}/methodproof/hooks/openclaw_install.py +0 -0
- {methodproof-0.3.3 → methodproof-0.4.0}/methodproof/integrity.py +0 -0
- {methodproof-0.3.3 → methodproof-0.4.0}/methodproof/mcp.py +0 -0
- {methodproof-0.3.3 → methodproof-0.4.0}/methodproof/repos.py +0 -0
- {methodproof-0.3.3 → methodproof-0.4.0}/methodproof/skills/methodproof/SKILL.md +0 -0
- {methodproof-0.3.3 → methodproof-0.4.0}/methodproof/store.py +0 -0
- {methodproof-0.3.3 → methodproof-0.4.0}/methodproof/viewer.py +0 -0
- {methodproof-0.3.3 → methodproof-0.4.0}/test_windows_compat.py +0 -0
- {methodproof-0.3.3 → methodproof-0.4.0}/tests/__init__.py +0 -0
- {methodproof-0.3.3 → methodproof-0.4.0}/tests/test_analysis.py +0 -0
- {methodproof-0.3.3 → methodproof-0.4.0}/tests/test_graph.py +0 -0
- {methodproof-0.3.3 → methodproof-0.4.0}/tests/test_hooks.py +0 -0
- {methodproof-0.3.3 → methodproof-0.4.0}/tests/test_live.py +0 -0
- {methodproof-0.3.3 → methodproof-0.4.0}/tests/test_openclaw_hooks.py +0 -0
- {methodproof-0.3.3 → methodproof-0.4.0}/tests/test_store.py +0 -0
- {methodproof-0.3.3 → methodproof-0.4.0}/tests/test_wrappers.py +0 -0
- {methodproof-0.3.3 → methodproof-0.4.0}/uv.lock +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: methodproof
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.0
|
|
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
|
|
@@ -9,6 +9,8 @@ Requires-Dist: watchdog>=4.0
|
|
|
9
9
|
Requires-Dist: websocket-client>=1.7
|
|
10
10
|
Provides-Extra: e2e
|
|
11
11
|
Requires-Dist: cryptography>=43.0; extra == 'e2e'
|
|
12
|
+
Provides-Extra: proxy
|
|
13
|
+
Requires-Dist: mitmproxy>=10.0; extra == 'proxy'
|
|
12
14
|
Provides-Extra: signing
|
|
13
15
|
Requires-Dist: cryptography>=43.0; extra == 'signing'
|
|
14
16
|
Description-Content-Type: text/markdown
|
|
@@ -47,6 +47,23 @@ _EVENT_GATES: dict[str, str] = {
|
|
|
47
47
|
"music_playing": "music",
|
|
48
48
|
"environment_profile": "environment_analysis",
|
|
49
49
|
"prompt_outcomes": "ai_prompts",
|
|
50
|
+
"ai_cli_start": "ai_prompts",
|
|
51
|
+
"ai_cli_end": "ai_responses",
|
|
52
|
+
# Hook lifecycle events (all AI tools)
|
|
53
|
+
"user_prompt": "ai_prompts",
|
|
54
|
+
"tool_call": "ai_responses",
|
|
55
|
+
"tool_result": "ai_responses",
|
|
56
|
+
"task_start": "ai_responses",
|
|
57
|
+
"task_end": "ai_responses",
|
|
58
|
+
"agent_launch": "ai_responses",
|
|
59
|
+
"agent_complete": "ai_responses",
|
|
60
|
+
"claude_session_start": "ai_prompts",
|
|
61
|
+
"codex_session_start": "ai_prompts",
|
|
62
|
+
"codex_session_end": "ai_responses",
|
|
63
|
+
"gemini_session_start": "ai_prompts",
|
|
64
|
+
"gemini_session_end": "ai_responses",
|
|
65
|
+
"kiro_session_start": "ai_prompts",
|
|
66
|
+
"kiro_session_end": "ai_responses",
|
|
50
67
|
}
|
|
51
68
|
|
|
52
69
|
# Maps capture categories to (event_type, field) pairs for field-level gating.
|
|
@@ -57,9 +74,11 @@ _FIELD_GATES: dict[str, list[tuple[str, str]]] = {
|
|
|
57
74
|
"code_capture": [("file_edit", "diff"), ("git_commit", "diff")],
|
|
58
75
|
}
|
|
59
76
|
|
|
77
|
+
_journal_mode = False
|
|
78
|
+
|
|
60
79
|
|
|
61
80
|
def init(session_id: str, live: bool = False) -> None:
|
|
62
|
-
global _session_id, _initialized, _e2e_key, _capture, _live_mode, _prev_hash
|
|
81
|
+
global _session_id, _initialized, _e2e_key, _capture, _live_mode, _prev_hash, _journal_mode
|
|
63
82
|
_session_id = session_id
|
|
64
83
|
_initialized = True
|
|
65
84
|
_live_mode = live
|
|
@@ -69,6 +88,7 @@ def init(session_id: str, live: bool = False) -> None:
|
|
|
69
88
|
raw = cfg.get("e2e_key", "")
|
|
70
89
|
_e2e_key = bytes.fromhex(raw) if raw else None
|
|
71
90
|
_capture = cfg.get("capture", {})
|
|
91
|
+
_journal_mode = cfg.get("journal_mode", False)
|
|
72
92
|
|
|
73
93
|
|
|
74
94
|
def log(level: str, event: str, **kw: object) -> None:
|
|
@@ -91,6 +111,15 @@ def emit(event_type: str, metadata: dict[str, Any]) -> None:
|
|
|
91
111
|
if event_type == etype and not _capture.get(category, True):
|
|
92
112
|
metadata.pop(field, None)
|
|
93
113
|
|
|
114
|
+
# Journal mode gate — strip content fields when journal is OFF (default).
|
|
115
|
+
# Structural equivalents (prompt_length, etc.) are always kept.
|
|
116
|
+
# Journal ON = complete explicit record. Journal OFF = structural only.
|
|
117
|
+
if not _journal_mode:
|
|
118
|
+
from methodproof.config import JOURNAL_CONTENT_FIELDS
|
|
119
|
+
for etype, field in JOURNAL_CONTENT_FIELDS:
|
|
120
|
+
if event_type == etype and field in metadata:
|
|
121
|
+
metadata.pop(field, None)
|
|
122
|
+
|
|
94
123
|
entry = {
|
|
95
124
|
"id": uuid.uuid4().hex,
|
|
96
125
|
"session_id": _session_id,
|
|
@@ -439,6 +439,71 @@ def analyze_prompt(text: str) -> dict[str, Any]:
|
|
|
439
439
|
}
|
|
440
440
|
|
|
441
441
|
|
|
442
|
+
def compose_summary(meta: dict[str, Any]) -> str:
|
|
443
|
+
"""Compose a readable prompt summary from structural analysis fields.
|
|
444
|
+
|
|
445
|
+
Replaces dumb 200-char truncation with a meaningful, searchable summary.
|
|
446
|
+
Zero API calls — purely local composition from already-extracted sa_* fields.
|
|
447
|
+
|
|
448
|
+
Examples:
|
|
449
|
+
"[instruction/synthesis] refactor app/auth/middleware.py — dependency injection"
|
|
450
|
+
"[bug_report/execution] fix TypeError in utils.py:parse_config — Python, JSON"
|
|
451
|
+
"[design_question/analysis] caching strategy — Redis, PostgreSQL, 3 constraints"
|
|
452
|
+
"[correction] not that approach — references prior turn"
|
|
453
|
+
"""
|
|
454
|
+
intent = meta.get("sa_intent", "unknown")
|
|
455
|
+
cognitive = meta.get("sa_cognitive_level", "")
|
|
456
|
+
files = meta.get("sa_named_files", [])
|
|
457
|
+
functions = meta.get("sa_named_functions", [])
|
|
458
|
+
classes = meta.get("sa_named_classes", [])
|
|
459
|
+
technologies = meta.get("sa_named_technologies", [])
|
|
460
|
+
languages = meta.get("sa_code_languages", [])
|
|
461
|
+
collab = meta.get("sa_collaboration_mode", "")
|
|
462
|
+
|
|
463
|
+
# Header: [intent/cognitive]
|
|
464
|
+
header = f"[{intent}"
|
|
465
|
+
if cognitive and cognitive != "none":
|
|
466
|
+
header += f"/{cognitive}"
|
|
467
|
+
header += "]"
|
|
468
|
+
|
|
469
|
+
# Entities: files, functions, classes, technologies
|
|
470
|
+
parts: list[str] = []
|
|
471
|
+
if files:
|
|
472
|
+
parts.extend(files[:3])
|
|
473
|
+
if functions:
|
|
474
|
+
parts.extend(functions[:2])
|
|
475
|
+
if classes:
|
|
476
|
+
parts.extend(classes[:2])
|
|
477
|
+
if technologies:
|
|
478
|
+
parts.extend(technologies[:4])
|
|
479
|
+
elif languages:
|
|
480
|
+
parts.extend(languages[:2])
|
|
481
|
+
|
|
482
|
+
# Qualifiers
|
|
483
|
+
qualifiers: list[str] = []
|
|
484
|
+
if meta.get("sa_has_error_trace"):
|
|
485
|
+
qualifiers.append(f"{meta.get('sa_error_trace_lines', 0)} error lines")
|
|
486
|
+
if meta.get("sa_has_constraints"):
|
|
487
|
+
qualifiers.append("constrained")
|
|
488
|
+
if meta.get("sa_has_acceptance_criteria"):
|
|
489
|
+
qualifiers.append("with criteria")
|
|
490
|
+
if meta.get("sa_is_compound"):
|
|
491
|
+
qualifiers.append("multi-part")
|
|
492
|
+
if meta.get("sa_references_prior_turn"):
|
|
493
|
+
qualifiers.append("references prior turn")
|
|
494
|
+
if meta.get("sa_code_block_count", 0) > 0:
|
|
495
|
+
qualifiers.append(f"{meta['sa_code_block_count']} code blocks")
|
|
496
|
+
|
|
497
|
+
# Compose
|
|
498
|
+
summary = header
|
|
499
|
+
if parts:
|
|
500
|
+
summary += " " + ", ".join(dict.fromkeys(parts)) # dedupe preserving order
|
|
501
|
+
if qualifiers:
|
|
502
|
+
summary += " — " + ", ".join(qualifiers[:3])
|
|
503
|
+
|
|
504
|
+
return summary
|
|
505
|
+
|
|
506
|
+
|
|
442
507
|
def scan_environment(watch_dir: str) -> dict[str, Any]:
|
|
443
508
|
"""Scan filesystem for AI instruction files and config. No content stored."""
|
|
444
509
|
home = Path.home()
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"""MethodProof CLI. See how you code.
|
|
2
2
|
|
|
3
3
|
Usage:
|
|
4
|
-
methodproof init Install hooks, configure capture
|
|
4
|
+
methodproof init Install hooks, configure capture
|
|
5
5
|
methodproof start [--dir .] Start recording a session
|
|
6
6
|
methodproof stop Stop recording, build process graph
|
|
7
7
|
methodproof view [id] View session graph in browser
|
|
@@ -60,6 +60,69 @@ def _banner() -> str:
|
|
|
60
60
|
return f"MethodProof — {_rainbow('Full Spectrum')}"
|
|
61
61
|
|
|
62
62
|
|
|
63
|
+
def _app_url(api_url: str) -> str:
|
|
64
|
+
"""Derive dashboard URL from API URL."""
|
|
65
|
+
if "localhost" in api_url or "127.0.0.1" in api_url:
|
|
66
|
+
return "http://localhost:5173"
|
|
67
|
+
return api_url.replace("api.", "app.", 1)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _print_intro() -> None:
|
|
71
|
+
"""Show the 3-layer architecture intro with rainbow borders."""
|
|
72
|
+
if not sys.stdout.isatty():
|
|
73
|
+
_print_intro_plain()
|
|
74
|
+
return
|
|
75
|
+
|
|
76
|
+
W = "\033[1;97m"
|
|
77
|
+
G = "\033[92m"
|
|
78
|
+
C = "\033[96m"
|
|
79
|
+
Y = "\033[93m"
|
|
80
|
+
D = "\033[90m"
|
|
81
|
+
R = _RESET
|
|
82
|
+
|
|
83
|
+
bar = _rainbow("━" * 51)
|
|
84
|
+
|
|
85
|
+
print(f"\n {bar}\n")
|
|
86
|
+
print(f" {W}M E T H O D P R O O F{R}")
|
|
87
|
+
print(f" {D}See how you code. Prove how you build.{R}")
|
|
88
|
+
print(f"\n {bar}\n")
|
|
89
|
+
print(f" ┌────────────────────────────────────────────────┐")
|
|
90
|
+
print(f" │ {Y}SHARE{R} push · publish · live stream │")
|
|
91
|
+
print(f" ├────────────────────────────────────────────────┤")
|
|
92
|
+
print(f" │ {C}GRAPH{R} knowledge graph · moments · edges │")
|
|
93
|
+
print(f" ├────────────────────────────────────────────────┤")
|
|
94
|
+
print(f" │ {G}CAPTURE{R} hooks · proxy · plugin │")
|
|
95
|
+
print(f" └────────────────────────────────────────────────┘")
|
|
96
|
+
print()
|
|
97
|
+
print(f" {D}1.{R} {G}mp start{R} begin recording")
|
|
98
|
+
print(f" {D}2.{R} code normally {D}MethodProof watches silently{R}")
|
|
99
|
+
print(f" {D}3.{R} {G}mp stop{R} build your process graph")
|
|
100
|
+
print(f" {D}4.{R} {G}mp push{R} upload to your profile")
|
|
101
|
+
print()
|
|
102
|
+
print(f" {D}All data stays local until you push.{R}\n")
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def _print_intro_plain() -> None:
|
|
106
|
+
print("\n ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n")
|
|
107
|
+
print(" M E T H O D P R O O F")
|
|
108
|
+
print(" See how you code. Prove how you build.")
|
|
109
|
+
print("\n ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n")
|
|
110
|
+
print(" ┌────────────────────────────────────────────────┐")
|
|
111
|
+
print(" │ SHARE push · publish · live stream │")
|
|
112
|
+
print(" ├────────────────────────────────────────────────┤")
|
|
113
|
+
print(" │ GRAPH knowledge graph · moments · edges │")
|
|
114
|
+
print(" ├────────────────────────────────────────────────┤")
|
|
115
|
+
print(" │ CAPTURE hooks · proxy · plugin │")
|
|
116
|
+
print(" └────────────────────────────────────────────────┘")
|
|
117
|
+
print()
|
|
118
|
+
print(" 1. mp start begin recording")
|
|
119
|
+
print(" 2. code normally MethodProof watches silently")
|
|
120
|
+
print(" 3. mp stop build your process graph")
|
|
121
|
+
print(" 4. mp push upload to your profile")
|
|
122
|
+
print()
|
|
123
|
+
print(" All data stays local until you push.\n")
|
|
124
|
+
|
|
125
|
+
|
|
63
126
|
_ALIAS_MARKER = "# methodproof-alias"
|
|
64
127
|
|
|
65
128
|
|
|
@@ -75,20 +138,77 @@ def _install_alias() -> None:
|
|
|
75
138
|
f.write(alias)
|
|
76
139
|
|
|
77
140
|
|
|
141
|
+
def _print_journal_intro(credits: int) -> None:
|
|
142
|
+
"""Show journal mode introduction with remaining credits."""
|
|
143
|
+
print(" ┌─────────────────────────────────────────────────────┐")
|
|
144
|
+
print(" │ Journal Mode — full content capture │")
|
|
145
|
+
print(" └─────────────────────────────────────────────────────┘")
|
|
146
|
+
print(f" You have {credits} free journal credit{'s' if credits != 1 else ''} "
|
|
147
|
+
f"(sessions up to {config.FREE_JOURNAL_MAX_HOURS}h each).")
|
|
148
|
+
print()
|
|
149
|
+
print(" By default, MethodProof captures structural metadata only —")
|
|
150
|
+
print(" file paths, line counts, timing, tool names. Journal mode")
|
|
151
|
+
print(" preserves the full picture: prompts, AI responses, diffs,")
|
|
152
|
+
print(" and terminal output. All encrypted (AES-256-GCM).")
|
|
153
|
+
print()
|
|
154
|
+
print(" Try it: mp start --journal")
|
|
155
|
+
print(" Toggle: mp journal on / off / status")
|
|
156
|
+
print()
|
|
157
|
+
|
|
158
|
+
|
|
78
159
|
def _run_consent(cfg: dict) -> dict:
|
|
79
|
-
"""
|
|
160
|
+
"""Simplified consent flow: accept defaults or customize."""
|
|
161
|
+
print(f"\n{_banner()}\n")
|
|
162
|
+
print(" Built by engineers, for engineers.\n")
|
|
163
|
+
print(" All data stays local in ~/.methodproof/. Nothing leaves your")
|
|
164
|
+
print(" machine unless you explicitly run `mp push` or `mp publish`.\n")
|
|
165
|
+
print(" MethodProof records structural metadata about your workflow:")
|
|
166
|
+
print(" commands, file changes, git commits, AI interactions, and more.")
|
|
167
|
+
print(" No code content is stored by default.\n")
|
|
168
|
+
|
|
169
|
+
choice = input(" Enable recommended capture? [Y/n/c to customize]: ").strip().lower()
|
|
170
|
+
|
|
171
|
+
if choice in ("c", "customize"):
|
|
172
|
+
return _run_consent_detailed(cfg)
|
|
173
|
+
|
|
174
|
+
if choice in ("n", "no"):
|
|
175
|
+
# Minimal: only file changes + git commits
|
|
176
|
+
capture = {k: False for k in config.STANDARD_CATEGORIES}
|
|
177
|
+
capture["file_changes"] = True
|
|
178
|
+
capture["git_commits"] = True
|
|
179
|
+
capture["code_capture"] = False
|
|
180
|
+
cfg["capture"] = capture
|
|
181
|
+
cfg["research_consent"] = False
|
|
182
|
+
cfg["publish_redact"] = dict(config._DEFAULTS["publish_redact"])
|
|
183
|
+
cfg["consent_acknowledged"] = True
|
|
184
|
+
credits = cfg.get("journal_credits", config._DEFAULTS["journal_credits"])
|
|
185
|
+
print("\n Minimal capture enabled (file changes + git commits).\n")
|
|
186
|
+
_print_journal_intro(credits)
|
|
187
|
+
print(" Customize anytime: `methodproof consent`\n")
|
|
188
|
+
return cfg
|
|
189
|
+
|
|
190
|
+
# Default: all 10 standard on, code_capture off
|
|
191
|
+
cfg["capture"] = dict(config._DEFAULTS["capture"])
|
|
192
|
+
cfg["research_consent"] = False
|
|
193
|
+
cfg["publish_redact"] = dict(config._DEFAULTS["publish_redact"])
|
|
194
|
+
cfg["consent_acknowledged"] = True
|
|
195
|
+
credits = cfg.get("journal_credits", config._DEFAULTS["journal_credits"])
|
|
196
|
+
print(f"\n {_rainbow('Full Spectrum')} enabled — free live streaming unlocked.\n")
|
|
197
|
+
_print_journal_intro(credits)
|
|
198
|
+
print(" Customize anytime: `methodproof consent`\n")
|
|
199
|
+
return cfg
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def _run_consent_detailed(cfg: dict) -> dict:
|
|
203
|
+
"""Full interactive consent flow with three sections: capture, research, redaction."""
|
|
80
204
|
capture = cfg.get("capture", dict(config._DEFAULTS["capture"]))
|
|
81
205
|
publish_redact = cfg.get("publish_redact", dict(config._DEFAULTS["publish_redact"]))
|
|
82
206
|
std = config.STANDARD_CATEGORIES
|
|
83
207
|
|
|
84
|
-
print(f"\n{_banner()}\n")
|
|
85
|
-
print("Built by engineers, for engineers.\n")
|
|
86
|
-
print("All data stays local in ~/.methodproof/. Nothing leaves your")
|
|
87
|
-
print("machine unless you explicitly run `mp push` or `mp publish`.\n")
|
|
88
|
-
|
|
89
208
|
# --- Section 1: Capture ---
|
|
209
|
+
print()
|
|
90
210
|
print("=" * 60)
|
|
91
|
-
print("
|
|
211
|
+
print(" What gets recorded locally")
|
|
92
212
|
print("=" * 60)
|
|
93
213
|
print()
|
|
94
214
|
print(" These 10 categories are structural metadata only.")
|
|
@@ -143,7 +263,7 @@ def _run_consent(cfg: dict) -> dict:
|
|
|
143
263
|
|
|
144
264
|
# --- Section 2: Research Data ---
|
|
145
265
|
print("=" * 60)
|
|
146
|
-
print("
|
|
266
|
+
print(" Contribute to AI research (optional)")
|
|
147
267
|
print("=" * 60)
|
|
148
268
|
print()
|
|
149
269
|
|
|
@@ -171,7 +291,7 @@ def _run_consent(cfg: dict) -> dict:
|
|
|
171
291
|
|
|
172
292
|
# --- Section 3: Publish Redaction ---
|
|
173
293
|
print("=" * 60)
|
|
174
|
-
print("
|
|
294
|
+
print(" Default redactions for public sessions")
|
|
175
295
|
print("=" * 60)
|
|
176
296
|
print()
|
|
177
297
|
print(" `mp push` uploads privately to your account (nothing public).")
|
|
@@ -288,9 +408,8 @@ def cmd_init(args: argparse.Namespace) -> None:
|
|
|
288
408
|
else:
|
|
289
409
|
print("Signing key: exists")
|
|
290
410
|
|
|
291
|
-
|
|
292
|
-
print("Restart your shell, then run:
|
|
293
|
-
_print_commands()
|
|
411
|
+
_print_intro()
|
|
412
|
+
print(" Restart your shell, then run: mp start\n")
|
|
294
413
|
|
|
295
414
|
|
|
296
415
|
def _print_commands() -> None:
|
|
@@ -310,7 +429,11 @@ def _print_commands() -> None:
|
|
|
310
429
|
print(f" {_W}RECORD{R}")
|
|
311
430
|
print(f" {_G}mp start{R} Start recording a session")
|
|
312
431
|
print(f" {_G}mp stop{R} Stop recording, build process graph")
|
|
313
|
-
print(f" {_G}mp start --live{R} Stream your graph
|
|
432
|
+
print(f" {_G}mp start --live{R} Stream your graph privately (only you can view)")
|
|
433
|
+
print(f" {_G}mp start --live-public{R} Stream your graph publicly (shareable link)")
|
|
434
|
+
print(f" {_G}mp start --journal{R} Full content capture (2 free credits, then Pro)")
|
|
435
|
+
print(f" {_G}mp journal on{R} Enable persistent journal mode")
|
|
436
|
+
print(f" {_G}mp journal status{R} Check journal mode and remaining credits")
|
|
314
437
|
print()
|
|
315
438
|
print(f" {_W}REVIEW{R}")
|
|
316
439
|
print(f" {_C}mp view{R} {_D}[id]{R} View session graph in browser")
|
|
@@ -449,12 +572,65 @@ def cmd_uninstall(args: argparse.Namespace) -> None:
|
|
|
449
572
|
def cmd_consent(args: argparse.Namespace) -> None:
|
|
450
573
|
"""Review or change capture, research, and redaction settings."""
|
|
451
574
|
cfg = config.load()
|
|
452
|
-
|
|
575
|
+
print(f"\n{_banner()}\n")
|
|
576
|
+
cfg = _run_consent_detailed(cfg)
|
|
453
577
|
config.save(cfg)
|
|
454
578
|
print(f"\n{_banner()} settings saved.\n")
|
|
455
579
|
_print_commands()
|
|
456
580
|
|
|
457
581
|
|
|
582
|
+
def cmd_journal(args: argparse.Namespace) -> None:
|
|
583
|
+
"""Journal mode — full content capture."""
|
|
584
|
+
subcmd = getattr(args, "journal_cmd", None)
|
|
585
|
+
cfg = config.load()
|
|
586
|
+
credits = cfg.get("journal_credits", 0)
|
|
587
|
+
|
|
588
|
+
if subcmd == "on":
|
|
589
|
+
print("Journal Mode — Full Content Capture\n")
|
|
590
|
+
print("When enabled, MethodProof persists full content alongside structural data:")
|
|
591
|
+
print(" • Full prompt text (not just length)")
|
|
592
|
+
print(" • Full AI completion text (not just length)")
|
|
593
|
+
print(" • Full file diffs and git patches")
|
|
594
|
+
print(" • Terminal output (not just commands)")
|
|
595
|
+
print(" • Tool call parameters and results\n")
|
|
596
|
+
print("All content is encrypted (AES-256-GCM) and subject to your consent settings.\n")
|
|
597
|
+
if credits > 0:
|
|
598
|
+
print(f"You have {credits} free journal credit{'s' if credits != 1 else ''} "
|
|
599
|
+
f"(sessions up to {config.FREE_JOURNAL_MAX_HOURS}h each).")
|
|
600
|
+
print("After credits are used, journal mode requires a Pro plan.\n")
|
|
601
|
+
else:
|
|
602
|
+
print("Journal mode requires a Pro plan (or free credits if available).\n")
|
|
603
|
+
answer = input("Enable journal mode? [y/N] ").strip().lower()
|
|
604
|
+
if answer != "y":
|
|
605
|
+
print("Journal mode not enabled.")
|
|
606
|
+
return
|
|
607
|
+
cfg["journal_mode"] = True
|
|
608
|
+
config.save(cfg)
|
|
609
|
+
print("\nJournal mode ON. Full content will be captured in your next session.")
|
|
610
|
+
print("Run `methodproof start` to begin recording.\n")
|
|
611
|
+
|
|
612
|
+
elif subcmd == "off":
|
|
613
|
+
cfg["journal_mode"] = False
|
|
614
|
+
config.save(cfg)
|
|
615
|
+
print("Journal mode OFF. Only structural metadata will be captured.")
|
|
616
|
+
|
|
617
|
+
elif subcmd == "status":
|
|
618
|
+
enabled = cfg.get("journal_mode", False)
|
|
619
|
+
if enabled:
|
|
620
|
+
print("Journal mode: ON (full content capture)")
|
|
621
|
+
print(" Prompts, completions, diffs, and output are persisted and encrypted.")
|
|
622
|
+
else:
|
|
623
|
+
print("Journal mode: OFF (structural only)")
|
|
624
|
+
print(" Only metadata captured: lengths, types, timing, file paths.")
|
|
625
|
+
print(" Enable with: methodproof journal on")
|
|
626
|
+
if credits > 0:
|
|
627
|
+
print(f" Free journal credits: {credits} "
|
|
628
|
+
f"(up to {config.FREE_JOURNAL_MAX_HOURS}h per session)")
|
|
629
|
+
|
|
630
|
+
else:
|
|
631
|
+
print("Usage: methodproof journal [on|off|status]")
|
|
632
|
+
|
|
633
|
+
|
|
458
634
|
def _is_daemon_alive() -> bool:
|
|
459
635
|
"""Check if the recording daemon is still running."""
|
|
460
636
|
if not PIDFILE.exists():
|
|
@@ -511,7 +687,9 @@ def cmd_start(args: argparse.Namespace) -> None:
|
|
|
511
687
|
capture = cfg.get("capture", {})
|
|
512
688
|
|
|
513
689
|
live_url = ""
|
|
514
|
-
|
|
690
|
+
want_live = args.live or getattr(args, "live_public", False)
|
|
691
|
+
live_visibility = "public" if getattr(args, "live_public", False) else "private"
|
|
692
|
+
if want_live:
|
|
515
693
|
if not cfg.get("token"):
|
|
516
694
|
print("Live mode requires login. Run `methodproof login` first.")
|
|
517
695
|
sys.exit(1)
|
|
@@ -522,10 +700,27 @@ def cmd_start(args: argparse.Namespace) -> None:
|
|
|
522
700
|
store.mark_synced(sid, remote_id)
|
|
523
701
|
# Connect live WebSocket
|
|
524
702
|
from methodproof import live as live_mod
|
|
525
|
-
live_url = live_mod.start(cfg["api_url"], cfg["token"], remote_id, capture) or ""
|
|
703
|
+
live_url = live_mod.start(cfg["api_url"], cfg["token"], remote_id, capture, live_visibility) or ""
|
|
526
704
|
if not live_url:
|
|
527
|
-
print("Live stream rejected — requires Pro plan or
|
|
705
|
+
print("Live stream rejected — requires Pro plan or Full Spectrum.")
|
|
528
706
|
sys.exit(1)
|
|
707
|
+
if live_visibility == "private":
|
|
708
|
+
print(f"Live (private): {live_url}")
|
|
709
|
+
print(" Only you can view this stream while logged in.")
|
|
710
|
+
else:
|
|
711
|
+
print(f"Live (public): {live_url}")
|
|
712
|
+
print(" Anyone with this link can watch your session build in real time.")
|
|
713
|
+
|
|
714
|
+
# Journal mode — per-session override or persistent config
|
|
715
|
+
if getattr(args, "journal", False):
|
|
716
|
+
cfg["journal_mode"] = True
|
|
717
|
+
credits = cfg.get("journal_credits", 0)
|
|
718
|
+
if credits > 0:
|
|
719
|
+
cfg["journal_credits"] = credits - 1
|
|
720
|
+
print(f"Journal mode ON (free credit used — {credits - 1} remaining, {config.FREE_JOURNAL_MAX_HOURS}h cap).")
|
|
721
|
+
else:
|
|
722
|
+
print("Journal mode ON for this session (full content capture).")
|
|
723
|
+
config.save(cfg)
|
|
529
724
|
|
|
530
725
|
base.init(sid, live=bool(live_url))
|
|
531
726
|
|
|
@@ -708,13 +903,17 @@ def cmd_log(args: argparse.Namespace) -> None:
|
|
|
708
903
|
if not sessions:
|
|
709
904
|
print("No sessions yet.")
|
|
710
905
|
return
|
|
906
|
+
unsynced = [s for s in sessions if not s["synced"] and s.get("completed_at") and s["total_events"] > 0]
|
|
907
|
+
if unsynced:
|
|
908
|
+
print(f" [{len(unsynced)} session{'s' if len(unsynced) != 1 else ''} behind sync] — run `mp push` to upload\n")
|
|
711
909
|
for s in sessions:
|
|
712
910
|
sync_tag = "synced" if s["synced"] else "local"
|
|
911
|
+
status = _session_status(s)
|
|
713
912
|
dt = datetime.fromtimestamp(s["created_at"], tz=UTC).strftime("%Y-%m-%d %H:%M")
|
|
714
913
|
dur = _duration(s)
|
|
715
914
|
vis = s.get("visibility", "private")
|
|
716
915
|
tags = json.loads(s.get("tags") or "[]")
|
|
717
|
-
suffix = f" [{sync_tag}]"
|
|
916
|
+
suffix = f" [{sync_tag}] {status}"
|
|
718
917
|
if vis != "private":
|
|
719
918
|
suffix += f" {vis}"
|
|
720
919
|
if tags:
|
|
@@ -770,8 +969,11 @@ def cmd_push(args: argparse.Namespace) -> None:
|
|
|
770
969
|
print("No sessions to push.")
|
|
771
970
|
sys.exit(1)
|
|
772
971
|
from methodproof.sync import push
|
|
773
|
-
push(sid, cfg["token"], cfg["api_url"])
|
|
774
|
-
|
|
972
|
+
remote_id = push(sid, cfg["token"], cfg["api_url"])
|
|
973
|
+
app = _app_url(cfg["api_url"])
|
|
974
|
+
print(f"Pushed {sid[:8]} (private).")
|
|
975
|
+
print(f" View: {app}/personal/sessions/{remote_id}")
|
|
976
|
+
print(f" Publish: mp publish {sid[:8]}")
|
|
775
977
|
|
|
776
978
|
|
|
777
979
|
def cmd_tag(args: argparse.Namespace) -> None:
|
|
@@ -801,11 +1003,15 @@ def cmd_publish(args: argparse.Namespace) -> None:
|
|
|
801
1003
|
session["visibility"] = "public"
|
|
802
1004
|
if not session["synced"]:
|
|
803
1005
|
from methodproof.sync import push
|
|
804
|
-
push(session["id"], cfg["token"], cfg["api_url"])
|
|
1006
|
+
remote_id = push(session["id"], cfg["token"], cfg["api_url"])
|
|
805
1007
|
else:
|
|
806
1008
|
from methodproof.sync import sync_metadata
|
|
807
1009
|
sync_metadata(session, cfg["token"], cfg["api_url"])
|
|
1010
|
+
remote_id = session.get("remote_id", "")
|
|
1011
|
+
app = _app_url(cfg["api_url"])
|
|
808
1012
|
print(f"Published {session['id'][:8]} (public).")
|
|
1013
|
+
if remote_id:
|
|
1014
|
+
print(f" View: {app}/sessions/{remote_id}/cover")
|
|
809
1015
|
|
|
810
1016
|
|
|
811
1017
|
def _resolve_session(session_id: str) -> dict:
|
|
@@ -976,6 +1182,19 @@ def _latest() -> str | None:
|
|
|
976
1182
|
return sessions[0]["id"] if sessions else None
|
|
977
1183
|
|
|
978
1184
|
|
|
1185
|
+
def _session_status(s: dict) -> str:
|
|
1186
|
+
active = config.load().get("active_session")
|
|
1187
|
+
if s["id"] == active:
|
|
1188
|
+
return "recording"
|
|
1189
|
+
if not s.get("completed_at"):
|
|
1190
|
+
return "abandoned"
|
|
1191
|
+
if s["total_events"] == 0:
|
|
1192
|
+
return "empty"
|
|
1193
|
+
if s["synced"]:
|
|
1194
|
+
return "pushed"
|
|
1195
|
+
return "stopped"
|
|
1196
|
+
|
|
1197
|
+
|
|
979
1198
|
def _duration(s: dict) -> str:
|
|
980
1199
|
if not s.get("completed_at") or not s.get("created_at"):
|
|
981
1200
|
return "--:--"
|
|
@@ -1105,7 +1324,9 @@ def main() -> None:
|
|
|
1105
1324
|
s.add_argument("--repo", help="Git remote URL (overrides auto-detect)")
|
|
1106
1325
|
s.add_argument("--public", action="store_true", help="Set visibility to public")
|
|
1107
1326
|
s.add_argument("--tags", help="Comma-separated tags")
|
|
1108
|
-
s.add_argument("--live", action="store_true", help="Stream
|
|
1327
|
+
s.add_argument("--live", action="store_true", help="Stream graph live to your private profile")
|
|
1328
|
+
s.add_argument("--live-public", action="store_true", help="Stream graph live — visible to anyone with the link")
|
|
1329
|
+
s.add_argument("--journal", action="store_true", help="Journal mode — full content capture (2 free credits, then Pro)")
|
|
1109
1330
|
sub.add_parser("stop", help="Stop recording")
|
|
1110
1331
|
v = sub.add_parser("view", help="Inspect captured session data")
|
|
1111
1332
|
v.add_argument("session_id", nargs="?")
|
|
@@ -1133,8 +1354,20 @@ def main() -> None:
|
|
|
1133
1354
|
ext_sub.add_parser("pair", help="Pair extension to active session")
|
|
1134
1355
|
ext_sub.add_parser("status", help="Check extension connection")
|
|
1135
1356
|
ext_sub.add_parser("install", help="Open Chrome Web Store listing")
|
|
1357
|
+
jr = sub.add_parser("journal", help="Journal mode — full content capture (2 free credits, then Pro)")
|
|
1358
|
+
jr_sub = jr.add_subparsers(dest="journal_cmd")
|
|
1359
|
+
jr_sub.add_parser("on", help="Enable journal mode (persists full content)")
|
|
1360
|
+
jr_sub.add_parser("off", help="Disable journal mode (structural only)")
|
|
1361
|
+
jr_sub.add_parser("status", help="Show journal mode status")
|
|
1362
|
+
sub.add_parser("intro", help="Show the MethodProof intro")
|
|
1136
1363
|
sub.add_parser("help", help="Show command reference")
|
|
1137
1364
|
sub.add_parser("mcp-serve", help="Run MCP server (used by Claude Code)")
|
|
1365
|
+
px = sub.add_parser("proxy", help="Local AI API proxy (deep capture)")
|
|
1366
|
+
px_sub = px.add_subparsers(dest="proxy_cmd")
|
|
1367
|
+
px_sub.add_parser("start", help="Start proxy (requires consent)")
|
|
1368
|
+
px_sub.add_parser("stop", help="Stop proxy")
|
|
1369
|
+
px_sub.add_parser("status", help="Show proxy status")
|
|
1370
|
+
px_sub.add_parser("cert", help="CA certificate install instructions")
|
|
1138
1371
|
|
|
1139
1372
|
args = p.parse_args()
|
|
1140
1373
|
cmds = {
|
|
@@ -1144,11 +1377,15 @@ def main() -> None:
|
|
|
1144
1377
|
"delete": cmd_delete, "review": cmd_review, "consent": cmd_consent,
|
|
1145
1378
|
"update": cmd_update, "uninstall": cmd_uninstall,
|
|
1146
1379
|
"extension": cmd_extension,
|
|
1380
|
+
"journal": cmd_journal,
|
|
1381
|
+
"intro": lambda _: _print_intro(),
|
|
1147
1382
|
"help": lambda _: _print_commands(),
|
|
1148
1383
|
"mcp-serve": cmd_mcp_serve,
|
|
1384
|
+
"proxy": lambda a: __import__("methodproof.proxy", fromlist=["cmd_proxy"]).cmd_proxy(a),
|
|
1149
1385
|
}
|
|
1150
1386
|
fn = cmds.get(args.cmd)
|
|
1151
1387
|
if not fn:
|
|
1388
|
+
_print_intro()
|
|
1152
1389
|
_print_commands()
|
|
1153
1390
|
sys.exit(1)
|
|
1154
1391
|
|
|
@@ -33,6 +33,8 @@ _DEFAULTS: dict[str, Any] = {
|
|
|
33
33
|
"code_capture": False,
|
|
34
34
|
},
|
|
35
35
|
"research_consent": False,
|
|
36
|
+
"journal_mode": False,
|
|
37
|
+
"journal_credits": 2,
|
|
36
38
|
"publish_redact": {
|
|
37
39
|
"command_output": True,
|
|
38
40
|
"ai_prompts": True,
|
|
@@ -41,6 +43,8 @@ _DEFAULTS: dict[str, Any] = {
|
|
|
41
43
|
},
|
|
42
44
|
}
|
|
43
45
|
|
|
46
|
+
FREE_JOURNAL_MAX_HOURS = 4
|
|
47
|
+
|
|
44
48
|
# The 10 standard categories (excludes code_capture)
|
|
45
49
|
STANDARD_CATEGORIES = [
|
|
46
50
|
"terminal_commands", "command_output", "test_results", "file_changes",
|
|
@@ -63,6 +67,44 @@ CAPTURE_DESCRIPTIONS: dict[str, str] = {
|
|
|
63
67
|
"code_capture": "Full file diffs and git patches (Pro only, encrypted, private by default)",
|
|
64
68
|
}
|
|
65
69
|
|
|
70
|
+
# Content fields that Journal Mode unlocks. When journal_mode is OFF (default),
|
|
71
|
+
# these fields are stripped — only structural equivalents remain (lengths, counts, types).
|
|
72
|
+
# When journal_mode is ON (Pro+), EVERYTHING is persisted and encrypted.
|
|
73
|
+
# Journal = the complete, explicit record of the session.
|
|
74
|
+
JOURNAL_CONTENT_FIELDS: list[tuple[str, str]] = [
|
|
75
|
+
# AI prompts — full prompt text
|
|
76
|
+
("llm_prompt", "prompt_text"),
|
|
77
|
+
("agent_prompt", "prompt_preview"),
|
|
78
|
+
("user_prompt", "prompt_preview"),
|
|
79
|
+
# AI responses — full completion text
|
|
80
|
+
("llm_completion", "response_text"),
|
|
81
|
+
("agent_completion", "response_preview"),
|
|
82
|
+
("agent_tool_dispatch", "tool_input_preview"),
|
|
83
|
+
("agent_tool_result", "result_preview"),
|
|
84
|
+
("agent_skill_invoke", "skill_input_preview"),
|
|
85
|
+
# Terminal — full command output
|
|
86
|
+
("terminal_cmd", "output_snippet"),
|
|
87
|
+
("terminal_cmd", "command"),
|
|
88
|
+
# Code — full diffs and commit messages
|
|
89
|
+
("file_edit", "diff"),
|
|
90
|
+
("git_commit", "diff"),
|
|
91
|
+
("git_commit", "message"),
|
|
92
|
+
# Web — full search queries, URLs, page titles
|
|
93
|
+
("web_search", "query"),
|
|
94
|
+
("web_search", "clicked_results"),
|
|
95
|
+
("web_visit", "url"),
|
|
96
|
+
("web_visit", "title"),
|
|
97
|
+
# Browser — full search queries, URLs, copy content, AI chat input
|
|
98
|
+
("browser_search", "query"),
|
|
99
|
+
("browser_visit", "url"),
|
|
100
|
+
("browser_visit", "title"),
|
|
101
|
+
("browser_copy", "text_snippet"),
|
|
102
|
+
("browser_ai_chat", "detected_input"),
|
|
103
|
+
("browser_ai_chat", "url"),
|
|
104
|
+
# Inline completions — provider detail
|
|
105
|
+
("inline_completion_accepted", "text_length"),
|
|
106
|
+
]
|
|
107
|
+
|
|
66
108
|
|
|
67
109
|
def ensure_dirs() -> None:
|
|
68
110
|
DIR.mkdir(exist_ok=True)
|