@quark.clip/quark 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.
@@ -0,0 +1,23 @@
1
+ {
2
+ "name": "quark-daemon",
3
+ "version": "1.0.0",
4
+ "description": "AI-native cross-platform clipboard micro-daemon",
5
+ "main": "src/daemon.js",
6
+ "bin": {
7
+ "quark": "bin/quark.js"
8
+ },
9
+ "dependencies": {
10
+ "@modelcontextprotocol/sdk": "^1.0.1",
11
+ "bonjour-service": "^1.2.1",
12
+ "marked": "^17.0.3",
13
+ "ws": "^8.16.0"
14
+ },
15
+ "scripts": {
16
+ "start": "node bin/quark.js start",
17
+ "stop": "node bin/quark.js stop",
18
+ "install-service": "node bin/quark.js install",
19
+ "mcp": "node bin/quark.js mcp"
20
+ },
21
+ "author": "Adarsh Agrahari",
22
+ "license": "MIT"
23
+ }
@@ -0,0 +1,81 @@
1
+ const { execSync } = require('child_process');
2
+ const os = require('os');
3
+
4
+ function read() {
5
+ let text = '';
6
+ let html = '';
7
+ try {
8
+ const platform = os.platform();
9
+ if (platform === 'darwin') {
10
+ text = execSync('pbpaste', { encoding: 'utf8' }).toString();
11
+ try {
12
+ const hex = execSync(`osascript -e 'the clipboard as "HTML"' 2>/dev/null`, { encoding: 'utf8' });
13
+ const match = hex.match(/«data HTML([0-9A-F]+)»/i);
14
+ if (match) html = Buffer.from(match[1], 'hex').toString('utf8');
15
+ } catch(e) {}
16
+ } else if (platform === 'linux') {
17
+ text = execSync('xclip -selection clipboard -o', { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).toString();
18
+ try {
19
+ html = execSync('xclip -selection clipboard -o -t text/html', { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).toString();
20
+ } catch(e) {}
21
+ } else if (platform === 'win32') {
22
+ text = execSync('powershell -NoProfile -Command "Get-Clipboard -Format Text"', { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).toString().replace(/\r\n$/, '');
23
+ try {
24
+ const ps = `Add-Type -AssemblyName System.Windows.Forms; [Windows.Forms.Clipboard]::GetText([Windows.Forms.TextDataFormat]::Html)`;
25
+ let rawHtml = execSync(`powershell -NoProfile -Command "${ps}"`, { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).toString();
26
+ const fragmentMatch = rawHtml.match(/<!--StartFragment-->(.*?)<!--EndFragment-->/is);
27
+ if (fragmentMatch) {
28
+ html = fragmentMatch[1];
29
+ } else {
30
+ html = rawHtml.replace(/^Version:.*?\r?\nStartHTML:.*?\r?\nEndHTML:.*?\r?\nStartFragment:.*?\r?\nEndFragment:.*?\r?\n/is, '');
31
+ }
32
+ } catch(e) {}
33
+ }
34
+ } catch (e) {
35
+ return { text: '', html: '' };
36
+ }
37
+ return { text, html };
38
+ }
39
+
40
+ function writeHtml(html, plainText) {
41
+ try {
42
+ const platform = os.platform();
43
+ if (platform === 'darwin') {
44
+ // textutil expects input on stdin
45
+ execSync('textutil -stdin -format html -convert rtf -stdout | pbcopy', { input: html, stdio: ['pipe', 'ignore', 'ignore'] });
46
+ } else if (platform === 'linux') {
47
+ execSync('xclip -selection clipboard -t text/html', { input: html, stdio: ['pipe', 'ignore', 'ignore'] });
48
+ } else if (platform === 'win32') {
49
+ const payload = `Version:0.9\r\nStartHTML:0000000000\r\nEndHTML:0000000000\r\nStartFragment:0000000000\r\nEndFragment:0000000000\r\n<html><body><!--StartFragment-->${html}<!--EndFragment--></body></html>`;
50
+ const base64Html = Buffer.from(payload, 'utf8').toString('base64');
51
+ const ps = `
52
+ Add-Type -AssemblyName System.Windows.Forms
53
+ $bytes = [System.Convert]::FromBase64String('${base64Html}')
54
+ $html = [System.Text.Encoding]::UTF8.GetString($bytes)
55
+ [Windows.Forms.Clipboard]::SetText($html, [Windows.Forms.TextDataFormat]::Html)
56
+ `;
57
+ execSync(`powershell -NoProfile -Command -`, { input: ps, stdio: ['pipe', 'ignore', 'ignore'] });
58
+ }
59
+ } catch (e) { console.error('Error setting clipboard HTML:', e.message); }
60
+ }
61
+
62
+ function writeText(text) {
63
+ try {
64
+ const platform = os.platform();
65
+ if (platform === 'darwin') {
66
+ execSync('pbcopy', { input: text, stdio: ['pipe', 'ignore', 'ignore'] });
67
+ } else if (platform === 'linux') {
68
+ execSync('xclip -selection clipboard -i', { input: text, stdio: ['pipe', 'ignore', 'ignore'] });
69
+ } else if (platform === 'win32') {
70
+ const base64Text = Buffer.from(text, 'utf8').toString('base64');
71
+ const ps = `
72
+ $bytes = [System.Convert]::FromBase64String('${base64Text}')
73
+ $text = [System.Text.Encoding]::UTF8.GetString($bytes)
74
+ Set-Clipboard -Value $text
75
+ `;
76
+ execSync(`powershell -NoProfile -Command -`, { input: ps, stdio: ['pipe', 'ignore', 'ignore'] });
77
+ }
78
+ } catch (e) { console.error('Error setting clipboard text:', e.message); }
79
+ }
80
+
81
+ module.exports = { read, writeHtml, writeText };
@@ -0,0 +1,104 @@
1
+ const clipboard = require('./clipboard');
2
+ const transformers = require('./transformers');
3
+ const network = require('./network');
4
+ const http = require('http');
5
+
6
+ console.log('🌌 Quark Daemon Started at', new Date().toISOString());
7
+
8
+ let lastClip = clipboard.read();
9
+ let isSyncingFromNetwork = false;
10
+
11
+ // Initialize P2P Network
12
+ network.init((remoteData) => {
13
+ isSyncingFromNetwork = true;
14
+ console.log('📥 Received clipboard from network peer');
15
+ if (remoteData.html) {
16
+ clipboard.writeHtml(remoteData.html, remoteData.text);
17
+ } else {
18
+ clipboard.writeText(remoteData.text);
19
+ }
20
+ lastClip = clipboard.read(); // Update local state
21
+ setTimeout(() => { isSyncingFromNetwork = false; }, 1000);
22
+ });
23
+
24
+ // Local HTTP Server for MCP Bridge
25
+ // The MCP stdio process will call this to read/write clipboard
26
+ const API_PORT = 14314;
27
+ const server = http.createServer((req, res) => {
28
+ if (req.url === '/clipboard' && req.method === 'GET') {
29
+ res.writeHead(200, { 'Content-Type': 'application/json' });
30
+ res.end(JSON.stringify({ text: clipboard.read().text }));
31
+ } else if (req.url === '/clipboard' && req.method === 'POST') {
32
+ let body = '';
33
+ req.on('data', chunk => body += chunk.toString());
34
+ req.on('end', () => {
35
+ try {
36
+ const data = JSON.parse(body);
37
+ if (data.text) {
38
+ clipboard.writeText(data.text);
39
+ lastClip = clipboard.read();
40
+ network.broadcast(data.text);
41
+ }
42
+ res.writeHead(200);
43
+ res.end(JSON.stringify({ success: true }));
44
+ } catch (e) {
45
+ res.writeHead(400);
46
+ res.end();
47
+ }
48
+ });
49
+ } else {
50
+ res.writeHead(404);
51
+ res.end();
52
+ }
53
+ });
54
+ server.listen(API_PORT, '127.0.0.1', () => {
55
+ console.log(`🧠 Local API for MCP Bridge listening on port ${API_PORT}`);
56
+ });
57
+
58
+ // Main Clipboard Polling Loop
59
+ const pollInterval = setInterval(async () => {
60
+ if (isSyncingFromNetwork) return; // Don't re-process network syncs
61
+
62
+ const currentClip = clipboard.read();
63
+ if (currentClip.text && (currentClip.text !== lastClip.text || currentClip.html !== lastClip.html)) {
64
+ lastClip = currentClip;
65
+
66
+ // Process through transformers
67
+ const result = await transformers.processClipboard(currentClip.text, currentClip.html);
68
+
69
+ if (result.changed) {
70
+ console.log('✨ Transformed clipboard data');
71
+ if (result.html) {
72
+ clipboard.writeHtml(result.html, result.text);
73
+ } else {
74
+ clipboard.writeText(result.text);
75
+ }
76
+ lastClip = clipboard.read(); // Update to OS state
77
+ network.broadcast(result.text, result.html);
78
+ } else {
79
+ // No transformation, just broadcast raw text and html to peers
80
+ network.broadcast(currentClip.text, currentClip.html);
81
+ }
82
+ }
83
+ }, 500);
84
+
85
+ // Graceful Shutdown
86
+ function shutdown() {
87
+ console.log('\n🛑 Shutting down Quark Daemon gracefully...');
88
+ clearInterval(pollInterval);
89
+ server.close(() => {
90
+ console.log('🧠 MCP Bridge API closed.');
91
+ process.exit(0);
92
+ });
93
+
94
+ // Force exit if server takes too long
95
+ setTimeout(() => {
96
+ console.error('⚠️ Forced shutdown due to timeout.');
97
+ process.exit(1);
98
+ }, 3000);
99
+ }
100
+
101
+ process.on('SIGINT', shutdown);
102
+ process.on('SIGTERM', shutdown);
103
+ process.on('SIGHUP', shutdown);
104
+
@@ -0,0 +1,125 @@
1
+ const os = require('os');
2
+ const fs = require('fs');
3
+ const path = require('path');
4
+ const { execSync } = require('child_process');
5
+
6
+ function installService() {
7
+ const platform = os.platform();
8
+ const nodePath = process.execPath;
9
+ const daemonPath = path.join(__dirname, 'daemon.js');
10
+
11
+ console.log(`
12
+ o-------o
13
+ | \\ / |
14
+ | o |
15
+ | / \\ |
16
+ o-------o
17
+ `);
18
+ console.log(`⚙️ Installing Quark as a background service on ${platform}...`);
19
+
20
+ try {
21
+ if (platform === 'darwin') {
22
+ const plistPath = path.join(os.homedir(), 'Library', 'LaunchAgents', 'com.quark.daemon.plist');
23
+ const plistContent = `<?xml version="1.0" encoding="UTF-8"?>
24
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
25
+ <plist version="1.0">
26
+ <dict>
27
+ <key>Label</key>
28
+ <string>com.quark.daemon</string>
29
+ <key>ProgramArguments</key>
30
+ <array>
31
+ <string>${nodePath}</string>
32
+ <string>${daemonPath}</string>
33
+ </array>
34
+ <key>RunAtLoad</key>
35
+ <true/>
36
+ <key>KeepAlive</key>
37
+ <true/>
38
+ <key>StandardOutPath</key>
39
+ <string>${path.join(os.homedir(), '.quark.log')}</string>
40
+ <key>StandardErrorPath</key>
41
+ <string>${path.join(os.homedir(), '.quark.err')}</string>
42
+ </dict>
43
+ </plist>`;
44
+
45
+ fs.mkdirSync(path.dirname(plistPath), { recursive: true });
46
+ fs.writeFileSync(plistPath, plistContent);
47
+ try { execSync(`launchctl unload ${plistPath}`, { stdio: 'ignore' }); } catch(e) {}
48
+ execSync(`launchctl load ${plistPath}`);
49
+ console.log('✅ macOS LaunchAgent installed and started.');
50
+
51
+ } else if (platform === 'linux') {
52
+ const serviceDir = path.join(os.homedir(), '.config', 'systemd', 'user');
53
+ const servicePath = path.join(serviceDir, 'quark.service');
54
+ const serviceContent = `[Unit]
55
+ Description=Quark Clipboard Daemon
56
+ After=network.target
57
+
58
+ [Service]
59
+ ExecStart=${nodePath} ${daemonPath}
60
+ Restart=always
61
+ RestartSec=3
62
+ StandardOutput=append:%h/.quark.log
63
+ StandardError=append:%h/.quark.err
64
+
65
+ [Install]
66
+ WantedBy=default.target`;
67
+
68
+ fs.mkdirSync(serviceDir, { recursive: true });
69
+ fs.writeFileSync(servicePath, serviceContent);
70
+ execSync('systemctl --user daemon-reload');
71
+ execSync('systemctl --user enable --now quark.service');
72
+ console.log('✅ Linux systemd service installed and started.');
73
+
74
+ } else if (platform === 'win32') {
75
+ const startupDir = path.join(process.env.APPDATA, 'Microsoft', 'Windows', 'Start Menu', 'Programs', 'Startup');
76
+ const vbsPath = path.join(startupDir, 'quark.vbs');
77
+ const vbsContent = `Set WshShell = CreateObject("WScript.Shell")\nWshShell.Run """${nodePath}"" ""${daemonPath}""", 0, False`;
78
+
79
+ fs.mkdirSync(startupDir, { recursive: true });
80
+ fs.writeFileSync(vbsPath, vbsContent);
81
+
82
+ // Start it immediately for this session
83
+ execSync(`wscript "${vbsPath}"`);
84
+ console.log('✅ Windows Startup script installed and started.');
85
+ } else {
86
+ console.log(`⚠️ Unsupported OS for automatic installation: ${platform}`);
87
+ }
88
+ } catch (error) {
89
+ console.error('❌ Failed to install service:', error.message);
90
+ }
91
+ }
92
+
93
+ function uninstallService() {
94
+ const platform = os.platform();
95
+ console.log(`\n🗑️ Uninstalling Quark service from ${platform}...`);
96
+
97
+ try {
98
+ if (platform === 'darwin') {
99
+ const plistPath = path.join(os.homedir(), 'Library', 'LaunchAgents', 'com.quark.daemon.plist');
100
+ if (fs.existsSync(plistPath)) {
101
+ execSync(`launchctl unload ${plistPath}`);
102
+ fs.unlinkSync(plistPath);
103
+ }
104
+ } else if (platform === 'linux') {
105
+ const servicePath = path.join(os.homedir(), '.config', 'systemd', 'user', 'quark.service');
106
+ if (fs.existsSync(servicePath)) {
107
+ execSync('systemctl --user disable --now quark.service');
108
+ fs.unlinkSync(servicePath);
109
+ execSync('systemctl --user daemon-reload');
110
+ }
111
+ } else if (platform === 'win32') {
112
+ const vbsPath = path.join(process.env.APPDATA, 'Microsoft', 'Windows', 'Start Menu', 'Programs', 'Startup', 'quark.vbs');
113
+ if (fs.existsSync(vbsPath)) {
114
+ fs.unlinkSync(vbsPath);
115
+ // Kill node processes running daemon.js (simplified)
116
+ try { execSync(`wmic process where "CommandLine like '%daemon.js%'" call terminate`, { stdio: 'ignore' }); } catch(e) {}
117
+ }
118
+ }
119
+ console.log('✅ Service uninstalled successfully.');
120
+ } catch (error) {
121
+ console.error('❌ Failed to uninstall service:', error.message);
122
+ }
123
+ }
124
+
125
+ module.exports = { installService, uninstallService };
package/cli/src/mcp.js ADDED
@@ -0,0 +1,90 @@
1
+ const { Server } = require("@modelcontextprotocol/sdk/server/index.js");
2
+ const { StdioServerTransport } = require("@modelcontextprotocol/sdk/server/stdio.js");
3
+ const {
4
+ CallToolRequestSchema,
5
+ ListToolsRequestSchema,
6
+ } = require("@modelcontextprotocol/sdk/types.js");
7
+ const http = require('http');
8
+
9
+ // This script is executed by LLMs (Claude Desktop, Cursor) via `quark mcp`
10
+ // It communicates with the background Quark daemon via local HTTP.
11
+
12
+ const API_PORT = 14314;
13
+
14
+ async function getClipboardFromDaemon() {
15
+ return new Promise((resolve, reject) => {
16
+ http.get(`http://127.0.0.1:${API_PORT}/clipboard`, (res) => {
17
+ let data = '';
18
+ res.on('data', chunk => data += chunk);
19
+ res.on('end', () => {
20
+ try { resolve(JSON.parse(data).text); } catch(e) { reject(e); }
21
+ });
22
+ }).on('error', reject);
23
+ });
24
+ }
25
+
26
+ async function setClipboardToDaemon(text) {
27
+ return new Promise((resolve, reject) => {
28
+ const req = http.request(`http://127.0.0.1:${API_PORT}/clipboard`, {
29
+ method: 'POST',
30
+ headers: { 'Content-Type': 'application/json' }
31
+ }, (res) => {
32
+ res.on('data', () => {});
33
+ res.on('end', resolve);
34
+ });
35
+ req.on('error', reject);
36
+ req.write(JSON.stringify({ text }));
37
+ req.end();
38
+ });
39
+ }
40
+
41
+ async function runMCPServer() {
42
+ const server = new Server(
43
+ { name: "quark-mcp", version: "1.0.0" },
44
+ { capabilities: { tools: {} } }
45
+ );
46
+
47
+ server.setRequestHandler(ListToolsRequestSchema, async () => ({
48
+ tools: [
49
+ {
50
+ name: "get_clipboard",
51
+ description: "Read the user's current operating system clipboard.",
52
+ inputSchema: { type: "object", properties: {} }
53
+ },
54
+ {
55
+ name: "set_clipboard",
56
+ description: "Write text directly to the user's operating system clipboard.",
57
+ inputSchema: {
58
+ type: "object",
59
+ properties: {
60
+ text: { type: "string", description: "The text to place on the clipboard" }
61
+ },
62
+ required: ["text"]
63
+ }
64
+ }
65
+ ]
66
+ }));
67
+
68
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
69
+ try {
70
+ if (request.params.name === "get_clipboard") {
71
+ const text = await getClipboardFromDaemon();
72
+ return { content: [{ type: "text", text: text || "(Clipboard is empty)" }] };
73
+ }
74
+
75
+ if (request.params.name === "set_clipboard") {
76
+ await setClipboardToDaemon(request.params.arguments.text);
77
+ return { content: [{ type: "text", text: "Successfully updated clipboard." }] };
78
+ }
79
+
80
+ throw new Error("Unknown tool");
81
+ } catch (error) {
82
+ return { content: [{ type: "text", text: `Error: ${error.message}. Is the Quark daemon running? (Run 'quark start')` }], isError: true };
83
+ }
84
+ });
85
+
86
+ const transport = new StdioServerTransport();
87
+ await server.connect(transport);
88
+ }
89
+
90
+ runMCPServer().catch(console.error);
@@ -0,0 +1,99 @@
1
+ const { Bonjour } = require('bonjour-service');
2
+ const WebSocket = require('ws');
3
+ const os = require('os');
4
+ const crypto = require('crypto');
5
+
6
+ const PORT = 41235; // Fixed port for P2P WS
7
+ const SERVICE_TYPE = 'quark-clip';
8
+ const NODE_ID = crypto.randomUUID();
9
+
10
+ let wss = null;
11
+ let peers = new Map(); // ip -> ws
12
+ let bonjourInstance = null;
13
+ let onRemoteClipboardReceived = null;
14
+
15
+ function init(clipboardCallback) {
16
+ onRemoteClipboardReceived = clipboardCallback;
17
+
18
+ // 1. Start WebSocket Server
19
+ wss = new WebSocket.Server({ port: PORT, host: '0.0.0.0' });
20
+
21
+ wss.on('connection', (ws, req) => {
22
+ const ip = req.socket.remoteAddress;
23
+ peers.set(ip, ws);
24
+
25
+ ws.on('message', (message) => {
26
+ try {
27
+ const data = JSON.parse(message);
28
+ if (data.type === 'CLIPBOARD_SYNC' && data.nodeId !== NODE_ID) {
29
+ if (onRemoteClipboardReceived) {
30
+ onRemoteClipboardReceived(data.payload);
31
+ }
32
+ }
33
+ } catch (e) { console.error('WS Parse Error', e); }
34
+ });
35
+
36
+ ws.on('close', () => peers.delete(ip));
37
+ });
38
+
39
+ // 2. Start mDNS Discovery
40
+ bonjourInstance = new Bonjour();
41
+
42
+ // Publish our node
43
+ bonjourInstance.publish({ name: `Quark-${os.hostname()}`, type: SERVICE_TYPE, port: PORT });
44
+
45
+ // Discover other nodes
46
+ const browser = bonjourInstance.find({ type: SERVICE_TYPE });
47
+ browser.on('up', (service) => {
48
+ if (service.port === PORT) {
49
+ const address = service.addresses.find(a => a.includes('.')) || service.addresses[0];
50
+ if (address && !peers.has(address)) {
51
+ connectToPeer(address);
52
+ }
53
+ }
54
+ });
55
+
56
+ console.log(`🌐 P2P Mesh Network initialized on port ${PORT}`);
57
+ }
58
+
59
+ function connectToPeer(ip) {
60
+ // Prevent connecting to self
61
+ const interfaces = os.networkInterfaces();
62
+ for (const name of Object.keys(interfaces)) {
63
+ for (const iface of interfaces[name]) {
64
+ if (iface.address === ip) return;
65
+ }
66
+ }
67
+
68
+ const ws = new WebSocket(`ws://${ip}:${PORT}`);
69
+ ws.on('open', () => {
70
+ peers.set(ip, ws);
71
+ console.log(`🔗 Connected to peer: ${ip}`);
72
+ });
73
+ ws.on('message', (message) => {
74
+ try {
75
+ const data = JSON.parse(message);
76
+ if (data.type === 'CLIPBOARD_SYNC' && data.nodeId !== NODE_ID) {
77
+ if (onRemoteClipboardReceived) onRemoteClipboardReceived(data.payload);
78
+ }
79
+ } catch (e) {}
80
+ });
81
+ ws.on('close', () => peers.delete(ip));
82
+ ws.on('error', () => peers.delete(ip));
83
+ }
84
+
85
+ function broadcast(text, html = null) {
86
+ const payload = JSON.stringify({
87
+ type: 'CLIPBOARD_SYNC',
88
+ nodeId: NODE_ID,
89
+ payload: { text, html }
90
+ });
91
+
92
+ peers.forEach((ws) => {
93
+ if (ws.readyState === WebSocket.OPEN) {
94
+ ws.send(payload);
95
+ }
96
+ });
97
+ }
98
+
99
+ module.exports = { init, broadcast };