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