ghosttrap-cli 0.3.17__tar.gz → 0.3.18__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ghosttrap-cli
3
- Version: 0.3.17
3
+ Version: 0.3.18
4
4
  Summary: Watch for errors streaming from ghosttrap.io
5
5
  Project-URL: Homepage, https://github.com/alex-rowley/ghosttrap-cli
6
6
  Requires-Python: >=3.10
@@ -41,6 +41,8 @@ After fixing, Claude restarts peek and waits for the next one. Errors become a r
41
41
 
42
42
  Your app needs [ghosttrap-sdk](https://github.com/alex-rowley/ghosttrap-sdk) to report errors. The Claude Code skill handles the integration automatically — it installs the SDK, wires it into your app, and adds Django/Celery hooks if applicable. You shouldn't need to touch the SDK manually.
43
43
 
44
+ If you want to manually flag a caught exception or a non-exception condition, call `ghosttrap.trap(exc_or_message)` from your app code — see the [SDK README](https://github.com/alex-rowley/ghosttrap-sdk#manually-trap-an-event).
45
+
44
46
  ## Commands
45
47
 
46
48
  | Command | What it does |
@@ -32,6 +32,8 @@ After fixing, Claude restarts peek and waits for the next one. Errors become a r
32
32
 
33
33
  Your app needs [ghosttrap-sdk](https://github.com/alex-rowley/ghosttrap-sdk) to report errors. The Claude Code skill handles the integration automatically — it installs the SDK, wires it into your app, and adds Django/Celery hooks if applicable. You shouldn't need to touch the SDK manually.
34
34
 
35
+ If you want to manually flag a caught exception or a non-exception condition, call `ghosttrap.trap(exc_or_message)` from your app code — see the [SDK README](https://github.com/alex-rowley/ghosttrap-sdk#manually-trap-an-event).
36
+
35
37
  ## Commands
36
38
 
37
39
  | Command | What it does |
@@ -2,31 +2,18 @@
2
2
 
3
3
  import argparse
4
4
  import asyncio
5
- import hashlib
6
5
  import json
7
6
  import os
8
7
  import subprocess
9
8
  import sys
9
+ import tempfile
10
10
  import time
11
11
  import urllib.error
12
12
  import urllib.request
13
13
 
14
14
  import websockets
15
15
 
16
- KNOWN_SKILL_HASHES = {
17
- "aeda67bc5971bd8af4d7ebe819ebcce5acead562fa618227a1798b4b5ae7143e", # v0.2.0
18
- "0f2d2f4105e393fc69084d404d5a8154ba5d97fd23f92810c51345e3dc68e9a0", # v0.3.0
19
- "8564b65b8ab5c63283cda1706e30ca62bc4e111d33ba8918220f4b556ad01da1", # v0.3.1..v0.3.3
20
- "5759b2e0dc8ca47c3801915fd688cc8da878a7ab8d405f5183ffd7e8c8df4c55", # v0.3.4..v0.3.7
21
- "0651bb4247cf5c68960ff5b63d6a5d0c85ff1ce08e7966ab4823601ff02cf1f4", # v0.3.9
22
- "38810f43867a2a91420cc3dacbc71d2acabd7125596fd5b43f222b49725c9696", # v0.3.10
23
- "19b67d913dc5214ee4db3610bd8749da67324c174b904b5da71ee6de13e23e63", # v0.3.11
24
- "bf7768c3de266b7018d5c722c6c9991b487e7897786b3a406c460842cdcde8b5", # v0.3.12
25
- "d3f594c4c3601a4594c18ebc5e16dfd4abad4ab97d56285ecc20634b919b6731", # v0.3.14
26
- "1c3a0507fab027abc0f176ce2fd88cad28276bef4fcef46cdb497f53967346e1", # v0.3.16
27
- }
28
-
29
- __version__ = "0.3.17"
16
+ __version__ = "0.3.18"
30
17
 
31
18
  GHOSTTRAP_SERVER = "wss://ghosttrap.io/stream/"
32
19
  CONFIG_DIR = os.path.expanduser("~/.ghosttrap")
@@ -76,6 +63,10 @@ Read `~/.ghosttrap/config.json` for state. It contains:
76
63
  3. If `sdk_installed` is false or missing: install the SDK (`pip install ghosttrap-sdk`), wire `ghosttrap.init("<token>")` into the app startup. For Django projects, also add `"ghosttrap.django.GhostTrapApp"` to INSTALLED_APPS (re-attaches logging handler after Django's dictConfig) and `"ghosttrap.django.GhostTrapMiddleware"` to MIDDLEWARE (catches unhandled view exceptions). The SDK auto-hooks into Celery task_failure if Celery is installed, and attaches a logging handler for logger.exception() calls. Use whatever pattern the project already uses for configuration (env vars, settings files, hardcoded — match the existing style). Then update the config: set `sdk_installed: true`, `sdk_version`, `init_file` to record what you did. Only pass `send_user=True` to `init()` if the user explicitly asks for user context in reports — it's PII and stays off by default.
77
64
  4. Run `ghosttrap peek --clear` with `run_in_background: true`. The `--clear` flag skips any stale backlog from prior sessions so you only get fresh errors.
78
65
 
66
+ ## Manual capture
67
+
68
+ For caught exceptions or non-exception conditions the user explicitly wants reported, use `ghosttrap.trap(exc_or_message)` from app code — pass an exception instance or a string. Synthetic string events arrive as type `TrappedEvent` with the caller's stack. Only wire this in when the user asks for it; don't add `trap()` calls speculatively.
69
+
79
70
  ## When peek returns
80
71
 
81
72
  1. **Immediately restart peek** in the background before doing anything else — this ensures you're listening for the next error while you work on the current one. Use plain `ghosttrap peek` here (no `--clear`) — you only want to skip backlog at session start.
@@ -306,22 +297,69 @@ def _require_setup():
306
297
  sys.exit(1)
307
298
 
308
299
 
309
- def _write_skill():
300
+ def _write_skill(config=None):
310
301
  os.makedirs(SKILL_DIR, exist_ok=True)
311
302
  with open(SKILL_FILE, "w") as f:
312
303
  f.write(SKILL_CONTENT)
304
+ if config is None:
305
+ config = _load_config()
306
+ config["skill_baseline"] = SKILL_CONTENT
307
+ _save_config(config)
308
+
309
+
310
+ def _merge_skill(base, local, remote):
311
+ """3-way merge via `git merge-file -p`. Returns (merged_text, clean)."""
312
+ with tempfile.TemporaryDirectory() as d:
313
+ bp = os.path.join(d, "base")
314
+ lp = os.path.join(d, "local")
315
+ rp = os.path.join(d, "remote")
316
+ for path, text in [(bp, base), (lp, local), (rp, remote)]:
317
+ with open(path, "w") as f:
318
+ f.write(text)
319
+ result = subprocess.run(
320
+ ["git", "merge-file", "-p",
321
+ "-L", "your edits", "-L", "previous release", "-L", "new release",
322
+ lp, bp, rp],
323
+ capture_output=True, text=True,
324
+ )
325
+ return result.stdout, result.returncode == 0
313
326
 
314
327
 
315
328
  def _refresh_skill_if_stale():
316
329
  if not os.path.exists(SKILL_FILE):
317
330
  return
318
331
  with open(SKILL_FILE) as f:
319
- content = f.read()
320
- if content == SKILL_CONTENT:
332
+ on_disk = f.read()
333
+ if on_disk == SKILL_CONTENT:
334
+ return
335
+ config = _load_config()
336
+ baseline = config.get("skill_baseline")
337
+ if baseline is None:
338
+ # Pre-baseline install: adopt current on-disk content as the baseline
339
+ # so future releases can 3-way-merge instead of clobbering local edits.
340
+ config["skill_baseline"] = on_disk
341
+ _save_config(config)
321
342
  return
322
- if hashlib.sha256(content.encode()).hexdigest() in KNOWN_SKILL_HASHES:
323
- _write_skill()
343
+ if baseline == on_disk:
344
+ _write_skill(config)
324
345
  print("ghosttrap skill file updated", file=sys.stderr)
346
+ return
347
+ merged, clean = _merge_skill(baseline, on_disk, SKILL_CONTENT)
348
+ if clean:
349
+ with open(SKILL_FILE, "w") as f:
350
+ f.write(merged)
351
+ config["skill_baseline"] = SKILL_CONTENT
352
+ _save_config(config)
353
+ print("ghosttrap skill file updated (merged with your local edits)", file=sys.stderr)
354
+ return
355
+ new_path = SKILL_FILE + ".new"
356
+ with open(new_path, "w") as f:
357
+ f.write(merged)
358
+ print(
359
+ f"ghosttrap skill update has conflicts with your local edits; "
360
+ f"merged candidate at {new_path} — resolve, copy to {SKILL_FILE}, and rerun.",
361
+ file=sys.stderr,
362
+ )
325
363
 
326
364
 
327
365
  async def setup(server_url, token):
@@ -350,7 +388,7 @@ async def setup(server_url, token):
350
388
 
351
389
  repos = event.get("repos", [])
352
390
  _save_repos(config, repos)
353
- _write_skill()
391
+ _write_skill(config)
354
392
 
355
393
  target = repos[0] if repos else None
356
394
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ghosttrap-cli
3
- Version: 0.3.17
3
+ Version: 0.3.18
4
4
  Summary: Watch for errors streaming from ghosttrap.io
5
5
  Project-URL: Homepage, https://github.com/alex-rowley/ghosttrap-cli
6
6
  Requires-Python: >=3.10
@@ -41,6 +41,8 @@ After fixing, Claude restarts peek and waits for the next one. Errors become a r
41
41
 
42
42
  Your app needs [ghosttrap-sdk](https://github.com/alex-rowley/ghosttrap-sdk) to report errors. The Claude Code skill handles the integration automatically — it installs the SDK, wires it into your app, and adds Django/Celery hooks if applicable. You shouldn't need to touch the SDK manually.
43
43
 
44
+ If you want to manually flag a caught exception or a non-exception condition, call `ghosttrap.trap(exc_or_message)` from your app code — see the [SDK README](https://github.com/alex-rowley/ghosttrap-sdk#manually-trap-an-event).
45
+
44
46
  ## Commands
45
47
 
46
48
  | Command | What it does |
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "ghosttrap-cli"
7
- version = "0.3.17"
7
+ version = "0.3.18"
8
8
  description = "Watch for errors streaming from ghosttrap.io"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
File without changes