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.
Files changed (56) hide show
  1. {methodproof-0.3.3 → methodproof-0.4.0}/CHANGELOG.md +6 -0
  2. {methodproof-0.3.3 → methodproof-0.4.0}/PKG-INFO +3 -1
  3. {methodproof-0.3.3 → methodproof-0.4.0}/methodproof/__init__.py +1 -1
  4. {methodproof-0.3.3 → methodproof-0.4.0}/methodproof/agents/base.py +30 -1
  5. {methodproof-0.3.3 → methodproof-0.4.0}/methodproof/analysis.py +65 -0
  6. {methodproof-0.3.3 → methodproof-0.4.0}/methodproof/cli.py +260 -23
  7. {methodproof-0.3.3 → methodproof-0.4.0}/methodproof/config.py +42 -0
  8. {methodproof-0.3.3 → methodproof-0.4.0}/methodproof/hooks/claude_code.py +10 -6
  9. methodproof-0.4.0/methodproof/hooks/cline_hook.sh +47 -0
  10. methodproof-0.4.0/methodproof/hooks/codex_hook.sh +62 -0
  11. methodproof-0.4.0/methodproof/hooks/gemini_hook.sh +58 -0
  12. methodproof-0.4.0/methodproof/hooks/install.py +246 -0
  13. methodproof-0.4.0/methodproof/hooks/kiro_hook.sh +70 -0
  14. methodproof-0.4.0/methodproof/hooks/mcp_register.py +92 -0
  15. methodproof-0.4.0/methodproof/hooks/opencode_plugin.js +33 -0
  16. {methodproof-0.3.3 → methodproof-0.4.0}/methodproof/hooks/wrappers.py +20 -0
  17. {methodproof-0.3.3 → methodproof-0.4.0}/methodproof/live.py +52 -3
  18. methodproof-0.4.0/methodproof/proxy.py +162 -0
  19. methodproof-0.4.0/methodproof/proxy_daemon.py +161 -0
  20. {methodproof-0.3.3 → methodproof-0.4.0}/methodproof/sync.py +3 -2
  21. {methodproof-0.3.3 → methodproof-0.4.0}/pyproject.toml +2 -1
  22. methodproof-0.3.3/methodproof/hooks/install.py +0 -87
  23. {methodproof-0.3.3 → methodproof-0.4.0}/.github/workflows/ci.yml +0 -0
  24. {methodproof-0.3.3 → methodproof-0.4.0}/.gitignore +0 -0
  25. {methodproof-0.3.3 → methodproof-0.4.0}/LICENSE +0 -0
  26. {methodproof-0.3.3 → methodproof-0.4.0}/README.md +0 -0
  27. {methodproof-0.3.3 → methodproof-0.4.0}/methodproof/__main__.py +0 -0
  28. {methodproof-0.3.3 → methodproof-0.4.0}/methodproof/agents/__init__.py +0 -0
  29. {methodproof-0.3.3 → methodproof-0.4.0}/methodproof/agents/music.py +0 -0
  30. {methodproof-0.3.3 → methodproof-0.4.0}/methodproof/agents/terminal.py +0 -0
  31. {methodproof-0.3.3 → methodproof-0.4.0}/methodproof/agents/watcher.py +0 -0
  32. {methodproof-0.3.3 → methodproof-0.4.0}/methodproof/bridge.py +0 -0
  33. {methodproof-0.3.3 → methodproof-0.4.0}/methodproof/crypto.py +0 -0
  34. {methodproof-0.3.3 → methodproof-0.4.0}/methodproof/graph.py +0 -0
  35. {methodproof-0.3.3 → methodproof-0.4.0}/methodproof/hook.py +0 -0
  36. {methodproof-0.3.3 → methodproof-0.4.0}/methodproof/hooks/__init__.py +0 -0
  37. {methodproof-0.3.3 → methodproof-0.4.0}/methodproof/hooks/claude_code.sh +0 -0
  38. {methodproof-0.3.3 → methodproof-0.4.0}/methodproof/hooks/openclaw/HOOK.md +0 -0
  39. {methodproof-0.3.3 → methodproof-0.4.0}/methodproof/hooks/openclaw/handler.ts +0 -0
  40. {methodproof-0.3.3 → methodproof-0.4.0}/methodproof/hooks/openclaw_install.py +0 -0
  41. {methodproof-0.3.3 → methodproof-0.4.0}/methodproof/integrity.py +0 -0
  42. {methodproof-0.3.3 → methodproof-0.4.0}/methodproof/mcp.py +0 -0
  43. {methodproof-0.3.3 → methodproof-0.4.0}/methodproof/repos.py +0 -0
  44. {methodproof-0.3.3 → methodproof-0.4.0}/methodproof/skills/methodproof/SKILL.md +0 -0
  45. {methodproof-0.3.3 → methodproof-0.4.0}/methodproof/store.py +0 -0
  46. {methodproof-0.3.3 → methodproof-0.4.0}/methodproof/viewer.py +0 -0
  47. {methodproof-0.3.3 → methodproof-0.4.0}/test_windows_compat.py +0 -0
  48. {methodproof-0.3.3 → methodproof-0.4.0}/tests/__init__.py +0 -0
  49. {methodproof-0.3.3 → methodproof-0.4.0}/tests/test_analysis.py +0 -0
  50. {methodproof-0.3.3 → methodproof-0.4.0}/tests/test_graph.py +0 -0
  51. {methodproof-0.3.3 → methodproof-0.4.0}/tests/test_hooks.py +0 -0
  52. {methodproof-0.3.3 → methodproof-0.4.0}/tests/test_live.py +0 -0
  53. {methodproof-0.3.3 → methodproof-0.4.0}/tests/test_openclaw_hooks.py +0 -0
  54. {methodproof-0.3.3 → methodproof-0.4.0}/tests/test_store.py +0 -0
  55. {methodproof-0.3.3 → methodproof-0.4.0}/tests/test_wrappers.py +0 -0
  56. {methodproof-0.3.3 → methodproof-0.4.0}/uv.lock +0 -0
@@ -1,5 +1,11 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.3.4] — 2026-04-05
4
+
5
+ ### Added
6
+ - `mp log` shows session status: recording, stopped, pushed, empty, abandoned
7
+ - `mp log` prints sync reminder when unsynced sessions exist
8
+
3
9
  ## [0.3.3] — 2026-04-05
4
10
 
5
11
  ### Fixed
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: methodproof
3
- Version: 0.3.3
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
@@ -1,3 +1,3 @@
1
1
  """MethodProof — see how you code."""
2
2
 
3
- __version__ = "0.1.0"
3
+ __version__ = "0.4.0"
@@ -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 + research + redaction
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
- """Interactive consent flow with three sections: capture, research, redaction."""
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(" SECTION 1: What gets recorded locally")
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(" SECTION 2: Contribute to AI research (optional)")
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(" SECTION 3: Default redactions for public sessions")
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
- print(f"\n{_banner()}")
292
- print("Restart your shell, then run: methodproof start\n")
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 in real time")
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
- cfg = _run_consent(cfg)
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
- if args.live:
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 full-spectrum consent.")
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
- print(f"Pushed {sid[:8]} (private). Use `mp publish` to make public.")
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 events live to platform")
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)