ghosttrap-cli 0.3.16__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.
- {ghosttrap_cli-0.3.16 → ghosttrap_cli-0.3.18}/PKG-INFO +5 -1
- {ghosttrap_cli-0.3.16 → ghosttrap_cli-0.3.18}/README.md +4 -0
- {ghosttrap_cli-0.3.16 → ghosttrap_cli-0.3.18}/ghosttrap_cli/cli.py +211 -38
- {ghosttrap_cli-0.3.16 → ghosttrap_cli-0.3.18}/ghosttrap_cli.egg-info/PKG-INFO +5 -1
- {ghosttrap_cli-0.3.16 → ghosttrap_cli-0.3.18}/pyproject.toml +1 -1
- {ghosttrap_cli-0.3.16 → ghosttrap_cli-0.3.18}/ghosttrap_cli/__init__.py +0 -0
- {ghosttrap_cli-0.3.16 → ghosttrap_cli-0.3.18}/ghosttrap_cli.egg-info/SOURCES.txt +0 -0
- {ghosttrap_cli-0.3.16 → ghosttrap_cli-0.3.18}/ghosttrap_cli.egg-info/dependency_links.txt +0 -0
- {ghosttrap_cli-0.3.16 → ghosttrap_cli-0.3.18}/ghosttrap_cli.egg-info/entry_points.txt +0 -0
- {ghosttrap_cli-0.3.16 → ghosttrap_cli-0.3.18}/ghosttrap_cli.egg-info/requires.txt +0 -0
- {ghosttrap_cli-0.3.16 → ghosttrap_cli-0.3.18}/ghosttrap_cli.egg-info/top_level.txt +0 -0
- {ghosttrap_cli-0.3.16 → ghosttrap_cli-0.3.18}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: ghosttrap-cli
|
|
3
|
-
Version: 0.3.
|
|
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 |
|
|
@@ -51,6 +53,8 @@ Your app needs [ghosttrap-sdk](https://github.com/alex-rowley/ghosttrap-sdk) to
|
|
|
51
53
|
| `ghosttrap last` | Fetch the most recent error and exit (no waiting) |
|
|
52
54
|
| `ghosttrap last --clear` | Fetch the most recent error and skip everything older |
|
|
53
55
|
| `ghosttrap watch` | Stream all errors continuously |
|
|
56
|
+
| `ghosttrap list [n]` | Print a numbered summary of the most recent `n` errors (default 10, max 50). Doesn't move the cursor. |
|
|
57
|
+
| `ghosttrap show <i>` | Full details for row `i` from the last `list` (1-based). Doesn't move the cursor. |
|
|
54
58
|
| `ghosttrap clear` | Skip all outstanding errors |
|
|
55
59
|
| `ghosttrap nuke` | Permanently delete every server-side row for the current repo (errors + token). Requires typed confirmation. |
|
|
56
60
|
|
|
@@ -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 |
|
|
@@ -42,6 +44,8 @@ Your app needs [ghosttrap-sdk](https://github.com/alex-rowley/ghosttrap-sdk) to
|
|
|
42
44
|
| `ghosttrap last` | Fetch the most recent error and exit (no waiting) |
|
|
43
45
|
| `ghosttrap last --clear` | Fetch the most recent error and skip everything older |
|
|
44
46
|
| `ghosttrap watch` | Stream all errors continuously |
|
|
47
|
+
| `ghosttrap list [n]` | Print a numbered summary of the most recent `n` errors (default 10, max 50). Doesn't move the cursor. |
|
|
48
|
+
| `ghosttrap show <i>` | Full details for row `i` from the last `list` (1-based). Doesn't move the cursor. |
|
|
45
49
|
| `ghosttrap clear` | Skip all outstanding errors |
|
|
46
50
|
| `ghosttrap nuke` | Permanently delete every server-side row for the current repo (errors + token). Requires typed confirmation. |
|
|
47
51
|
|
|
@@ -2,29 +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
|
+
import urllib.error
|
|
11
12
|
import urllib.request
|
|
12
13
|
|
|
13
14
|
import websockets
|
|
14
15
|
|
|
15
|
-
|
|
16
|
-
"aeda67bc5971bd8af4d7ebe819ebcce5acead562fa618227a1798b4b5ae7143e", # v0.2.0
|
|
17
|
-
"0f2d2f4105e393fc69084d404d5a8154ba5d97fd23f92810c51345e3dc68e9a0", # v0.3.0
|
|
18
|
-
"8564b65b8ab5c63283cda1706e30ca62bc4e111d33ba8918220f4b556ad01da1", # v0.3.1..v0.3.3
|
|
19
|
-
"5759b2e0dc8ca47c3801915fd688cc8da878a7ab8d405f5183ffd7e8c8df4c55", # v0.3.4..v0.3.7
|
|
20
|
-
"0651bb4247cf5c68960ff5b63d6a5d0c85ff1ce08e7966ab4823601ff02cf1f4", # v0.3.9
|
|
21
|
-
"38810f43867a2a91420cc3dacbc71d2acabd7125596fd5b43f222b49725c9696", # v0.3.10
|
|
22
|
-
"19b67d913dc5214ee4db3610bd8749da67324c174b904b5da71ee6de13e23e63", # v0.3.11
|
|
23
|
-
"bf7768c3de266b7018d5c722c6c9991b487e7897786b3a406c460842cdcde8b5", # v0.3.12
|
|
24
|
-
"d3f594c4c3601a4594c18ebc5e16dfd4abad4ab97d56285ecc20634b919b6731", # v0.3.14
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
__version__ = "0.3.16"
|
|
16
|
+
__version__ = "0.3.18"
|
|
28
17
|
|
|
29
18
|
GHOSTTRAP_SERVER = "wss://ghosttrap.io/stream/"
|
|
30
19
|
CONFIG_DIR = os.path.expanduser("~/.ghosttrap")
|
|
@@ -74,6 +63,10 @@ Read `~/.ghosttrap/config.json` for state. It contains:
|
|
|
74
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.
|
|
75
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.
|
|
76
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
|
+
|
|
77
70
|
## When peek returns
|
|
78
71
|
|
|
79
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.
|
|
@@ -83,6 +76,8 @@ Read `~/.ghosttrap/config.json` for state. It contains:
|
|
|
83
76
|
## Other commands
|
|
84
77
|
|
|
85
78
|
- `ghosttrap last` — fetch the single most recent error and exit immediately, no waiting. Useful when the user wants to look at the latest error without starting a watch. Add `--clear` to also skip everything older in one shot.
|
|
79
|
+
- `ghosttrap list [n]` — print a numbered summary of the most recent `n` errors (default 10, max 50). Does not move the cursor. Caches the ordered ids in config so a follow-up `ghosttrap show <i>` returns full details for that row.
|
|
80
|
+
- `ghosttrap show <i>` — full details for the i-th row from the most recent `ghosttrap list`. Does not move the cursor.
|
|
86
81
|
- `ghosttrap clear` — manually skip outstanding errors without waiting. Useful if the user explicitly wants to drop the queue.
|
|
87
82
|
- `ghosttrap nuke` — permanently delete every server-side row for the current repo (errors + the Repo row + its token). Requires the user to type the repo name `owner/name` to confirm. Only run if the user explicitly asks to wipe server data — never proactively. After it succeeds the token is dead; the user would need to `ghosttrap setup` again to use this repo.
|
|
88
83
|
|
|
@@ -201,26 +196,33 @@ def get_gh_token():
|
|
|
201
196
|
sys.exit(1)
|
|
202
197
|
|
|
203
198
|
|
|
204
|
-
def
|
|
205
|
-
"""
|
|
199
|
+
def _get_repo_entry(config, requested=None):
|
|
200
|
+
"""Return (key, entry) for the chosen repo. Same resolution rules as _get_repo_token."""
|
|
206
201
|
repos = config.get("repos", {})
|
|
207
202
|
if not repos:
|
|
208
203
|
print("error: no repos configured. run 'ghosttrap setup' first.", file=sys.stderr)
|
|
209
204
|
sys.exit(1)
|
|
210
205
|
if requested:
|
|
211
|
-
for entry in repos.
|
|
206
|
+
for k, entry in repos.items():
|
|
212
207
|
if f"{entry.get('owner')}/{entry.get('name')}" == requested:
|
|
213
|
-
return entry
|
|
208
|
+
return k, entry
|
|
214
209
|
available = sorted(f"{e['owner']}/{e['name']}" for e in repos.values())
|
|
215
210
|
print(f"error: '{requested}' is not in your config.", file=sys.stderr)
|
|
216
211
|
print(f"available: {', '.join(available)}", file=sys.stderr)
|
|
217
212
|
sys.exit(1)
|
|
218
213
|
cwd_repo = _detect_repo_from_cwd()
|
|
219
214
|
if cwd_repo:
|
|
220
|
-
for entry in repos.
|
|
215
|
+
for k, entry in repos.items():
|
|
221
216
|
if f"{entry.get('owner')}/{entry.get('name')}" == cwd_repo:
|
|
222
|
-
return entry
|
|
223
|
-
|
|
217
|
+
return k, entry
|
|
218
|
+
k = next(iter(repos))
|
|
219
|
+
return k, repos[k]
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def _get_repo_token(config, requested=None):
|
|
223
|
+
"""Get the repo token. If `requested` is 'owner/name', match strictly. Else cwd, else first."""
|
|
224
|
+
_, entry = _get_repo_entry(config, requested)
|
|
225
|
+
return entry["token"]
|
|
224
226
|
|
|
225
227
|
|
|
226
228
|
async def _connect_and_handle(server_url, token, config, once=False):
|
|
@@ -295,22 +297,69 @@ def _require_setup():
|
|
|
295
297
|
sys.exit(1)
|
|
296
298
|
|
|
297
299
|
|
|
298
|
-
def _write_skill():
|
|
300
|
+
def _write_skill(config=None):
|
|
299
301
|
os.makedirs(SKILL_DIR, exist_ok=True)
|
|
300
302
|
with open(SKILL_FILE, "w") as f:
|
|
301
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
|
|
302
326
|
|
|
303
327
|
|
|
304
328
|
def _refresh_skill_if_stale():
|
|
305
329
|
if not os.path.exists(SKILL_FILE):
|
|
306
330
|
return
|
|
307
331
|
with open(SKILL_FILE) as f:
|
|
308
|
-
|
|
309
|
-
if
|
|
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)
|
|
310
342
|
return
|
|
311
|
-
if
|
|
312
|
-
_write_skill()
|
|
343
|
+
if baseline == on_disk:
|
|
344
|
+
_write_skill(config)
|
|
313
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
|
+
)
|
|
314
363
|
|
|
315
364
|
|
|
316
365
|
async def setup(server_url, token):
|
|
@@ -339,7 +388,7 @@ async def setup(server_url, token):
|
|
|
339
388
|
|
|
340
389
|
repos = event.get("repos", [])
|
|
341
390
|
_save_repos(config, repos)
|
|
342
|
-
_write_skill()
|
|
391
|
+
_write_skill(config)
|
|
343
392
|
|
|
344
393
|
target = repos[0] if repos else None
|
|
345
394
|
|
|
@@ -464,6 +513,126 @@ def nuke():
|
|
|
464
513
|
_save_config(config)
|
|
465
514
|
|
|
466
515
|
|
|
516
|
+
def _rel_time(iso):
|
|
517
|
+
if not iso:
|
|
518
|
+
return "?"
|
|
519
|
+
try:
|
|
520
|
+
from datetime import datetime, timezone
|
|
521
|
+
dt = datetime.fromisoformat(iso)
|
|
522
|
+
if dt.tzinfo is None:
|
|
523
|
+
dt = dt.replace(tzinfo=timezone.utc)
|
|
524
|
+
delta = datetime.now(timezone.utc) - dt
|
|
525
|
+
s = int(delta.total_seconds())
|
|
526
|
+
if s < 60:
|
|
527
|
+
return f"{s}s ago"
|
|
528
|
+
if s < 3600:
|
|
529
|
+
return f"{s // 60}m ago"
|
|
530
|
+
if s < 86400:
|
|
531
|
+
return f"{s // 3600}h ago"
|
|
532
|
+
if s < 86400 * 30:
|
|
533
|
+
return f"{s // 86400}d ago"
|
|
534
|
+
return dt.strftime("%Y-%m-%d")
|
|
535
|
+
except Exception:
|
|
536
|
+
return iso
|
|
537
|
+
|
|
538
|
+
|
|
539
|
+
def _print_error_details(error):
|
|
540
|
+
print(json.dumps({"type": "error", "error": error}))
|
|
541
|
+
sys.stdout.flush()
|
|
542
|
+
print(f"\n{'='*60}", file=sys.stderr)
|
|
543
|
+
print(f" {error.get('repo', '?')}", file=sys.stderr)
|
|
544
|
+
print(f" {error.get('type', '?')}: {error.get('message', '')}", file=sys.stderr)
|
|
545
|
+
frames = error.get("frames", [])
|
|
546
|
+
if frames:
|
|
547
|
+
f = frames[-1]
|
|
548
|
+
print(f" at {f.get('file', '?')}:{f.get('line', '?')} in {f.get('function', '?')}", file=sys.stderr)
|
|
549
|
+
print(f"{'='*60}", file=sys.stderr)
|
|
550
|
+
|
|
551
|
+
|
|
552
|
+
def list_recent(n=10, requested=None):
|
|
553
|
+
_require_setup()
|
|
554
|
+
config = _load_config()
|
|
555
|
+
_check_cli_version(config)
|
|
556
|
+
n = max(1, min(int(n), 50))
|
|
557
|
+
key, entry = _get_repo_entry(config, requested)
|
|
558
|
+
token = entry["token"]
|
|
559
|
+
server = GHOSTTRAP_SERVER.replace("wss://", "https://").replace("/stream/", "")
|
|
560
|
+
url = f"{server}/list/{token}/?n={n}"
|
|
561
|
+
try:
|
|
562
|
+
req = urllib.request.Request(url, headers={"User-Agent": "ghosttrap-cli"})
|
|
563
|
+
with urllib.request.urlopen(req, timeout=10) as resp:
|
|
564
|
+
data = json.loads(resp.read())
|
|
565
|
+
except Exception as e:
|
|
566
|
+
print(f"error: {e}", file=sys.stderr)
|
|
567
|
+
sys.exit(1)
|
|
568
|
+
|
|
569
|
+
errors = data.get("errors", [])
|
|
570
|
+
if not errors:
|
|
571
|
+
print("no errors yet", file=sys.stderr)
|
|
572
|
+
entry["recent"] = []
|
|
573
|
+
_save_config(config)
|
|
574
|
+
return
|
|
575
|
+
|
|
576
|
+
entry["recent"] = [e["id"] for e in errors]
|
|
577
|
+
_save_config(config)
|
|
578
|
+
|
|
579
|
+
width = len(str(len(errors)))
|
|
580
|
+
for i, e in enumerate(errors, 1):
|
|
581
|
+
when = _rel_time(e.get("created_at"))
|
|
582
|
+
etype = e.get("type") or "?"
|
|
583
|
+
msg = (e.get("message") or "").splitlines()[0] if e.get("message") else ""
|
|
584
|
+
if len(msg) > 60:
|
|
585
|
+
msg = msg[:57] + "..."
|
|
586
|
+
loc = ""
|
|
587
|
+
if e.get("file"):
|
|
588
|
+
loc = f"{e['file']}:{e.get('line', '?')}"
|
|
589
|
+
if e.get("function"):
|
|
590
|
+
loc += f" ({e['function']})"
|
|
591
|
+
print(f" {i:>{width}} {when:<12} {etype:<20} {msg:<60} {loc}")
|
|
592
|
+
print(f"\nrun 'ghosttrap show <n>' to see full details. cursor unchanged.", file=sys.stderr)
|
|
593
|
+
|
|
594
|
+
|
|
595
|
+
def show(index, requested=None):
|
|
596
|
+
_require_setup()
|
|
597
|
+
config = _load_config()
|
|
598
|
+
_check_cli_version(config)
|
|
599
|
+
key, entry = _get_repo_entry(config, requested)
|
|
600
|
+
recent = entry.get("recent") or []
|
|
601
|
+
if not recent:
|
|
602
|
+
print("error: no recent list cached. run 'ghosttrap list' first.", file=sys.stderr)
|
|
603
|
+
sys.exit(1)
|
|
604
|
+
try:
|
|
605
|
+
i = int(index)
|
|
606
|
+
except (TypeError, ValueError):
|
|
607
|
+
print(f"error: '{index}' is not a number.", file=sys.stderr)
|
|
608
|
+
sys.exit(1)
|
|
609
|
+
if i < 1 or i > len(recent):
|
|
610
|
+
print(f"error: index out of range. last list had {len(recent)} entries.", file=sys.stderr)
|
|
611
|
+
sys.exit(1)
|
|
612
|
+
db_id = recent[i - 1]
|
|
613
|
+
token = entry["token"]
|
|
614
|
+
server = GHOSTTRAP_SERVER.replace("wss://", "https://").replace("/stream/", "")
|
|
615
|
+
url = f"{server}/error/{token}/{db_id}/"
|
|
616
|
+
try:
|
|
617
|
+
req = urllib.request.Request(url, headers={"User-Agent": "ghosttrap-cli"})
|
|
618
|
+
with urllib.request.urlopen(req, timeout=10) as resp:
|
|
619
|
+
data = json.loads(resp.read())
|
|
620
|
+
except urllib.error.HTTPError as e:
|
|
621
|
+
if e.code == 404:
|
|
622
|
+
print(f"error: this error no longer exists on the server (id #{db_id}).", file=sys.stderr)
|
|
623
|
+
else:
|
|
624
|
+
print(f"error: {e}", file=sys.stderr)
|
|
625
|
+
sys.exit(1)
|
|
626
|
+
except Exception as e:
|
|
627
|
+
print(f"error: {e}", file=sys.stderr)
|
|
628
|
+
sys.exit(1)
|
|
629
|
+
error = data.get("error")
|
|
630
|
+
if not error:
|
|
631
|
+
print(f"error: empty response", file=sys.stderr)
|
|
632
|
+
sys.exit(1)
|
|
633
|
+
_print_error_details(error)
|
|
634
|
+
|
|
635
|
+
|
|
467
636
|
def last(do_clear=False, requested=None):
|
|
468
637
|
_require_setup()
|
|
469
638
|
config = _load_config()
|
|
@@ -484,17 +653,7 @@ def last(do_clear=False, requested=None):
|
|
|
484
653
|
print("no errors yet", file=sys.stderr)
|
|
485
654
|
return
|
|
486
655
|
|
|
487
|
-
|
|
488
|
-
sys.stdout.flush()
|
|
489
|
-
|
|
490
|
-
print(f"\n{'='*60}", file=sys.stderr)
|
|
491
|
-
print(f" {error.get('repo', '?')}", file=sys.stderr)
|
|
492
|
-
print(f" {error.get('type', '?')}: {error.get('message', '')}", file=sys.stderr)
|
|
493
|
-
frames = error.get("frames", [])
|
|
494
|
-
if frames:
|
|
495
|
-
f = frames[-1]
|
|
496
|
-
print(f" at {f.get('file', '?')}:{f.get('line', '?')} in {f.get('function', '?')}", file=sys.stderr)
|
|
497
|
-
print(f"{'='*60}", file=sys.stderr)
|
|
656
|
+
_print_error_details(error)
|
|
498
657
|
|
|
499
658
|
if do_clear:
|
|
500
659
|
try:
|
|
@@ -526,6 +685,14 @@ def main():
|
|
|
526
685
|
last_parser.add_argument("--clear", action="store_true", help="Also skip remaining outstanding errors")
|
|
527
686
|
last_parser.add_argument("--repo", help="Target repo as owner/name (overrides cwd detection)")
|
|
528
687
|
|
|
688
|
+
list_parser = sub.add_parser("list", help="List the most recent N errors (summary only, cursor unchanged)")
|
|
689
|
+
list_parser.add_argument("n", nargs="?", type=int, default=10, help="How many to list (default 10, max 50)")
|
|
690
|
+
list_parser.add_argument("--repo", help="Target repo as owner/name (overrides cwd detection)")
|
|
691
|
+
|
|
692
|
+
show_parser = sub.add_parser("show", help="Show full details for an index from the last 'list' (cursor unchanged)")
|
|
693
|
+
show_parser.add_argument("index", type=int, help="1-based index from the last 'ghosttrap list'")
|
|
694
|
+
show_parser.add_argument("--repo", help="Target repo as owner/name (overrides cwd detection)")
|
|
695
|
+
|
|
529
696
|
sub.add_parser("nuke", help="Permanently delete all server data for the current repo")
|
|
530
697
|
|
|
531
698
|
args = parser.parse_args()
|
|
@@ -556,6 +723,12 @@ def main():
|
|
|
556
723
|
elif args.command == "last":
|
|
557
724
|
_refresh_skill_if_stale()
|
|
558
725
|
last(do_clear=args.clear, requested=args.repo)
|
|
726
|
+
elif args.command == "list":
|
|
727
|
+
_refresh_skill_if_stale()
|
|
728
|
+
list_recent(n=args.n, requested=args.repo)
|
|
729
|
+
elif args.command == "show":
|
|
730
|
+
_refresh_skill_if_stale()
|
|
731
|
+
show(args.index, requested=args.repo)
|
|
559
732
|
elif args.command == "nuke":
|
|
560
733
|
nuke()
|
|
561
734
|
else:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: ghosttrap-cli
|
|
3
|
-
Version: 0.3.
|
|
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 |
|
|
@@ -51,6 +53,8 @@ Your app needs [ghosttrap-sdk](https://github.com/alex-rowley/ghosttrap-sdk) to
|
|
|
51
53
|
| `ghosttrap last` | Fetch the most recent error and exit (no waiting) |
|
|
52
54
|
| `ghosttrap last --clear` | Fetch the most recent error and skip everything older |
|
|
53
55
|
| `ghosttrap watch` | Stream all errors continuously |
|
|
56
|
+
| `ghosttrap list [n]` | Print a numbered summary of the most recent `n` errors (default 10, max 50). Doesn't move the cursor. |
|
|
57
|
+
| `ghosttrap show <i>` | Full details for row `i` from the last `list` (1-based). Doesn't move the cursor. |
|
|
54
58
|
| `ghosttrap clear` | Skip all outstanding errors |
|
|
55
59
|
| `ghosttrap nuke` | Permanently delete every server-side row for the current repo (errors + token). Requires typed confirmation. |
|
|
56
60
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|