@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.
- package/LICENSE +21 -0
- package/README.md +122 -0
- package/cli/bin/quark.js +120 -0
- package/cli/package-lock.json +1225 -0
- package/cli/package.json +23 -0
- package/cli/src/clipboard.js +81 -0
- package/cli/src/daemon.js +104 -0
- package/cli/src/installer.js +125 -0
- package/cli/src/mcp.js +90 -0
- package/cli/src/network.js +99 -0
- package/cli/src/transformers.js +222 -0
- package/dist/404.html +23 -0
- package/dist/assets/index-CaferGuT.js +79 -0
- package/dist/assets/index-Dn1mbcJe.css +1 -0
- package/dist/index.html +29 -0
- package/dist/logo.svg +5 -0
- package/package.json +57 -0
- package/server.ts +60 -0
package/cli/package.json
ADDED
|
@@ -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 };
|