clipto 0.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.
@@ -0,0 +1,28 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+ branches: [main]
8
+
9
+ jobs:
10
+ test:
11
+ runs-on: ubuntu-latest
12
+ strategy:
13
+ matrix:
14
+ python-version: ["3.9", "3.10", "3.11", "3.12"]
15
+
16
+ steps:
17
+ - uses: actions/checkout@v4
18
+ - name: Set up Python ${{ matrix.python-version }}
19
+ uses: actions/setup-python@v5
20
+ with:
21
+ python-version: ${{ matrix.python-version }}
22
+ - name: Install dependencies
23
+ run: |
24
+ python -m pip install --upgrade pip
25
+ pip install .[dev]
26
+ - name: Run tests
27
+ run: |
28
+ pytest
@@ -0,0 +1,45 @@
1
+ name: Publish to PyPI
2
+
3
+ on:
4
+ release:
5
+ types: [published]
6
+ workflow_dispatch:
7
+
8
+ jobs:
9
+ build:
10
+ name: Build distribution šŸ“¦
11
+ runs-on: ubuntu-latest
12
+ steps:
13
+ - uses: actions/checkout@v4
14
+ - name: Set up Python
15
+ uses: actions/setup-python@v5
16
+ with:
17
+ python-version: "3.12"
18
+ - name: Install pypa/build
19
+ run: python3 -m pip install build --user
20
+ - name: Build a binary wheel and a source tarball
21
+ run: python3 -m build
22
+ - name: Store the distribution packages
23
+ uses: actions/upload-artifact@v4
24
+ with:
25
+ name: python-package-distributions
26
+ path: dist/
27
+
28
+ publish-to-pypi:
29
+ name: Publish to PyPI šŸš€
30
+ needs:
31
+ - build
32
+ runs-on: ubuntu-latest
33
+ environment:
34
+ name: pypi
35
+ url: https://pypi.org/p/clipto
36
+ permissions:
37
+ id-token: write # Mandatory for PyPI Trusted Publishing
38
+ steps:
39
+ - name: Download distribution packages
40
+ uses: actions/download-artifact@v4
41
+ with:
42
+ name: python-package-distributions
43
+ path: dist/
44
+ - name: Publish distribution to PyPI
45
+ uses: pypa/gh-action-pypi-publish@release/v1
@@ -0,0 +1,27 @@
1
+ # Byte-compiled / optimized / DLL files
2
+ __pycache__/
3
+ *.py[cod]
4
+ *$py.class
5
+
6
+ # Distribution / packaging
7
+ dist/
8
+ build/
9
+ *.egg-info/
10
+ .eggs/
11
+
12
+ # Virtual environments
13
+ .venv/
14
+ venv/
15
+ ENV/
16
+ env/
17
+
18
+ # IDE & OS
19
+ .DS_Store
20
+ .idea/
21
+ .vscode/
22
+ *.swp
23
+
24
+ # Testing & coverage
25
+ .pytest_cache/
26
+ .coverage
27
+ htmlcov/
clipto-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Matt
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
clipto-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,151 @@
1
+ Metadata-Version: 2.5
2
+ Name: clipto
3
+ Version: 0.1.0
4
+ Summary: Instant clipboard, screenshot, and file bridge from your browser to your terminal/remote server.
5
+ Project-URL: Homepage, https://github.com/mattintech/clipto
6
+ Project-URL: Repository, https://github.com/mattintech/clipto
7
+ Project-URL: Issues, https://github.com/mattintech/clipto/issues
8
+ Author-email: Matt <matt@example.com>
9
+ License-Expression: MIT
10
+ License-File: LICENSE
11
+ Keywords: ai-tools,cli,clipboard,developer-tools,remote-transfer,screenshot,upload
12
+ Classifier: Development Status :: 4 - Beta
13
+ Classifier: Environment :: Console
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: License :: OSI Approved :: MIT License
16
+ Classifier: Operating System :: OS Independent
17
+ Classifier: Programming Language :: Python :: 3
18
+ Classifier: Programming Language :: Python :: 3.9
19
+ Classifier: Programming Language :: Python :: 3.10
20
+ Classifier: Programming Language :: Python :: 3.11
21
+ Classifier: Programming Language :: Python :: 3.12
22
+ Classifier: Topic :: Utilities
23
+ Requires-Python: >=3.9
24
+ Provides-Extra: dev
25
+ Requires-Dist: build>=1.0.0; extra == 'dev'
26
+ Requires-Dist: pytest>=8.0.0; extra == 'dev'
27
+ Requires-Dist: twine>=5.0.0; extra == 'dev'
28
+ Provides-Extra: qr
29
+ Requires-Dist: segno>=1.6.0; extra == 'qr'
30
+ Description-Content-Type: text/markdown
31
+
32
+ # šŸ“Ž Clipto
33
+
34
+ [![PyPI](https://img.shields.io/pypi/v/clipto.svg)](https://pypi.org/project/clipto/)
35
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)
36
+ [![CI](https://github.com/mattintech/clipto/actions/workflows/ci.yml/badge.svg)](https://github.com/mattintech/clipto/actions/workflows/ci.yml)
37
+
38
+ > **Instant clipboard, screenshot, and file bridge from your browser to your terminal or remote server.**
39
+
40
+ Ever run an AI agent, Docker container, or SSH session on a remote server, but need to get a screenshot or log file from your local machine to that server?
41
+
42
+ **Clipto** solves this. Run `clipto` in any directory on your machine or remote server, open the link in your browser, and hit **Cmd+V / Ctrl+V**. Your screenshot or clipboard contents are immediately saved right into that folder.
43
+
44
+ ---
45
+
46
+ ## ✨ Features
47
+
48
+ - **⚔ Zero-Click Paste:** Open the page and press `Cmd+V` or `Ctrl+V` anywhere. No buttons or input selection required.
49
+ - **šŸŽÆ Drag & Drop:** Drop multiple files, images, or documents straight onto the window.
50
+ - **šŸ“ Text & Note Box:** Paste stack traces, error logs, or notes to save directly as `.txt` files.
51
+ - **šŸ¤– AI Agent Friendly (`--once`):** One-shot mode spins up the server, waits for an upload, writes the file, prints the saved path to `stdout`, and exits cleanly.
52
+ - **šŸ”€ Smart Port Hunting:** Automatically picks the next free port (`8765`, `8766`, etc.) so multiple instances never collide.
53
+ - **šŸ·ļø Multi-Instance Context:** The UI prominently shows the host machine, target directory, and optional custom `--title`.
54
+ - **🪶 Zero Dependencies:** Powered purely by Python's standard library. Installs in milliseconds with zero dependency conflicts.
55
+
56
+ ---
57
+
58
+ ## šŸš€ Quick Start
59
+
60
+ ### Installation
61
+
62
+ ```bash
63
+ pip install clipto
64
+ ```
65
+
66
+ ### Basic Usage
67
+
68
+ Run `clipto` in whichever directory you want files to land:
69
+
70
+ ```bash
71
+ clipto
72
+ ```
73
+
74
+ You'll see a clean terminal banner with the local and LAN URLs:
75
+
76
+ ```text
77
+ ā”Œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”
78
+ │ šŸ“Ž Clipto v0.1.0 │
79
+ │ Saving to: /Users/matt/code/my-project │
80
+ │ Local: http://localhost:8765 │
81
+ │ Network: http://192.168.1.50:8765 │
82
+ ā””ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”˜
83
+ ```
84
+
85
+ 1. Open the URL in your browser (or pass `-o / --open` to auto-open).
86
+ 2. Hit `Cmd+V` to paste a screenshot, or drag files onto the page.
87
+ 3. The files are instantly written to your current directory!
88
+
89
+ ---
90
+
91
+ ## šŸ¤– Using with AI Agents & Scripts
92
+
93
+ Clipto is built from the ground up to integrate cleanly into automated agent workflows (Antigravity, Claude Code, Aider, OpenHands, etc.).
94
+
95
+ ### One-Shot Mode (`--once`)
96
+
97
+ When run with `--once` (or `-1`), Clipto waits for a single upload batch, saves the file(s), prints the resolved absolute path to `stdout`, and shuts down:
98
+
99
+ ```bash
100
+ # Agent or script runs this:
101
+ FILE_PATH=$(clipto --once --title "Submit login bug screenshot" --timeout 120)
102
+
103
+ echo "Agent received file at: $FILE_PATH"
104
+ ```
105
+
106
+ The agent gets the exact file path back into its visual/multimodal context!
107
+
108
+ ---
109
+
110
+ ## āš™ļø CLI Reference
111
+
112
+ ```text
113
+ usage: clipto [-h] [-v] [-p PORT] [--no-hunt] [-d DIR] [-1] [-t TITLE] [-o] [--host HOST] [--timeout TIMEOUT]
114
+
115
+ Instant clipboard, screenshot, and file bridge from browser to terminal.
116
+
117
+ options:
118
+ -h, --help show this help message and exit
119
+ -v, --version show program's version number and exit
120
+ -p PORT, --port PORT Port to listen on (default: 8765; use 0 for random).
121
+ --no-hunt Disable automatic port hunting if port is busy.
122
+ -d DIR, --dir DIR Target directory to save files (default: .).
123
+ -1, --once One-shot mode: exit after upload and print file path(s).
124
+ -t TITLE, --title TITLE
125
+ Custom session title or prompt shown in the UI header.
126
+ -o, --open Automatically open web UI in default browser on launch.
127
+ --host HOST Host to bind to (default: 0.0.0.0).
128
+ --timeout TIMEOUT Timeout in seconds (useful with --once).
129
+ ```
130
+
131
+ ---
132
+
133
+ ## šŸ› ļø Multi-Instance Support
134
+
135
+ You can run multiple `clipto` instances simultaneously across different projects or terminals:
136
+ * Each instance automatically detects occupied ports and claims the next available one.
137
+ * Each browser tab displays its target folder path and optional session title so you always know where your file is landing.
138
+
139
+ ---
140
+
141
+ ## šŸ”’ Security
142
+
143
+ * By default, Clipto binds to `0.0.0.0` for convenience across local networks (e.g. testing with phone/laptop).
144
+ * If you only want local machine access, pass `--host 127.0.0.1`.
145
+ * Filenames are strictly sanitized to prevent directory traversal attacks.
146
+
147
+ ---
148
+
149
+ ## šŸ“„ License
150
+
151
+ MIT Ā© Matt
clipto-0.1.0/README.md ADDED
@@ -0,0 +1,120 @@
1
+ # šŸ“Ž Clipto
2
+
3
+ [![PyPI](https://img.shields.io/pypi/v/clipto.svg)](https://pypi.org/project/clipto/)
4
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)
5
+ [![CI](https://github.com/mattintech/clipto/actions/workflows/ci.yml/badge.svg)](https://github.com/mattintech/clipto/actions/workflows/ci.yml)
6
+
7
+ > **Instant clipboard, screenshot, and file bridge from your browser to your terminal or remote server.**
8
+
9
+ Ever run an AI agent, Docker container, or SSH session on a remote server, but need to get a screenshot or log file from your local machine to that server?
10
+
11
+ **Clipto** solves this. Run `clipto` in any directory on your machine or remote server, open the link in your browser, and hit **Cmd+V / Ctrl+V**. Your screenshot or clipboard contents are immediately saved right into that folder.
12
+
13
+ ---
14
+
15
+ ## ✨ Features
16
+
17
+ - **⚔ Zero-Click Paste:** Open the page and press `Cmd+V` or `Ctrl+V` anywhere. No buttons or input selection required.
18
+ - **šŸŽÆ Drag & Drop:** Drop multiple files, images, or documents straight onto the window.
19
+ - **šŸ“ Text & Note Box:** Paste stack traces, error logs, or notes to save directly as `.txt` files.
20
+ - **šŸ¤– AI Agent Friendly (`--once`):** One-shot mode spins up the server, waits for an upload, writes the file, prints the saved path to `stdout`, and exits cleanly.
21
+ - **šŸ”€ Smart Port Hunting:** Automatically picks the next free port (`8765`, `8766`, etc.) so multiple instances never collide.
22
+ - **šŸ·ļø Multi-Instance Context:** The UI prominently shows the host machine, target directory, and optional custom `--title`.
23
+ - **🪶 Zero Dependencies:** Powered purely by Python's standard library. Installs in milliseconds with zero dependency conflicts.
24
+
25
+ ---
26
+
27
+ ## šŸš€ Quick Start
28
+
29
+ ### Installation
30
+
31
+ ```bash
32
+ pip install clipto
33
+ ```
34
+
35
+ ### Basic Usage
36
+
37
+ Run `clipto` in whichever directory you want files to land:
38
+
39
+ ```bash
40
+ clipto
41
+ ```
42
+
43
+ You'll see a clean terminal banner with the local and LAN URLs:
44
+
45
+ ```text
46
+ ā”Œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”
47
+ │ šŸ“Ž Clipto v0.1.0 │
48
+ │ Saving to: /Users/matt/code/my-project │
49
+ │ Local: http://localhost:8765 │
50
+ │ Network: http://192.168.1.50:8765 │
51
+ ā””ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”˜
52
+ ```
53
+
54
+ 1. Open the URL in your browser (or pass `-o / --open` to auto-open).
55
+ 2. Hit `Cmd+V` to paste a screenshot, or drag files onto the page.
56
+ 3. The files are instantly written to your current directory!
57
+
58
+ ---
59
+
60
+ ## šŸ¤– Using with AI Agents & Scripts
61
+
62
+ Clipto is built from the ground up to integrate cleanly into automated agent workflows (Antigravity, Claude Code, Aider, OpenHands, etc.).
63
+
64
+ ### One-Shot Mode (`--once`)
65
+
66
+ When run with `--once` (or `-1`), Clipto waits for a single upload batch, saves the file(s), prints the resolved absolute path to `stdout`, and shuts down:
67
+
68
+ ```bash
69
+ # Agent or script runs this:
70
+ FILE_PATH=$(clipto --once --title "Submit login bug screenshot" --timeout 120)
71
+
72
+ echo "Agent received file at: $FILE_PATH"
73
+ ```
74
+
75
+ The agent gets the exact file path back into its visual/multimodal context!
76
+
77
+ ---
78
+
79
+ ## āš™ļø CLI Reference
80
+
81
+ ```text
82
+ usage: clipto [-h] [-v] [-p PORT] [--no-hunt] [-d DIR] [-1] [-t TITLE] [-o] [--host HOST] [--timeout TIMEOUT]
83
+
84
+ Instant clipboard, screenshot, and file bridge from browser to terminal.
85
+
86
+ options:
87
+ -h, --help show this help message and exit
88
+ -v, --version show program's version number and exit
89
+ -p PORT, --port PORT Port to listen on (default: 8765; use 0 for random).
90
+ --no-hunt Disable automatic port hunting if port is busy.
91
+ -d DIR, --dir DIR Target directory to save files (default: .).
92
+ -1, --once One-shot mode: exit after upload and print file path(s).
93
+ -t TITLE, --title TITLE
94
+ Custom session title or prompt shown in the UI header.
95
+ -o, --open Automatically open web UI in default browser on launch.
96
+ --host HOST Host to bind to (default: 0.0.0.0).
97
+ --timeout TIMEOUT Timeout in seconds (useful with --once).
98
+ ```
99
+
100
+ ---
101
+
102
+ ## šŸ› ļø Multi-Instance Support
103
+
104
+ You can run multiple `clipto` instances simultaneously across different projects or terminals:
105
+ * Each instance automatically detects occupied ports and claims the next available one.
106
+ * Each browser tab displays its target folder path and optional session title so you always know where your file is landing.
107
+
108
+ ---
109
+
110
+ ## šŸ”’ Security
111
+
112
+ * By default, Clipto binds to `0.0.0.0` for convenience across local networks (e.g. testing with phone/laptop).
113
+ * If you only want local machine access, pass `--host 127.0.0.1`.
114
+ * Filenames are strictly sanitized to prevent directory traversal attacks.
115
+
116
+ ---
117
+
118
+ ## šŸ“„ License
119
+
120
+ MIT Ā© Matt
@@ -0,0 +1,56 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "clipto"
7
+ version = "0.1.0"
8
+ description = "Instant clipboard, screenshot, and file bridge from your browser to your terminal/remote server."
9
+ readme = "README.md"
10
+ license = "MIT"
11
+ requires-python = ">=3.9"
12
+ authors = [
13
+ { name = "Matt", email = "matt@example.com" }
14
+ ]
15
+ keywords = [
16
+ "clipboard",
17
+ "screenshot",
18
+ "upload",
19
+ "cli",
20
+ "developer-tools",
21
+ "ai-tools",
22
+ "remote-transfer"
23
+ ]
24
+ classifiers = [
25
+ "Development Status :: 4 - Beta",
26
+ "Environment :: Console",
27
+ "Intended Audience :: Developers",
28
+ "License :: OSI Approved :: MIT License",
29
+ "Operating System :: OS Independent",
30
+ "Programming Language :: Python :: 3",
31
+ "Programming Language :: Python :: 3.9",
32
+ "Programming Language :: Python :: 3.10",
33
+ "Programming Language :: Python :: 3.11",
34
+ "Programming Language :: Python :: 3.12",
35
+ "Topic :: Utilities",
36
+ ]
37
+ dependencies = []
38
+
39
+ [project.optional-dependencies]
40
+ qr = ["segno>=1.6.0"]
41
+ dev = [
42
+ "pytest>=8.0.0",
43
+ "build>=1.0.0",
44
+ "twine>=5.0.0",
45
+ ]
46
+
47
+ [project.scripts]
48
+ clipto = "clipto.cli:main"
49
+
50
+ [project.urls]
51
+ Homepage = "https://github.com/mattintech/clipto"
52
+ Repository = "https://github.com/mattintech/clipto"
53
+ Issues = "https://github.com/mattintech/clipto/issues"
54
+
55
+ [tool.hatch.build.targets.wheel]
56
+ packages = ["src/clipto"]
@@ -0,0 +1,4 @@
1
+ """Clipto: Instant clipboard, screenshot, and file bridge from browser to terminal."""
2
+
3
+ __version__ = "0.1.0"
4
+ __all__ = ["__version__"]
@@ -0,0 +1,4 @@
1
+ from clipto.cli import main
2
+
3
+ if __name__ == "__main__":
4
+ main()
@@ -0,0 +1,174 @@
1
+ import argparse
2
+ import os
3
+ import sys
4
+ import threading
5
+ import time
6
+ import webbrowser
7
+ from pathlib import Path
8
+ from typing import Optional
9
+
10
+ from clipto import __version__
11
+ from clipto.server import CliptoHTTPServer, CliptoRequestHandler
12
+ from clipto.utils import (
13
+ find_available_port,
14
+ get_local_ip,
15
+ get_tailscale_ip,
16
+ terminal_hyperlink,
17
+ )
18
+
19
+
20
+ def build_parser() -> argparse.ArgumentParser:
21
+ parser = argparse.ArgumentParser(
22
+ prog="clipto",
23
+ description="Instant clipboard, screenshot, and file bridge from browser to terminal.",
24
+ )
25
+ parser.add_argument(
26
+ "-v", "--version",
27
+ action="version",
28
+ version=f"%(prog)s {__version__}",
29
+ )
30
+ parser.add_argument(
31
+ "-p", "--port",
32
+ type=int,
33
+ default=8765,
34
+ help="Port to listen on (default: 8765, auto-increments if occupied; use 0 for random).",
35
+ )
36
+ parser.add_argument(
37
+ "--no-hunt",
38
+ action="store_true",
39
+ help="Disable automatic port hunting if specified port is in use.",
40
+ )
41
+ parser.add_argument(
42
+ "-d", "--dir",
43
+ type=Path,
44
+ default=Path("."),
45
+ help="Target directory where files will be saved (default: current directory).",
46
+ )
47
+ parser.add_argument(
48
+ "-1", "--once",
49
+ action="store_true",
50
+ help="One-shot mode: exit immediately after receiving an upload and print saved file path(s).",
51
+ )
52
+ parser.add_argument(
53
+ "-t", "--title",
54
+ type=str,
55
+ default=None,
56
+ help="Custom session title or prompt shown in the web UI header.",
57
+ )
58
+ parser.add_argument(
59
+ "-o", "--open",
60
+ action="store_true",
61
+ help="Automatically open the web UI in the default browser on launch.",
62
+ )
63
+ parser.add_argument(
64
+ "--host",
65
+ type=str,
66
+ default="0.0.0.0",
67
+ help="Host address to bind to (default: 0.0.0.0).",
68
+ )
69
+ parser.add_argument(
70
+ "--timeout",
71
+ type=int,
72
+ default=None,
73
+ help="Timeout in seconds (useful with --once to prevent hanging indefinitely).",
74
+ )
75
+ return parser
76
+
77
+
78
+ def print_banner(
79
+ port: int,
80
+ target_dir: Path,
81
+ title: Optional[str] = None,
82
+ once: bool = False,
83
+ ):
84
+ local_url = f"http://localhost:{port}"
85
+ lan_ip = get_local_ip()
86
+ tailscale_ip = get_tailscale_ip()
87
+
88
+ lines = [
89
+ f"šŸ“Ž Clipto v{__version__}" + (" [ONE-SHOT MODE]" if once else ""),
90
+ f"Saving to: {target_dir.resolve()}",
91
+ ]
92
+ if title:
93
+ lines.append(f"Session: {title}")
94
+
95
+ lines.append(f"Local: {terminal_hyperlink(local_url)}")
96
+ if lan_ip and lan_ip != "127.0.0.1":
97
+ lines.append(f"Network: {terminal_hyperlink(f'http://{lan_ip}:{port}')}")
98
+ if tailscale_ip:
99
+ lines.append(f"Tailscale: {terminal_hyperlink(f'http://{tailscale_ip}:{port}')}")
100
+
101
+ max_len = max(len(line) for line in lines)
102
+ border = "─" * (max_len + 4)
103
+
104
+ print(f"\nā”Œ{border}┐", file=sys.stderr)
105
+ for line in lines:
106
+ padding = " " * (max_len - len(line))
107
+ print(f"│ {line}{padding} │", file=sys.stderr)
108
+ print(f"ā””{border}ā”˜\n", file=sys.stderr)
109
+ print("Press Cmd+V or drag files into the browser tab. Press Ctrl+C to stop.\n", file=sys.stderr)
110
+
111
+
112
+ def main():
113
+ parser = build_parser()
114
+ args = parser.parse_args()
115
+
116
+ target_dir = args.dir.resolve()
117
+ target_dir.mkdir(parents=True, exist_ok=True)
118
+
119
+ # Determine port
120
+ if args.no_hunt:
121
+ port = args.port
122
+ else:
123
+ try:
124
+ port = find_available_port(args.port)
125
+ except RuntimeError as e:
126
+ print(f"Error: {e}", file=sys.stderr)
127
+ sys.exit(1)
128
+
129
+ try:
130
+ server = CliptoHTTPServer(
131
+ (args.host, port),
132
+ CliptoRequestHandler,
133
+ upload_dir=target_dir,
134
+ title=args.title,
135
+ once=args.once,
136
+ )
137
+ except OSError as e:
138
+ print(f"Error binding to port {port}: {e}", file=sys.stderr)
139
+ sys.exit(1)
140
+
141
+ print_banner(port, target_dir, title=args.title, once=args.once)
142
+
143
+ if args.open:
144
+ webbrowser.open(f"http://localhost:{port}")
145
+
146
+ # Start server thread
147
+ server_thread = threading.Thread(target=server.serve_forever, daemon=True)
148
+ server_thread.start()
149
+
150
+ start_time = time.time()
151
+
152
+ try:
153
+ while not server.shutdown_event.is_set():
154
+ if args.timeout and (time.time() - start_time) > args.timeout:
155
+ print("\nOperation timed out.", file=sys.stderr)
156
+ server.shutdown()
157
+ sys.exit(124)
158
+ time.sleep(0.1)
159
+
160
+ # Shutdown triggered (in once mode)
161
+ server.shutdown()
162
+
163
+ # In once mode, output the uploaded file path(s) to stdout
164
+ for file_path in server.uploaded_files:
165
+ print(str(file_path.resolve()))
166
+
167
+ except KeyboardInterrupt:
168
+ print("\nStopping Clipto server...", file=sys.stderr)
169
+ server.shutdown()
170
+ sys.exit(0)
171
+
172
+
173
+ if __name__ == "__main__":
174
+ main()