ghostbit-cli 1.0.0__tar.gz → 1.1.0__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: ghostbit-cli
3
- Version: 1.0.0
3
+ Version: 1.1.0
4
4
  Summary: Ghostbit CLI — create end-to-end encrypted pastes from the terminal
5
5
  License-Expression: MIT
6
6
  Project-URL: Homepage, https://github.com/stackopshq/ghostbit
@@ -43,26 +43,43 @@ All content is encrypted **in the client** before being sent to the server. The
43
43
  pip install ghostbit-cli
44
44
  ```
45
45
 
46
+ With syntax highlighting and Markdown rendering:
47
+
48
+ ```bash
49
+ pip install "ghostbit-cli[all]" # pygments + rich
50
+ pip install "ghostbit-cli[color]" # pygments only (syntax highlighting)
51
+ pip install "ghostbit-cli[markdown]" # rich only (Markdown rendering)
52
+ ```
53
+
46
54
  ## Usage
47
55
 
48
56
  ```bash
49
57
  # Paste from stdin
50
- cat file.py | gb
58
+ cat file.py | gbit
51
59
 
52
60
  # Paste a file (language auto-detected from extension)
53
- gb file.py
61
+ gbit file.py
54
62
 
55
63
  # With options
56
- gb file.py --lang python --burn --expires 3600
64
+ gbit file.py --lang python --burn --expires 3600
65
+
66
+ # Password-protected paste (prompted securely)
67
+ echo "secret" | gbit -p
57
68
 
58
- # Password-protected paste
59
- echo "secret" | gb --password mysecret
69
+ # Or pass inline (visible in process list)
70
+ gbit file.py --password mysecret
71
+
72
+ # View and decrypt a paste in the terminal
73
+ gbit view https://paste.example.com/abc123#KEY~TOKEN
74
+
75
+ # View a password-protected paste (prompts for password)
76
+ gbit view https://paste.example.com/abc123#~TOKEN
60
77
 
61
78
  # Output JSON (includes full URL with decryption key)
62
- cat data.json | gb --json
79
+ cat data.json | gbit --json
63
80
 
64
81
  # Point to your self-hosted instance
65
- gb config set server https://paste.example.com
82
+ gbit config set server https://paste.example.com
66
83
  ```
67
84
 
68
85
  ## Options
@@ -73,21 +90,27 @@ gb config set server https://paste.example.com
73
90
  | `--expires` | `-e` | TTL in seconds (3600 = 1h, 86400 = 1d) |
74
91
  | `--burn` | `-b` | Delete after the first view |
75
92
  | `--max-views` | `-m` | Delete after N views |
76
- | `--password` | `-p` | Encrypt with a password |
93
+ | `--password` | `-p` | Encrypt with a password (prompted if no value given) |
77
94
  | `--server` | `-s` | Override server URL for this call |
78
95
  | `--quiet` | `-q` | Print URL only |
79
96
  | `--json` | | Print full JSON response |
97
+ | `--no-history` | | Don't save to local history |
98
+ | `--version` | `-V` | Print version and exit |
80
99
 
81
100
  ## Configuration
82
101
 
83
102
  ```bash
84
- gb config set server https://paste.example.com
85
- gb config show
86
- gb config unset server
103
+ gbit config set server https://paste.example.com
104
+ gbit config show
105
+ gbit config unset server
87
106
  ```
88
107
 
89
108
  Config is stored at `~/.config/ghostbit.toml`.
90
109
 
110
+ ## Security Note
111
+
112
+ The local history (`~/.local/share/ghostbit/history.jsonl`) stores full URLs **including decryption keys**. Use `--no-history` for sensitive pastes, or clear history with `gbit list --clear`.
113
+
91
114
  ## Self-hosting
92
115
 
93
116
  See the [Ghostbit server repository](https://github.com/stackopshq/ghostbit) for Docker and manual setup instructions.
@@ -0,0 +1,87 @@
1
+ # Ghostbit CLI
2
+
3
+ Command-line tool for [Ghostbit](https://github.com/stackopshq/ghostbit) — a self-hosted, end-to-end encrypted paste service.
4
+
5
+ All content is encrypted **in the client** before being sent to the server. The server never sees your plaintext.
6
+
7
+ ## Install
8
+
9
+ ```bash
10
+ pip install ghostbit-cli
11
+ ```
12
+
13
+ With syntax highlighting and Markdown rendering:
14
+
15
+ ```bash
16
+ pip install "ghostbit-cli[all]" # pygments + rich
17
+ pip install "ghostbit-cli[color]" # pygments only (syntax highlighting)
18
+ pip install "ghostbit-cli[markdown]" # rich only (Markdown rendering)
19
+ ```
20
+
21
+ ## Usage
22
+
23
+ ```bash
24
+ # Paste from stdin
25
+ cat file.py | gbit
26
+
27
+ # Paste a file (language auto-detected from extension)
28
+ gbit file.py
29
+
30
+ # With options
31
+ gbit file.py --lang python --burn --expires 3600
32
+
33
+ # Password-protected paste (prompted securely)
34
+ echo "secret" | gbit -p
35
+
36
+ # Or pass inline (visible in process list)
37
+ gbit file.py --password mysecret
38
+
39
+ # View and decrypt a paste in the terminal
40
+ gbit view https://paste.example.com/abc123#KEY~TOKEN
41
+
42
+ # View a password-protected paste (prompts for password)
43
+ gbit view https://paste.example.com/abc123#~TOKEN
44
+
45
+ # Output JSON (includes full URL with decryption key)
46
+ cat data.json | gbit --json
47
+
48
+ # Point to your self-hosted instance
49
+ gbit config set server https://paste.example.com
50
+ ```
51
+
52
+ ## Options
53
+
54
+ | Flag | Short | Description |
55
+ |------|-------|-------------|
56
+ | `--lang` | `-l` | Language hint (python, go, rust, …) |
57
+ | `--expires` | `-e` | TTL in seconds (3600 = 1h, 86400 = 1d) |
58
+ | `--burn` | `-b` | Delete after the first view |
59
+ | `--max-views` | `-m` | Delete after N views |
60
+ | `--password` | `-p` | Encrypt with a password (prompted if no value given) |
61
+ | `--server` | `-s` | Override server URL for this call |
62
+ | `--quiet` | `-q` | Print URL only |
63
+ | `--json` | | Print full JSON response |
64
+ | `--no-history` | | Don't save to local history |
65
+ | `--version` | `-V` | Print version and exit |
66
+
67
+ ## Configuration
68
+
69
+ ```bash
70
+ gbit config set server https://paste.example.com
71
+ gbit config show
72
+ gbit config unset server
73
+ ```
74
+
75
+ Config is stored at `~/.config/ghostbit.toml`.
76
+
77
+ ## Security Note
78
+
79
+ The local history (`~/.local/share/ghostbit/history.jsonl`) stores full URLs **including decryption keys**. Use `--no-history` for sensitive pastes, or clear history with `gbit list --clear`.
80
+
81
+ ## Self-hosting
82
+
83
+ See the [Ghostbit server repository](https://github.com/stackopshq/ghostbit) for Docker and manual setup instructions.
84
+
85
+ ## License
86
+
87
+ MIT
@@ -3,16 +3,17 @@
3
3
  Ghostbit CLI — create and view pastes from the terminal.
4
4
 
5
5
  Usage:
6
- cat file.py | gb
7
- gb file.py
8
- gb file.py --lang python --burn
9
- gb view https://paste.example.com/abc123#KEY~TOKEN
10
- gb config set server https://paste.example.com
11
- gb config show
6
+ cat file.py | gbit
7
+ gbit file.py
8
+ gbit file.py --lang python --burn
9
+ gbit view https://paste.example.com/abc123#KEY~TOKEN
10
+ gbit config set server https://paste.example.com
11
+ gbit config show
12
12
  """
13
13
 
14
14
  import argparse
15
15
  import base64
16
+ import getpass
16
17
  import json
17
18
  import os
18
19
  import sys
@@ -21,9 +22,16 @@ import urllib.error
21
22
  import urllib.request
22
23
  from pathlib import Path
23
24
 
24
- __version__ = "1.0.0"
25
+ __version__ = "1.1.0"
25
26
  _USER_AGENT = f"Ghostbit-CLI/{__version__}"
26
27
 
28
+ LANGUAGES = [
29
+ "python", "javascript", "typescript", "go", "rust", "ruby", "php",
30
+ "java", "c", "cpp", "csharp", "bash", "powershell", "html", "css",
31
+ "sql", "json", "yaml", "toml", "xml", "markdown", "dockerfile",
32
+ "kotlin", "swift", "lua", "r", "diff",
33
+ ]
34
+
27
35
  try:
28
36
  from cryptography.hazmat.primitives.ciphers.aead import AESGCM
29
37
  from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC
@@ -67,7 +75,8 @@ def _key_to_fragment(key: bytes) -> str:
67
75
 
68
76
  # ── Config ────────────────────────────────────────────────────────────────────
69
77
 
70
- CONFIG_PATH = Path.home() / ".config" / "ghostbit.toml"
78
+ CONFIG_PATH = Path.home() / ".config" / "ghostbit.toml"
79
+ HISTORY_PATH = Path.home() / ".local" / "share" / "ghostbit" / "history.jsonl"
71
80
  DEFAULT_SERVER = "http://localhost:8000"
72
81
 
73
82
  VALID_KEYS = {"server"}
@@ -96,7 +105,7 @@ def _write_config(cfg: dict) -> None:
96
105
  # ── Subcommands ───────────────────────────────────────────────────────────────
97
106
 
98
107
  def _run_config(argv):
99
- parser = argparse.ArgumentParser(prog="gb config", description="Manage Ghostbit CLI configuration.")
108
+ parser = argparse.ArgumentParser(prog="gbit config", description="Manage Ghostbit CLI configuration.")
100
109
  sub = parser.add_subparsers(dest="action")
101
110
 
102
111
  sub.add_parser("show", help="Show current configuration.")
@@ -174,7 +183,11 @@ def cmd_paste(args):
174
183
  ".dockerfile": "dockerfile", ".kt": "kotlin", ".swift": "swift",
175
184
  ".lua": "lua", ".r": "r", ".diff": "diff", ".patch": "diff",
176
185
  }
177
- args.lang = ext_map.get(path.suffix.lower())
186
+ # Handle extensionless files by name (e.g. Dockerfile, Makefile)
187
+ name_map = {
188
+ "dockerfile": "dockerfile", "makefile": "makefile",
189
+ }
190
+ args.lang = ext_map.get(path.suffix.lower()) or name_map.get(path.name.lower())
178
191
  else:
179
192
  if sys.stdin.isatty():
180
193
  print("Reading from stdin… (Ctrl+D to finish)", file=sys.stderr)
@@ -187,9 +200,17 @@ def cmd_paste(args):
187
200
  _require_crypto()
188
201
 
189
202
  # ── Encrypt client-side (mirrors browser e2e.js) ──
190
- if args.password:
203
+ # If --password was given without a value, prompt interactively
204
+ password = args.password
205
+ if password is True or password == '':
206
+ password = getpass.getpass('Password: ')
207
+ if not password:
208
+ print('Error: password cannot be empty.', file=sys.stderr)
209
+ sys.exit(1)
210
+
211
+ if password:
191
212
  kdf_salt = _gen_salt()
192
- key = _derive_key(args.password, kdf_salt)
213
+ key = _derive_key(password, kdf_salt)
193
214
  else:
194
215
  key = _gen_key()
195
216
  kdf_salt = None
@@ -209,13 +230,24 @@ def cmd_paste(args):
209
230
  result = _api_create(server, payload)
210
231
 
211
232
  # Build full URL with fragment (key + delete token)
212
- if args.password:
233
+ if password:
213
234
  fragment = f"~{result['delete_token']}"
214
235
  else:
215
236
  fragment = f"{_key_to_fragment(key)}~{result['delete_token']}"
216
237
 
217
238
  full_url = f"{result['url']}#{fragment}"
218
239
 
240
+ # Append to local history (best-effort, privacy-first — stays on disk only)
241
+ if not args.no_history:
242
+ _history_append({
243
+ "id": result["id"],
244
+ "url": result["url"],
245
+ "full_url": full_url,
246
+ "created_at": int(time.time()),
247
+ "language": args.lang,
248
+ "expires_at": result.get("expires_at"),
249
+ })
250
+
219
251
  if args.json:
220
252
  result["full_url"] = full_url
221
253
  print(json.dumps(result, indent=2))
@@ -237,11 +269,11 @@ def cmd_paste(args):
237
269
  parts.append("burn after read")
238
270
  if result.get("max_views"):
239
271
  parts.append(f"max {result['max_views']} views")
240
- if args.password:
272
+ if password:
241
273
  parts.append("password protected")
242
274
  if parts:
243
275
  print(" " + " · ".join(parts), file=sys.stderr)
244
- if not args.password:
276
+ if not password:
245
277
  print(" Share the full URL — the decryption key is in the #fragment.", file=sys.stderr)
246
278
 
247
279
 
@@ -281,7 +313,6 @@ def _print_highlighted(content: str, language: str | None) -> None:
281
313
 
282
314
 
283
315
  def cmd_view(args):
284
- import getpass
285
316
  from urllib.parse import urldefrag, urlparse
286
317
 
287
318
  url_no_frag, fragment = urldefrag(args.url)
@@ -375,10 +406,275 @@ def _api_create(server: str, payload: dict) -> dict:
375
406
  sys.exit(1)
376
407
  except urllib.error.URLError as e:
377
408
  print(f"Could not connect to {server}: {e.reason}", file=sys.stderr)
378
- print(f" Tip: run `gb config set server <URL>` to set your server.", file=sys.stderr)
409
+ print(f" Tip: run `gbit config set server <URL>` to set your server.", file=sys.stderr)
410
+ sys.exit(1)
411
+
412
+
413
+ # ── Local history ────────────────────────────────────────────────────────────
414
+
415
+ def _history_append(entry: dict) -> None:
416
+ """Append one entry to the local history file (JSONL, one JSON object per line)."""
417
+ try:
418
+ HISTORY_PATH.parent.mkdir(parents=True, exist_ok=True)
419
+ with HISTORY_PATH.open("a", encoding="utf-8") as f:
420
+ f.write(json.dumps(entry) + "\n")
421
+ except Exception:
422
+ pass # history is best-effort, never block a paste creation
423
+
424
+
425
+ def _history_load() -> list[dict]:
426
+ if not HISTORY_PATH.exists():
427
+ return []
428
+ entries = []
429
+ for line in HISTORY_PATH.read_text(encoding="utf-8").splitlines():
430
+ line = line.strip()
431
+ if line:
432
+ try:
433
+ entries.append(json.loads(line))
434
+ except json.JSONDecodeError:
435
+ pass
436
+ return entries
437
+
438
+
439
+ def cmd_delete(url: str) -> None:
440
+ from urllib.parse import urldefrag, urlparse
441
+
442
+ url_no_frag, fragment = urldefrag(url)
443
+ parsed = urlparse(url_no_frag)
444
+ server = f"{parsed.scheme}://{parsed.netloc}"
445
+ paste_id = parsed.path.strip("/")
446
+
447
+ if not paste_id or not parsed.scheme:
448
+ print("Error: invalid URL.", file=sys.stderr)
449
+ sys.exit(1)
450
+
451
+ # Fragment: KEY~DELETE_TOKEN or ~DELETE_TOKEN
452
+ delete_token = fragment.partition("~")[2]
453
+ if not delete_token:
454
+ print("Error: delete token missing from URL fragment (expected KEY~TOKEN or ~TOKEN).", file=sys.stderr)
455
+ sys.exit(1)
456
+
457
+ api_url = f"{server}/api/v1/pastes/{paste_id}"
458
+ req = urllib.request.Request(
459
+ api_url,
460
+ headers={"User-Agent": _USER_AGENT, "X-Delete-Token": delete_token},
461
+ method="DELETE",
462
+ )
463
+ try:
464
+ with urllib.request.urlopen(req, timeout=15):
465
+ pass
466
+ print(f"Deleted {paste_id}.")
467
+ except urllib.error.HTTPError as e:
468
+ if e.code == 403:
469
+ print("Error: invalid delete token.", file=sys.stderr)
470
+ elif e.code == 404:
471
+ print("Error: paste not found (already deleted or expired).", file=sys.stderr)
472
+ else:
473
+ print(f"Error {e.code}.", file=sys.stderr)
474
+ sys.exit(1)
475
+ except urllib.error.URLError as e:
476
+ print(f"Could not connect to {server}: {e.reason}", file=sys.stderr)
379
477
  sys.exit(1)
380
478
 
381
479
 
480
+ def cmd_list(clear: bool = False) -> None:
481
+ if clear:
482
+ if HISTORY_PATH.exists():
483
+ HISTORY_PATH.unlink()
484
+ print("History cleared.")
485
+ else:
486
+ print("No history file found.")
487
+ return
488
+
489
+ entries = _history_load()
490
+ if not entries:
491
+ print("No pastes in local history.", file=sys.stderr)
492
+ print(f" History file: {HISTORY_PATH}", file=sys.stderr)
493
+ return
494
+
495
+ now = int(time.time())
496
+
497
+ def _age(ts: int) -> str:
498
+ delta = now - ts
499
+ if delta < 120: return "just now"
500
+ if delta < 3600: return f"{delta // 60}m ago"
501
+ if delta < 86400: return f"{delta // 3600}h ago"
502
+ return f"{delta // 86400}d ago"
503
+
504
+ def _expiry(expires_at) -> str:
505
+ if not expires_at:
506
+ return "never"
507
+ delta = expires_at - now
508
+ if delta <= 0: return "expired"
509
+ if delta < 3600: return f"in {delta // 60}m"
510
+ if delta < 86400: return f"in {delta // 3600}h"
511
+ return f"in {delta // 86400}d"
512
+
513
+ print(f"{'ID':<12} {'Lang':<14} {'Created':<12} {'Expires':<10} URL")
514
+ print("-" * 80)
515
+ for e in reversed(entries):
516
+ row_id = e.get("id", "?")[:10]
517
+ lang = (e.get("language") or "plain")[:12]
518
+ created = _age(e.get("created_at", 0))
519
+ expires = _expiry(e.get("expires_at"))
520
+ full_url = e.get("full_url", e.get("url", ""))
521
+ print(f"{row_id:<12} {lang:<14} {created:<12} {expires:<10} {full_url}")
522
+
523
+
524
+ # ── Shell completion ──────────────────────────────────────────────────────────
525
+
526
+ _COMPLETION_BASH = r"""
527
+ # Ghostbit bash completion
528
+ # Usage: eval "$(gbit completion bash)" or add to ~/.bashrc
529
+
530
+ _gb_completion() {
531
+ local cur prev words cword
532
+ _init_completion 2>/dev/null || {
533
+ COMPREPLY=()
534
+ cur="${COMP_WORDS[COMP_CWORD]}"
535
+ prev="${COMP_WORDS[COMP_CWORD-1]}"
536
+ words=("${COMP_WORDS[@]}")
537
+ cword=$COMP_CWORD
538
+ }
539
+
540
+ local langs="LANGS_PLACEHOLDER"
541
+ local main_opts="--lang --expires --burn --max-views --password --server --quiet --json -l -e -b -m -p -s -q"
542
+
543
+ # Option argument completions
544
+ case "$prev" in
545
+ --lang|-l) COMPREPLY=($(compgen -W "$langs" -- "$cur")); return ;;
546
+ --server|-s) COMPREPLY=($(compgen -W "http:// https://" -- "$cur")); return ;;
547
+ --expires|-e|--max-views|-m|--password|-p) return ;;
548
+ esac
549
+
550
+ local subcmd="${words[1]}"
551
+
552
+ case "$subcmd" in
553
+ config)
554
+ case "$cword" in
555
+ 2) COMPREPLY=($(compgen -W "show set unset" -- "$cur")) ;;
556
+ 3) [[ "${words[2]}" == "set" || "${words[2]}" == "unset" ]] \
557
+ && COMPREPLY=($(compgen -W "server" -- "$cur")) ;;
558
+ esac
559
+ return ;;
560
+ completion)
561
+ COMPREPLY=($(compgen -W "bash zsh fish" -- "$cur"))
562
+ return ;;
563
+ view) return ;;
564
+ esac
565
+
566
+ if [[ $cword -eq 1 ]]; then
567
+ COMPREPLY=($(compgen -W "config view delete list completion $main_opts" -- "$cur"))
568
+ COMPREPLY+=($(compgen -f -- "$cur"))
569
+ else
570
+ COMPREPLY=($(compgen -W "$main_opts" -- "$cur"))
571
+ COMPREPLY+=($(compgen -f -- "$cur"))
572
+ fi
573
+ }
574
+
575
+ complete -o filenames -F _gb_completion gbit
576
+ complete -o filenames -F _gb_completion ghostbit
577
+ """
578
+
579
+ _COMPLETION_ZSH = r"""
580
+ #compdef gbit ghostbit
581
+ # Ghostbit zsh completion
582
+ # Usage: eval "$(gbit completion zsh)" or add to ~/.zshrc
583
+
584
+ _gb() {
585
+ local langs=(LANGS_PLACEHOLDER)
586
+ local main_opts=(
587
+ '(-l --lang)'{-l,--lang}'[language hint]:language:('"${langs[*]}"')'
588
+ '(-e --expires)'{-e,--expires}'[TTL in seconds]:seconds:'
589
+ '(-b --burn)'{-b,--burn}'[delete after first view]'
590
+ '(-m --max-views)'{-m,--max-views}'[delete after N views]:count:'
591
+ '(-p --password)'{-p,--password}'[encrypt with password]:password:'
592
+ '(-s --server)'{-s,--server}'[server URL]:url:'
593
+ '(-q --quiet)'{-q,--quiet}'[print URL only]'
594
+ '--json[print full JSON response]'
595
+ )
596
+
597
+ local state
598
+ _arguments -C \
599
+ '1: :->first' \
600
+ '*: :->rest' && return 0
601
+
602
+ case $state in
603
+ first)
604
+ _alternative \
605
+ 'subcommands:subcommand:((config\:"manage config" view\:"view a paste" delete\:"delete a paste" list\:"list local history" completion\:"shell completion"))' \
606
+ "options: :_arguments ${main_opts[*]}" \
607
+ 'files:file:_files'
608
+ ;;
609
+ rest)
610
+ case $words[2] in
611
+ config)
612
+ case $CURRENT in
613
+ 3) _values 'action' show set unset ;;
614
+ 4) [[ $words[3] == (set|unset) ]] && _values 'key' server ;;
615
+ esac ;;
616
+ view) _nothing ;;
617
+ completion) _values 'shell' bash zsh fish ;;
618
+ *) _arguments "${main_opts[@]}" && _files ;;
619
+ esac
620
+ ;;
621
+ esac
622
+ }
623
+
624
+ _gb
625
+ """
626
+
627
+ _COMPLETION_FISH = """
628
+ # Ghostbit fish completion
629
+ # Usage: gbit completion fish | source or save to ~/.config/fish/completions/gbit.fish
630
+
631
+ set -l langs LANGS_PLACEHOLDER
632
+
633
+ # Disable file completion when a subcommand is active and not needed
634
+ function __gb_no_subcommand
635
+ not __fish_seen_subcommand_from config view delete list completion
636
+ end
637
+
638
+ # Subcommands
639
+ complete -c gbit -f -n '__gb_no_subcommand' -a config -d 'Manage configuration'
640
+ complete -c gbit -f -n '__gb_no_subcommand' -a view -d 'View and decrypt a paste'
641
+ complete -c gbit -f -n '__gb_no_subcommand' -a delete -d 'Delete a paste'
642
+ complete -c gbit -f -n '__gb_no_subcommand' -a list -d 'List local paste history'
643
+ complete -c gbit -f -n '__gb_no_subcommand' -a completion -d 'Output shell completion script'
644
+
645
+ # config actions
646
+ complete -c gbit -f -n '__fish_seen_subcommand_from config' -a show -d 'Show current config'
647
+ complete -c gbit -f -n '__fish_seen_subcommand_from config' -a set -d 'Set a value'
648
+ complete -c gbit -f -n '__fish_seen_subcommand_from config' -a unset -d 'Remove a value'
649
+
650
+ # completion shells
651
+ complete -c gbit -f -n '__fish_seen_subcommand_from completion' -a bash -d 'Bash completion'
652
+ complete -c gbit -f -n '__fish_seen_subcommand_from completion' -a zsh -d 'Zsh completion'
653
+ complete -c gbit -f -n '__fish_seen_subcommand_from completion' -a fish -d 'Fish completion'
654
+
655
+ # Main options
656
+ complete -c gbit -n '__gb_no_subcommand' -s l -l lang -d 'Language hint' -a "$langs"
657
+ complete -c gbit -n '__gb_no_subcommand' -s e -l expires -d 'TTL in seconds'
658
+ complete -c gbit -n '__gb_no_subcommand' -s b -l burn -d 'Delete after first view'
659
+ complete -c gbit -n '__gb_no_subcommand' -s m -l max-views -d 'Delete after N views'
660
+ complete -c gbit -n '__gb_no_subcommand' -s p -l password -d 'Encrypt with password'
661
+ complete -c gbit -n '__gb_no_subcommand' -s s -l server -d 'Server URL'
662
+ complete -c gbit -n '__gb_no_subcommand' -s q -l quiet -d 'Print URL only'
663
+ complete -c gbit -n '__gb_no_subcommand' -l json -d 'Print full JSON response'
664
+ """
665
+
666
+
667
+ def cmd_completion(shell: str) -> None:
668
+ langs_str = " ".join(LANGUAGES)
669
+ templates = {
670
+ "bash": _COMPLETION_BASH,
671
+ "zsh": _COMPLETION_ZSH,
672
+ "fish": _COMPLETION_FISH,
673
+ }
674
+ script = templates[shell].replace("LANGS_PLACEHOLDER", langs_str)
675
+ print(script.strip())
676
+
677
+
382
678
  # ── Entry point ───────────────────────────────────────────────────────────────
383
679
 
384
680
  def main():
@@ -387,12 +683,35 @@ def main():
387
683
  _run_config(sys.argv[2:])
388
684
  return
389
685
 
686
+ # Route to delete subcommand
687
+ if len(sys.argv) > 1 and sys.argv[1] == "delete":
688
+ if len(sys.argv) < 3:
689
+ print("Usage: gbit delete <url>", file=sys.stderr)
690
+ sys.exit(1)
691
+ cmd_delete(sys.argv[2])
692
+ return
693
+
694
+ # Route to list subcommand
695
+ if len(sys.argv) > 1 and sys.argv[1] == "list":
696
+ clear = "--clear" in sys.argv
697
+ cmd_list(clear=clear)
698
+ return
699
+
700
+ # Route to completion subcommand
701
+ if len(sys.argv) > 1 and sys.argv[1] == "completion":
702
+ shells = ["bash", "zsh", "fish"]
703
+ if len(sys.argv) < 3 or sys.argv[2] not in shells:
704
+ print(f"Usage: gbit completion [{'|'.join(shells)}]", file=sys.stderr)
705
+ sys.exit(1)
706
+ cmd_completion(sys.argv[2])
707
+ return
708
+
390
709
  # Route to view subcommand
391
710
  if len(sys.argv) > 1 and sys.argv[1] == "view":
392
711
  if len(sys.argv) < 3:
393
- print("Usage: gb view <url>", file=sys.stderr)
712
+ print("Usage: gbit view <url>", file=sys.stderr)
394
713
  sys.exit(1)
395
- view_parser = argparse.ArgumentParser(prog="gb view")
714
+ view_parser = argparse.ArgumentParser(prog="gbit view")
396
715
  view_parser.add_argument("url", help="Full paste URL (including #fragment).")
397
716
  cmd_view(view_parser.parse_args(sys.argv[2:]))
398
717
  return
@@ -401,24 +720,24 @@ def main():
401
720
  current_server = cfg.get("server", DEFAULT_SERVER)
402
721
 
403
722
  parser = argparse.ArgumentParser(
404
- prog="gb",
723
+ prog="gbit",
405
724
  description="Ghostbit CLI — create encrypted pastes from the terminal.",
406
725
  formatter_class=argparse.RawDescriptionHelpFormatter,
407
726
  epilog=f"""
408
727
  examples:
409
- cat main.py | gb
410
- gb main.py
411
- gb main.py --lang python --burn
412
- gb main.py --expires 3600 --password secret
413
- gb view https://paste.example.com/abc123#KEY~TOKEN
414
- gb config set server https://paste.example.com
415
- gb config show
728
+ cat main.py | gbit
729
+ gbit main.py
730
+ gbit main.py --lang python --burn
731
+ gbit main.py --expires 3600 --password secret
732
+ gbit view https://paste.example.com/abc123#KEY~TOKEN
733
+ gbit config set server https://paste.example.com
734
+ gbit config show
416
735
 
417
736
  current server: {current_server}
418
737
  """,
419
738
  )
420
739
 
421
- # ── gb [file] ──
740
+ # ── gbit [file] ──
422
741
  parser.add_argument(
423
742
  "file",
424
743
  nargs="?",
@@ -456,9 +775,11 @@ current server: {current_server}
456
775
  )
457
776
  parser.add_argument(
458
777
  "--password", "-p",
778
+ nargs="?",
779
+ const=True,
459
780
  default=None,
460
781
  metavar="PASS",
461
- help="Encrypt with a password.",
782
+ help="Encrypt with a password. Omit value to be prompted securely.",
462
783
  )
463
784
  parser.add_argument(
464
785
  "--quiet", "-q",
@@ -470,6 +791,16 @@ current server: {current_server}
470
791
  action="store_true",
471
792
  help="Print the full JSON response.",
472
793
  )
794
+ parser.add_argument(
795
+ "--no-history",
796
+ action="store_true",
797
+ help="Don't save this paste to local history.",
798
+ )
799
+ parser.add_argument(
800
+ "--version", "-V",
801
+ action="version",
802
+ version=f"%(prog)s {__version__}",
803
+ )
473
804
 
474
805
  args = parser.parse_args()
475
806
  cmd_paste(args)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ghostbit-cli
3
- Version: 1.0.0
3
+ Version: 1.1.0
4
4
  Summary: Ghostbit CLI — create end-to-end encrypted pastes from the terminal
5
5
  License-Expression: MIT
6
6
  Project-URL: Homepage, https://github.com/stackopshq/ghostbit
@@ -43,26 +43,43 @@ All content is encrypted **in the client** before being sent to the server. The
43
43
  pip install ghostbit-cli
44
44
  ```
45
45
 
46
+ With syntax highlighting and Markdown rendering:
47
+
48
+ ```bash
49
+ pip install "ghostbit-cli[all]" # pygments + rich
50
+ pip install "ghostbit-cli[color]" # pygments only (syntax highlighting)
51
+ pip install "ghostbit-cli[markdown]" # rich only (Markdown rendering)
52
+ ```
53
+
46
54
  ## Usage
47
55
 
48
56
  ```bash
49
57
  # Paste from stdin
50
- cat file.py | gb
58
+ cat file.py | gbit
51
59
 
52
60
  # Paste a file (language auto-detected from extension)
53
- gb file.py
61
+ gbit file.py
54
62
 
55
63
  # With options
56
- gb file.py --lang python --burn --expires 3600
64
+ gbit file.py --lang python --burn --expires 3600
65
+
66
+ # Password-protected paste (prompted securely)
67
+ echo "secret" | gbit -p
57
68
 
58
- # Password-protected paste
59
- echo "secret" | gb --password mysecret
69
+ # Or pass inline (visible in process list)
70
+ gbit file.py --password mysecret
71
+
72
+ # View and decrypt a paste in the terminal
73
+ gbit view https://paste.example.com/abc123#KEY~TOKEN
74
+
75
+ # View a password-protected paste (prompts for password)
76
+ gbit view https://paste.example.com/abc123#~TOKEN
60
77
 
61
78
  # Output JSON (includes full URL with decryption key)
62
- cat data.json | gb --json
79
+ cat data.json | gbit --json
63
80
 
64
81
  # Point to your self-hosted instance
65
- gb config set server https://paste.example.com
82
+ gbit config set server https://paste.example.com
66
83
  ```
67
84
 
68
85
  ## Options
@@ -73,21 +90,27 @@ gb config set server https://paste.example.com
73
90
  | `--expires` | `-e` | TTL in seconds (3600 = 1h, 86400 = 1d) |
74
91
  | `--burn` | `-b` | Delete after the first view |
75
92
  | `--max-views` | `-m` | Delete after N views |
76
- | `--password` | `-p` | Encrypt with a password |
93
+ | `--password` | `-p` | Encrypt with a password (prompted if no value given) |
77
94
  | `--server` | `-s` | Override server URL for this call |
78
95
  | `--quiet` | `-q` | Print URL only |
79
96
  | `--json` | | Print full JSON response |
97
+ | `--no-history` | | Don't save to local history |
98
+ | `--version` | `-V` | Print version and exit |
80
99
 
81
100
  ## Configuration
82
101
 
83
102
  ```bash
84
- gb config set server https://paste.example.com
85
- gb config show
86
- gb config unset server
103
+ gbit config set server https://paste.example.com
104
+ gbit config show
105
+ gbit config unset server
87
106
  ```
88
107
 
89
108
  Config is stored at `~/.config/ghostbit.toml`.
90
109
 
110
+ ## Security Note
111
+
112
+ The local history (`~/.local/share/ghostbit/history.jsonl`) stores full URLs **including decryption keys**. Use `--no-history` for sensitive pastes, or clear history with `gbit list --clear`.
113
+
91
114
  ## Self-hosting
92
115
 
93
116
  See the [Ghostbit server repository](https://github.com/stackopshq/ghostbit) for Docker and manual setup instructions.
@@ -1,3 +1,3 @@
1
1
  [console_scripts]
2
- gb = cli:main
2
+ gbit = cli:main
3
3
  ghostbit = cli:main
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "ghostbit-cli"
7
- version = "1.0.0"
7
+ version = "1.1.0"
8
8
  description = "Ghostbit CLI — create end-to-end encrypted pastes from the terminal"
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -40,7 +40,7 @@ Repository = "https://github.com/stackopshq/ghostbit"
40
40
  "Bug Tracker" = "https://github.com/stackopshq/ghostbit/issues"
41
41
 
42
42
  [project.scripts]
43
- gb = "cli:main"
43
+ gbit = "cli:main"
44
44
  ghostbit = "cli:main"
45
45
 
46
46
  [tool.setuptools]
@@ -1,64 +0,0 @@
1
- # Ghostbit CLI
2
-
3
- Command-line tool for [Ghostbit](https://github.com/stackopshq/ghostbit) — a self-hosted, end-to-end encrypted paste service.
4
-
5
- All content is encrypted **in the client** before being sent to the server. The server never sees your plaintext.
6
-
7
- ## Install
8
-
9
- ```bash
10
- pip install ghostbit-cli
11
- ```
12
-
13
- ## Usage
14
-
15
- ```bash
16
- # Paste from stdin
17
- cat file.py | gb
18
-
19
- # Paste a file (language auto-detected from extension)
20
- gb file.py
21
-
22
- # With options
23
- gb file.py --lang python --burn --expires 3600
24
-
25
- # Password-protected paste
26
- echo "secret" | gb --password mysecret
27
-
28
- # Output JSON (includes full URL with decryption key)
29
- cat data.json | gb --json
30
-
31
- # Point to your self-hosted instance
32
- gb config set server https://paste.example.com
33
- ```
34
-
35
- ## Options
36
-
37
- | Flag | Short | Description |
38
- |------|-------|-------------|
39
- | `--lang` | `-l` | Language hint (python, go, rust, …) |
40
- | `--expires` | `-e` | TTL in seconds (3600 = 1h, 86400 = 1d) |
41
- | `--burn` | `-b` | Delete after the first view |
42
- | `--max-views` | `-m` | Delete after N views |
43
- | `--password` | `-p` | Encrypt with a password |
44
- | `--server` | `-s` | Override server URL for this call |
45
- | `--quiet` | `-q` | Print URL only |
46
- | `--json` | | Print full JSON response |
47
-
48
- ## Configuration
49
-
50
- ```bash
51
- gb config set server https://paste.example.com
52
- gb config show
53
- gb config unset server
54
- ```
55
-
56
- Config is stored at `~/.config/ghostbit.toml`.
57
-
58
- ## Self-hosting
59
-
60
- See the [Ghostbit server repository](https://github.com/stackopshq/ghostbit) for Docker and manual setup instructions.
61
-
62
- ## License
63
-
64
- MIT
File without changes
File without changes