ghosttrap-cli 0.3.17__tar.gz → 0.3.19__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.19
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.19"
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.
@@ -215,9 +206,14 @@ def _get_repo_entry(config, requested=None):
215
206
  for k, entry in repos.items():
216
207
  if f"{entry.get('owner')}/{entry.get('name')}" == requested:
217
208
  return k, entry
218
- available = sorted(f"{e['owner']}/{e['name']}" for e in repos.values())
209
+ available = sorted(
210
+ f"{e.get('owner')}/{e.get('name')}"
211
+ for e in repos.values()
212
+ if e.get('owner') and e.get('name')
213
+ )
219
214
  print(f"error: '{requested}' is not in your config.", file=sys.stderr)
220
- print(f"available: {', '.join(available)}", file=sys.stderr)
215
+ if available:
216
+ print(f"available: {', '.join(available)}", file=sys.stderr)
221
217
  sys.exit(1)
222
218
  cwd_repo = _detect_repo_from_cwd()
223
219
  if cwd_repo:
@@ -306,22 +302,69 @@ def _require_setup():
306
302
  sys.exit(1)
307
303
 
308
304
 
309
- def _write_skill():
305
+ def _write_skill(config=None):
310
306
  os.makedirs(SKILL_DIR, exist_ok=True)
311
307
  with open(SKILL_FILE, "w") as f:
312
308
  f.write(SKILL_CONTENT)
309
+ if config is None:
310
+ config = _load_config()
311
+ config["skill_baseline"] = SKILL_CONTENT
312
+ _save_config(config)
313
+
314
+
315
+ def _merge_skill(base, local, remote):
316
+ """3-way merge via `git merge-file -p`. Returns (merged_text, clean)."""
317
+ with tempfile.TemporaryDirectory() as d:
318
+ bp = os.path.join(d, "base")
319
+ lp = os.path.join(d, "local")
320
+ rp = os.path.join(d, "remote")
321
+ for path, text in [(bp, base), (lp, local), (rp, remote)]:
322
+ with open(path, "w") as f:
323
+ f.write(text)
324
+ result = subprocess.run(
325
+ ["git", "merge-file", "-p",
326
+ "-L", "your edits", "-L", "previous release", "-L", "new release",
327
+ lp, bp, rp],
328
+ capture_output=True, text=True,
329
+ )
330
+ return result.stdout, result.returncode == 0
313
331
 
314
332
 
315
333
  def _refresh_skill_if_stale():
316
334
  if not os.path.exists(SKILL_FILE):
317
335
  return
318
336
  with open(SKILL_FILE) as f:
319
- content = f.read()
320
- if content == SKILL_CONTENT:
337
+ on_disk = f.read()
338
+ if on_disk == SKILL_CONTENT:
339
+ return
340
+ config = _load_config()
341
+ baseline = config.get("skill_baseline")
342
+ if baseline is None:
343
+ # Pre-baseline install: adopt current on-disk content as the baseline
344
+ # so future releases can 3-way-merge instead of clobbering local edits.
345
+ config["skill_baseline"] = on_disk
346
+ _save_config(config)
321
347
  return
322
- if hashlib.sha256(content.encode()).hexdigest() in KNOWN_SKILL_HASHES:
323
- _write_skill()
348
+ if baseline == on_disk:
349
+ _write_skill(config)
324
350
  print("ghosttrap skill file updated", file=sys.stderr)
351
+ return
352
+ merged, clean = _merge_skill(baseline, on_disk, SKILL_CONTENT)
353
+ if clean:
354
+ with open(SKILL_FILE, "w") as f:
355
+ f.write(merged)
356
+ config["skill_baseline"] = SKILL_CONTENT
357
+ _save_config(config)
358
+ print("ghosttrap skill file updated (merged with your local edits)", file=sys.stderr)
359
+ return
360
+ new_path = SKILL_FILE + ".new"
361
+ with open(new_path, "w") as f:
362
+ f.write(merged)
363
+ print(
364
+ f"ghosttrap skill update has conflicts with your local edits; "
365
+ f"merged candidate at {new_path} — resolve, copy to {SKILL_FILE}, and rerun.",
366
+ file=sys.stderr,
367
+ )
325
368
 
326
369
 
327
370
  async def setup(server_url, token):
@@ -350,7 +393,7 @@ async def setup(server_url, token):
350
393
 
351
394
  repos = event.get("repos", [])
352
395
  _save_repos(config, repos)
353
- _write_skill()
396
+ _write_skill(config)
354
397
 
355
398
  target = repos[0] if repos else None
356
399
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ghosttrap-cli
3
- Version: 0.3.17
3
+ Version: 0.3.19
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.19"
8
8
  description = "Watch for errors streaming from ghosttrap.io"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
File without changes