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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ghosttrap-cli
3
- Version: 0.3.9
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.0"
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 of `"owner/repo"` to `{"token": "t_xxx", "sdk_installed": bool, "sdk_version": str, "init_file": str}`
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. Look it up in the config. If the repo isn't there, tell the user to run `ghosttrap setup`.
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 _is_known_repo(config, owner, name):
105
- return f"{owner}/{name}" in config.get("repos", {})
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 = f"{r['owner']}/{r['name']}"
113
- config["repos"][key] = {"token": r["token"]}
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['owner']}/{r['name']}" == cwd_slug:
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["owner"], r["name"])]
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 and cwd_repo in config.get("repos", {}):
222
- installed = config["repos"][cwd_repo].get("sdk_version")
223
- if installed and installed != sdk_latest:
224
- print(f"ghosttrap-sdk {sdk_latest} available (you have {installed})", file=sys.stderr)
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.9
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
 
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "ghosttrap-cli"
7
- version = "0.3.9"
7
+ version = "0.3.11"
8
8
  description = "Watch for errors streaming from ghosttrap.io"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
File without changes