methodproof 0.7.27__tar.gz → 0.7.29__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 (86) hide show
  1. {methodproof-0.7.27 → methodproof-0.7.29}/CHANGELOG.md +17 -0
  2. {methodproof-0.7.27 → methodproof-0.7.29}/PKG-INFO +1 -1
  3. {methodproof-0.7.27 → methodproof-0.7.29}/methodproof/agents/terminal.py +18 -16
  4. {methodproof-0.7.27 → methodproof-0.7.29}/methodproof/agents/watcher.py +23 -6
  5. {methodproof-0.7.27 → methodproof-0.7.29}/methodproof/cli.py +61 -25
  6. {methodproof-0.7.27 → methodproof-0.7.29}/methodproof/config.py +3 -0
  7. {methodproof-0.7.27 → methodproof-0.7.29}/methodproof/hook.py +8 -5
  8. {methodproof-0.7.27 → methodproof-0.7.29}/pyproject.toml +1 -1
  9. {methodproof-0.7.27 → methodproof-0.7.29}/tests/test_cli_start.py +3 -2
  10. {methodproof-0.7.27 → methodproof-0.7.29}/.github/workflows/ci.yml +0 -0
  11. {methodproof-0.7.27 → methodproof-0.7.29}/.gitignore +0 -0
  12. {methodproof-0.7.27 → methodproof-0.7.29}/LICENSE +0 -0
  13. {methodproof-0.7.27 → methodproof-0.7.29}/README.md +0 -0
  14. {methodproof-0.7.27 → methodproof-0.7.29}/methodproof/__init__.py +0 -0
  15. {methodproof-0.7.27 → methodproof-0.7.29}/methodproof/__main__.py +0 -0
  16. {methodproof-0.7.27 → methodproof-0.7.29}/methodproof/_daemon.py +0 -0
  17. {methodproof-0.7.27 → methodproof-0.7.29}/methodproof/agents/__init__.py +0 -0
  18. {methodproof-0.7.27 → methodproof-0.7.29}/methodproof/agents/base.py +0 -0
  19. {methodproof-0.7.27 → methodproof-0.7.29}/methodproof/agents/music.py +0 -0
  20. {methodproof-0.7.27 → methodproof-0.7.29}/methodproof/analysis.py +0 -0
  21. {methodproof-0.7.27 → methodproof-0.7.29}/methodproof/binding.py +0 -0
  22. {methodproof-0.7.27 → methodproof-0.7.29}/methodproof/bip39.py +0 -0
  23. {methodproof-0.7.27 → methodproof-0.7.29}/methodproof/bridge.py +0 -0
  24. {methodproof-0.7.27 → methodproof-0.7.29}/methodproof/crypto.py +0 -0
  25. {methodproof-0.7.27 → methodproof-0.7.29}/methodproof/e2e.py +0 -0
  26. {methodproof-0.7.27 → methodproof-0.7.29}/methodproof/graph.py +0 -0
  27. {methodproof-0.7.27 → methodproof-0.7.29}/methodproof/hooks/__init__.py +0 -0
  28. {methodproof-0.7.27 → methodproof-0.7.29}/methodproof/hooks/claude_code.py +0 -0
  29. {methodproof-0.7.27 → methodproof-0.7.29}/methodproof/hooks/claude_code.sh +0 -0
  30. {methodproof-0.7.27 → methodproof-0.7.29}/methodproof/hooks/cline_hook.sh +0 -0
  31. {methodproof-0.7.27 → methodproof-0.7.29}/methodproof/hooks/codex_hook.sh +0 -0
  32. {methodproof-0.7.27 → methodproof-0.7.29}/methodproof/hooks/gemini_hook.sh +0 -0
  33. {methodproof-0.7.27 → methodproof-0.7.29}/methodproof/hooks/install.py +0 -0
  34. {methodproof-0.7.27 → methodproof-0.7.29}/methodproof/hooks/kiro_hook.sh +0 -0
  35. {methodproof-0.7.27 → methodproof-0.7.29}/methodproof/hooks/mcp_register.py +0 -0
  36. {methodproof-0.7.27 → methodproof-0.7.29}/methodproof/hooks/openclaw/HOOK.md +0 -0
  37. {methodproof-0.7.27 → methodproof-0.7.29}/methodproof/hooks/openclaw/handler.ts +0 -0
  38. {methodproof-0.7.27 → methodproof-0.7.29}/methodproof/hooks/openclaw_install.py +0 -0
  39. {methodproof-0.7.27 → methodproof-0.7.29}/methodproof/hooks/opencode_plugin.js +0 -0
  40. {methodproof-0.7.27 → methodproof-0.7.29}/methodproof/hooks/wrappers.py +0 -0
  41. {methodproof-0.7.27 → methodproof-0.7.29}/methodproof/integrity.py +0 -0
  42. {methodproof-0.7.27 → methodproof-0.7.29}/methodproof/kdf.py +0 -0
  43. {methodproof-0.7.27 → methodproof-0.7.29}/methodproof/keychain.py +0 -0
  44. {methodproof-0.7.27 → methodproof-0.7.29}/methodproof/live.py +0 -0
  45. {methodproof-0.7.27 → methodproof-0.7.29}/methodproof/lock.py +0 -0
  46. {methodproof-0.7.27 → methodproof-0.7.29}/methodproof/mcp.py +0 -0
  47. {methodproof-0.7.27 → methodproof-0.7.29}/methodproof/migrate_db.py +0 -0
  48. {methodproof-0.7.27 → methodproof-0.7.29}/methodproof/proxy.py +0 -0
  49. {methodproof-0.7.27 → methodproof-0.7.29}/methodproof/proxy_daemon.py +0 -0
  50. {methodproof-0.7.27 → methodproof-0.7.29}/methodproof/repos.py +0 -0
  51. {methodproof-0.7.27 → methodproof-0.7.29}/methodproof/skills/methodproof/SKILL.md +0 -0
  52. {methodproof-0.7.27 → methodproof-0.7.29}/methodproof/store.py +0 -0
  53. {methodproof-0.7.27 → methodproof-0.7.29}/methodproof/sync.py +0 -0
  54. {methodproof-0.7.27 → methodproof-0.7.29}/methodproof/tui/__init__.py +0 -0
  55. {methodproof-0.7.27 → methodproof-0.7.29}/methodproof/tui/consent.py +0 -0
  56. {methodproof-0.7.27 → methodproof-0.7.29}/methodproof/tui/init.py +0 -0
  57. {methodproof-0.7.27 → methodproof-0.7.29}/methodproof/tui/log.py +0 -0
  58. {methodproof-0.7.27 → methodproof-0.7.29}/methodproof/tui/login_success.py +0 -0
  59. {methodproof-0.7.27 → methodproof-0.7.29}/methodproof/tui/review.py +0 -0
  60. {methodproof-0.7.27 → methodproof-0.7.29}/methodproof/tui/start.py +0 -0
  61. {methodproof-0.7.27 → methodproof-0.7.29}/methodproof/tui/status.py +0 -0
  62. {methodproof-0.7.27 → methodproof-0.7.29}/methodproof/tui/theme.py +0 -0
  63. {methodproof-0.7.27 → methodproof-0.7.29}/methodproof/viewer.py +0 -0
  64. {methodproof-0.7.27 → methodproof-0.7.29}/methodproof/wordlist.py +0 -0
  65. {methodproof-0.7.27 → methodproof-0.7.29}/test_windows_compat.py +0 -0
  66. {methodproof-0.7.27 → methodproof-0.7.29}/tests/__init__.py +0 -0
  67. {methodproof-0.7.27 → methodproof-0.7.29}/tests/conftest.py +0 -0
  68. {methodproof-0.7.27 → methodproof-0.7.29}/tests/test_analysis.py +0 -0
  69. {methodproof-0.7.27 → methodproof-0.7.29}/tests/test_cli_auth.py +0 -0
  70. {methodproof-0.7.27 → methodproof-0.7.29}/tests/test_cli_config.py +0 -0
  71. {methodproof-0.7.27 → methodproof-0.7.29}/tests/test_cli_helpers.py +0 -0
  72. {methodproof-0.7.27 → methodproof-0.7.29}/tests/test_cli_session.py +0 -0
  73. {methodproof-0.7.27 → methodproof-0.7.29}/tests/test_cli_share.py +0 -0
  74. {methodproof-0.7.27 → methodproof-0.7.29}/tests/test_cli_update.py +0 -0
  75. {methodproof-0.7.27 → methodproof-0.7.29}/tests/test_e2e_integration.py +0 -0
  76. {methodproof-0.7.27 → methodproof-0.7.29}/tests/test_graph.py +0 -0
  77. {methodproof-0.7.27 → methodproof-0.7.29}/tests/test_hooks.py +0 -0
  78. {methodproof-0.7.27 → methodproof-0.7.29}/tests/test_live.py +0 -0
  79. {methodproof-0.7.27 → methodproof-0.7.29}/tests/test_openclaw_hooks.py +0 -0
  80. {methodproof-0.7.27 → methodproof-0.7.29}/tests/test_profiles.py +0 -0
  81. {methodproof-0.7.27 → methodproof-0.7.29}/tests/test_security.py +0 -0
  82. {methodproof-0.7.27 → methodproof-0.7.29}/tests/test_store.py +0 -0
  83. {methodproof-0.7.27 → methodproof-0.7.29}/tests/test_sync.py +0 -0
  84. {methodproof-0.7.27 → methodproof-0.7.29}/tests/test_viewer.py +0 -0
  85. {methodproof-0.7.27 → methodproof-0.7.29}/tests/test_wrappers.py +0 -0
  86. {methodproof-0.7.27 → methodproof-0.7.29}/uv.lock +0 -0
@@ -1,5 +1,22 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.7.29] — 2026-04-12
4
+
5
+ ### Changed
6
+ - **Journal mode output is now tier-aware** — Pro/Team/Admin/Superadmin users see "Unlimited journal entries (Pro plan)" everywhere journal credits are displayed (`mp journal on`, `mp journal status`, `mp start --journal`, session mode line, consent flow). Basic users get a live credit count fetched from `GET /auth/me` (falls back to local cache if offline). Free users see their local credit count as before. Credit deduction on `--journal` is skipped for unlimited tiers.
7
+
8
+ ## [0.7.28] — 2026-04-12
9
+
10
+ ### Added
11
+ - **`cwd` in terminal events** — shell hook (bash/zsh/PowerShell) now writes working directory to `commands.jsonl`; terminal agent reads it into `terminal_cmd` metadata. Requires re-running `mp init` to update the installed hook.
12
+ - **Git commit enrichment** — `git_commit` events now include `author`, `author_email`, `committed_at` (ISO 8601), `parent_hash`, and `body` (full commit message body when it differs from subject). Single `git log` call instead of multiple.
13
+ - **`language` on `file_delete`** — was always empty string; now extracted from file extension same as `file_create`.
14
+ - **`skipped` count in `test_run`** — pytest, jest, and go test output now parsed for skipped test count.
15
+
16
+ ### Fixed
17
+ - **`agent_complete.last_message_preview` not journal-gated** — preview of agent's final message leaked content when journal mode was off. Now stripped with other content fields.
18
+ - **`git_commit.body` not journal-gated** — full commit body (multi-line messages) added to journal gate.
19
+
3
20
  ## [0.7.27] — 2026-04-12
4
21
 
5
22
  ### Fixed
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: methodproof
3
- Version: 0.7.27
3
+ Version: 0.7.29
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
@@ -22,8 +22,8 @@ TEST_FRAMEWORKS = {
22
22
  "cargo_test": re.compile(r"\bcargo test\b"),
23
23
  }
24
24
 
25
- _PYTEST_RE = re.compile(r"(\d+) passed(?:.*?(\d+) failed)?")
26
- _JEST_RE = re.compile(r"Tests:\s+(?:(\d+) failed,\s+)?(\d+) passed")
25
+ _PYTEST_RE = re.compile(r"(\d+) passed(?:.*?(\d+) failed)?(?:.*?(\d+) skipped)?")
26
+ _JEST_RE = re.compile(r"Tests:\s+(?:(\d+) failed,\s+)?(\d+) passed(?:,\s+(\d+) skipped)?")
27
27
  _CARGO_RE = re.compile(r"(\d+) passed.*?(\d+) failed")
28
28
 
29
29
 
@@ -34,26 +34,25 @@ def _detect_test(command: str) -> str | None:
34
34
  return None
35
35
 
36
36
 
37
- def _parse_test_results(output: str, framework: str, exit_code: int) -> tuple[int, int]:
38
- """Extract pass/fail counts from test output. Falls back to exit code heuristic."""
37
+ def _parse_test_results(output: str, framework: str, exit_code: int) -> tuple[int, int, int]:
38
+ """Extract pass/fail/skipped counts from test output. Falls back to exit code heuristic."""
39
39
  if framework == "pytest":
40
40
  m = _PYTEST_RE.search(output)
41
41
  if m:
42
- return int(m.group(1)), int(m.group(2) or 0)
42
+ return int(m.group(1)), int(m.group(2) or 0), int(m.group(3) or 0)
43
43
  elif framework == "jest":
44
44
  m = _JEST_RE.search(output)
45
45
  if m:
46
- return int(m.group(2) or 0), int(m.group(1) or 0)
46
+ return int(m.group(2) or 0), int(m.group(1) or 0), int(m.group(3) or 0)
47
47
  elif framework == "go_test":
48
- return output.count("--- PASS"), output.count("--- FAIL")
48
+ return output.count("--- PASS"), output.count("--- FAIL"), output.count("--- SKIP")
49
49
  elif framework == "cargo_test":
50
50
  m = _CARGO_RE.search(output)
51
51
  if m:
52
- return int(m.group(1)), int(m.group(2))
53
- # Fallback: exit code 0 = all passed, non-zero = at least 1 failure
52
+ return int(m.group(1)), int(m.group(2)), 0
54
53
  if exit_code == 0:
55
- return 1, 0
56
- return 0, 1
54
+ return 1, 0, 0
55
+ return 0, 1, 0
57
56
 
58
57
 
59
58
  def start(stop: threading.Event) -> None:
@@ -93,19 +92,22 @@ def _process(line: str) -> None:
93
92
  exit_code = entry.get("exit_code", 0)
94
93
  duration = entry.get("duration_ms", 0)
95
94
  output = entry.get("output", "")[:500]
96
- # Redact output if it contains secrets
97
95
  if SENSITIVE.search(output):
98
96
  output = "[redacted — contains sensitive content]"
97
+ cwd = entry.get("cwd", "")
99
98
 
100
- base.emit("terminal_cmd", {
99
+ meta: dict = {
101
100
  "command": command, "exit_code": exit_code,
102
101
  "output_snippet": output, "duration_ms": duration,
103
- })
102
+ }
103
+ if cwd:
104
+ meta["cwd"] = cwd
105
+ base.emit("terminal_cmd", meta)
104
106
 
105
107
  framework = _detect_test(command)
106
108
  if framework:
107
- passed, failed = _parse_test_results(output, framework, exit_code)
109
+ passed, failed, skipped = _parse_test_results(output, framework, exit_code)
108
110
  base.emit("test_run", {
109
111
  "framework": framework, "passed": passed, "failed": failed,
110
- "duration_ms": duration,
112
+ "skipped": skipped, "duration_ms": duration,
111
113
  })
@@ -139,7 +139,8 @@ class _Handler(FileSystemEventHandler):
139
139
  return
140
140
  path = self._relpath(event.src_path)
141
141
  self._hashes.pop(path, None)
142
- base.emit("file_delete", {"path": path})
142
+ lang = Path(event.src_path).suffix.lstrip(".")
143
+ base.emit("file_delete", {"path": path, "language": lang})
143
144
 
144
145
 
145
146
  def _poll_git(watch_dir: str, stop: threading.Event) -> None:
@@ -164,17 +165,33 @@ def _poll_git(watch_dir: str, stop: threading.Event) -> None:
164
165
 
165
166
  def _log_commit(watch_dir: str, sha: str) -> None:
166
167
  try:
167
- msg = subprocess.run(
168
- ["git", "-C", watch_dir, "log", "-1", "--format=%s", sha],
168
+ # Single git call: subject\x00author\x00email\x00iso_date\x00parent_hash\x00full_body
169
+ fmt = subprocess.run(
170
+ ["git", "-C", watch_dir, "log", "-1",
171
+ "--format=%s%x00%an%x00%ae%x00%ai%x00%P%x00%B", sha],
169
172
  capture_output=True, text=True, timeout=5,
170
- ).stdout.strip()
173
+ ).stdout
174
+ parts = fmt.split("\x00", 5)
175
+ subject = parts[0].strip() if len(parts) > 0 else ""
176
+ author = parts[1].strip() if len(parts) > 1 else ""
177
+ author_email = parts[2].strip() if len(parts) > 2 else ""
178
+ committed_at = parts[3].strip() if len(parts) > 3 else ""
179
+ parent_hash = parts[4].strip()[:7] if len(parts) > 4 else ""
180
+ body = parts[5].strip() if len(parts) > 5 else ""
171
181
  files = subprocess.run(
172
182
  ["git", "-C", watch_dir, "diff-tree", "--no-commit-id", "-r", "--name-only", sha],
173
183
  capture_output=True, text=True, timeout=5,
174
184
  ).stdout.strip().splitlines()
175
185
  except Exception:
176
- msg, files = "", []
177
- meta: dict[str, object] = {"hash": sha[:7], "message": msg, "files_changed": files}
186
+ subject, author, author_email, committed_at, parent_hash, body, files = "", "", "", "", "", "", []
187
+ meta: dict[str, object] = {
188
+ "hash": sha[:7], "message": subject, "files_changed": files,
189
+ "author": author, "author_email": author_email, "committed_at": committed_at,
190
+ }
191
+ if parent_hash:
192
+ meta["parent_hash"] = parent_hash
193
+ if body and body != subject:
194
+ meta["body"] = body[:2000]
178
195
  diff = _git_show_diff(watch_dir, sha)
179
196
  if diff:
180
197
  meta["diff"] = diff
@@ -156,13 +156,50 @@ def _install_alias() -> None:
156
156
  f.write(alias)
157
157
 
158
158
 
159
- def _print_journal_intro(credits: int) -> None:
160
- """Show journal mode introduction with remaining credits."""
159
+ _PRO_TIERS = {"pro", "team", "admin", "superadmin"}
160
+
161
+
162
+ def _journal_entitlement(cfg: dict) -> str | int:
163
+ """Return 'unlimited' for Pro+ users, or an int credit count for others.
164
+
165
+ For Basic users, fetches live credit count from the platform; falls back
166
+ to the locally cached value if the request fails (e.g. offline).
167
+ """
168
+ token = cfg.get("token", "")
169
+ claims = _decode_jwt_claims(token) if token else {}
170
+ account_type = claims.get("account_type", "free").lower()
171
+
172
+ if account_type in _PRO_TIERS:
173
+ return "unlimited"
174
+
175
+ if account_type == "basic":
176
+ try:
177
+ profile = _request("GET", "/auth/me", cfg.get("api_url", config.LOCAL_API_URL), token)
178
+ server_credits = profile.get("journal_credits")
179
+ if server_credits is not None:
180
+ cfg["journal_credits"] = int(server_credits)
181
+ config.save(cfg)
182
+ except Exception:
183
+ pass # offline — use local cache
184
+
185
+ return int(cfg.get("journal_credits", 0))
186
+
187
+
188
+ def _journal_credits_line(entitlement: str | int) -> str:
189
+ if entitlement == "unlimited":
190
+ return "Unlimited journal entries (Pro plan)"
191
+ n = int(entitlement)
192
+ label = f"{n} free journal credit{'s' if n != 1 else ''}"
193
+ return f"{label} (up to {config.FREE_JOURNAL_MAX_HOURS}h per session)"
194
+
195
+
196
+ def _print_journal_intro(cfg: dict) -> None:
197
+ """Show journal mode introduction with tier-aware credit status."""
198
+ entitlement = _journal_entitlement(cfg)
161
199
  print(" ┌─────────────────────────────────────────────────────┐")
162
200
  print(" │ Journal Mode — full content capture │")
163
201
  print(" └─────────────────────────────────────────────────────┘")
164
- print(f" You have {credits} free journal credit{'s' if credits != 1 else ''} "
165
- f"(sessions up to {config.FREE_JOURNAL_MAX_HOURS}h each).")
202
+ print(f" {_journal_credits_line(entitlement)}")
166
203
  print()
167
204
  print(" By default, MethodProof captures structural metadata only —")
168
205
  print(" file paths, line counts, timing, tool names. Journal mode")
@@ -199,9 +236,8 @@ def _run_consent(cfg: dict) -> dict:
199
236
  cfg["research_consent"] = False
200
237
  cfg["publish_redact"] = dict(config._DEFAULTS["publish_redact"])
201
238
  cfg["consent_acknowledged"] = True
202
- credits = cfg.get("journal_credits", config._DEFAULTS["journal_credits"])
203
239
  print("\n Minimal capture enabled (file changes + git commits).\n")
204
- _print_journal_intro(credits)
240
+ _print_journal_intro(cfg)
205
241
  print(" Customize anytime: `methodproof consent`\n")
206
242
  return cfg
207
243
 
@@ -210,9 +246,8 @@ def _run_consent(cfg: dict) -> dict:
210
246
  cfg["research_consent"] = False
211
247
  cfg["publish_redact"] = dict(config._DEFAULTS["publish_redact"])
212
248
  cfg["consent_acknowledged"] = True
213
- credits = cfg.get("journal_credits", config._DEFAULTS["journal_credits"])
214
249
  print(f"\n {_rainbow('Full Spectrum')} enabled — free live streaming unlocked.\n")
215
- _print_journal_intro(credits)
250
+ _print_journal_intro(cfg)
216
251
  print(" Customize anytime: `methodproof consent`\n")
217
252
  return cfg
218
253
 
@@ -873,7 +908,8 @@ def cmd_journal(args: argparse.Namespace) -> None:
873
908
  """Journal mode — full content capture."""
874
909
  subcmd = getattr(args, "journal_cmd", None)
875
910
  cfg = config.load()
876
- credits = cfg.get("journal_credits", 0)
911
+ entitlement = _journal_entitlement(cfg)
912
+ is_unlimited = entitlement == "unlimited"
877
913
 
878
914
  if subcmd == "on":
879
915
  print("Journal Mode — Full Content Capture\n")
@@ -884,12 +920,9 @@ def cmd_journal(args: argparse.Namespace) -> None:
884
920
  print(" • Terminal output (not just commands)")
885
921
  print(" • Tool call parameters and results\n")
886
922
  print("All content is encrypted (AES-256-GCM) and subject to your consent settings.\n")
887
- if credits > 0:
888
- print(f"You have {credits} free journal credit{'s' if credits != 1 else ''} "
889
- f"(sessions up to {config.FREE_JOURNAL_MAX_HOURS}h each).")
890
- print("After credits are used, journal mode requires a Pro plan.\n")
891
- else:
892
- print("Journal mode requires a Pro plan (or free credits if available).\n")
923
+ print(f" {_journal_credits_line(entitlement)}\n")
924
+ if not is_unlimited and int(entitlement) == 0:
925
+ print("You have no credits remaining. Upgrade to Pro for unlimited journal entries.\n")
893
926
  answer = input("Enable journal mode? [y/N] ").strip().lower()
894
927
  if answer != "y":
895
928
  print("Journal mode not enabled.")
@@ -913,9 +946,7 @@ def cmd_journal(args: argparse.Namespace) -> None:
913
946
  print("Journal mode: OFF (structural only)")
914
947
  print(" Only metadata captured: lengths, types, timing, file paths.")
915
948
  print(" Enable with: methodproof journal on")
916
- if credits > 0:
917
- print(f" Free journal credits: {credits} "
918
- f"(up to {config.FREE_JOURNAL_MAX_HOURS}h per session)")
949
+ print(f" {_journal_credits_line(entitlement)}")
919
950
 
920
951
  else:
921
952
  print("Usage: methodproof journal [on|off|status]")
@@ -1196,12 +1227,17 @@ def cmd_start(args: argparse.Namespace) -> None:
1196
1227
  # Journal mode
1197
1228
  if getattr(args, "journal", False):
1198
1229
  cfg["journal_mode"] = True
1199
- credits = cfg.get("journal_credits", 0)
1200
- if credits > 0:
1201
- cfg["journal_credits"] = credits - 1
1202
- print(f"Journal mode ON (free credit used — {credits - 1} remaining, {config.FREE_JOURNAL_MAX_HOURS}h cap).")
1230
+ entitlement = _journal_entitlement(cfg)
1231
+ if entitlement == "unlimited":
1232
+ print("Journal mode ON (full content capture).")
1203
1233
  else:
1204
- print("Journal mode ON for this session (full content capture).")
1234
+ credits = int(entitlement)
1235
+ if credits > 0:
1236
+ cfg["journal_credits"] = credits - 1
1237
+ remaining = credits - 1
1238
+ print(f"Journal mode ON (free credit used — {remaining} remaining, {config.FREE_JOURNAL_MAX_HOURS}h cap).")
1239
+ else:
1240
+ print("Journal mode ON for this session (full content capture).")
1205
1241
  config.save(cfg)
1206
1242
 
1207
1243
  # E2E mode
@@ -1558,8 +1594,8 @@ def cmd_status(args: argparse.Namespace) -> None:
1558
1594
  # Modes
1559
1595
  modes = []
1560
1596
  if cfg.get("journal_mode"):
1561
- credits = cfg.get("journal_credits", 0)
1562
- modes.append(f"journal ({credits} credits)")
1597
+ ent = _journal_entitlement(cfg)
1598
+ modes.append("journal (unlimited)" if ent == "unlimited" else f"journal ({ent} credits)")
1563
1599
  if cfg.get("e2e_mode"):
1564
1600
  fp = cfg.get("e2e_fingerprint", "")
1565
1601
  modes.append(f"e2e ({fp[:8]})" if fp else "e2e")
@@ -118,6 +118,9 @@ JOURNAL_CONTENT_FIELDS: list[tuple[str, str]] = [
118
118
  ("user_prompt", "prompt_text"),
119
119
  ("tool_call", "tool_input_preview"),
120
120
  ("tool_result", "result_preview"),
121
+ # Agent final message and commit body reveal content
122
+ ("agent_complete", "last_message_preview"),
123
+ ("git_commit", "body"),
121
124
  ]
122
125
 
123
126
 
@@ -10,10 +10,11 @@ _BASH = '''
10
10
  # methodproof-hook
11
11
  _mp_pre() { _MP_CMD="$BASH_COMMAND"; _MP_T=$SECONDS; }
12
12
  _mp_post() {
13
- local ec=$? cmd="$_MP_CMD" dur=$(( (SECONDS - ${_MP_T:-$SECONDS}) * 1000 ))
13
+ local ec=$? cmd="$_MP_CMD" dur=$(( (SECONDS - ${_MP_T:-$SECONDS}) * 1000 )) cwd
14
14
  [ -z "$cmd" ] && return
15
15
  cmd="${cmd//\\\\/\\\\\\\\}"; cmd="${cmd//\\"/\\\\\\"}"
16
- echo "{\\"command\\":\\"$cmd\\",\\"exit_code\\":$ec,\\"duration_ms\\":$dur}" >> ~/.methodproof/commands.jsonl
16
+ cwd="$(pwd)"; cwd="${cwd//\\\\/\\\\\\\\}"; cwd="${cwd//\\"/\\\\\\"}"
17
+ echo "{\\"command\\":\\"$cmd\\",\\"exit_code\\":$ec,\\"duration_ms\\":$dur,\\"cwd\\":\\"$cwd\\"}" >> ~/.methodproof/commands.jsonl
17
18
  unset _MP_CMD
18
19
  }
19
20
  trap '_mp_pre' DEBUG
@@ -24,10 +25,11 @@ _ZSH = '''
24
25
  # methodproof-hook
25
26
  _mp_pre() { _MP_CMD="$1"; _MP_T=$SECONDS; }
26
27
  _mp_post() {
27
- local ec=$? cmd="$_MP_CMD" dur=$(( (SECONDS - ${_MP_T:-$SECONDS}) * 1000 ))
28
+ local ec=$? cmd="$_MP_CMD" dur=$(( (SECONDS - ${_MP_T:-$SECONDS}) * 1000 )) cwd
28
29
  [[ -z "$cmd" ]] && return
29
30
  cmd="${cmd//\\\\/\\\\\\\\}"; cmd="${cmd//\\"/\\\\\\"}"
30
- echo "{\\"command\\":\\"$cmd\\",\\"exit_code\\":$ec,\\"duration_ms\\":$dur}" >> ~/.methodproof/commands.jsonl
31
+ cwd="$(pwd)"; cwd="${cwd//\\\\/\\\\\\\\}"; cwd="${cwd//\\"/\\\\\\"}"
32
+ echo "{\\"command\\":\\"$cmd\\",\\"exit_code\\":$ec,\\"duration_ms\\":$dur,\\"cwd\\":\\"$cwd\\"}" >> ~/.methodproof/commands.jsonl
31
33
  unset _MP_CMD
32
34
  }
33
35
  autoload -Uz add-zsh-hook
@@ -52,7 +54,8 @@ function prompt {
52
54
  if ($global:_mpCmd) {
53
55
  $dur = [DateTimeOffset]::UtcNow.ToUnixTimeMilliseconds() - $global:_mpT
54
56
  $cmd = $global:_mpCmd -replace '\\\\','\\\\\\\\' -replace '"','\\"'
55
- $line = "{`"command`":`"$cmd`",`"exit_code`":$ec,`"duration_ms`":$dur}"
57
+ $cwd = (Get-Location).Path -replace '\\\\','\\\\\\\\' -replace '"','\\"'
58
+ $line = "{`"command`":`"$cmd`",`"exit_code`":$ec,`"duration_ms`":$dur,`"cwd`":`"$cwd`"}"
56
59
  $logPath = Join-Path $HOME ".methodproof" "commands.jsonl"
57
60
  Add-Content -Path $logPath -Value $line
58
61
  $global:_mpCmd = $null
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "methodproof"
3
- version = "0.7.27"
3
+ version = "0.7.29"
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"]
@@ -118,9 +118,10 @@ def test_start_creates_session_and_spawns_daemon(mock_sleep, mock_popen, mock_re
118
118
  @patch("subprocess.Popen")
119
119
  @patch("time.sleep")
120
120
  def test_start_journal_decrements_credits(mock_sleep, mock_popen, mock_req, mock_repo, mock_update, mock_auth,
121
- mock_hook, mock_alive, logged_in_cfg, cli_args,
121
+ mock_hook, mock_alive, logged_in_cfg, fake_jwt, cli_args,
122
122
  monkeypatch, capsys):
123
- cfg = logged_in_cfg(account_id="acct-1")
123
+ # Use free-tier JWT — pro/team skips credit deduction (unlimited)
124
+ cfg = logged_in_cfg(account_id="acct-1", token=fake_jwt(user_id="acct-1", account_type="free"))
124
125
  cfg["journal_credits"] = 2
125
126
  config.save(cfg)
126
127
  pidfile = config.DIR / "methodproof.pid"
File without changes
File without changes
File without changes
File without changes