shotmon-cli 1.0.0

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.
package/README.md ADDED
@@ -0,0 +1,35 @@
1
+ # shotmon
2
+
3
+ Screenshot monitor CLI. Watches clipboard for screenshots and uploads to remote server via SSH, or saves locally.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ npm install -g shotmon
9
+ ```
10
+
11
+ ## Commands
12
+
13
+ ```
14
+ shotmon Setup config and start monitoring
15
+ shotmon start Start monitoring (select target)
16
+ shotmon stop Stop monitoring
17
+ shotmon status Show running status and target
18
+ shotmon config Modify remotes configuration
19
+ shotmon uninstall Remove config files
20
+ ```
21
+
22
+ ## Features
23
+
24
+ - Auto-detects SSH remotes from `~/.ssh/config` and shell history
25
+ - **Local mode**: Saves to `~/shotmon-screenshots/`, copies path to clipboard
26
+ - **Remote mode**: Uploads via SSH, copies remote path to clipboard
27
+ - Fast SSH with ControlMaster connection reuse
28
+ - WSL support (reads Windows clipboard)
29
+
30
+ ## How it works
31
+
32
+ 1. Polls clipboard for new images (200ms interval)
33
+ 2. Detects changes via MD5 hash comparison
34
+ 3. Uploads via SSH or saves locally
35
+ 4. Copies absolute path to clipboard for easy pasting
package/dist/config.js ADDED
@@ -0,0 +1,136 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.getConfigPath = getConfigPath;
37
+ exports.loadConfig = loadConfig;
38
+ exports.saveConfig = saveConfig;
39
+ exports.detectSSHRemotes = detectSSHRemotes;
40
+ exports.detectSSHFromHistory = detectSSHFromHistory;
41
+ const os = __importStar(require("os"));
42
+ const fs = __importStar(require("fs"));
43
+ const path = __importStar(require("path"));
44
+ function getConfigPath() {
45
+ return path.join(os.homedir(), ".config", "shotmon", "config.json");
46
+ }
47
+ function loadConfig() {
48
+ const configPath = getConfigPath();
49
+ if (!fs.existsSync(configPath)) {
50
+ return null;
51
+ }
52
+ const content = fs.readFileSync(configPath, "utf-8");
53
+ return JSON.parse(content);
54
+ }
55
+ function saveConfig(config) {
56
+ const configPath = getConfigPath();
57
+ const configDir = path.dirname(configPath);
58
+ if (!fs.existsSync(configDir)) {
59
+ fs.mkdirSync(configDir, { recursive: true });
60
+ }
61
+ fs.writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n");
62
+ }
63
+ function detectSSHRemotes() {
64
+ const home = os.homedir();
65
+ const sshConfigPath = path.join(home, ".ssh", "config");
66
+ if (!fs.existsSync(sshConfigPath)) {
67
+ return [];
68
+ }
69
+ const content = fs.readFileSync(sshConfigPath, "utf-8");
70
+ const lines = content.split("\n");
71
+ const hosts = [];
72
+ let currentHost = null;
73
+ for (const line of lines) {
74
+ const trimmed = line.trim();
75
+ if (trimmed.toLowerCase().startsWith("host ")) {
76
+ if (currentHost) {
77
+ hosts.push(currentHost);
78
+ }
79
+ const hostName = trimmed.slice(5).trim().split(/\s+/)[0];
80
+ // Skip wildcard patterns
81
+ if (!hostName.includes("*")) {
82
+ currentHost = { name: hostName };
83
+ }
84
+ else {
85
+ currentHost = null;
86
+ }
87
+ }
88
+ else if (currentHost) {
89
+ if (trimmed.toLowerCase().startsWith("hostname ")) {
90
+ currentHost.hostname = trimmed.slice(9).trim();
91
+ }
92
+ else if (trimmed.toLowerCase().startsWith("user ")) {
93
+ currentHost.user = trimmed.slice(5).trim();
94
+ }
95
+ }
96
+ }
97
+ if (currentHost) {
98
+ hosts.push(currentHost);
99
+ }
100
+ return hosts;
101
+ }
102
+ function detectSSHFromHistory() {
103
+ const home = os.homedir();
104
+ const historyFiles = [
105
+ path.join(home, ".bash_history"),
106
+ path.join(home, ".zsh_history"),
107
+ ];
108
+ const remotes = new Set();
109
+ for (const histFile of historyFiles) {
110
+ if (!fs.existsSync(histFile))
111
+ continue;
112
+ try {
113
+ const content = fs.readFileSync(histFile, "utf-8");
114
+ const lines = content.split("\n");
115
+ for (const line of lines) {
116
+ // Match ssh commands: ssh [options] user@host or ssh [options] host
117
+ const match = line.match(/\bssh\s+(?:[^@\s]+\s+)*?(\S+@\S+?)(?:\s|$)/);
118
+ if (match) {
119
+ const remote = match[1];
120
+ // Filter out flags and invalid entries
121
+ if (remote.includes("@") && !remote.startsWith("-")) {
122
+ // Clean up any trailing characters
123
+ const clean = remote.replace(/[;|&].*$/, "");
124
+ if (clean.match(/^[\w.-]+@[\w.-]+$/)) {
125
+ remotes.add(clean);
126
+ }
127
+ }
128
+ }
129
+ }
130
+ }
131
+ catch {
132
+ // Ignore read errors
133
+ }
134
+ }
135
+ return Array.from(remotes);
136
+ }
package/dist/index.js ADDED
@@ -0,0 +1,293 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
4
+ if (k2 === undefined) k2 = k;
5
+ var desc = Object.getOwnPropertyDescriptor(m, k);
6
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
7
+ desc = { enumerable: true, get: function() { return m[k]; } };
8
+ }
9
+ Object.defineProperty(o, k2, desc);
10
+ }) : (function(o, m, k, k2) {
11
+ if (k2 === undefined) k2 = k;
12
+ o[k2] = m[k];
13
+ }));
14
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
15
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
16
+ }) : function(o, v) {
17
+ o["default"] = v;
18
+ });
19
+ var __importStar = (this && this.__importStar) || (function () {
20
+ var ownKeys = function(o) {
21
+ ownKeys = Object.getOwnPropertyNames || function (o) {
22
+ var ar = [];
23
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
24
+ return ar;
25
+ };
26
+ return ownKeys(o);
27
+ };
28
+ return function (mod) {
29
+ if (mod && mod.__esModule) return mod;
30
+ var result = {};
31
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
32
+ __setModuleDefault(result, mod);
33
+ return result;
34
+ };
35
+ })();
36
+ Object.defineProperty(exports, "__esModule", { value: true });
37
+ const fs = __importStar(require("fs"));
38
+ const path = __importStar(require("path"));
39
+ const os = __importStar(require("os"));
40
+ const child_process_1 = require("child_process");
41
+ const config_1 = require("./config");
42
+ const prompts_1 = require("./prompts");
43
+ const monitor_1 = require("./monitor");
44
+ function getVersion() {
45
+ const pkgPath = path.join(__dirname, "..", "package.json");
46
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
47
+ return pkg.version;
48
+ }
49
+ async function addRemotes(existing) {
50
+ const remotes = [...existing];
51
+ // Collect all detected remotes
52
+ const allDetected = [];
53
+ // From SSH config
54
+ const sshHosts = (0, config_1.detectSSHRemotes)();
55
+ for (const host of sshHosts) {
56
+ if (!remotes.includes(host.name)) {
57
+ const details = [host.user, host.hostname].filter(Boolean).join("@");
58
+ allDetected.push({
59
+ name: host.name,
60
+ source: details ? `config: ${details}` : "config",
61
+ });
62
+ }
63
+ }
64
+ // From bash/zsh history
65
+ const historyRemotes = (0, config_1.detectSSHFromHistory)();
66
+ for (const remote of historyRemotes) {
67
+ if (!remotes.includes(remote) && !allDetected.find(d => d.name === remote)) {
68
+ allDetected.push({
69
+ name: remote,
70
+ source: "history",
71
+ });
72
+ }
73
+ }
74
+ if (allDetected.length > 0) {
75
+ const choices = allDetected.map(d => `${d.name} (${d.source})`);
76
+ const selected = await (0, prompts_1.promptMultiSelect)("Select SSH remotes to add (space to toggle, enter to confirm)", choices);
77
+ for (const msg of selected) {
78
+ const detected = allDetected.find(d => `${d.name} (${d.source})` === msg);
79
+ if (detected) {
80
+ remotes.push(detected.name);
81
+ }
82
+ }
83
+ }
84
+ // Add custom remotes
85
+ let addMore = await (0, prompts_1.promptConfirm)("Add a custom SSH remote?");
86
+ while (addMore) {
87
+ const remoteName = await (0, prompts_1.promptInput)("Enter SSH remote (e.g., user@host)");
88
+ if (remoteName && !remotes.includes(remoteName)) {
89
+ remotes.push(remoteName);
90
+ console.log(`Added: ${remoteName}`);
91
+ }
92
+ addMore = await (0, prompts_1.promptConfirm)("Add another?");
93
+ }
94
+ return remotes;
95
+ }
96
+ function startBackground(remote) {
97
+ const child = (0, child_process_1.spawn)(process.execPath, [__filename, "--daemon", remote], {
98
+ detached: true,
99
+ stdio: "ignore",
100
+ env: { ...process.env, SHOTMON_BACKGROUND: "1" },
101
+ });
102
+ child.unref();
103
+ console.log(`Started in background (PID: ${child.pid})`);
104
+ console.log(`Logs: ~/.config/shotmon/logs/`);
105
+ }
106
+ function showHelp() {
107
+ console.log(`Usage: shotmon <command>
108
+
109
+ Commands:
110
+ start Start monitoring in background
111
+ stop Stop background process
112
+ status Show if running
113
+ config Modify configuration
114
+ uninstall Remove config and stop process
115
+
116
+ Run without command to setup/configure.
117
+ `);
118
+ }
119
+ function uninstall() {
120
+ // Stop any running process
121
+ try {
122
+ const result = (0, child_process_1.execSync)("pgrep -f 'node.*[s]hotmon.*--daemon'", { encoding: "utf8" });
123
+ const pids = result.trim().split("\n").filter(Boolean);
124
+ for (const pid of pids) {
125
+ process.kill(parseInt(pid), "SIGTERM");
126
+ }
127
+ if (pids.length > 0) {
128
+ console.log("Stopped running process");
129
+ }
130
+ }
131
+ catch {
132
+ // Not running
133
+ }
134
+ // Remove config directory
135
+ const configDir = path.join(os.homedir(), ".config", "shotmon");
136
+ if (fs.existsSync(configDir)) {
137
+ fs.rmSync(configDir, { recursive: true });
138
+ console.log(`Removed ${configDir}`);
139
+ }
140
+ console.log("\nNow run: npm uninstall -g shotmon");
141
+ }
142
+ function stopBackground() {
143
+ try {
144
+ // Use bracket trick to avoid pgrep matching itself
145
+ const result = (0, child_process_1.execSync)("pgrep -f 'node.*[s]hotmon.*--daemon'", { encoding: "utf8" });
146
+ const pids = result.trim().split("\n").filter(Boolean);
147
+ for (const pid of pids) {
148
+ process.kill(parseInt(pid), "SIGTERM");
149
+ console.log(`Stopped process ${pid}`);
150
+ }
151
+ if (pids.length === 0) {
152
+ console.log("No shotmon process running");
153
+ }
154
+ }
155
+ catch {
156
+ console.log("No shotmon process running");
157
+ }
158
+ }
159
+ function showStatus() {
160
+ try {
161
+ // Use bracket trick to avoid pgrep matching itself
162
+ const result = (0, child_process_1.execSync)("pgrep -af 'node.*[s]hotmon.*--daemon'", { encoding: "utf8" });
163
+ const lines = result.trim().split("\n").filter(Boolean);
164
+ if (lines.length > 0) {
165
+ for (const line of lines) {
166
+ // Parse "PID command args"
167
+ const match = line.match(/^(\d+)\s+.*--daemon\s+(.+)$/);
168
+ if (match) {
169
+ const pid = match[1];
170
+ const target = match[2];
171
+ console.log(`Running (PID: ${pid}) -> ${target}`);
172
+ }
173
+ else {
174
+ const pid = line.split(/\s+/)[0];
175
+ console.log(`Running (PID: ${pid})`);
176
+ }
177
+ }
178
+ }
179
+ else {
180
+ console.log("Not running");
181
+ }
182
+ }
183
+ catch {
184
+ console.log("Not running");
185
+ }
186
+ }
187
+ async function runConfig() {
188
+ let config = (0, config_1.loadConfig)();
189
+ if (!config || config.remotes.length === 0) {
190
+ if (!config) {
191
+ console.log("Welcome! Let's add some SSH remotes.\n");
192
+ }
193
+ else {
194
+ console.log("No remotes configured. Let's add some.\n");
195
+ }
196
+ const remotes = await addRemotes([]);
197
+ config = { remotes };
198
+ (0, config_1.saveConfig)(config);
199
+ if (remotes.length > 0) {
200
+ console.log(`\nSaved ${remotes.length} remote(s).`);
201
+ }
202
+ }
203
+ else {
204
+ console.log(`SSH remotes: ${config.remotes.join(", ")}\n`);
205
+ const modify = await (0, prompts_1.promptConfirm)("Modify remotes?");
206
+ if (modify) {
207
+ const toKeep = await (0, prompts_1.promptMultiSelect)("Select remotes to keep (space to toggle, enter to confirm)", config.remotes);
208
+ const remotes = await addRemotes(toKeep);
209
+ config = { remotes };
210
+ (0, config_1.saveConfig)(config);
211
+ console.log(`\nSaved ${remotes.length} remote(s).`);
212
+ }
213
+ }
214
+ return config;
215
+ }
216
+ async function startCommand() {
217
+ const config = (0, config_1.loadConfig)();
218
+ if (!config || config.remotes.length === 0) {
219
+ console.log("No remotes configured. Run 'shotmon' first to set up.");
220
+ process.exit(1);
221
+ }
222
+ // Add "local" option to the list
223
+ const options = ["local", ...config.remotes];
224
+ let selected;
225
+ if (options.length === 1) {
226
+ selected = options[0];
227
+ }
228
+ else {
229
+ selected = await (0, prompts_1.promptSelect)("Select target", options);
230
+ }
231
+ // Stop any existing process before starting new one
232
+ try {
233
+ const result = (0, child_process_1.execSync)("pgrep -f 'node.*[s]hotmon.*--daemon'", { encoding: "utf8" });
234
+ const pids = result.trim().split("\n").filter(Boolean);
235
+ for (const pid of pids) {
236
+ process.kill(parseInt(pid), "SIGTERM");
237
+ }
238
+ if (pids.length > 0) {
239
+ console.log(`Stopped previous process`);
240
+ }
241
+ }
242
+ catch {
243
+ // Not running, continue
244
+ }
245
+ startBackground(selected);
246
+ }
247
+ async function main() {
248
+ const args = process.argv.slice(2);
249
+ const command = args[0];
250
+ // Handle --daemon (internal use)
251
+ if (command === "--daemon") {
252
+ const remote = args[1];
253
+ if (remote) {
254
+ await (0, monitor_1.startMonitor)(remote);
255
+ }
256
+ return;
257
+ }
258
+ console.log(`shotmon v${getVersion()}\n`);
259
+ // Handle commands
260
+ if (command === "help" || command === "--help" || command === "-h") {
261
+ showHelp();
262
+ return;
263
+ }
264
+ if (command === "stop") {
265
+ stopBackground();
266
+ return;
267
+ }
268
+ if (command === "status") {
269
+ showStatus();
270
+ return;
271
+ }
272
+ if (command === "start") {
273
+ await startCommand();
274
+ return;
275
+ }
276
+ if (command === "config") {
277
+ await runConfig();
278
+ return;
279
+ }
280
+ if (command === "uninstall") {
281
+ uninstall();
282
+ return;
283
+ }
284
+ // No command - run config flow then auto-start
285
+ const config = await runConfig();
286
+ if (config.remotes.length === 0) {
287
+ console.log("No remotes configured. Run shotmon again to add remotes.");
288
+ process.exit(0);
289
+ }
290
+ console.log("\n--- Starting monitor ---\n");
291
+ await startCommand();
292
+ }
293
+ main().catch(console.error);
@@ -0,0 +1,283 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.startMonitor = startMonitor;
37
+ const child_process_1 = require("child_process");
38
+ const crypto = __importStar(require("crypto"));
39
+ const fs = __importStar(require("fs"));
40
+ const path = __importStar(require("path"));
41
+ const os = __importStar(require("os"));
42
+ const POLL_INTERVAL_MS = 200;
43
+ const LOG_MAX_AGE_MS = 60 * 60 * 1000; // 1 hour
44
+ let lastImageHash = null;
45
+ let logFile = null;
46
+ let logStartTime = 0;
47
+ function isWSL() {
48
+ try {
49
+ const release = fs.readFileSync("/proc/version", "utf8");
50
+ return release.toLowerCase().includes("microsoft") || release.toLowerCase().includes("wsl");
51
+ }
52
+ catch {
53
+ return false;
54
+ }
55
+ }
56
+ function getLogDir() {
57
+ return path.join(os.homedir(), ".config", "shotmon", "logs");
58
+ }
59
+ function ensureLogDir() {
60
+ const logDir = getLogDir();
61
+ if (!fs.existsSync(logDir)) {
62
+ fs.mkdirSync(logDir, { recursive: true });
63
+ }
64
+ }
65
+ function createNewLogFile() {
66
+ ensureLogDir();
67
+ const timestamp = new Date().toISOString().replace(/[:.]/g, "-").slice(0, 19);
68
+ const filename = `shotmon-${timestamp}.log`;
69
+ return path.join(getLogDir(), filename);
70
+ }
71
+ function log(message) {
72
+ const now = Date.now();
73
+ const timestamp = new Date().toISOString();
74
+ const line = `[${timestamp}] ${message}\n`;
75
+ // Check if we need a new log file
76
+ if (!logFile || (now - logStartTime) > LOG_MAX_AGE_MS) {
77
+ logFile = createNewLogFile();
78
+ logStartTime = now;
79
+ }
80
+ // Write to file
81
+ fs.appendFileSync(logFile, line);
82
+ // Also print to console if not in background
83
+ if (!process.env.SHOTMON_BACKGROUND) {
84
+ process.stdout.write(message + "\n");
85
+ }
86
+ }
87
+ async function getClipboardImageWSL() {
88
+ try {
89
+ // PowerShell script to get clipboard image as base64
90
+ const psScript = `
91
+ Add-Type -AssemblyName System.Windows.Forms
92
+ $img = [System.Windows.Forms.Clipboard]::GetImage()
93
+ if ($img -ne $null) {
94
+ $ms = New-Object System.IO.MemoryStream
95
+ $img.Save($ms, [System.Drawing.Imaging.ImageFormat]::Png)
96
+ [Convert]::ToBase64String($ms.ToArray())
97
+ }
98
+ `;
99
+ // Encode as UTF-16LE base64 for -EncodedCommand
100
+ const encoded = Buffer.from(psScript, "utf16le").toString("base64");
101
+ const result = (0, child_process_1.execSync)(`powershell.exe -NoProfile -EncodedCommand ${encoded}`, {
102
+ encoding: "utf8",
103
+ timeout: 5000,
104
+ }).trim();
105
+ if (result && result.length > 0) {
106
+ return Buffer.from(result, "base64");
107
+ }
108
+ return null;
109
+ }
110
+ catch {
111
+ return null;
112
+ }
113
+ }
114
+ async function getClipboardImageNative() {
115
+ try {
116
+ // Try using @crosscopy/clipboard for native Linux/macOS
117
+ // @ts-ignore
118
+ const Clipboard = require("@crosscopy/clipboard").default;
119
+ const hasImage = await Clipboard.hasImage();
120
+ if (!hasImage) {
121
+ return null;
122
+ }
123
+ const base64 = await Clipboard.getImageBase64();
124
+ if (!base64) {
125
+ return null;
126
+ }
127
+ return Buffer.from(base64, "base64");
128
+ }
129
+ catch {
130
+ return null;
131
+ }
132
+ }
133
+ async function getClipboardImage() {
134
+ if (isWSL()) {
135
+ return getClipboardImageWSL();
136
+ }
137
+ return getClipboardImageNative();
138
+ }
139
+ function getImageHash(buffer) {
140
+ return crypto.createHash("md5").update(buffer).digest("hex");
141
+ }
142
+ function generateFilename() {
143
+ const now = new Date();
144
+ const timestamp = now.toISOString().replace(/[:.]/g, "-").slice(0, 19);
145
+ return `screenshot-${timestamp}.png`;
146
+ }
147
+ function getLocalScreenshotDir() {
148
+ return path.join(os.homedir(), "shotmon-screenshots");
149
+ }
150
+ function saveLocal(imageBuffer, filename) {
151
+ const dir = getLocalScreenshotDir();
152
+ const filePath = path.join(dir, filename);
153
+ try {
154
+ if (!fs.existsSync(dir)) {
155
+ fs.mkdirSync(dir, { recursive: true });
156
+ }
157
+ fs.writeFileSync(filePath, imageBuffer);
158
+ return { success: true, path: filePath };
159
+ }
160
+ catch {
161
+ return { success: false, path: filePath };
162
+ }
163
+ }
164
+ function getRemoteHomeDir(remote) {
165
+ // Extract username from user@host format
166
+ const match = remote.match(/^([^@]+)@/);
167
+ const user = match ? match[1] : "root";
168
+ return user === "root" ? "/root" : `/home/${user}`;
169
+ }
170
+ async function pipeToRemote(imageBuffer, remote, filename) {
171
+ const homeDir = getRemoteHomeDir(remote);
172
+ const remotePath = `${homeDir}/shotmon-screenshots/${filename}`;
173
+ return new Promise((resolve) => {
174
+ // Ensure directory exists and write file
175
+ // Use ControlMaster options for faster repeated connections
176
+ const proc = (0, child_process_1.spawn)("ssh", [
177
+ "-o", "ControlMaster=auto",
178
+ "-o", "ControlPath=~/.ssh/shotmon-%r@%h:%p",
179
+ "-o", "ControlPersist=60",
180
+ remote,
181
+ `mkdir -p ${homeDir}/shotmon-screenshots && cat > ${remotePath}`
182
+ ]);
183
+ proc.stdin.write(imageBuffer);
184
+ proc.stdin.end();
185
+ proc.on("close", (code) => {
186
+ resolve({ success: code === 0, path: remotePath });
187
+ });
188
+ proc.on("error", () => {
189
+ resolve({ success: false, path: remotePath });
190
+ });
191
+ });
192
+ }
193
+ function copyToClipboardWSL(text) {
194
+ try {
195
+ // Use clip.exe which is much faster than PowerShell
196
+ (0, child_process_1.execSync)(`echo -n '${text.replace(/'/g, "'\\''")}' | clip.exe`, { timeout: 2000 });
197
+ }
198
+ catch {
199
+ // Ignore clipboard errors
200
+ }
201
+ }
202
+ async function copyToClipboardNative(text) {
203
+ try {
204
+ // @ts-ignore
205
+ const Clipboard = require("@crosscopy/clipboard").default;
206
+ await Clipboard.setText(text);
207
+ }
208
+ catch {
209
+ // Ignore clipboard errors
210
+ }
211
+ }
212
+ async function copyToClipboard(text) {
213
+ if (isWSL()) {
214
+ copyToClipboardWSL(text);
215
+ }
216
+ else {
217
+ await copyToClipboardNative(text);
218
+ }
219
+ }
220
+ async function startMonitor(remote) {
221
+ // Initialize logging
222
+ logFile = createNewLogFile();
223
+ logStartTime = Date.now();
224
+ const wsl = isWSL();
225
+ log(`Starting monitor for: ${remote}`);
226
+ log(`Environment: ${wsl ? "WSL" : "Native"}`);
227
+ log(`Log file: ${logFile}`);
228
+ if (remote === "local") {
229
+ log(`Saving to: ${getLocalScreenshotDir()}`);
230
+ }
231
+ log("");
232
+ log("Monitoring clipboard... (Ctrl+C to stop)");
233
+ log("");
234
+ // Initialize with current clipboard state
235
+ const initialImage = await getClipboardImage();
236
+ if (initialImage) {
237
+ lastImageHash = getImageHash(initialImage);
238
+ }
239
+ const poll = async () => {
240
+ try {
241
+ const imageBuffer = await getClipboardImage();
242
+ if (!imageBuffer) {
243
+ return;
244
+ }
245
+ const currentHash = getImageHash(imageBuffer);
246
+ if (currentHash !== lastImageHash) {
247
+ lastImageHash = currentHash;
248
+ const filename = generateFilename();
249
+ const size = Math.round(imageBuffer.length / 1024);
250
+ log(`New screenshot: ${filename} (${size}KB)`);
251
+ if (remote === "local") {
252
+ const result = saveLocal(imageBuffer, filename);
253
+ if (result.success) {
254
+ log(` -> Saved: ${result.path}`);
255
+ await copyToClipboard(result.path);
256
+ log(` -> Copied to clipboard`);
257
+ }
258
+ else {
259
+ log(` -> Failed to save locally`);
260
+ }
261
+ }
262
+ else {
263
+ const result = await pipeToRemote(imageBuffer, remote, filename);
264
+ if (result.success) {
265
+ log(` -> Sent to ${remote}:${result.path}`);
266
+ await copyToClipboard(result.path);
267
+ log(` -> Copied to clipboard`);
268
+ }
269
+ else {
270
+ log(` -> Failed to send to ${remote}`);
271
+ }
272
+ }
273
+ }
274
+ }
275
+ catch (err) {
276
+ log(`Error: ${err}`);
277
+ }
278
+ };
279
+ // Start polling
280
+ setInterval(poll, POLL_INTERVAL_MS);
281
+ // Keep process running
282
+ await new Promise(() => { });
283
+ }
@@ -0,0 +1,33 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.promptConfirm = promptConfirm;
4
+ exports.promptInput = promptInput;
5
+ exports.promptSelect = promptSelect;
6
+ exports.promptMultiSelect = promptMultiSelect;
7
+ // @ts-ignore
8
+ const { Select, Confirm, Input, MultiSelect } = require("enquirer");
9
+ async function promptConfirm(message) {
10
+ const prompt = new Confirm({ name: "confirm", message });
11
+ return prompt.run();
12
+ }
13
+ async function promptInput(message) {
14
+ const prompt = new Input({ name: "input", message });
15
+ return prompt.run();
16
+ }
17
+ async function promptSelect(message, choices) {
18
+ const prompt = new Select({ name: "select", message, choices });
19
+ return prompt.run();
20
+ }
21
+ async function promptMultiSelect(message, choices) {
22
+ const prompt = new MultiSelect({
23
+ name: "multiselect",
24
+ message,
25
+ choices: choices,
26
+ initial: choices,
27
+ hint: "(space to toggle, enter to confirm)",
28
+ indicator(state, choice) {
29
+ return choice.enabled ? "●" : "○";
30
+ },
31
+ });
32
+ return prompt.run();
33
+ }
package/package.json ADDED
@@ -0,0 +1,28 @@
1
+ {
2
+ "name": "shotmon-cli",
3
+ "version": "1.0.0",
4
+ "description": "Screenshot monitor CLI tool",
5
+ "main": "dist/index.js",
6
+ "bin": {
7
+ "shotmon": "dist/index.js"
8
+ },
9
+ "scripts": {
10
+ "build": "tsc",
11
+ "prepublishOnly": "npm run build"
12
+ },
13
+ "keywords": [
14
+ "screenshot",
15
+ "monitor",
16
+ "clipboard"
17
+ ],
18
+ "author": "",
19
+ "license": "MIT",
20
+ "devDependencies": {
21
+ "@types/node": "^25.0.10",
22
+ "typescript": "^5.9.3"
23
+ },
24
+ "dependencies": {
25
+ "@crosscopy/clipboard": "^0.2.8",
26
+ "enquirer": "^2.4.1"
27
+ }
28
+ }