ghosttrap-cli 0.3.8__tar.gz → 0.3.10__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.8
3
+ Version: 0.3.10
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
 
@@ -2,6 +2,7 @@
2
2
 
3
3
  import argparse
4
4
  import asyncio
5
+ import hashlib
5
6
  import json
6
7
  import os
7
8
  import subprocess
@@ -11,6 +12,14 @@ import urllib.request
11
12
 
12
13
  import websockets
13
14
 
15
+ KNOWN_SKILL_HASHES = {
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
+ }
22
+
14
23
  __version__ = "0.3.0"
15
24
 
16
25
  GHOSTTRAP_SERVER = "wss://ghosttrap.io/stream/"
@@ -69,6 +78,7 @@ Read `~/.ghosttrap/config.json` for state. It contains:
69
78
 
70
79
  ## Other commands
71
80
 
81
+ - `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.
72
82
  - `ghosttrap clear` — manually skip outstanding errors without waiting. Useful if the user explicitly wants to drop the queue.
73
83
 
74
84
  ## Rules
@@ -259,9 +269,12 @@ def _refresh_skill_if_stale():
259
269
  if not os.path.exists(SKILL_FILE):
260
270
  return
261
271
  with open(SKILL_FILE) as f:
262
- if f.read() != SKILL_CONTENT:
263
- _write_skill()
264
- print("ghosttrap skill file updated", file=sys.stderr)
272
+ content = f.read()
273
+ if content == SKILL_CONTENT:
274
+ return
275
+ if hashlib.sha256(content.encode()).hexdigest() in KNOWN_SKILL_HASHES:
276
+ _write_skill()
277
+ print("ghosttrap skill file updated", file=sys.stderr)
265
278
 
266
279
 
267
280
  async def setup(server_url, token):
@@ -356,6 +369,46 @@ def clear():
356
369
  sys.exit(1)
357
370
 
358
371
 
372
+ def last(do_clear=False):
373
+ _require_setup()
374
+ config = _load_config()
375
+ _check_cli_version(config)
376
+ token = _get_repo_token(config)
377
+ server = GHOSTTRAP_SERVER.replace("wss://", "https://").replace("/stream/", "")
378
+ url = f"{server}/last/{token}/"
379
+ try:
380
+ req = urllib.request.Request(url, headers={"User-Agent": "ghosttrap-cli"})
381
+ with urllib.request.urlopen(req, timeout=10) as resp:
382
+ data = json.loads(resp.read())
383
+ except Exception as e:
384
+ print(f"error: {e}", file=sys.stderr)
385
+ sys.exit(1)
386
+
387
+ error = data.get("error")
388
+ if not error:
389
+ print("no errors yet", file=sys.stderr)
390
+ return
391
+
392
+ print(json.dumps({"type": "error", "error": error}))
393
+ sys.stdout.flush()
394
+
395
+ print(f"\n{'='*60}", file=sys.stderr)
396
+ print(f" {error.get('repo', '?')}", file=sys.stderr)
397
+ print(f" {error.get('type', '?')}: {error.get('message', '')}", file=sys.stderr)
398
+ frames = error.get("frames", [])
399
+ if frames:
400
+ f = frames[-1]
401
+ print(f" at {f.get('file', '?')}:{f.get('line', '?')} in {f.get('function', '?')}", file=sys.stderr)
402
+ print(f"{'='*60}", file=sys.stderr)
403
+
404
+ if do_clear:
405
+ try:
406
+ _advance_cursor(config, token)
407
+ except Exception as e:
408
+ print(f"error: {e}", file=sys.stderr)
409
+ sys.exit(1)
410
+
411
+
359
412
  def main():
360
413
  parser = argparse.ArgumentParser(prog="ghosttrap", description="Watch for errors from ghosttrap.io")
361
414
  sub = parser.add_subparsers(dest="command")
@@ -370,6 +423,9 @@ def main():
370
423
  peek_parser.add_argument("--server", default=GHOSTTRAP_SERVER, help="WebSocket server URL")
371
424
  peek_parser.add_argument("--clear", action="store_true", help="Skip outstanding errors before waiting")
372
425
 
426
+ last_parser = sub.add_parser("last", help="Fetch the most recent error then exit")
427
+ last_parser.add_argument("--clear", action="store_true", help="Also skip remaining outstanding errors")
428
+
373
429
  args = parser.parse_args()
374
430
 
375
431
  if args.command == "setup":
@@ -395,6 +451,9 @@ def main():
395
451
  print(f"error: {e}", file=sys.stderr)
396
452
  sys.exit(1)
397
453
  asyncio.run(peek(args.server, token))
454
+ elif args.command == "last":
455
+ _refresh_skill_if_stale()
456
+ last(do_clear=args.clear)
398
457
  else:
399
458
  parser.print_help()
400
459
  sys.exit(1)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ghosttrap-cli
3
- Version: 0.3.8
3
+ Version: 0.3.10
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.8"
7
+ version = "0.3.10"
8
8
  description = "Watch for errors streaming from ghosttrap.io"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
File without changes