@quark.clip/quark 1.3.1 → 1.3.3

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 CHANGED
@@ -40,6 +40,20 @@ The operating system clipboard is fundamentally broken and stuck in the past:
40
40
 
41
41
  No `.exe` files. No `.dmg` files. Just pure, cross-platform JavaScript.
42
42
 
43
+ ### One-Click Install (macOS / Linux)
44
+
45
+ ```bash
46
+ curl -fsSL https://raw.githubusercontent.com/sir-ad/quark/main/install.sh | sh
47
+ ```
48
+
49
+ ### One-Click Install (Windows PowerShell)
50
+
51
+ ```powershell
52
+ iex (irm https://raw.githubusercontent.com/sir-ad/quark/main/install.ps1)
53
+ ```
54
+
55
+ ### Manual install
56
+
43
57
  ```bash
44
58
  # Run instantly without installing (Temporary Session)
45
59
  npx -y @quark.clip/quark start
package/cli/bin/quark.js CHANGED
@@ -2,16 +2,22 @@
2
2
  const { spawn } = require('child_process');
3
3
  const path = require('path');
4
4
  const fs = require('fs');
5
+ const os = require('os');
5
6
  const { installService, uninstallService } = require('../src/installer');
6
7
 
7
8
  const command = process.argv[2] || 'help';
8
9
  const daemonPath = path.join(__dirname, '../src/daemon.js');
9
10
  const mcpPath = path.join(__dirname, '../src/mcp.js');
10
- const pidFile = path.join(__dirname, '../quark.pid');
11
- const logFile = path.join(__dirname, '../quark.log');
11
+
12
+ // Bug fix: write state files to ~/.quark/ instead of inside the package dir (which
13
+ // can be read-only when installed globally via npm).
14
+ const quarkDir = path.join(os.homedir(), '.quark');
15
+ fs.mkdirSync(quarkDir, { recursive: true });
16
+ const pidFile = path.join(quarkDir, 'quark.pid');
17
+ const logFile = path.join(quarkDir, 'quark.log');
12
18
 
13
19
  switch (command) {
14
- case 'run':
20
+ case 'run': {
15
21
  console.log(`
16
22
  o-------o
17
23
  | \\ / |
@@ -23,8 +29,9 @@ switch (command) {
23
29
  console.log('šŸ’” Press Ctrl+C to stop. Logs will appear below:');
24
30
  require(daemonPath);
25
31
  break;
32
+ }
26
33
 
27
- case 'logs':
34
+ case 'logs': {
28
35
  if (fs.existsSync(logFile)) {
29
36
  console.log('šŸ“œ Tailing Quark logs... (Press Ctrl+C to exit)');
30
37
  const tail = spawn('tail', ['-f', logFile], { stdio: 'inherit' });
@@ -33,8 +40,9 @@ switch (command) {
33
40
  console.log('āš ļø No log file found at ' + logFile);
34
41
  }
35
42
  break;
43
+ }
36
44
 
37
- case 'start':
45
+ case 'start': {
38
46
  console.log(`
39
47
  o-------o
40
48
  | \\ / |
@@ -49,7 +57,9 @@ switch (command) {
49
57
  }
50
58
  const out = fs.openSync(logFile, 'a');
51
59
  const err = fs.openSync(logFile, 'a');
52
- const child = spawn('node', [daemonPath], {
60
+ // Bug fix: use process.execPath so the correct node binary is used even
61
+ // inside nvm / fnm / volta managed environments.
62
+ const child = spawn(process.execPath, [daemonPath], {
53
63
  detached: true,
54
64
  stdio: ['ignore', out, err]
55
65
  });
@@ -58,8 +68,9 @@ switch (command) {
58
68
  console.log('✨ Quark is now orbiting your clipboard in the background.');
59
69
  console.log('šŸ’” Tip: Run `quark install` to make it start automatically on boot.');
60
70
  break;
71
+ }
61
72
 
62
- case 'stop':
73
+ case 'stop': {
63
74
  if (fs.existsSync(pidFile)) {
64
75
  const pid = fs.readFileSync(pidFile, 'utf8');
65
76
  try {
@@ -73,6 +84,7 @@ switch (command) {
73
84
  console.log('Quark is not running via manual start.');
74
85
  }
75
86
  break;
87
+ }
76
88
 
77
89
  case 'install':
78
90
  installService();
@@ -82,34 +94,33 @@ switch (command) {
82
94
  uninstallService();
83
95
  break;
84
96
 
85
- case 'status':
97
+ case 'status': {
86
98
  const isRunning = fs.existsSync(pidFile);
87
99
  console.log(`\n🌌 Quark Status`);
88
100
  console.log(`----------------`);
89
101
  console.log(`Manual Session: ${isRunning ? '🟢 Running' : 'šŸ”“ Stopped'}`);
90
102
 
91
103
  // Check OS Service Status
92
- const os = require('os');
93
104
  const platform = os.platform();
94
105
  let serviceRunning = false;
95
106
  try {
96
107
  if (platform === 'darwin') {
97
108
  const plistPath = path.join(os.homedir(), 'Library', 'LaunchAgents', 'com.quark.daemon.plist');
98
109
  if (fs.existsSync(plistPath)) {
99
- const out = require('child_process').execSync('launchctl list | grep com.quark.daemon', { encoding: 'utf8' });
100
- serviceRunning = out.includes('com.quark.daemon');
110
+ const svcOut = require('child_process').execSync('launchctl list | grep com.quark.daemon', { encoding: 'utf8' });
111
+ serviceRunning = svcOut.includes('com.quark.daemon');
101
112
  }
102
113
  } else if (platform === 'linux') {
103
114
  const servicePath = path.join(os.homedir(), '.config', 'systemd', 'user', 'quark.service');
104
115
  if (fs.existsSync(servicePath)) {
105
- const out = require('child_process').execSync('systemctl --user is-active quark.service', { encoding: 'utf8' });
106
- serviceRunning = out.trim() === 'active';
116
+ const svcOut = require('child_process').execSync('systemctl --user is-active quark.service', { encoding: 'utf8' });
117
+ serviceRunning = svcOut.trim() === 'active';
107
118
  }
108
119
  } else if (platform === 'win32') {
109
120
  const vbsPath = path.join(process.env.APPDATA, 'Microsoft', 'Windows', 'Start Menu', 'Programs', 'Startup', 'quark.vbs');
110
121
  if (fs.existsSync(vbsPath)) {
111
- const out = require('child_process').execSync('wmic process where "CommandLine like \'%daemon.js%\'" get ProcessId', { encoding: 'utf8' });
112
- serviceRunning = out.includes('ProcessId') && out.trim().split('\n').length > 1;
122
+ const svcOut = require('child_process').execSync('wmic process where "CommandLine like \'%daemon.js%\'" get ProcessId', { encoding: 'utf8' });
123
+ serviceRunning = svcOut.includes('ProcessId') && svcOut.trim().split('\n').length > 1;
113
124
  }
114
125
  }
115
126
  } catch (e) {
@@ -122,6 +133,7 @@ switch (command) {
122
133
  console.log(`\nšŸ’” Tip: Run 'quark install' to configure auto-start on boot.`);
123
134
  }
124
135
  break;
136
+ }
125
137
 
126
138
  case 'mcp':
127
139
  // Starts the MCP Stdio server (used by LLMs like Claude Desktop)
@@ -9,17 +9,20 @@ function read() {
9
9
  if (platform === 'darwin') {
10
10
  text = execSync('pbpaste', { encoding: 'utf8' }).toString();
11
11
  try {
12
- const jxa = `
13
- ObjC.import("AppKit");
14
- var pb = $.NSPasteboard.generalPasteboard;
15
- var html = pb.stringForType("public.html");
16
- if (html) { html.js; } else { ""; }
17
- `;
18
- const raw = execSync(`osascript -l JavaScript -e '${jxa.replace(/\n/g, ' ')}' 2>/dev/null`, { encoding: 'utf8' });
12
+ const jxaRead = `
13
+ ObjC.import("AppKit");
14
+ var pb = $.NSPasteboard.generalPasteboard;
15
+ var html = pb.stringForType("public.html");
16
+ var res = (html && html.js) ? html.js : "";
17
+ res;
18
+ `.trim();
19
+ const raw = execSync('osascript -l JavaScript -', { input: jxaRead, encoding: 'utf8' });
19
20
  if (raw && raw.trim() !== '') {
20
- html = raw;
21
+ html = raw.trim();
21
22
  }
22
- } catch (e) { }
23
+ } catch (e) {
24
+ // Fallback to plain text if HTML read fails
25
+ }
23
26
  } else if (platform === 'linux') {
24
27
  text = execSync('xclip -selection clipboard -o', { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).toString();
25
28
  try {
@@ -48,19 +51,25 @@ function writeHtml(html, plainText) {
48
51
  try {
49
52
  const platform = os.platform();
50
53
  if (platform === 'darwin') {
51
- // Use JXA to write both public.html and public.utf8-plain-text to NSPasteboard
52
- // This ensures Electron apps (Teams, Slack) can read the HTML correctly
53
- const escapedHtml = html.replace(/\\/g, '\\\\').replace(/'/g, "\\'").replace(/\n/g, '\\n');
54
- const escapedText = plainText.replace(/\\/g, '\\\\').replace(/'/g, "\\'").replace(/\n/g, '\\n');
55
- const jxa = `
56
- ObjC.import("AppKit");
57
- var pb = $.NSPasteboard.generalPasteboard;
58
- pb.clearContents;
59
- pb.setStringForType($('${escapedHtml}'), $('public.html'));
60
- pb.setStringForType($('${escapedText}'), $('public.utf8-plain-text'));
61
- "ok";
62
- `;
63
- execSync(`osascript -l JavaScript -e '${jxa.replace(/\n/g, ' ')}' 2>/dev/null`);
54
+ // CRITICAL: We must NOT pass HTML as inline shell arg — angle brackets crash the shell.
55
+ // Instead, we use stdin piping and environment variables to pass the data safely.
56
+ const jxaScript = `
57
+ ObjC.import("AppKit");
58
+ var env = $.NSProcessInfo.processInfo.environment.js;
59
+ var htmlStr = env["QUARK_HTML"] || "";
60
+ var textStr = env["QUARK_TEXT"] || "";
61
+
62
+ var pb = $.NSPasteboard.generalPasteboard;
63
+ pb.clearContents;
64
+ pb.setStringForType($(htmlStr), $("public.html"));
65
+ pb.setStringForType($(textStr), $("public.utf8-plain-text"));
66
+ "ok";
67
+ `.trim();
68
+ execSync('osascript -l JavaScript -', {
69
+ input: jxaScript,
70
+ env: { ...process.env, QUARK_HTML: html, QUARK_TEXT: plainText },
71
+ stdio: ['pipe', 'ignore', 'ignore']
72
+ });
64
73
  } else if (platform === 'linux') {
65
74
  execSync('xclip -selection clipboard -t text/html', { input: html, stdio: ['pipe', 'ignore', 'ignore'] });
66
75
  } else if (platform === 'win32') {
package/cli/src/daemon.js CHANGED
@@ -160,6 +160,7 @@ function injectForTarget(app, result) {
160
160
  function shutdown() {
161
161
  logEvent('INFO', 'Shutting down Quark Daemon gracefully...');
162
162
  clearInterval(pollInterval);
163
+ network.shutdown();
163
164
  server.close(() => {
164
165
  logEvent('INFO', 'MCP Bridge API closed.');
165
166
  process.exit(0);
@@ -44,8 +44,10 @@ function installService() {
44
44
 
45
45
  fs.mkdirSync(path.dirname(plistPath), { recursive: true });
46
46
  fs.writeFileSync(plistPath, plistContent);
47
- try { execSync(`launchctl unload ${plistPath}`, { stdio: 'ignore' }); } catch(e) {}
48
- execSync(`launchctl load ${plistPath}`);
47
+ // Use bootstrap/bootout (macOS 12+ preferred API) with load/unload as fallback
48
+ const uid = execSync('id -u', { encoding: 'utf8' }).trim();
49
+ try { execSync(`launchctl bootout gui/${uid} ${plistPath}`, { stdio: 'ignore' }); } catch(e) {}
50
+ execSync(`launchctl bootstrap gui/${uid} ${plistPath}`);
49
51
  console.log('āœ… macOS LaunchAgent installed and started.');
50
52
 
51
53
  } else if (platform === 'linux') {
@@ -98,7 +100,8 @@ function uninstallService() {
98
100
  if (platform === 'darwin') {
99
101
  const plistPath = path.join(os.homedir(), 'Library', 'LaunchAgents', 'com.quark.daemon.plist');
100
102
  if (fs.existsSync(plistPath)) {
101
- execSync(`launchctl unload ${plistPath}`);
103
+ const uid = execSync('id -u', { encoding: 'utf8' }).trim();
104
+ try { execSync(`launchctl bootout gui/${uid} com.quark.daemon`, { stdio: 'ignore' }); } catch(e) {}
102
105
  fs.unlinkSync(plistPath);
103
106
  }
104
107
  } else if (platform === 'linux') {
@@ -88,7 +88,7 @@ function broadcast(text, html = null) {
88
88
  nodeId: NODE_ID,
89
89
  payload: { text, html }
90
90
  });
91
-
91
+
92
92
  peers.forEach((ws) => {
93
93
  if (ws.readyState === WebSocket.OPEN) {
94
94
  ws.send(payload);
@@ -96,4 +96,11 @@ function broadcast(text, html = null) {
96
96
  });
97
97
  }
98
98
 
99
- module.exports = { init, broadcast };
99
+ function shutdown() {
100
+ peers.forEach((ws) => ws.terminate());
101
+ peers.clear();
102
+ if (wss) wss.close();
103
+ if (bonjourInstance) bonjourInstance.destroy();
104
+ }
105
+
106
+ module.exports = { init, broadcast, shutdown };