ghosttrap-cli 0.3.9__tar.gz → 0.3.11__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.9 → ghosttrap_cli-0.3.11}/PKG-INFO +3 -1
- {ghosttrap_cli-0.3.9 → ghosttrap_cli-0.3.11}/README.md +2 -0
- {ghosttrap_cli-0.3.9 → ghosttrap_cli-0.3.11}/ghosttrap_cli/cli.py +94 -19
- {ghosttrap_cli-0.3.9 → ghosttrap_cli-0.3.11}/ghosttrap_cli.egg-info/PKG-INFO +3 -1
- {ghosttrap_cli-0.3.9 → ghosttrap_cli-0.3.11}/pyproject.toml +1 -1
- {ghosttrap_cli-0.3.9 → ghosttrap_cli-0.3.11}/ghosttrap_cli/__init__.py +0 -0
- {ghosttrap_cli-0.3.9 → ghosttrap_cli-0.3.11}/ghosttrap_cli.egg-info/SOURCES.txt +0 -0
- {ghosttrap_cli-0.3.9 → ghosttrap_cli-0.3.11}/ghosttrap_cli.egg-info/dependency_links.txt +0 -0
- {ghosttrap_cli-0.3.9 → ghosttrap_cli-0.3.11}/ghosttrap_cli.egg-info/entry_points.txt +0 -0
- {ghosttrap_cli-0.3.9 → ghosttrap_cli-0.3.11}/ghosttrap_cli.egg-info/requires.txt +0 -0
- {ghosttrap_cli-0.3.9 → ghosttrap_cli-0.3.11}/ghosttrap_cli.egg-info/top_level.txt +0 -0
- {ghosttrap_cli-0.3.9 → ghosttrap_cli-0.3.11}/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.11
|
|
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
|
|
@@ -48,6 +48,8 @@ Your app needs [ghosttrap-sdk](https://github.com/alex-rowley/ghosttrap-sdk) to
|
|
|
48
48
|
| `ghosttrap setup` | Claim a repo, install the Claude Code skill |
|
|
49
49
|
| `ghosttrap peek` | Wait for the next error, print it, exit |
|
|
50
50
|
| `ghosttrap peek --clear` | Skip outstanding errors, then wait for the next one |
|
|
51
|
+
| `ghosttrap last` | Fetch the most recent error and exit (no waiting) |
|
|
52
|
+
| `ghosttrap last --clear` | Fetch the most recent error and skip everything older |
|
|
51
53
|
| `ghosttrap watch` | Stream all errors continuously |
|
|
52
54
|
| `ghosttrap clear` | Skip all outstanding errors |
|
|
53
55
|
|
|
@@ -39,6 +39,8 @@ Your app needs [ghosttrap-sdk](https://github.com/alex-rowley/ghosttrap-sdk) to
|
|
|
39
39
|
| `ghosttrap setup` | Claim a repo, install the Claude Code skill |
|
|
40
40
|
| `ghosttrap peek` | Wait for the next error, print it, exit |
|
|
41
41
|
| `ghosttrap peek --clear` | Skip outstanding errors, then wait for the next one |
|
|
42
|
+
| `ghosttrap last` | Fetch the most recent error and exit (no waiting) |
|
|
43
|
+
| `ghosttrap last --clear` | Fetch the most recent error and skip everything older |
|
|
42
44
|
| `ghosttrap watch` | Stream all errors continuously |
|
|
43
45
|
| `ghosttrap clear` | Skip all outstanding errors |
|
|
44
46
|
|
|
@@ -17,9 +17,11 @@ KNOWN_SKILL_HASHES = {
|
|
|
17
17
|
"0f2d2f4105e393fc69084d404d5a8154ba5d97fd23f92810c51345e3dc68e9a0", # v0.3.0
|
|
18
18
|
"8564b65b8ab5c63283cda1706e30ca62bc4e111d33ba8918220f4b556ad01da1", # v0.3.1..v0.3.3
|
|
19
19
|
"5759b2e0dc8ca47c3801915fd688cc8da878a7ab8d405f5183ffd7e8c8df4c55", # v0.3.4..v0.3.7
|
|
20
|
+
"0651bb4247cf5c68960ff5b63d6a5d0c85ff1ce08e7966ab4823601ff02cf1f4", # v0.3.9
|
|
21
|
+
"38810f43867a2a91420cc3dacbc71d2acabd7125596fd5b43f222b49725c9696", # v0.3.10
|
|
20
22
|
}
|
|
21
23
|
|
|
22
|
-
__version__ = "0.3.
|
|
24
|
+
__version__ = "0.3.11"
|
|
23
25
|
|
|
24
26
|
GHOSTTRAP_SERVER = "wss://ghosttrap.io/stream/"
|
|
25
27
|
CONFIG_DIR = os.path.expanduser("~/.ghosttrap")
|
|
@@ -59,13 +61,13 @@ description: Production error monitoring via ghosttrap.io. Trigger when starting
|
|
|
59
61
|
# Ghosttrap
|
|
60
62
|
|
|
61
63
|
Read `~/.ghosttrap/config.json` for state. It contains:
|
|
62
|
-
- `repos`: map
|
|
64
|
+
- `repos`: map keyed by GitHub repo id (stringified int) to `{"github_id": int, "owner": str, "name": str, "token": "t_xxx", "sdk_installed": bool, "sdk_version": str, "init_file": str}`. Older configs may still be keyed by `"owner/name"` — same shape inside.
|
|
63
65
|
- `cursor`: last seen error ID
|
|
64
66
|
|
|
65
67
|
## On session start
|
|
66
68
|
|
|
67
|
-
1. Detect the current repo from `git config --get remote.origin.url
|
|
68
|
-
2.
|
|
69
|
+
1. Detect the current repo from `git config --get remote.origin.url` (returns `owner/name`).
|
|
70
|
+
2. Find a matching entry in config by looking for one whose `owner`/`name` equals the detected slug. If no match, tell the user to run `ghosttrap setup`. (The owner/name on a config entry auto-refreshes from the server when the repo is renamed or transferred, so always match against the entry's stored owner/name, not the config key.)
|
|
69
71
|
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.
|
|
70
72
|
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.
|
|
71
73
|
|
|
@@ -77,6 +79,7 @@ Read `~/.ghosttrap/config.json` for state. It contains:
|
|
|
77
79
|
|
|
78
80
|
## Other commands
|
|
79
81
|
|
|
82
|
+
- `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.
|
|
80
83
|
- `ghosttrap clear` — manually skip outstanding errors without waiting. Useful if the user explicitly wants to drop the queue.
|
|
81
84
|
|
|
82
85
|
## Rules
|
|
@@ -101,16 +104,35 @@ def _save_config(config):
|
|
|
101
104
|
json.dump(config, f, indent=2)
|
|
102
105
|
|
|
103
106
|
|
|
104
|
-
def
|
|
105
|
-
|
|
107
|
+
def _repo_key(r):
|
|
108
|
+
gid = r.get("github_id")
|
|
109
|
+
if gid is not None:
|
|
110
|
+
return str(gid)
|
|
111
|
+
return f"{r.get('owner')}/{r.get('name')}"
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def _is_known_repo(config, repo_entry):
|
|
115
|
+
return _repo_key(repo_entry) in config.get("repos", {})
|
|
106
116
|
|
|
107
117
|
|
|
108
118
|
def _save_repos(config, repos):
|
|
109
119
|
if "repos" not in config:
|
|
110
120
|
config["repos"] = {}
|
|
111
121
|
for r in repos:
|
|
112
|
-
key =
|
|
113
|
-
config["repos"]
|
|
122
|
+
key = _repo_key(r)
|
|
123
|
+
existing = config["repos"].get(key, {})
|
|
124
|
+
# Drop any legacy slug-keyed entry now superseded by a github_id key.
|
|
125
|
+
if r.get("github_id") is not None:
|
|
126
|
+
legacy_key = f"{r.get('owner')}/{r.get('name')}"
|
|
127
|
+
if legacy_key in config["repos"] and legacy_key != key:
|
|
128
|
+
existing = {**config["repos"].pop(legacy_key), **existing}
|
|
129
|
+
existing.update({
|
|
130
|
+
"github_id": r.get("github_id"),
|
|
131
|
+
"owner": r["owner"],
|
|
132
|
+
"name": r["name"],
|
|
133
|
+
"token": r["token"],
|
|
134
|
+
})
|
|
135
|
+
config["repos"][key] = existing
|
|
114
136
|
_save_config(config)
|
|
115
137
|
|
|
116
138
|
|
|
@@ -143,7 +165,7 @@ def _find_target_repo(repos):
|
|
|
143
165
|
cwd_slug = _detect_repo_from_cwd()
|
|
144
166
|
if cwd_slug:
|
|
145
167
|
for r in repos:
|
|
146
|
-
if f"{r
|
|
168
|
+
if f"{r.get('owner')}/{r.get('name')}" == cwd_slug:
|
|
147
169
|
return r
|
|
148
170
|
return repos[0] if repos else None
|
|
149
171
|
|
|
@@ -182,11 +204,14 @@ def get_gh_token():
|
|
|
182
204
|
|
|
183
205
|
def _get_repo_token(config):
|
|
184
206
|
"""Get the repo token for the current directory from config."""
|
|
185
|
-
cwd_repo = _detect_repo_from_cwd()
|
|
186
|
-
if cwd_repo and cwd_repo in config.get("repos", {}):
|
|
187
|
-
return config["repos"][cwd_repo]["token"]
|
|
188
|
-
# Fall back to first repo in config
|
|
189
207
|
repos = config.get("repos", {})
|
|
208
|
+
cwd_repo = _detect_repo_from_cwd()
|
|
209
|
+
if cwd_repo:
|
|
210
|
+
for entry in repos.values():
|
|
211
|
+
if f"{entry.get('owner')}/{entry.get('name')}" == cwd_repo:
|
|
212
|
+
return entry["token"]
|
|
213
|
+
if cwd_repo in repos:
|
|
214
|
+
return repos[cwd_repo]["token"]
|
|
190
215
|
if repos:
|
|
191
216
|
return next(iter(repos.values()))["token"]
|
|
192
217
|
print("error: no repos configured. run 'ghosttrap setup' first.", file=sys.stderr)
|
|
@@ -208,9 +233,10 @@ async def _connect_and_handle(server_url, token, config, once=False):
|
|
|
208
233
|
repos = event.get("repos", [])
|
|
209
234
|
print(f"watching {len(repos)} repo(s)", file=sys.stderr)
|
|
210
235
|
|
|
211
|
-
new_repos = [r for r in repos if not _is_known_repo(config, r
|
|
236
|
+
new_repos = [r for r in repos if not _is_known_repo(config, r)]
|
|
237
|
+
# Always sync — picks up renamed/transferred repos by github_id.
|
|
238
|
+
_save_repos(config, repos)
|
|
212
239
|
if new_repos:
|
|
213
|
-
_save_repos(config, repos)
|
|
214
240
|
target = _find_target_repo(new_repos)
|
|
215
241
|
if target:
|
|
216
242
|
_print_setup_snippet(target)
|
|
@@ -218,10 +244,13 @@ async def _connect_and_handle(server_url, token, config, once=False):
|
|
|
218
244
|
sdk_latest = event.get("sdk_latest")
|
|
219
245
|
if sdk_latest:
|
|
220
246
|
cwd_repo = _detect_repo_from_cwd()
|
|
221
|
-
if cwd_repo
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
247
|
+
if cwd_repo:
|
|
248
|
+
for entry in config.get("repos", {}).values():
|
|
249
|
+
if f"{entry.get('owner')}/{entry.get('name')}" == cwd_repo:
|
|
250
|
+
installed = entry.get("sdk_version")
|
|
251
|
+
if installed and installed != sdk_latest:
|
|
252
|
+
print(f"ghosttrap-sdk {sdk_latest} available (you have {installed})", file=sys.stderr)
|
|
253
|
+
break
|
|
225
254
|
|
|
226
255
|
if not once:
|
|
227
256
|
print(f"waiting for errors...", file=sys.stderr)
|
|
@@ -367,6 +396,46 @@ def clear():
|
|
|
367
396
|
sys.exit(1)
|
|
368
397
|
|
|
369
398
|
|
|
399
|
+
def last(do_clear=False):
|
|
400
|
+
_require_setup()
|
|
401
|
+
config = _load_config()
|
|
402
|
+
_check_cli_version(config)
|
|
403
|
+
token = _get_repo_token(config)
|
|
404
|
+
server = GHOSTTRAP_SERVER.replace("wss://", "https://").replace("/stream/", "")
|
|
405
|
+
url = f"{server}/last/{token}/"
|
|
406
|
+
try:
|
|
407
|
+
req = urllib.request.Request(url, headers={"User-Agent": "ghosttrap-cli"})
|
|
408
|
+
with urllib.request.urlopen(req, timeout=10) as resp:
|
|
409
|
+
data = json.loads(resp.read())
|
|
410
|
+
except Exception as e:
|
|
411
|
+
print(f"error: {e}", file=sys.stderr)
|
|
412
|
+
sys.exit(1)
|
|
413
|
+
|
|
414
|
+
error = data.get("error")
|
|
415
|
+
if not error:
|
|
416
|
+
print("no errors yet", file=sys.stderr)
|
|
417
|
+
return
|
|
418
|
+
|
|
419
|
+
print(json.dumps({"type": "error", "error": error}))
|
|
420
|
+
sys.stdout.flush()
|
|
421
|
+
|
|
422
|
+
print(f"\n{'='*60}", file=sys.stderr)
|
|
423
|
+
print(f" {error.get('repo', '?')}", file=sys.stderr)
|
|
424
|
+
print(f" {error.get('type', '?')}: {error.get('message', '')}", file=sys.stderr)
|
|
425
|
+
frames = error.get("frames", [])
|
|
426
|
+
if frames:
|
|
427
|
+
f = frames[-1]
|
|
428
|
+
print(f" at {f.get('file', '?')}:{f.get('line', '?')} in {f.get('function', '?')}", file=sys.stderr)
|
|
429
|
+
print(f"{'='*60}", file=sys.stderr)
|
|
430
|
+
|
|
431
|
+
if do_clear:
|
|
432
|
+
try:
|
|
433
|
+
_advance_cursor(config, token)
|
|
434
|
+
except Exception as e:
|
|
435
|
+
print(f"error: {e}", file=sys.stderr)
|
|
436
|
+
sys.exit(1)
|
|
437
|
+
|
|
438
|
+
|
|
370
439
|
def main():
|
|
371
440
|
parser = argparse.ArgumentParser(prog="ghosttrap", description="Watch for errors from ghosttrap.io")
|
|
372
441
|
sub = parser.add_subparsers(dest="command")
|
|
@@ -381,6 +450,9 @@ def main():
|
|
|
381
450
|
peek_parser.add_argument("--server", default=GHOSTTRAP_SERVER, help="WebSocket server URL")
|
|
382
451
|
peek_parser.add_argument("--clear", action="store_true", help="Skip outstanding errors before waiting")
|
|
383
452
|
|
|
453
|
+
last_parser = sub.add_parser("last", help="Fetch the most recent error then exit")
|
|
454
|
+
last_parser.add_argument("--clear", action="store_true", help="Also skip remaining outstanding errors")
|
|
455
|
+
|
|
384
456
|
args = parser.parse_args()
|
|
385
457
|
|
|
386
458
|
if args.command == "setup":
|
|
@@ -406,6 +478,9 @@ def main():
|
|
|
406
478
|
print(f"error: {e}", file=sys.stderr)
|
|
407
479
|
sys.exit(1)
|
|
408
480
|
asyncio.run(peek(args.server, token))
|
|
481
|
+
elif args.command == "last":
|
|
482
|
+
_refresh_skill_if_stale()
|
|
483
|
+
last(do_clear=args.clear)
|
|
409
484
|
else:
|
|
410
485
|
parser.print_help()
|
|
411
486
|
sys.exit(1)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: ghosttrap-cli
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.11
|
|
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
|
|
@@ -48,6 +48,8 @@ Your app needs [ghosttrap-sdk](https://github.com/alex-rowley/ghosttrap-sdk) to
|
|
|
48
48
|
| `ghosttrap setup` | Claim a repo, install the Claude Code skill |
|
|
49
49
|
| `ghosttrap peek` | Wait for the next error, print it, exit |
|
|
50
50
|
| `ghosttrap peek --clear` | Skip outstanding errors, then wait for the next one |
|
|
51
|
+
| `ghosttrap last` | Fetch the most recent error and exit (no waiting) |
|
|
52
|
+
| `ghosttrap last --clear` | Fetch the most recent error and skip everything older |
|
|
51
53
|
| `ghosttrap watch` | Stream all errors continuously |
|
|
52
54
|
| `ghosttrap clear` | Skip all outstanding errors |
|
|
53
55
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|