@quark.clip/quark 1.1.0 β†’ 1.3.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 CHANGED
@@ -30,6 +30,7 @@ The operating system clipboard is fundamentally broken and stuck in the past:
30
30
  - **Heals** broken PDF text automatically.
31
31
  - **Strips** tracking parameters from URLs.
32
32
  - **Transforms** raw Excel data, Markdown, and LaTeX into clean, rich HTML.
33
+ - **Omni-Context Engine**: Detects the active target app (Notion, Obsidian, VS Code, Pages) and dynamically shifts clipboard flavors (Markdown, HTML, RTF) to ensure flawless structure on every paste.
33
34
  - **Syncs** across your Mac, Windows, and Linux machines instantly via P2P.
34
35
  - **Exposes** your clipboard to local AI models via MCP.
35
36
 
package/cli/bin/quark.js CHANGED
@@ -11,6 +11,29 @@ const pidFile = path.join(__dirname, '../quark.pid');
11
11
  const logFile = path.join(__dirname, '../quark.log');
12
12
 
13
13
  switch (command) {
14
+ case 'run':
15
+ console.log(`
16
+ o-------o
17
+ | \\ / |
18
+ | o |
19
+ | / \\ |
20
+ o-------o
21
+ `);
22
+ console.log('πŸš€ Starting Quark Daemon (Foreground Session)...');
23
+ console.log('πŸ’‘ Press Ctrl+C to stop. Logs will appear below:');
24
+ require(daemonPath);
25
+ break;
26
+
27
+ case 'logs':
28
+ if (fs.existsSync(logFile)) {
29
+ console.log('πŸ“œ Tailing Quark logs... (Press Ctrl+C to exit)');
30
+ const tail = spawn('tail', ['-f', logFile], { stdio: 'inherit' });
31
+ tail.on('exit', () => process.exit(0));
32
+ } else {
33
+ console.log('⚠️ No log file found at ' + logFile);
34
+ }
35
+ break;
36
+
14
37
  case 'start':
15
38
  console.log(`
16
39
  o-------o
@@ -64,7 +87,7 @@ switch (command) {
64
87
  console.log(`\n🌌 Quark Status`);
65
88
  console.log(`----------------`);
66
89
  console.log(`Manual Session: ${isRunning ? '🟒 Running' : 'πŸ”΄ Stopped'}`);
67
-
90
+
68
91
  // Check OS Service Status
69
92
  const os = require('os');
70
93
  const platform = os.platform();
@@ -92,7 +115,7 @@ switch (command) {
92
115
  } catch (e) {
93
116
  // Ignore errors from execSync if service is not running
94
117
  }
95
-
118
+
96
119
  console.log(`OS Service: ${serviceRunning ? '🟒 Installed & Running' : 'πŸ”΄ Not Installed / Stopped'}`);
97
120
  console.log(`\nLog file: ${logFile}`);
98
121
  if (!serviceRunning) {
@@ -112,6 +135,8 @@ switch (command) {
112
135
  Usage:
113
136
  quark start Start the daemon in the background (current session)
114
137
  quark stop Stop the manually started daemon
138
+ quark run Start the daemon in the foreground (live logs)
139
+ quark logs Tail the background daemon logs
115
140
  quark install Install Quark as a native OS background service (Auto-start on boot)
116
141
  quark uninstall Remove the native OS background service
117
142
  quark status Check daemon status
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "quark-daemon",
3
- "version": "1.0.0",
3
+ "version": "1.1.1",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "quark-daemon",
9
- "version": "1.0.0",
9
+ "version": "1.1.1",
10
10
  "license": "MIT",
11
11
  "dependencies": {
12
12
  "@modelcontextprotocol/sdk": "^1.0.1",
package/cli/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "quark-daemon",
3
- "version": "1.1.0",
3
+ "version": "1.1.1",
4
4
  "description": "AI-native cross-platform clipboard micro-daemon",
5
5
  "main": "src/daemon.js",
6
6
  "bin": {
@@ -20,4 +20,4 @@
20
20
  },
21
21
  "author": "Adarsh Agrahari",
22
22
  "license": "MIT"
23
- }
23
+ }
@@ -9,9 +9,16 @@ function read() {
9
9
  if (platform === 'darwin') {
10
10
  text = execSync('pbpaste', { encoding: 'utf8' }).toString();
11
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');
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' });
19
+ if (raw && raw.trim() !== '') {
20
+ html = raw;
21
+ }
15
22
  } catch (e) { }
16
23
  } else if (platform === 'linux') {
17
24
  text = execSync('xclip -selection clipboard -o', { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).toString();
@@ -86,4 +93,15 @@ function writeText(text) {
86
93
  } catch (e) { console.error('Error setting clipboard text:', e.message); }
87
94
  }
88
95
 
89
- module.exports = { read, writeHtml, writeText };
96
+ function getActiveApp() {
97
+ try {
98
+ const platform = os.platform();
99
+ if (platform === 'darwin') {
100
+ const jxa = `ObjC.import("AppKit"); $.NSWorkspace.sharedWorkspace.frontmostApplication.localizedName.js;`;
101
+ return execSync(`osascript -l JavaScript -e '${jxa}' 2>/dev/null`, { encoding: 'utf8' }).trim();
102
+ }
103
+ } catch (e) { }
104
+ return null;
105
+ }
106
+
107
+ module.exports = { read, writeHtml, writeText, getActiveApp };
package/cli/src/daemon.js CHANGED
@@ -6,6 +6,8 @@ const http = require('http');
6
6
  console.log('🌌 Quark Daemon Started at', new Date().toISOString());
7
7
 
8
8
  let lastClip = clipboard.read();
9
+ let lastActiveApp = clipboard.getActiveApp();
10
+ let lastProcessedResult = null;
9
11
  let isSyncingFromNetwork = false;
10
12
 
11
13
  // Initialize P2P Network
@@ -101,19 +103,31 @@ const pollInterval = setInterval(async () => {
101
103
  hasHtml: !!currentClip.html
102
104
  });
103
105
 
104
- // Process through transformers
105
- const result = await transformers.processClipboard(currentClip.text, currentClip.html);
106
+ const activeApp = clipboard.getActiveApp();
107
+ if (activeApp !== lastActiveApp) {
108
+ logEvent('INFO', `Active App changed: ${activeApp}`);
109
+ // JIT Injection: If we have a cached result, re-optimize for the new app
110
+ if (lastProcessedResult) {
111
+ writeGuard = true;
112
+ injectForTarget(activeApp, lastProcessedResult);
113
+ }
114
+ lastActiveApp = activeApp;
115
+ }
116
+
117
+ // Process through transformers with context
118
+ const result = await transformers.processClipboard(currentClip.text, currentClip.html, activeApp);
106
119
 
107
- if (result.changed) {
108
- logEvent('TRANSFORM', result.skipReason || 'Applying transformations');
109
- writeGuard = true;
110
- if (result.html) {
111
- clipboard.writeHtml(result.html, result.text);
112
- } else {
113
- clipboard.writeText(result.text);
120
+ if (result.changed || result.markdown) {
121
+ lastProcessedResult = result;
122
+
123
+ if (result.changed) {
124
+ logEvent('TRANSFORM', result.skipReason || 'Applying transformations');
125
+ writeGuard = true;
126
+ injectForTarget(activeApp, result);
127
+
128
+ lastClip = clipboard.read(); // Update to OS state
129
+ network.broadcast(result.text, result.html);
114
130
  }
115
- lastClip = clipboard.read(); // Update to OS state
116
- network.broadcast(result.text, result.html);
117
131
  } else {
118
132
  if (result.skipReason) {
119
133
  logEvent('INFO', `Skipped: ${result.skipReason}`);
@@ -124,6 +138,24 @@ const pollInterval = setInterval(async () => {
124
138
  }
125
139
  }, 500);
126
140
 
141
+ function injectForTarget(app, result) {
142
+ const markdownApps = ['Obsidian', 'Visual Studio Code', 'Cursor', 'Linear', 'GitHub', 'Teams'];
143
+ const isMarkdownTarget = markdownApps.some(m => app && app.includes(m));
144
+
145
+ if (isMarkdownTarget && result.markdown) {
146
+ // For Markdown-friendly apps, we prioritize Markdown in the plain-text flavor
147
+ // This allows pasting into Obsidian to get MD, while keeping HTML for table support if they want it
148
+ clipboard.writeHtml(result.html, result.markdown);
149
+ } else {
150
+ // Standard injection
151
+ if (result.html) {
152
+ clipboard.writeHtml(result.html, result.text);
153
+ } else {
154
+ clipboard.writeText(result.text);
155
+ }
156
+ }
157
+ }
158
+
127
159
  // Graceful Shutdown
128
160
  function shutdown() {
129
161
  logEvent('INFO', 'Shutting down Quark Daemon gracefully...');
@@ -1,30 +1,53 @@
1
1
  const { marked } = require('marked');
2
2
 
3
- function isExcelTSV(text) {
4
- const lines = text.trim().split('\n').filter(l => l.trim().length > 0);
5
- if (lines.length < 2) return false;
6
- const tabs = lines[0].split('\t').length;
7
- return tabs > 1 && lines.every(l => l.split('\t').length === tabs);
3
+ function analyzeGridProperties(text, delimiter) {
4
+ const lines = text.split('\n').map(l => l.trim('\r'));
5
+ if (lines.length < 2) return { isGrid: false };
6
+
7
+ const rows = lines.map(l => l.split(delimiter));
8
+ const counts = rows.map(r => r.length);
9
+
10
+ // A perfect grid has all rows equal length
11
+ const maxCols = Math.max(...counts);
12
+ const minCols = Math.min(...counts);
13
+
14
+ // Mathematical sparse matrix inference:
15
+ // If the delimiter appears consistently across multiple lines, it's a grid.
16
+ // For TSV, even a single tab in multiple lines strongly implies a spreadsheet payload.
17
+ let isGrid = false;
18
+ if (delimiter === '\t') {
19
+ const linesWithTabs = lines.filter(l => l.includes('\t')).length;
20
+ // If at least 50% of lines have tabs, or we have distinct multi-column structure
21
+ isGrid = (linesWithTabs > 0 && linesWithTabs >= (lines.length / 2)) || (maxCols > 1 && counts[0] === counts[1]);
22
+ } else {
23
+ isGrid = maxCols > 2 && minCols === maxCols;
24
+ }
25
+
26
+ return { isGrid, rows, cols: maxCols };
8
27
  }
9
28
 
10
- function isCSV(text) {
11
- const lines = text.trim().split('\n').filter(l => l.trim().length > 0);
12
- if (lines.length < 2) return false;
13
- const commas = lines[0].split(',').length;
14
- return commas > 2 && lines.every(l => l.split(',').length === commas);
29
+ function isExcelTSV(text) {
30
+ return analyzeGridProperties(text, '\t').isGrid;
15
31
  }
16
32
 
17
33
  function convertToHTML(text, delimiter) {
18
- const rows = text.trim().split('\n').filter(l => l.trim().length > 0).map(r => r.split(delimiter));
34
+ const { rows, cols } = analyzeGridProperties(text, delimiter);
19
35
  let html = '<table style="border-collapse: collapse; font-family: sans-serif; font-size: 14px;">\n';
20
36
  rows.forEach((row, i) => {
37
+ // Drop completely empty trailing rows
38
+ if (i === rows.length - 1 && row.join('').trim() === '') return;
39
+
21
40
  html += ' <tr>\n';
22
- row.forEach(cell => {
41
+
42
+ // Pad sparse rows with empty cells to maintain grid geometry
43
+ for (let j = 0; j < cols; j++) {
44
+ const cell = row[j] || '';
23
45
  const tag = i === 0 ? 'th' : 'td';
24
46
  const bg = i === 0 ? 'background-color: #f3f2f1;' : '';
25
47
  const style = `border: 1px solid #d1d1d1; padding: 6px 12px; ${bg}`;
26
48
  html += ` <${tag} style="${style}">${cell.trim()}</${tag}>\n`;
27
- });
49
+ }
50
+
28
51
  html += ' </tr>\n';
29
52
  });
30
53
  html += '</table>';
@@ -155,9 +178,66 @@ function normalizeShouting(text) {
155
178
  return text;
156
179
  }
157
180
 
181
+ function convertToMarkdown(html) {
182
+ if (!html) return '';
183
+
184
+ // Minimalist HTML to Markdown converter
185
+ let md = html
186
+ .replace(/<style([\s\S]*?)<\/style>/gi, '')
187
+ .replace(/<head([\s\S]*?)<\/head>/gi, '')
188
+ .replace(/<(h[1-6])(.*?)>(.*?)<\/\1>/gi, (m, h, a, c) => `${'#'.repeat(parseInt(h[1]))} ${c}\n\n`)
189
+ .replace(/<p(.*?)>(.*?)<\/p>/gi, '$2\n\n')
190
+ .replace(/<br\s*\/?>/gi, '\n')
191
+ .replace(/<b(.*?)>(.*?)<\/b>/gi, '**$2**')
192
+ .replace(/<strong(.*?)>(.*?)<\/strong>/gi, '**$2**')
193
+ .replace(/<i(.*?)>(.*?)<\/i>/gi, '*$2*')
194
+ .replace(/<em(.*?)>(.*?)<\/em>/gi, '*$2*')
195
+ .replace(/<a.*?href="(.*?)".*?>(.*?)<\/a>/gi, '[$2]($1)')
196
+ .replace(/<ul>([\s\S]*?)<\/ul>/gi, (m, c) => c.replace(/<li(.*?)>(.*?)<\/li>/gi, '- $2\n') + '\n')
197
+ .replace(/<ol>([\s\S]*?)<\/ol>/gi, (m, c) => {
198
+ let i = 1;
199
+ return c.replace(/<li(.*?)>(.*?)<\/li>/gi, () => `${i++}. $2\n`) + '\n';
200
+ });
201
+
202
+ // Table handling
203
+ md = md.replace(/<table([\s\S]*?)<\/table>/gi, (m, c) => {
204
+ const rows = [];
205
+ const trMatches = c.match(/<tr([\s\S]*?)<\/tr>/gi);
206
+ if (!trMatches) return '';
207
+
208
+ trMatches.forEach(tr => {
209
+ const cells = [];
210
+ const tdMatches = tr.match(/<(td|th)([\s\S]*?)<\/\1>/gi);
211
+ if (tdMatches) {
212
+ tdMatches.forEach(td => {
213
+ cells.push(td.replace(/<(?:.|\n)*?>/gm, '').trim().replace(/\|/g, '\\|'));
214
+ });
215
+ rows.push('| ' + cells.join(' | ') + ' |');
216
+ }
217
+ });
218
+
219
+ if (rows.length === 0) return '';
220
+
221
+ // Header separator
222
+ const colCount = rows[0].split('|').length - 2;
223
+ const separator = '| ' + Array(colCount).fill('---').join(' | ') + ' |';
224
+ rows.splice(1, 0, separator);
225
+
226
+ return rows.join('\n') + '\n\n';
227
+ });
228
+
229
+ // Strip remaining tags
230
+ return md.replace(/<(?:.|\n)*?>/gm, '').trim();
231
+ }
232
+
158
233
  // The master pipeline
159
- async function processClipboard(text, originalHtml) {
160
- let result = { changed: false, text: text, html: originalHtml };
234
+ async function processClipboard(text, originalHtml, targetApp = null) {
235
+ let result = { changed: false, text: text, html: originalHtml, markdown: null };
236
+
237
+ // Calculate markdown if HTML exists
238
+ if (originalHtml) {
239
+ result.markdown = convertToMarkdown(originalHtml);
240
+ }
161
241
 
162
242
  // PRESERVATION HEURISTIC: Yield if high-quality HTML already exists
163
243
  if (originalHtml) {
@@ -166,14 +246,14 @@ async function processClipboard(text, originalHtml) {
166
246
  'data-sheets-userformat',
167
247
  'br-re-calc',
168
248
  'x-office-spreadsheet',
169
- '<table>',
170
- '<tbody>',
171
- '<thead>'
249
+ '<table',
250
+ '<tbody',
251
+ '<thead'
172
252
  ];
173
253
 
174
254
  const hasStructure = forensicMarkers.some(marker => originalHtml.toLowerCase().includes(marker));
175
255
  if (hasStructure) {
176
- return { changed: false, text: text, html: originalHtml, skipReason: 'Forensic structure detected' };
256
+ return { changed: false, text: text, html: originalHtml, markdown: result.markdown, skipReason: 'Forensic structure detected' };
177
257
  }
178
258
  }
179
259
 
@@ -200,37 +280,54 @@ async function processClipboard(text, originalHtml) {
200
280
  // 1. URL Tracking Stripper
201
281
  if (text.startsWith('http') && text.includes('utm_')) {
202
282
  const clean = stripTrackingParams(text);
203
- if (clean !== text) return { changed: true, text: clean, html: null };
283
+ if (clean !== text) return { changed: true, text: clean, html: null, markdown: clean };
204
284
  }
205
285
 
206
286
  // 2. JSON Prettier
207
287
  if ((text.startsWith('{') && text.endsWith('}')) || (text.startsWith('[') && text.endsWith(']'))) {
208
288
  const pretty = prettifyJSON(text);
209
- if (pretty !== text && pretty.includes('\n')) return { changed: true, text: pretty, html: null };
289
+ if (pretty !== text && pretty.includes('\n')) return { changed: true, text: pretty, html: null, markdown: `\`\`\`json\n${pretty}\n\`\`\`` };
210
290
  }
211
291
 
212
292
  // 3. Excel TSV to HTML
213
293
  if (isExcelTSV(text)) {
214
- return { changed: true, text: text, html: convertToHTML(text, '\t') };
294
+ const tableHtml = convertToHTML(text, '\t');
295
+ return {
296
+ changed: true,
297
+ text: text,
298
+ html: tableHtml,
299
+ markdown: convertToMarkdown(tableHtml)
300
+ };
215
301
  }
216
302
 
217
303
  // 4. CSV to HTML
218
- if (isCSV(text) && !originalHtml) {
219
- return { changed: true, text: text, html: convertToHTML(text, ',') };
304
+ if (!originalHtml && analyzeGridProperties(text, ',').isGrid) {
305
+ const tableHtml = convertToHTML(text, ',');
306
+ return {
307
+ changed: true,
308
+ text: text,
309
+ html: tableHtml,
310
+ markdown: convertToMarkdown(tableHtml)
311
+ };
220
312
  }
221
313
 
222
314
  // 5. Full Markdown & Math to HTML
223
315
  const mathResult = renderMathFormulas(text);
224
316
  if ((isMarkdown(text) || mathResult.hasMath) && !originalHtml) {
225
317
  const html = await convertMarkdownToHTML(text);
226
- return { changed: true, text: text, html: html };
318
+ return {
319
+ changed: true,
320
+ text: text,
321
+ html: html,
322
+ markdown: text // If input was MD, use it
323
+ };
227
324
  }
228
325
 
229
326
  // 6. PDF Line Breaks (Only apply if it looks like a broken paragraph, heuristic)
230
327
  if (text.length > 100 && text.split('\n').length > 3 && !text.includes('\t') && !originalHtml) {
231
328
  const fixed = fixPDFLineBreaks(text);
232
329
  if (fixed !== text && fixed.length < text.length) {
233
- return { changed: true, text: fixed, html: null };
330
+ return { changed: true, text: fixed, html: null, markdown: fixed };
234
331
  }
235
332
  }
236
333
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@quark.clip/quark",
3
- "version": "1.1.0",
3
+ "version": "1.3.0",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "files": [