@v0idd0/tabsnap 1.0.2 → 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 +19 -3
- package/bin/tabsnap.js +4 -0
- package/package.json +23 -8
- package/src/index.js +32 -12
package/README.md
CHANGED
|
@@ -6,14 +6,17 @@
|
|
|
6
6
|
[](https://developer.chrome.com/docs/extensions/mv3/intro/)
|
|
7
7
|
[](https://voiddo.com/)
|
|
8
8
|
|
|
9
|
-
> Capture every open tab as plain text, markdown, JSON, or a readme file.
|
|
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/) ·
|
|
15
15
|
[npm](https://www.npmjs.com/package/@v0idd0/tabsnap) ·
|
|
16
|
-
[Privacy](https://extensions.voiddo.com/tabsnap/privacy/)
|
|
16
|
+
[Privacy](https://extensions.voiddo.com/tabsnap/privacy/) ·
|
|
17
|
+
[Compare](https://extensions.voiddo.com/compare/tab-saver/)
|
|
18
|
+
|
|
19
|
+
If you think in tab backups, session exports, or a OneTab / Session Buddy alternative that still gives you a clean text artifact, tabsnap is the text-first path.
|
|
17
20
|
|
|
18
21
|
---
|
|
19
22
|
|
|
@@ -44,6 +47,7 @@ npm i -g @v0idd0/tabsnap
|
|
|
44
47
|
cat tabs.json | tabsnap # markdown (default)
|
|
45
48
|
cat tabs.json | tabsnap --format=readme # readme.md
|
|
46
49
|
cat tabs.json | tabsnap -f json --no-group # flat structured array
|
|
50
|
+
cat tabs.json | tabsnap --strip-tracking # clean tracking params first
|
|
47
51
|
|
|
48
52
|
# from file
|
|
49
53
|
tabsnap --file=tabs.json -f plain
|
|
@@ -51,6 +55,9 @@ tabsnap --file=tabs.json -f plain
|
|
|
51
55
|
# include pinned + incognito tabs (skipped by default)
|
|
52
56
|
tabsnap --include-pinned --include-incognito < tabs.json
|
|
53
57
|
|
|
58
|
+
# strip common marketing params from exported URLs
|
|
59
|
+
tabsnap --strip-tracking -f readme < tabs.json
|
|
60
|
+
|
|
54
61
|
# pipe to clipboard (macOS) or any tool
|
|
55
62
|
tabsnap --format=readme < tabs.json | pbcopy
|
|
56
63
|
```
|
|
@@ -125,6 +132,15 @@ node test.js # run all 40 tests
|
|
|
125
132
|
npm publish --access public
|
|
126
133
|
```
|
|
127
134
|
|
|
135
|
+
## From the same studio
|
|
136
|
+
|
|
137
|
+
- **[@v0idd0/interviewprep](https://www.npmjs.com/package/@v0idd0/interviewprep)** — turn a job posting into a prep brief, then export it in browser or CLI
|
|
138
|
+
- **[@v0idd0/jsonyo](https://www.npmjs.com/package/@v0idd0/jsonyo)** — JSON swiss army knife, 18 commands, zero limits
|
|
139
|
+
- **[@v0idd0/envguard](https://www.npmjs.com/package/@v0idd0/envguard)** — stop shipping `.env` drift to staging
|
|
140
|
+
- **[@v0idd0/depcheck](https://www.npmjs.com/package/@v0idd0/depcheck)** — find unused dependencies in one command
|
|
141
|
+
- **[@v0idd0/gitstats](https://www.npmjs.com/package/@v0idd0/gitstats)** — git repo analytics, one command
|
|
142
|
+
- **[View all tools →](https://voiddo.com/tools/)**
|
|
143
|
+
|
|
128
144
|
## License
|
|
129
145
|
|
|
130
146
|
MIT — see `LICENSE`.
|
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.
|
|
4
|
-
"description": "tabsnap —
|
|
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"
|
|
@@ -10,15 +10,22 @@
|
|
|
10
10
|
"test": "node test.js"
|
|
11
11
|
},
|
|
12
12
|
"files": [
|
|
13
|
-
"bin",
|
|
14
|
-
"src",
|
|
15
|
-
"
|
|
16
|
-
"
|
|
13
|
+
"bin/",
|
|
14
|
+
"src/",
|
|
15
|
+
"LICENSE",
|
|
16
|
+
"README.md"
|
|
17
17
|
],
|
|
18
18
|
"keywords": [
|
|
19
19
|
"tabs",
|
|
20
20
|
"tab-snapshot",
|
|
21
21
|
"tab-list",
|
|
22
|
+
"browser-session",
|
|
23
|
+
"browser-session-export",
|
|
24
|
+
"session-export",
|
|
25
|
+
"session-backup",
|
|
26
|
+
"tab-export",
|
|
27
|
+
"onetab",
|
|
28
|
+
"session-buddy",
|
|
22
29
|
"markdown",
|
|
23
30
|
"browser-extension",
|
|
24
31
|
"chrome-tabs",
|
|
@@ -27,6 +34,9 @@
|
|
|
27
34
|
"formatter",
|
|
28
35
|
"json-to-markdown",
|
|
29
36
|
"json-to-readme",
|
|
37
|
+
"tracking-params",
|
|
38
|
+
"utm",
|
|
39
|
+
"url-cleanup",
|
|
30
40
|
"snapshot",
|
|
31
41
|
"voiddo",
|
|
32
42
|
"free",
|
|
@@ -52,5 +62,10 @@
|
|
|
52
62
|
},
|
|
53
63
|
"engines": {
|
|
54
64
|
"node": ">=14"
|
|
55
|
-
}
|
|
56
|
-
|
|
65
|
+
},
|
|
66
|
+
"os": [
|
|
67
|
+
"darwin",
|
|
68
|
+
"linux",
|
|
69
|
+
"win32"
|
|
70
|
+
]
|
|
71
|
+
}
|
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;
|