vault-fetch 0.1.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 ADDED
@@ -0,0 +1,128 @@
1
+ # vault-fetch
2
+
3
+ Obsidian Clipper では取得できない、JavaScript レンダリングや認証が必要な Web ページを Playwright で取得し、Markdown に変換して Obsidian Vault に保存する CLI ツール。
4
+
5
+ ## 特徴
6
+
7
+ - Playwright (Chromium) による JS レンダリング後のページ取得
8
+ - Readability.js による記事本文の抽出(広告・ナビゲーション除去)
9
+ - Obsidian Clipper 互換のフロントマター(title, source, author, published, created, description, tags)
10
+ - セッション管理(`storageState`)によるログイン済みページの取得
11
+ - 設定の 3 層解決(CLI オプション > 環境変数 > 設定ファイル)
12
+
13
+ ## セットアップ
14
+
15
+ ```bash
16
+ npm install
17
+ npx playwright install chromium
18
+ ```
19
+
20
+ グローバルインストール:
21
+
22
+ ```bash
23
+ npm install -g vault-fetch
24
+ npx playwright install chromium
25
+ ```
26
+
27
+ ## 使い方
28
+
29
+ ### ページの取得・保存
30
+
31
+ ```bash
32
+ # Obsidian Vault に保存
33
+ vault-fetch fetch https://example.com/article --dest ~/Documents/Obsidian/Clippings
34
+
35
+ # 標準出力に出力(保存しない)
36
+ vault-fetch fetch https://example.com/article --dry-run --dest /tmp
37
+
38
+ # headed モードで実行(デバッグ用)
39
+ vault-fetch fetch https://example.com/article --dest ~/Documents/Obsidian/Clippings --headed
40
+
41
+ # 特定の CSS セレクタのみ抽出(Readability をスキップ)
42
+ vault-fetch fetch https://example.com/article --dest ~/Documents/Obsidian/Clippings --selector "article"
43
+
44
+ # タグを追加
45
+ vault-fetch fetch https://example.com/article --dest ~/Documents/Obsidian/Clippings --tag tech --tag ai
46
+ ```
47
+
48
+ ### ログイン(セッション保存)
49
+
50
+ 認証が必要なサイトの場合、事前にログインしてセッションを保存できます。
51
+
52
+ ```bash
53
+ vault-fetch login https://note.com
54
+ # → ブラウザが開く → 手動でログイン → ターミナルで Enter を押す
55
+ ```
56
+
57
+ 以降の `fetch` でそのドメインのセッションが自動的に使用されます。
58
+
59
+ ### fetch オプション
60
+
61
+ | オプション | 説明 |
62
+ |---|---|
63
+ | `--dest <path>` | 保存先ディレクトリ(必須) |
64
+ | `--headed` | ブラウザを表示して実行 |
65
+ | `--selector <css>` | CSS セレクタで要素を抽出 |
66
+ | `--timeout <sec>` | タイムアウト秒数(デフォルト: 30) |
67
+ | `--tag <name>` | タグ追加(複数指定可) |
68
+ | `--wait-until <event>` | 待機条件: `load` / `domcontentloaded` / `networkidle`(デフォルト: `networkidle`) |
69
+ | `--skip-session` | 保存済みセッションを使わない |
70
+ | `--dry-run` | 保存せず標準出力に出力 |
71
+
72
+ ## 設定
73
+
74
+ ### 設定ファイル
75
+
76
+ `~/.config/vault-fetch/config.yaml`:
77
+
78
+ ```yaml
79
+ # Obsidian Vault の保存先
80
+ dest: ~/Documents/Obsidian/Clippings
81
+
82
+ # デフォルトタグ
83
+ tags:
84
+ - clippings
85
+
86
+ timeout: 30
87
+ ```
88
+
89
+ ### 環境変数
90
+
91
+ | 変数 | 説明 |
92
+ |---|---|
93
+ | `VAULT_FETCH_DEST` | 保存先ディレクトリ |
94
+ | `VAULT_FETCH_TIMEOUT` | タイムアウト秒数 |
95
+
96
+ ### 優先順位
97
+
98
+ CLI オプション > 環境変数 > 設定ファイル > デフォルト値
99
+
100
+ ## 出力例
101
+
102
+ ```yaml
103
+ ---
104
+ title: ADHDの自分が毎日クッソ集中できるようになった習慣
105
+ source: https://note.com/simplearchitect/n/n8389e1b4fbde
106
+ author:
107
+ - "[[牛尾 剛]]"
108
+ published: 2025-06-14
109
+ created: 2025-07-03
110
+ description: 自分はADHDですので、もちろん集中力は暗黒です...
111
+ tags:
112
+ - clippings
113
+ ---
114
+
115
+ 記事の本文が Markdown で続きます...
116
+ ```
117
+
118
+ ## 開発
119
+
120
+ ```bash
121
+ npm run build # tsup でビルド
122
+ npm test # vitest でテスト実行
123
+ npm run typecheck # 型チェック
124
+ ```
125
+
126
+ ## ライセンス
127
+
128
+ MIT
package/dist/cli.d.ts ADDED
@@ -0,0 +1,2 @@
1
+
2
+ export { }
package/dist/cli.js ADDED
@@ -0,0 +1,392 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/cli.ts
4
+ import { Command } from "commander";
5
+ import { existsSync as existsSync2 } from "fs";
6
+ import { homedir as homedir3 } from "os";
7
+ import { join as join3 } from "path";
8
+
9
+ // src/config.ts
10
+ import { readFileSync } from "fs";
11
+ import { homedir } from "os";
12
+ import { resolve } from "path";
13
+ import yaml from "js-yaml";
14
+ var DEFAULT_TIMEOUT = 30;
15
+ var DEFAULT_WAIT_UNTIL = "networkidle";
16
+ var REQUIRED_TAG = "clippings";
17
+ function expandTilde(filePath) {
18
+ if (filePath.startsWith("~/")) {
19
+ return resolve(homedir(), filePath.slice(2));
20
+ }
21
+ return filePath;
22
+ }
23
+ function loadConfigFile(configPath) {
24
+ const content = readFileSync(configPath, "utf-8");
25
+ const parsed = yaml.load(content);
26
+ if (parsed === null || typeof parsed !== "object") {
27
+ throw new Error(`Invalid config file: ${configPath}`);
28
+ }
29
+ return parsed;
30
+ }
31
+ function resolveConfig(cliOptions, configPath) {
32
+ let fileConfig = {};
33
+ if (configPath) {
34
+ fileConfig = loadConfigFile(configPath);
35
+ }
36
+ const envDest = process.env.VAULT_FETCH_DEST;
37
+ const envTimeout = process.env.VAULT_FETCH_TIMEOUT;
38
+ const dest = cliOptions.dest ?? envDest ?? fileConfig.dest;
39
+ if (dest === void 0) {
40
+ throw new Error(
41
+ "dest is required. Set via --dest, VAULT_FETCH_DEST, or config file."
42
+ );
43
+ }
44
+ let timeout;
45
+ if (cliOptions.timeout !== void 0) {
46
+ timeout = cliOptions.timeout;
47
+ } else if (envTimeout !== void 0) {
48
+ const parsed = Number(envTimeout);
49
+ if (Number.isNaN(parsed)) {
50
+ throw new Error(`Invalid VAULT_FETCH_TIMEOUT value: ${envTimeout}`);
51
+ }
52
+ timeout = parsed;
53
+ } else {
54
+ timeout = fileConfig.timeout ?? DEFAULT_TIMEOUT;
55
+ }
56
+ const waitUntil = cliOptions.waitUntil ?? fileConfig.waitUntil ?? DEFAULT_WAIT_UNTIL;
57
+ const allTags = [
58
+ ...fileConfig.tags ?? [],
59
+ ...cliOptions.tags ?? [],
60
+ REQUIRED_TAG
61
+ ];
62
+ const tags = [...new Set(allTags)];
63
+ return {
64
+ dest: expandTilde(dest),
65
+ tags,
66
+ timeout,
67
+ waitUntil,
68
+ headed: cliOptions.headed ?? false,
69
+ selector: cliOptions.selector ?? null,
70
+ noSession: cliOptions.noSession ?? false,
71
+ dryRun: cliOptions.dryRun ?? false
72
+ };
73
+ }
74
+
75
+ // src/fetcher.ts
76
+ import { chromium } from "playwright";
77
+
78
+ // src/session.ts
79
+ import { existsSync, mkdirSync } from "fs";
80
+ import { join } from "path";
81
+ import { homedir as homedir2 } from "os";
82
+ var CONFIG_DIR = join(homedir2(), ".config", "vault-fetch");
83
+ var SESSIONS_DIR = join(CONFIG_DIR, "sessions");
84
+ function getSessionDir() {
85
+ return SESSIONS_DIR;
86
+ }
87
+ function extractDomain(url) {
88
+ const parsed = new URL(url);
89
+ return parsed.hostname ?? "";
90
+ }
91
+ function getSessionPath(url, sessionsDir) {
92
+ const domain = extractDomain(url);
93
+ return join(sessionsDir, `${domain}.json`);
94
+ }
95
+ function sessionExists(url, sessionsDir) {
96
+ const sessionPath = getSessionPath(url, sessionsDir);
97
+ return existsSync(sessionPath);
98
+ }
99
+ function ensureSessionDir(sessionsDir) {
100
+ if (!existsSync(sessionsDir)) {
101
+ mkdirSync(sessionsDir, { recursive: true });
102
+ }
103
+ }
104
+
105
+ // src/fetcher.ts
106
+ async function fetchPage(url, config, sessionsDir) {
107
+ const browser = await chromium.launch({
108
+ headless: !config.headed
109
+ });
110
+ try {
111
+ const contextOptions = {};
112
+ if (!config.noSession && sessionExists(url, sessionsDir)) {
113
+ const sessionPath = getSessionPath(url, sessionsDir);
114
+ contextOptions.storageState = sessionPath;
115
+ }
116
+ const context = await browser.newContext(contextOptions);
117
+ const page = await context.newPage();
118
+ const timeoutMs = config.timeout * 1e3;
119
+ const response = await page.goto(url, {
120
+ waitUntil: config.waitUntil,
121
+ timeout: timeoutMs
122
+ });
123
+ if (!response) {
124
+ throw new Error(`No response received from ${url}`);
125
+ }
126
+ const status = response.status();
127
+ if (status >= 400) {
128
+ throw new Error(`HTTP ${status} received from ${response.url()}`);
129
+ }
130
+ const finalUrl = response.url();
131
+ const fullHtml = await page.content();
132
+ let html;
133
+ if (config.selector) {
134
+ const element = await page.$(config.selector);
135
+ if (!element) {
136
+ throw new Error(`Selector not found: ${config.selector}`);
137
+ }
138
+ html = await element.innerHTML();
139
+ } else {
140
+ html = fullHtml;
141
+ }
142
+ await context.close();
143
+ return { html, fullHtml, url, finalUrl };
144
+ } finally {
145
+ await browser.close();
146
+ }
147
+ }
148
+
149
+ // src/extractor.ts
150
+ import { Readability } from "@mozilla/readability";
151
+ import { JSDOM } from "jsdom";
152
+ function getMetaContent(doc, selector) {
153
+ const el = doc.querySelector(selector);
154
+ return el?.getAttribute("content") ?? null;
155
+ }
156
+ function formatAuthor(raw) {
157
+ return `[[${raw.trim()}]]`;
158
+ }
159
+ function extractPublishedDate(doc) {
160
+ const published = getMetaContent(doc, 'meta[property="article:published_time"]') ?? getMetaContent(doc, 'meta[name="datePublished"]');
161
+ if (!published) {
162
+ const jsonLd = doc.querySelector('script[type="application/ld+json"]');
163
+ if (jsonLd?.textContent) {
164
+ try {
165
+ const data = JSON.parse(jsonLd.textContent);
166
+ if (typeof data.datePublished === "string") {
167
+ return data.datePublished.split("T")[0];
168
+ }
169
+ } catch {
170
+ }
171
+ }
172
+ return null;
173
+ }
174
+ return published.split("T")[0];
175
+ }
176
+ function extractAuthors(doc, readabilityByline) {
177
+ const articleAuthors = doc.querySelectorAll('meta[property="article:author"]');
178
+ if (articleAuthors.length > 0) {
179
+ return Array.from(articleAuthors).map((el) => el.getAttribute("content")).filter((v) => v !== null).map(formatAuthor);
180
+ }
181
+ const ogAuthor = getMetaContent(doc, 'meta[property="og:author"]');
182
+ if (ogAuthor) {
183
+ return [formatAuthor(ogAuthor)];
184
+ }
185
+ if (readabilityByline) {
186
+ return [formatAuthor(readabilityByline)];
187
+ }
188
+ return [];
189
+ }
190
+ function extract(html, finalUrl) {
191
+ const metaDom = new JSDOM(html, { url: finalUrl });
192
+ const doc = metaDom.window.document;
193
+ const readabilityDom = new JSDOM(html, { url: finalUrl });
194
+ const reader = new Readability(readabilityDom.window.document);
195
+ const article = reader.parse();
196
+ if (!article) {
197
+ throw new Error("Readability failed to extract content from the page");
198
+ }
199
+ if (!article.content) {
200
+ throw new Error("Readability returned empty content for the page");
201
+ }
202
+ const title = article.title ?? doc.title;
203
+ const authors = extractAuthors(doc, article.byline ?? null);
204
+ const published = extractPublishedDate(doc);
205
+ const description = getMetaContent(doc, 'meta[property="og:description"]') ?? getMetaContent(doc, 'meta[name="description"]') ?? (article.excerpt ?? null);
206
+ const today = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
207
+ return {
208
+ metadata: {
209
+ title,
210
+ source: finalUrl,
211
+ author: authors,
212
+ published,
213
+ created: today,
214
+ description
215
+ },
216
+ content: article.content
217
+ };
218
+ }
219
+ function extractMetadata(html, finalUrl) {
220
+ const metaDom = new JSDOM(html, { url: finalUrl });
221
+ const doc = metaDom.window.document;
222
+ const readabilityDom = new JSDOM(html, { url: finalUrl });
223
+ const reader = new Readability(readabilityDom.window.document);
224
+ const article = reader.parse();
225
+ const title = article?.title ?? doc.title;
226
+ const authors = extractAuthors(doc, article?.byline ?? null);
227
+ const published = extractPublishedDate(doc);
228
+ const description = getMetaContent(doc, 'meta[property="og:description"]') ?? getMetaContent(doc, 'meta[name="description"]') ?? (article?.excerpt ?? null);
229
+ const today = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
230
+ return {
231
+ title,
232
+ source: finalUrl,
233
+ author: authors,
234
+ published,
235
+ created: today,
236
+ description
237
+ };
238
+ }
239
+
240
+ // src/converter.ts
241
+ import TurndownService from "turndown";
242
+ function convertToMarkdown(html) {
243
+ const turndown = new TurndownService({
244
+ headingStyle: "atx",
245
+ codeBlockStyle: "fenced",
246
+ bulletListMarker: "-"
247
+ });
248
+ return turndown.turndown(html);
249
+ }
250
+
251
+ // src/writer.ts
252
+ import { writeFileSync } from "fs";
253
+ import { join as join2 } from "path";
254
+ import yaml2 from "js-yaml";
255
+ var UNSAFE_CHARS = /[/\\:*?"<>|]/g;
256
+ var MAX_FILENAME_LENGTH = 200;
257
+ function sanitizeFilename(title) {
258
+ const sanitized = title.replace(UNSAFE_CHARS, "").replace(/\s+/g, " ").trim();
259
+ const base = sanitized.slice(0, MAX_FILENAME_LENGTH) || "Untitled";
260
+ return `${base}.md`;
261
+ }
262
+ function buildFrontmatter(metadata, tags) {
263
+ const data = {
264
+ title: metadata.title,
265
+ source: metadata.source
266
+ };
267
+ if (metadata.author.length > 0) {
268
+ data.author = metadata.author;
269
+ }
270
+ if (metadata.published) {
271
+ data.published = metadata.published;
272
+ }
273
+ data.created = metadata.created;
274
+ if (metadata.description) {
275
+ data.description = metadata.description;
276
+ }
277
+ data.tags = tags;
278
+ const yamlStr = yaml2.dump(data, {
279
+ quotingType: '"',
280
+ forceQuotes: false,
281
+ lineWidth: -1,
282
+ sortKeys: false
283
+ });
284
+ return `---
285
+ ${yamlStr}---`;
286
+ }
287
+ function writeMarkdownFile(dest, metadata, markdownContent, tags) {
288
+ const filename = sanitizeFilename(metadata.title);
289
+ const filePath = join2(dest, filename);
290
+ const frontmatter = buildFrontmatter(metadata, tags);
291
+ const fullContent = `${frontmatter}
292
+
293
+ ${markdownContent}
294
+ `;
295
+ writeFileSync(filePath, fullContent, "utf-8");
296
+ return filePath;
297
+ }
298
+
299
+ // src/cli.ts
300
+ var CONFIG_PATH = join3(homedir3(), ".config", "vault-fetch", "config.yaml");
301
+ var program = new Command();
302
+ program.name("vault-fetch").description(
303
+ "Fetch JS-rendered web pages and save as Markdown to Obsidian Vault"
304
+ ).version("0.1.0");
305
+ program.command("fetch").description("Fetch a page and save as Markdown").argument("<url>", "URL to fetch").option("--dest <path>", "Destination directory").option("--headed", "Run browser in headed mode").option("--selector <css>", "CSS selector to extract").option("--timeout <seconds>", "Timeout in seconds", parseInt).option("--tag <name>", "Add tag (repeatable)", (val, acc) => {
306
+ acc.push(val);
307
+ return acc;
308
+ }, []).option(
309
+ "--wait-until <event>",
310
+ "Wait condition: load, domcontentloaded, networkidle"
311
+ ).option("--skip-session", "Do not use saved session").option("--dry-run", "Output to stdout instead of saving").action(async (url, options) => {
312
+ try {
313
+ const configPath = existsSync2(CONFIG_PATH) ? CONFIG_PATH : void 0;
314
+ const config = resolveConfig(
315
+ {
316
+ dest: options.dest,
317
+ tags: options.tag,
318
+ timeout: options.timeout,
319
+ waitUntil: options.waitUntil,
320
+ headed: options.headed,
321
+ selector: options.selector,
322
+ noSession: options.skipSession,
323
+ dryRun: options.dryRun
324
+ },
325
+ configPath
326
+ );
327
+ if (!config.dryRun && !existsSync2(config.dest)) {
328
+ throw new Error(`Destination directory does not exist: ${config.dest}`);
329
+ }
330
+ const sessionsDir = getSessionDir();
331
+ const fetchResult = await fetchPage(url, config, sessionsDir);
332
+ let contentHtml;
333
+ let metadata;
334
+ if (config.selector) {
335
+ contentHtml = fetchResult.html;
336
+ metadata = extractMetadata(fetchResult.fullHtml, fetchResult.finalUrl);
337
+ } else {
338
+ const result = extract(fetchResult.html, fetchResult.finalUrl);
339
+ metadata = result.metadata;
340
+ contentHtml = result.content;
341
+ }
342
+ const markdown = convertToMarkdown(contentHtml);
343
+ if (config.dryRun) {
344
+ const frontmatter = buildFrontmatter(metadata, config.tags);
345
+ process.stdout.write(`${frontmatter}
346
+
347
+ ${markdown}
348
+ `);
349
+ } else {
350
+ const filePath = writeMarkdownFile(
351
+ config.dest,
352
+ metadata,
353
+ markdown,
354
+ config.tags
355
+ );
356
+ console.error(`Saved: ${filePath}`);
357
+ }
358
+ } catch (error) {
359
+ const message = error instanceof Error ? error.message : String(error);
360
+ console.error(`Error: ${message}`);
361
+ process.exit(1);
362
+ }
363
+ });
364
+ program.command("login").description("Login to a site and save session").argument("<url>", "URL to login").option("--timeout <seconds>", "Login timeout in seconds", parseInt).action(async (url, options) => {
365
+ const { chromium: chromium2 } = await import("playwright");
366
+ const sessionsDir = getSessionDir();
367
+ ensureSessionDir(sessionsDir);
368
+ const timeoutSec = options.timeout ?? 300;
369
+ const browser = await chromium2.launch({ headless: false });
370
+ try {
371
+ const context = await browser.newContext();
372
+ const page = await context.newPage();
373
+ await page.goto(url, { waitUntil: "networkidle", timeout: timeoutSec * 1e3 });
374
+ console.error("Browser opened. Log in manually, then press Enter here to save session.");
375
+ await new Promise((resolve2) => {
376
+ process.stdin.once("data", () => {
377
+ resolve2();
378
+ });
379
+ });
380
+ const sessionPath = getSessionPath(url, sessionsDir);
381
+ await context.storageState({ path: sessionPath });
382
+ console.error(`Session saved: ${sessionPath}`);
383
+ } catch (error) {
384
+ const message = error instanceof Error ? error.message : String(error);
385
+ console.error(`Error: ${message}`);
386
+ process.exit(1);
387
+ } finally {
388
+ await browser.close();
389
+ }
390
+ });
391
+ program.parse();
392
+ //# sourceMappingURL=cli.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/cli.ts","../src/config.ts","../src/fetcher.ts","../src/session.ts","../src/extractor.ts","../src/converter.ts","../src/writer.ts"],"sourcesContent":["import { Command } from \"commander\";\nimport { existsSync } from \"node:fs\";\nimport { homedir } from \"node:os\";\nimport { join } from \"node:path\";\nimport { resolveConfig } from \"./config.js\";\nimport { fetchPage } from \"./fetcher.js\";\nimport { extract, extractMetadata } from \"./extractor.js\";\nimport { convertToMarkdown } from \"./converter.js\";\nimport { writeMarkdownFile, buildFrontmatter } from \"./writer.js\";\nimport {\n getSessionDir,\n getSessionPath,\n ensureSessionDir,\n} from \"./session.js\";\nimport type { WaitUntilOption } from \"./types.js\";\n\nconst CONFIG_PATH = join(homedir(), \".config\", \"vault-fetch\", \"config.yaml\");\n\nconst program = new Command();\n\nprogram\n .name(\"vault-fetch\")\n .description(\n \"Fetch JS-rendered web pages and save as Markdown to Obsidian Vault\",\n )\n .version(\"0.1.0\");\n\nprogram\n .command(\"fetch\")\n .description(\"Fetch a page and save as Markdown\")\n .argument(\"<url>\", \"URL to fetch\")\n .option(\"--dest <path>\", \"Destination directory\")\n .option(\"--headed\", \"Run browser in headed mode\")\n .option(\"--selector <css>\", \"CSS selector to extract\")\n .option(\"--timeout <seconds>\", \"Timeout in seconds\", parseInt)\n .option(\"--tag <name>\", \"Add tag (repeatable)\", (val: string, acc: string[]) => {\n acc.push(val);\n return acc;\n }, [] as string[])\n .option(\n \"--wait-until <event>\",\n \"Wait condition: load, domcontentloaded, networkidle\",\n )\n .option(\"--skip-session\", \"Do not use saved session\")\n .option(\"--dry-run\", \"Output to stdout instead of saving\")\n .action(async (url: string, options: Record<string, unknown>) => {\n try {\n const configPath = existsSync(CONFIG_PATH) ? CONFIG_PATH : undefined;\n const config = resolveConfig(\n {\n dest: options.dest as string | undefined,\n tags: options.tag as string[] | undefined,\n timeout: options.timeout as number | undefined,\n waitUntil: options.waitUntil as WaitUntilOption | undefined,\n headed: options.headed as boolean | undefined,\n selector: options.selector as string | undefined,\n noSession: options.skipSession as boolean | undefined,\n dryRun: options.dryRun as boolean | undefined,\n },\n configPath,\n );\n\n // Validate dest directory exists\n if (!config.dryRun && !existsSync(config.dest)) {\n throw new Error(`Destination directory does not exist: ${config.dest}`);\n }\n\n const sessionsDir = getSessionDir();\n const fetchResult = await fetchPage(url, config, sessionsDir);\n\n let contentHtml: string;\n let metadata;\n\n if (config.selector) {\n // --selector mode: skip Readability, extract metadata from full page\n contentHtml = fetchResult.html;\n metadata = extractMetadata(fetchResult.fullHtml, fetchResult.finalUrl);\n } else {\n const result = extract(fetchResult.html, fetchResult.finalUrl);\n metadata = result.metadata;\n contentHtml = result.content;\n }\n\n const markdown = convertToMarkdown(contentHtml);\n\n if (config.dryRun) {\n const frontmatter = buildFrontmatter(metadata, config.tags);\n process.stdout.write(`${frontmatter}\\n\\n${markdown}\\n`);\n } else {\n const filePath = writeMarkdownFile(\n config.dest,\n metadata,\n markdown,\n config.tags,\n );\n console.error(`Saved: ${filePath}`);\n }\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n console.error(`Error: ${message}`);\n process.exit(1);\n }\n });\n\nprogram\n .command(\"login\")\n .description(\"Login to a site and save session\")\n .argument(\"<url>\", \"URL to login\")\n .option(\"--timeout <seconds>\", \"Login timeout in seconds\", parseInt)\n .action(async (url: string, options: Record<string, unknown>) => {\n const { chromium } = await import(\"playwright\");\n const sessionsDir = getSessionDir();\n ensureSessionDir(sessionsDir);\n\n const timeoutSec = (options.timeout as number | undefined) ?? 300;\n const browser = await chromium.launch({ headless: false });\n\n try {\n const context = await browser.newContext();\n const page = await context.newPage();\n\n await page.goto(url, { waitUntil: \"networkidle\", timeout: timeoutSec * 1000 });\n\n console.error(\"Browser opened. Log in manually, then press Enter here to save session.\");\n\n await new Promise<void>((resolve) => {\n process.stdin.once(\"data\", () => {\n resolve();\n });\n });\n\n const sessionPath = getSessionPath(url, sessionsDir);\n await context.storageState({ path: sessionPath });\n console.error(`Session saved: ${sessionPath}`);\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n console.error(`Error: ${message}`);\n process.exit(1);\n } finally {\n await browser.close();\n }\n });\n\nprogram.parse();\n","import { readFileSync } from \"node:fs\";\nimport { homedir } from \"node:os\";\nimport { resolve } from \"node:path\";\nimport yaml from \"js-yaml\";\nimport type { ResolvedConfig, WaitUntilOption } from \"./types.js\";\n\nconst DEFAULT_TIMEOUT = 30;\nconst DEFAULT_WAIT_UNTIL: WaitUntilOption = \"networkidle\";\nconst REQUIRED_TAG = \"clippings\";\n\ninterface FileConfig {\n dest?: string;\n tags?: string[];\n timeout?: number;\n waitUntil?: WaitUntilOption;\n}\n\ninterface CliOptions {\n dest?: string;\n tags?: string[];\n timeout?: number;\n waitUntil?: WaitUntilOption;\n headed?: boolean;\n selector?: string;\n noSession?: boolean;\n dryRun?: boolean;\n}\n\nfunction expandTilde(filePath: string): string {\n if (filePath.startsWith(\"~/\")) {\n return resolve(homedir(), filePath.slice(2));\n }\n return filePath;\n}\n\nfunction loadConfigFile(configPath: string): FileConfig {\n const content = readFileSync(configPath, \"utf-8\");\n const parsed = yaml.load(content);\n if (parsed === null || typeof parsed !== \"object\") {\n throw new Error(`Invalid config file: ${configPath}`);\n }\n return parsed as FileConfig;\n}\n\nexport function resolveConfig(\n cliOptions: CliOptions,\n configPath: string | undefined,\n): ResolvedConfig {\n // Layer 1: Config file\n let fileConfig: FileConfig = {};\n if (configPath) {\n fileConfig = loadConfigFile(configPath);\n }\n\n // Layer 2: Environment variables\n const envDest = process.env.VAULT_FETCH_DEST;\n const envTimeout = process.env.VAULT_FETCH_TIMEOUT;\n\n // Resolve each field: CLI > env > file > default\n const dest = cliOptions.dest ?? envDest ?? fileConfig.dest;\n if (dest === undefined) {\n throw new Error(\n \"dest is required. Set via --dest, VAULT_FETCH_DEST, or config file.\",\n );\n }\n\n let timeout: number;\n if (cliOptions.timeout !== undefined) {\n timeout = cliOptions.timeout;\n } else if (envTimeout !== undefined) {\n const parsed = Number(envTimeout);\n if (Number.isNaN(parsed)) {\n throw new Error(`Invalid VAULT_FETCH_TIMEOUT value: ${envTimeout}`);\n }\n timeout = parsed;\n } else {\n timeout = fileConfig.timeout ?? DEFAULT_TIMEOUT;\n }\n\n const waitUntil =\n cliOptions.waitUntil ?? fileConfig.waitUntil ?? DEFAULT_WAIT_UNTIL;\n\n // Merge tags: file tags + CLI tags + always clippings\n const allTags = [\n ...(fileConfig.tags ?? []),\n ...(cliOptions.tags ?? []),\n REQUIRED_TAG,\n ];\n const tags = [...new Set(allTags)];\n\n return {\n dest: expandTilde(dest),\n tags,\n timeout,\n waitUntil,\n headed: cliOptions.headed ?? false,\n selector: cliOptions.selector ?? null,\n noSession: cliOptions.noSession ?? false,\n dryRun: cliOptions.dryRun ?? false,\n };\n}\n","import { chromium, type BrowserContext } from \"playwright\";\nimport type { FetchResult, ResolvedConfig } from \"./types.js\";\nimport { getSessionPath, sessionExists } from \"./session.js\";\n\nexport async function fetchPage(\n url: string,\n config: ResolvedConfig,\n sessionsDir: string,\n): Promise<FetchResult> {\n const browser = await chromium.launch({\n headless: !config.headed,\n });\n\n try {\n const contextOptions: Parameters<typeof browser.newContext>[0] = {};\n\n // Load session if available and not disabled\n if (!config.noSession && sessionExists(url, sessionsDir)) {\n const sessionPath = getSessionPath(url, sessionsDir);\n contextOptions.storageState = sessionPath;\n }\n\n const context: BrowserContext = await browser.newContext(contextOptions);\n const page = await context.newPage();\n\n const timeoutMs = config.timeout * 1000;\n const response = await page.goto(url, {\n waitUntil: config.waitUntil,\n timeout: timeoutMs,\n });\n\n if (!response) {\n throw new Error(`No response received from ${url}`);\n }\n\n const status = response.status();\n if (status >= 400) {\n throw new Error(`HTTP ${status} received from ${response.url()}`);\n }\n\n const finalUrl = response.url();\n const fullHtml = await page.content();\n let html: string;\n\n if (config.selector) {\n const element = await page.$(config.selector);\n if (!element) {\n throw new Error(`Selector not found: ${config.selector}`);\n }\n html = await element.innerHTML();\n } else {\n html = fullHtml;\n }\n\n await context.close();\n\n return { html, fullHtml, url, finalUrl };\n } finally {\n await browser.close();\n }\n}\n","import { existsSync, mkdirSync } from \"node:fs\";\nimport { join } from \"node:path\";\nimport { homedir } from \"node:os\";\n\nconst CONFIG_DIR = join(homedir(), \".config\", \"vault-fetch\");\nconst SESSIONS_DIR = join(CONFIG_DIR, \"sessions\");\n\nexport function getSessionDir(): string {\n return SESSIONS_DIR;\n}\n\nfunction extractDomain(url: string): string {\n const parsed = new URL(url);\n return parsed.hostname ?? \"\";\n}\n\nexport function getSessionPath(url: string, sessionsDir: string): string {\n const domain = extractDomain(url);\n return join(sessionsDir, `${domain}.json`);\n}\n\nexport function sessionExists(url: string, sessionsDir: string): boolean {\n const sessionPath = getSessionPath(url, sessionsDir);\n return existsSync(sessionPath);\n}\n\nexport function ensureSessionDir(sessionsDir: string): void {\n if (!existsSync(sessionsDir)) {\n mkdirSync(sessionsDir, { recursive: true });\n }\n}\n","import { Readability } from \"@mozilla/readability\";\nimport { JSDOM } from \"jsdom\";\nimport type { Metadata } from \"./types.js\";\n\nfunction getMetaContent(doc: Document, selector: string): string | null {\n const el = doc.querySelector(selector);\n return el?.getAttribute(\"content\") ?? null;\n}\n\nfunction formatAuthor(raw: string): string {\n return `[[${raw.trim()}]]`;\n}\n\nfunction extractPublishedDate(doc: Document): string | null {\n const published =\n getMetaContent(doc, 'meta[property=\"article:published_time\"]') ??\n getMetaContent(doc, 'meta[name=\"datePublished\"]');\n\n if (!published) {\n const jsonLd = doc.querySelector('script[type=\"application/ld+json\"]');\n if (jsonLd?.textContent) {\n try {\n const data = JSON.parse(jsonLd.textContent) as Record<string, unknown>;\n if (typeof data.datePublished === \"string\") {\n return data.datePublished.split(\"T\")[0];\n }\n } catch {\n // JSON-LD parse failed\n }\n }\n return null;\n }\n\n return published.split(\"T\")[0];\n}\n\nfunction extractAuthors(\n doc: Document,\n readabilityByline: string | null,\n): string[] {\n const articleAuthors = doc.querySelectorAll('meta[property=\"article:author\"]');\n if (articleAuthors.length > 0) {\n return Array.from(articleAuthors)\n .map((el) => el.getAttribute(\"content\"))\n .filter((v): v is string => v !== null)\n .map(formatAuthor);\n }\n\n const ogAuthor = getMetaContent(doc, 'meta[property=\"og:author\"]');\n if (ogAuthor) {\n return [formatAuthor(ogAuthor)];\n }\n\n if (readabilityByline) {\n return [formatAuthor(readabilityByline)];\n }\n\n return [];\n}\n\nexport interface ExtractResult {\n metadata: Metadata;\n content: string;\n}\n\nexport function extract(html: string, finalUrl: string): ExtractResult {\n // One JSDOM for metadata DOM queries\n const metaDom = new JSDOM(html, { url: finalUrl });\n const doc = metaDom.window.document;\n\n // One JSDOM for Readability (which mutates the DOM)\n const readabilityDom = new JSDOM(html, { url: finalUrl });\n const reader = new Readability(readabilityDom.window.document);\n const article = reader.parse();\n\n if (!article) {\n throw new Error(\"Readability failed to extract content from the page\");\n }\n\n if (!article.content) {\n throw new Error(\"Readability returned empty content for the page\");\n }\n\n const title = article.title ?? doc.title;\n const authors = extractAuthors(doc, article.byline ?? null);\n const published = extractPublishedDate(doc);\n\n const description =\n getMetaContent(doc, 'meta[property=\"og:description\"]') ??\n getMetaContent(doc, 'meta[name=\"description\"]') ??\n (article.excerpt ?? null);\n\n const today = new Date().toISOString().split(\"T\")[0];\n\n return {\n metadata: {\n title,\n source: finalUrl,\n author: authors,\n published,\n created: today,\n description,\n },\n content: article.content,\n };\n}\n\nexport function extractMetadata(html: string, finalUrl: string): Metadata {\n const metaDom = new JSDOM(html, { url: finalUrl });\n const doc = metaDom.window.document;\n\n const readabilityDom = new JSDOM(html, { url: finalUrl });\n const reader = new Readability(readabilityDom.window.document);\n const article = reader.parse();\n\n const title = article?.title ?? doc.title;\n const authors = extractAuthors(doc, article?.byline ?? null);\n const published = extractPublishedDate(doc);\n\n const description =\n getMetaContent(doc, 'meta[property=\"og:description\"]') ??\n getMetaContent(doc, 'meta[name=\"description\"]') ??\n (article?.excerpt ?? null);\n\n const today = new Date().toISOString().split(\"T\")[0];\n\n return {\n title,\n source: finalUrl,\n author: authors,\n published,\n created: today,\n description,\n };\n}\n","import TurndownService from \"turndown\";\n\nexport function convertToMarkdown(html: string): string {\n const turndown = new TurndownService({\n headingStyle: \"atx\",\n codeBlockStyle: \"fenced\",\n bulletListMarker: \"-\",\n });\n\n return turndown.turndown(html);\n}\n","import { writeFileSync } from \"node:fs\";\nimport { join } from \"node:path\";\nimport yaml from \"js-yaml\";\nimport type { Metadata } from \"./types.js\";\n\nconst UNSAFE_CHARS = /[/\\\\:*?\"<>|]/g;\nconst MAX_FILENAME_LENGTH = 200;\n\nexport function sanitizeFilename(title: string): string {\n const sanitized = title\n .replace(UNSAFE_CHARS, \"\")\n .replace(/\\s+/g, \" \")\n .trim();\n const base = sanitized.slice(0, MAX_FILENAME_LENGTH) || \"Untitled\";\n return `${base}.md`;\n}\n\nexport function buildFrontmatter(metadata: Metadata, tags: string[]): string {\n const data: Record<string, unknown> = {\n title: metadata.title,\n source: metadata.source,\n };\n\n if (metadata.author.length > 0) {\n data.author = metadata.author;\n }\n\n if (metadata.published) {\n data.published = metadata.published;\n }\n\n data.created = metadata.created;\n\n if (metadata.description) {\n data.description = metadata.description;\n }\n\n data.tags = tags;\n\n const yamlStr = yaml.dump(data, {\n quotingType: '\"',\n forceQuotes: false,\n lineWidth: -1,\n sortKeys: false,\n });\n\n return `---\\n${yamlStr}---`;\n}\n\nexport function writeMarkdownFile(\n dest: string,\n metadata: Metadata,\n markdownContent: string,\n tags: string[],\n): string {\n const filename = sanitizeFilename(metadata.title);\n const filePath = join(dest, filename);\n const frontmatter = buildFrontmatter(metadata, tags);\n const fullContent = `${frontmatter}\\n\\n${markdownContent}\\n`;\n\n writeFileSync(filePath, fullContent, \"utf-8\");\n\n return filePath;\n}\n"],"mappings":"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","names":["existsSync","homedir","join","homedir","join","yaml","join","homedir","existsSync","chromium","resolve"]}
package/package.json ADDED
@@ -0,0 +1,54 @@
1
+ {
2
+ "name": "vault-fetch",
3
+ "version": "0.1.0",
4
+ "description": "Fetch JS-rendered web pages with Playwright and save as Markdown to Obsidian Vault",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "author": "driller",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/drillan/vault-fetch.git"
11
+ },
12
+ "keywords": [
13
+ "obsidian",
14
+ "playwright",
15
+ "markdown",
16
+ "web-clipper",
17
+ "readability",
18
+ "cli"
19
+ ],
20
+ "files": [
21
+ "dist"
22
+ ],
23
+ "bin": {
24
+ "vault-fetch": "dist/cli.js"
25
+ },
26
+ "scripts": {
27
+ "build": "tsup",
28
+ "test": "vitest run",
29
+ "test:watch": "vitest",
30
+ "lint": "eslint src/ tests/",
31
+ "typecheck": "tsc --noEmit"
32
+ },
33
+ "engines": {
34
+ "node": ">=20"
35
+ },
36
+ "dependencies": {
37
+ "@mozilla/readability": "^0.6.0",
38
+ "commander": "^14.0.3",
39
+ "js-yaml": "^4.1.1",
40
+ "jsdom": "^29.0.1",
41
+ "playwright": "^1.58.2",
42
+ "turndown": "^7.2.2"
43
+ },
44
+ "devDependencies": {
45
+ "@types/js-yaml": "^4.0.9",
46
+ "@types/jsdom": "^28.0.1",
47
+ "@types/node": "^25.5.0",
48
+ "@types/turndown": "^5.0.6",
49
+ "eslint": "^10.1.0",
50
+ "tsup": "^8.5.1",
51
+ "typescript": "^5.9.3",
52
+ "vitest": "^4.1.0"
53
+ }
54
+ }