ghosttrap-cli 0.3.20__tar.gz → 0.3.22__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.20
3
+ Version: 0.3.22
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
@@ -52,18 +52,19 @@ If you want to manually flag a caught exception or a non-exception condition, ca
52
52
  | `ghosttrap peek --clear` | Skip outstanding errors, then wait for the next one |
53
53
  | `ghosttrap last` | Fetch the most recent error and exit (no waiting) |
54
54
  | `ghosttrap last --clear` | Fetch the most recent error and skip everything older |
55
- | `ghosttrap watch` | Stream all errors continuously |
55
+ | `ghosttrap watch` | Deprecated `peek` reconnects until an error arrives, which covers the streaming case |
56
56
  | `ghosttrap list [n]` | Print a numbered summary of the most recent `n` errors (default 10, max 50). Doesn't move the cursor. |
57
57
  | `ghosttrap show <i>` | Full details for row `i` from the last `list` (1-based). Doesn't move the cursor. |
58
58
  | `ghosttrap clear` | Skip all outstanding errors |
59
59
  | `ghosttrap nuke` | Permanently delete every server-side row for the current repo (errors + token). Requires typed confirmation. |
60
60
 
61
- `peek`, `watch`, `last`, and `clear` accept `--repo owner/name` to target a specific claimed repo when you're not inside its working tree (e.g. `ghosttrap peek --repo alex-rowley/ghosttrap-cli`). Otherwise they detect the repo from cwd. `nuke` is intentionally cwd-locked.
61
+ Every command except `setup` and `nuke` accepts `--repo owner/name` to target a specific claimed repo when you're not inside its working tree (e.g. `ghosttrap peek --repo alex-rowley/ghosttrap-cli`). Otherwise the repo is detected from cwd. `nuke` is intentionally cwd-locked.
62
62
 
63
63
  ## How it works
64
64
 
65
65
  - **Setup** authenticates with GitHub (via the active `gh` account) to prove you have access to the repo, then saves a repo token locally. If your active `gh` account can't see the repo, setup fails with a clear message; switch with `gh auth switch` and retry.
66
- - **Peek** and **watch** connect to ghosttrap.io using that token — no GitHub auth needed after setup
66
+ - **Peek** connects to ghosttrap.io using that token — no GitHub auth needed after setup
67
+ - If the connection drops or the server closes an idle socket, peek reconnects with a 60-second backoff for as long as it runs — it only exits once it has delivered an error (or hit a real failure, which it reports on stderr)
67
68
  - Errors that arrive while you're offline are replayed on next connect (cursor-based, no duplicates)
68
69
  - Repos are tracked by GitHub's immutable repo id, so a rename or transfer doesn't require any action — the next connect picks up the new `owner/name` and your token keeps working
69
70
  - Local state is stored in `~/.ghosttrap/config.json`, keyed by GitHub repo id
@@ -43,18 +43,19 @@ If you want to manually flag a caught exception or a non-exception condition, ca
43
43
  | `ghosttrap peek --clear` | Skip outstanding errors, then wait for the next one |
44
44
  | `ghosttrap last` | Fetch the most recent error and exit (no waiting) |
45
45
  | `ghosttrap last --clear` | Fetch the most recent error and skip everything older |
46
- | `ghosttrap watch` | Stream all errors continuously |
46
+ | `ghosttrap watch` | Deprecated `peek` reconnects until an error arrives, which covers the streaming case |
47
47
  | `ghosttrap list [n]` | Print a numbered summary of the most recent `n` errors (default 10, max 50). Doesn't move the cursor. |
48
48
  | `ghosttrap show <i>` | Full details for row `i` from the last `list` (1-based). Doesn't move the cursor. |
49
49
  | `ghosttrap clear` | Skip all outstanding errors |
50
50
  | `ghosttrap nuke` | Permanently delete every server-side row for the current repo (errors + token). Requires typed confirmation. |
51
51
 
52
- `peek`, `watch`, `last`, and `clear` accept `--repo owner/name` to target a specific claimed repo when you're not inside its working tree (e.g. `ghosttrap peek --repo alex-rowley/ghosttrap-cli`). Otherwise they detect the repo from cwd. `nuke` is intentionally cwd-locked.
52
+ Every command except `setup` and `nuke` accepts `--repo owner/name` to target a specific claimed repo when you're not inside its working tree (e.g. `ghosttrap peek --repo alex-rowley/ghosttrap-cli`). Otherwise the repo is detected from cwd. `nuke` is intentionally cwd-locked.
53
53
 
54
54
  ## How it works
55
55
 
56
56
  - **Setup** authenticates with GitHub (via the active `gh` account) to prove you have access to the repo, then saves a repo token locally. If your active `gh` account can't see the repo, setup fails with a clear message; switch with `gh auth switch` and retry.
57
- - **Peek** and **watch** connect to ghosttrap.io using that token — no GitHub auth needed after setup
57
+ - **Peek** connects to ghosttrap.io using that token — no GitHub auth needed after setup
58
+ - If the connection drops or the server closes an idle socket, peek reconnects with a 60-second backoff for as long as it runs — it only exits once it has delivered an error (or hit a real failure, which it reports on stderr)
58
59
  - Errors that arrive while you're offline are replayed on next connect (cursor-based, no duplicates)
59
60
  - Repos are tracked by GitHub's immutable repo id, so a rename or transfer doesn't require any action — the next connect picks up the new `owner/name` and your token keeps working
60
61
  - Local state is stored in `~/.ghosttrap/config.json`, keyed by GitHub repo id
@@ -4,6 +4,7 @@ import argparse
4
4
  import asyncio
5
5
  import json
6
6
  import os
7
+ import signal
7
8
  import subprocess
8
9
  import sys
9
10
  import tempfile
@@ -13,7 +14,20 @@ import urllib.request
13
14
 
14
15
  import websockets
15
16
 
16
- __version__ = "0.3.20"
17
+
18
+ def _harden_signals():
19
+ """Explicitly ignore SIGURG so no process supervisor can nudge peek out
20
+ with an out-of-band signal. POSIX default is already ignore; this makes
21
+ it defensive against layers that change the disposition.
22
+ """
23
+ s = getattr(signal, "SIGURG", None)
24
+ if s is not None:
25
+ try:
26
+ signal.signal(s, signal.SIG_IGN)
27
+ except (OSError, ValueError):
28
+ pass
29
+
30
+ __version__ = "0.3.22"
17
31
 
18
32
  GHOSTTRAP_SERVER = "wss://ghosttrap.io/stream/"
19
33
  CONFIG_DIR = os.path.expanduser("~/.ghosttrap")
@@ -53,8 +67,9 @@ description: Production error monitoring via ghosttrap.io. Trigger when starting
53
67
  # Ghosttrap
54
68
 
55
69
  Read `~/.ghosttrap/config.json` for state. It contains:
56
- - `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}`.
70
+ - `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}`. Entries may also carry `recent`, the error ids cached by the last `ghosttrap list` (managed by the CLI — leave it alone).
57
71
  - `cursor`: last seen error ID
72
+ - `skill_baseline`: the previous release's skill text, used to 3-way-merge skill updates with local edits. Never edit or delete it.
58
73
 
59
74
  ## On session start
60
75
 
@@ -75,17 +90,19 @@ For caught exceptions or non-exception conditions the user explicitly wants repo
75
90
 
76
91
  ## Other commands
77
92
 
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.
93
+ - `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 blocking on a peek. Add `--clear` to also skip everything older in one shot.
79
94
  - `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
95
  - `ghosttrap show <i>` — full details for the i-th row from the most recent `ghosttrap list`. Does not move the cursor.
81
96
  - `ghosttrap clear` — manually skip outstanding errors without waiting. Useful if the user explicitly wants to drop the queue.
82
97
  - `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.
83
98
 
99
+ `peek` and every command above except `nuke` accept `--repo owner/name` to target another claimed repo when the cwd isn't inside it (e.g. `ghosttrap list --repo owner/name`).
100
+
84
101
  ## Rules
85
102
 
86
103
  - Always `run_in_background: true` for peek — it blocks.
87
104
  - Don't run multiple peeks at once.
88
- - If peek exits without output (connection lost), restart it.
105
+ - Peek reconnects by itself (60s backoff) when the connection drops — a quiet peek is waiting, not hung. It only exits after printing an error event, or with a message on stderr if something is actually wrong; restart it only in that second case.
89
106
  - After installing/updating the SDK, write the state back to config.json.
90
107
  """
91
108
 
@@ -416,6 +433,31 @@ async def setup(server_url, token):
416
433
  sys.exit(1)
417
434
 
418
435
 
436
+ # Exception types we knowingly retry on. All represent a transient network/transport
437
+ # blip rather than a semantic error from the server:
438
+ # - ConnectionClosed: peer closed the WebSocket (either side of the handshake)
439
+ # - InvalidStatus: non-101 HTTP response during upgrade (e.g. 502 from a proxy)
440
+ # - ConnectionError: builtin — refused, reset, unreachable
441
+ # - OSError: DNS failure, transient socket errors (gaierror is a subclass)
442
+ # Anything else escapes and prints a diagnostic line first so we can add it here
443
+ # in the next release. Semantic rejections from the server ({"type": "rejected"})
444
+ # raise SystemExit, which we deliberately do NOT catch — those are real errors.
445
+ _RETRYABLE = (
446
+ websockets.ConnectionClosed,
447
+ websockets.InvalidStatus,
448
+ ConnectionError,
449
+ OSError,
450
+ )
451
+
452
+
453
+ def _log_unexpected(e):
454
+ print(
455
+ f"unexpected {type(e).__module__}.{type(e).__name__}: {e} — "
456
+ f"not currently in the retry list; please report so we can add it.",
457
+ file=sys.stderr,
458
+ )
459
+
460
+
419
461
  async def watch(server_url, token):
420
462
  config = _load_config()
421
463
  print(f"connecting to {server_url}...", file=sys.stderr)
@@ -424,8 +466,11 @@ async def watch(server_url, token):
424
466
  try:
425
467
  await _connect_and_handle(server_url, token, config, once=False)
426
468
  print("connection closed by server, reconnecting...", file=sys.stderr)
427
- except (websockets.ConnectionClosed, websockets.InvalidStatus, ConnectionError, OSError):
469
+ except _RETRYABLE:
428
470
  print("connection lost, reconnecting...", file=sys.stderr)
471
+ except Exception as e:
472
+ _log_unexpected(e)
473
+ raise
429
474
  await asyncio.sleep(60)
430
475
 
431
476
 
@@ -438,8 +483,11 @@ async def peek(server_url, token):
438
483
  if got_error:
439
484
  return
440
485
  print("connection closed by server, reconnecting...", file=sys.stderr)
441
- except (websockets.ConnectionClosed, websockets.InvalidStatus, ConnectionError, OSError):
486
+ except _RETRYABLE:
442
487
  print("connection lost, reconnecting...", file=sys.stderr)
488
+ except Exception as e:
489
+ _log_unexpected(e)
490
+ raise
443
491
  await asyncio.sleep(60)
444
492
 
445
493
 
@@ -676,6 +724,7 @@ def last(do_clear=False, requested=None):
676
724
 
677
725
 
678
726
  def main():
727
+ _harden_signals()
679
728
  parser = argparse.ArgumentParser(prog="ghosttrap", description="Watch for errors from ghosttrap.io")
680
729
  sub = parser.add_subparsers(dest="command")
681
730
 
@@ -684,7 +733,7 @@ def main():
684
733
  clear_parser = sub.add_parser("clear", help="Skip all outstanding errors")
685
734
  clear_parser.add_argument("--repo", help="Target repo as owner/name (overrides cwd detection)")
686
735
 
687
- watch_parser = sub.add_parser("watch", help="Stream errors in real time")
736
+ watch_parser = sub.add_parser("watch", help="Deprecated: stream errors in real time (use peek)")
688
737
  watch_parser.add_argument("--server", default=GHOSTTRAP_SERVER, help="WebSocket server URL")
689
738
  watch_parser.add_argument("--repo", help="Target repo as owner/name (overrides cwd detection)")
690
739
 
@@ -715,6 +764,11 @@ def main():
715
764
  elif args.command == "clear":
716
765
  clear(requested=args.repo)
717
766
  elif args.command == "watch":
767
+ print(
768
+ "warning: 'watch' is deprecated and may be removed in a future release — "
769
+ "'peek' now reconnects until an error arrives.",
770
+ file=sys.stderr,
771
+ )
718
772
  _require_setup()
719
773
  _refresh_skill_if_stale()
720
774
  config = _load_config()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ghosttrap-cli
3
- Version: 0.3.20
3
+ Version: 0.3.22
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
@@ -52,18 +52,19 @@ If you want to manually flag a caught exception or a non-exception condition, ca
52
52
  | `ghosttrap peek --clear` | Skip outstanding errors, then wait for the next one |
53
53
  | `ghosttrap last` | Fetch the most recent error and exit (no waiting) |
54
54
  | `ghosttrap last --clear` | Fetch the most recent error and skip everything older |
55
- | `ghosttrap watch` | Stream all errors continuously |
55
+ | `ghosttrap watch` | Deprecated `peek` reconnects until an error arrives, which covers the streaming case |
56
56
  | `ghosttrap list [n]` | Print a numbered summary of the most recent `n` errors (default 10, max 50). Doesn't move the cursor. |
57
57
  | `ghosttrap show <i>` | Full details for row `i` from the last `list` (1-based). Doesn't move the cursor. |
58
58
  | `ghosttrap clear` | Skip all outstanding errors |
59
59
  | `ghosttrap nuke` | Permanently delete every server-side row for the current repo (errors + token). Requires typed confirmation. |
60
60
 
61
- `peek`, `watch`, `last`, and `clear` accept `--repo owner/name` to target a specific claimed repo when you're not inside its working tree (e.g. `ghosttrap peek --repo alex-rowley/ghosttrap-cli`). Otherwise they detect the repo from cwd. `nuke` is intentionally cwd-locked.
61
+ Every command except `setup` and `nuke` accepts `--repo owner/name` to target a specific claimed repo when you're not inside its working tree (e.g. `ghosttrap peek --repo alex-rowley/ghosttrap-cli`). Otherwise the repo is detected from cwd. `nuke` is intentionally cwd-locked.
62
62
 
63
63
  ## How it works
64
64
 
65
65
  - **Setup** authenticates with GitHub (via the active `gh` account) to prove you have access to the repo, then saves a repo token locally. If your active `gh` account can't see the repo, setup fails with a clear message; switch with `gh auth switch` and retry.
66
- - **Peek** and **watch** connect to ghosttrap.io using that token — no GitHub auth needed after setup
66
+ - **Peek** connects to ghosttrap.io using that token — no GitHub auth needed after setup
67
+ - If the connection drops or the server closes an idle socket, peek reconnects with a 60-second backoff for as long as it runs — it only exits once it has delivered an error (or hit a real failure, which it reports on stderr)
67
68
  - Errors that arrive while you're offline are replayed on next connect (cursor-based, no duplicates)
68
69
  - Repos are tracked by GitHub's immutable repo id, so a rename or transfer doesn't require any action — the next connect picks up the new `owner/name` and your token keeps working
69
70
  - Local state is stored in `~/.ghosttrap/config.json`, keyed by GitHub repo id
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "ghosttrap-cli"
7
- version = "0.3.20"
7
+ version = "0.3.22"
8
8
  description = "Watch for errors streaming from ghosttrap.io"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
File without changes