@v0idd0/tabsnap 1.0.3 → 1.0.4

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
@@ -8,7 +8,7 @@
8
8
 
9
9
  > Capture every open tab as a browser-session export: plain text, markdown, JSON, or a readme file.
10
10
  > One click in the browser. One pipe in the terminal.
11
- > Free, MIT, zero telemetry.
11
+ > Free, MIT, zero telemetry. Optional tracking-param stripping for cleaner exports.
12
12
 
13
13
  [Browser-extension landing](https://extensions.voiddo.com/tabsnap/) ·
14
14
  [CLI landing](https://tools.voiddo.com/tabsnap/) ·
@@ -47,6 +47,7 @@ npm i -g @v0idd0/tabsnap
47
47
  cat tabs.json | tabsnap # markdown (default)
48
48
  cat tabs.json | tabsnap --format=readme # readme.md
49
49
  cat tabs.json | tabsnap -f json --no-group # flat structured array
50
+ cat tabs.json | tabsnap --strip-tracking # clean tracking params first
50
51
 
51
52
  # from file
52
53
  tabsnap --file=tabs.json -f plain
@@ -54,6 +55,9 @@ tabsnap --file=tabs.json -f plain
54
55
  # include pinned + incognito tabs (skipped by default)
55
56
  tabsnap --include-pinned --include-incognito < tabs.json
56
57
 
58
+ # strip common marketing params from exported URLs
59
+ tabsnap --strip-tracking -f readme < tabs.json
60
+
57
61
  # pipe to clipboard (macOS) or any tool
58
62
  tabsnap --format=readme < tabs.json | pbcopy
59
63
  ```
package/bin/tabsnap.js CHANGED
@@ -25,6 +25,7 @@ options:
25
25
  --no-group do not group by window (single flat list)
26
26
  --include-pinned include pinned tabs (default: skip)
27
27
  --include-incognito include incognito-window tabs (default: skip)
28
+ --strip-tracking remove common tracking params from exported urls
28
29
  -h, --help this help
29
30
  -v, --version print version
30
31
 
@@ -49,6 +50,7 @@ function parseArgs(argv) {
49
50
  groupByWindow: true,
50
51
  includePinned: false,
51
52
  includeIncognito: false,
53
+ stripTracking: false,
52
54
  help: false,
53
55
  version: false,
54
56
  };
@@ -59,6 +61,7 @@ function parseArgs(argv) {
59
61
  else if (a === '--no-group') opts.groupByWindow = false;
60
62
  else if (a === '--include-pinned') opts.includePinned = true;
61
63
  else if (a === '--include-incognito') opts.includeIncognito = true;
64
+ else if (a === '--strip-tracking') opts.stripTracking = true;
62
65
  else if (a.startsWith('--format=')) opts.format = a.slice(9);
63
66
  else if (a === '-f' || a === '--format') opts.format = argv[++i];
64
67
  else if (a.startsWith('--file=')) opts.file = a.slice(7);
@@ -142,6 +145,7 @@ async function main() {
142
145
  groupByWindow: opts.groupByWindow,
143
146
  includePinned: opts.includePinned,
144
147
  includeIncognito: opts.includeIncognito,
148
+ stripTracking: opts.stripTracking,
145
149
  });
146
150
  process.stdout.write(out);
147
151
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@v0idd0/tabsnap",
3
- "version": "1.0.3",
4
- "description": "tabsnap — browser session exporter and tab-list formatter for markdown, plain text, JSON, or a readme file. Library + CLI. Same formatters that power the tabsnap browser extension. Zero deps. Free forever from vøiddo.",
3
+ "version": "1.0.4",
4
+ "description": "tabsnap — browser session exporter and tab-list formatter for markdown, plain text, JSON, or a readme file. Library + CLI. Optional tracking-param stripping keeps exports clean. Same formatters that power the tabsnap browser extension. Zero deps. Free forever from vøiddo.",
5
5
  "main": "src/index.js",
6
6
  "bin": {
7
7
  "tabsnap": "./bin/tabsnap.js"
@@ -34,6 +34,9 @@
34
34
  "formatter",
35
35
  "json-to-markdown",
36
36
  "json-to-readme",
37
+ "tracking-params",
38
+ "utm",
39
+ "url-cleanup",
37
40
  "snapshot",
38
41
  "voiddo",
39
42
  "free",
package/src/index.js CHANGED
@@ -9,6 +9,26 @@ function escapeMarkdown(s) {
9
9
  return String(s).replace(/[\[\]\\`*_]/g, ch => '\\' + ch);
10
10
  }
11
11
 
12
+ const TRACKING_PARAM_RE = /^(?:utm_[a-z0-9_]+|gclid|dclid|fbclid|msclkid|mc_cid|mc_eid|ref|ref_src|referrer|trk|igshid|_hsenc|_hsmi)$/i;
13
+
14
+ function cleanUrl(url, opts) {
15
+ if (!url || !opts || !opts.stripTracking) return url;
16
+ try {
17
+ const parsed = new URL(url);
18
+ const keys = [...new Set([...parsed.searchParams.keys()])];
19
+ let changed = false;
20
+ for (const key of keys) {
21
+ if (TRACKING_PARAM_RE.test(key)) {
22
+ parsed.searchParams.delete(key);
23
+ changed = true;
24
+ }
25
+ }
26
+ return changed ? parsed.toString() : url;
27
+ } catch (_) {
28
+ return url;
29
+ }
30
+ }
31
+
12
32
  function groupTabsByWindow(tabs) {
13
33
  const groups = new Map();
14
34
  for (const t of tabs) {
@@ -37,30 +57,30 @@ function fmtMarkdown(tabs, opts) {
37
57
  const dt = new Date().toISOString().replace('T', ' ').replace(/\.\d+Z$/, ' UTC');
38
58
  const head = '# tab snapshot · ' + t.length + ' tab' + (t.length === 1 ? '' : 's') + '\n_' + dt + '_\n\n';
39
59
  if (!opts.groupByWindow) {
40
- return head + t.map(formatLineMd).join('\n') + '\n';
60
+ return head + t.map(tab => formatLineMd(tab, opts)).join('\n') + '\n';
41
61
  }
42
62
  const groups = groupTabsByWindow(t);
43
63
  return head + groups.map(g =>
44
64
  '## window ' + g.windowIndex + ' (' + g.tabs.length + ')\n' +
45
- g.tabs.map(formatLineMd).join('\n') + '\n'
65
+ g.tabs.map(tab => formatLineMd(tab, opts)).join('\n') + '\n'
46
66
  ).join('\n');
47
67
  }
48
68
 
49
- function formatLineMd(tab) {
69
+ function formatLineMd(tab, opts) {
50
70
  const title = escapeMarkdown(tab.title || '(untitled)');
51
- return '- [' + title + '](' + tab.url + ')';
71
+ return '- [' + title + '](' + cleanUrl(tab.url, opts) + ')';
52
72
  }
53
73
 
54
74
  function fmtPlain(tabs, opts) {
55
75
  const t = filterTabs(tabs, opts);
56
76
  if (!opts.groupByWindow) {
57
- return t.map(x => (x.title || '(untitled)') + '\n ' + x.url).join('\n\n') + '\n';
77
+ return t.map(x => (x.title || '(untitled)') + '\n ' + cleanUrl(x.url, opts)).join('\n\n') + '\n';
58
78
  }
59
79
  const groups = groupTabsByWindow(t);
60
80
  return groups.map(g =>
61
81
  'window ' + g.windowIndex + ' (' + g.tabs.length + ')\n' +
62
82
  '─'.repeat(40) + '\n' +
63
- g.tabs.map(x => (x.title || '(untitled)') + '\n ' + x.url).join('\n\n')
83
+ g.tabs.map(x => (x.title || '(untitled)') + '\n ' + cleanUrl(x.url, opts)).join('\n\n')
64
84
  ).join('\n\n') + '\n';
65
85
  }
66
86
 
@@ -75,18 +95,18 @@ function fmtJson(tabs, opts) {
75
95
  out.windows = groupTabsByWindow(t).map(g => ({
76
96
  window_index: g.windowIndex,
77
97
  tab_count: g.tabs.length,
78
- tabs: g.tabs.map(simplifyTab),
98
+ tabs: g.tabs.map(tab => simplifyTab(tab, opts)),
79
99
  }));
80
100
  } else {
81
- out.tabs = t.map(simplifyTab);
101
+ out.tabs = t.map(tab => simplifyTab(tab, opts));
82
102
  }
83
103
  return JSON.stringify(out, null, 2) + '\n';
84
104
  }
85
105
 
86
- function simplifyTab(t) {
106
+ function simplifyTab(t, opts) {
87
107
  return {
88
108
  title: t.title || null,
89
- url: t.url || null,
109
+ url: cleanUrl(t.url, opts) || null,
90
110
  pinned: !!t.pinned,
91
111
  active: !!t.active,
92
112
  audible: !!t.audible,
@@ -107,12 +127,12 @@ function fmtReadme(tabs, opts) {
107
127
  domains += '\n';
108
128
  let body;
109
129
  if (!opts.groupByWindow) {
110
- body = '## all tabs\n\n' + t.map(formatLineMd).join('\n') + '\n';
130
+ body = '## all tabs\n\n' + t.map(tab => formatLineMd(tab, opts)).join('\n') + '\n';
111
131
  } else {
112
132
  const groups = groupTabsByWindow(t);
113
133
  body = groups.map(g =>
114
134
  '## window ' + g.windowIndex + ' · ' + g.tabs.length + ' tab' + (g.tabs.length === 1 ? '' : 's') + '\n\n' +
115
- g.tabs.map(formatLineMd).join('\n') + '\n'
135
+ g.tabs.map(tab => formatLineMd(tab, opts)).join('\n') + '\n'
116
136
  ).join('\n');
117
137
  }
118
138
  return head + domains + body;