amenbo 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/LICENSE +21 -0
- package/README.md +171 -0
- package/dist/adapters/index.d.ts +22 -0
- package/dist/adapters/index.js +51 -0
- package/dist/adapters/index.js.map +1 -0
- package/dist/adapters/types.d.ts +21 -0
- package/dist/adapters/types.js +2 -0
- package/dist/adapters/types.js.map +1 -0
- package/dist/cache.d.ts +90 -0
- package/dist/cache.js +286 -0
- package/dist/cache.js.map +1 -0
- package/dist/cli.d.ts +46 -0
- package/dist/cli.js +327 -0
- package/dist/cli.js.map +1 -0
- package/dist/core.d.ts +78 -0
- package/dist/core.js +649 -0
- package/dist/core.js.map +1 -0
- package/dist/diff.d.ts +20 -0
- package/dist/diff.js +44 -0
- package/dist/diff.js.map +1 -0
- package/dist/errors.d.ts +97 -0
- package/dist/errors.js +209 -0
- package/dist/errors.js.map +1 -0
- package/dist/extract/dataSources.d.ts +10 -0
- package/dist/extract/dataSources.js +115 -0
- package/dist/extract/dataSources.js.map +1 -0
- package/dist/extract/geometry.d.ts +63 -0
- package/dist/extract/geometry.js +77 -0
- package/dist/extract/geometry.js.map +1 -0
- package/dist/extract/markdown.d.ts +24 -0
- package/dist/extract/markdown.js +221 -0
- package/dist/extract/markdown.js.map +1 -0
- package/dist/extract/outline.d.ts +33 -0
- package/dist/extract/outline.js +128 -0
- package/dist/extract/outline.js.map +1 -0
- package/dist/extract/pdf.d.ts +22 -0
- package/dist/extract/pdf.js +95 -0
- package/dist/extract/pdf.js.map +1 -0
- package/dist/extract/preview.d.ts +19 -0
- package/dist/extract/preview.js +94 -0
- package/dist/extract/preview.js.map +1 -0
- package/dist/extract/pruning.d.ts +61 -0
- package/dist/extract/pruning.js +101 -0
- package/dist/extract/pruning.js.map +1 -0
- package/dist/extract/qualityScore.d.ts +67 -0
- package/dist/extract/qualityScore.js +98 -0
- package/dist/extract/qualityScore.js.map +1 -0
- package/dist/fetcher/browser.d.ts +85 -0
- package/dist/fetcher/browser.js +343 -0
- package/dist/fetcher/browser.js.map +1 -0
- package/dist/fetcher/http.d.ts +105 -0
- package/dist/fetcher/http.js +538 -0
- package/dist/fetcher/http.js.map +1 -0
- package/dist/fetcher/index.d.ts +41 -0
- package/dist/fetcher/index.js +92 -0
- package/dist/fetcher/index.js.map +1 -0
- package/dist/jp/consentBanner.d.ts +27 -0
- package/dist/jp/consentBanner.js +48 -0
- package/dist/jp/consentBanner.js.map +1 -0
- package/dist/jp/normalize.d.ts +30 -0
- package/dist/jp/normalize.js +65 -0
- package/dist/jp/normalize.js.map +1 -0
- package/dist/links.d.ts +20 -0
- package/dist/links.js +223 -0
- package/dist/links.js.map +1 -0
- package/dist/politeness.d.ts +66 -0
- package/dist/politeness.js +170 -0
- package/dist/politeness.js.map +1 -0
- package/dist/screenshot.d.ts +38 -0
- package/dist/screenshot.js +90 -0
- package/dist/screenshot.js.map +1 -0
- package/dist/server.d.ts +3 -0
- package/dist/server.js +111 -0
- package/dist/server.js.map +1 -0
- package/dist/templateLearning.d.ts +11 -0
- package/dist/templateLearning.js +42 -0
- package/dist/templateLearning.js.map +1 -0
- package/dist/tokens.d.ts +47 -0
- package/dist/tokens.js +141 -0
- package/dist/tokens.js.map +1 -0
- package/package.json +61 -0
package/dist/cache.js
ADDED
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* cache.ts — better-sqlite3によるURL単位のローカルキャッシュ。
|
|
3
|
+
*
|
|
4
|
+
* 保存場所: $AMENBO_CACHE_DIR または ~/.cache/amenbo/cache.sqlite
|
|
5
|
+
* URL毎にETag/Last-Modified/本文ハッシュ/変換済みMarkdown/取得時刻を保存する。
|
|
6
|
+
* TTL内はローカル返却(fresh)、TTL超過時はIf-None-Match/If-Modified-Sinceで再検証し、
|
|
7
|
+
* 304なら再変換せず返す(revalidated)。未キャッシュ/再検証不可はmiss。
|
|
8
|
+
*
|
|
9
|
+
* スクリーンショット(screenshotツール/mode:auto→screenshot)もキャッシュ対象とする。
|
|
10
|
+
* バイナリ本体はSQLiteに入れず、$AMENBO_CACHE_DIR/screenshots/配下にファイル保存し、
|
|
11
|
+
* SQLiteにはそのパスのみを保存する(DBの肥大化を避けるため)。
|
|
12
|
+
* 画像はレンダリング結果でありETag等の安価な再検証手段が無いため、TTLベースの
|
|
13
|
+
* fresh/miss判定のみとする(revalidatedは無い)。
|
|
14
|
+
*
|
|
15
|
+
* Phase 4 テンプレート学習: ドメイン毎に直近ページのブロックハッシュ集合を domain_pages に
|
|
16
|
+
* 記録し、直近N ページ全てに共通して出現したハッシュを定型ブロック(ヘッダ/フッタ/定型ナビ等)
|
|
17
|
+
* と判定する(getTemplateBlockHashes)。除去処理自体はtemplateLearning.tsが行う。
|
|
18
|
+
*
|
|
19
|
+
* CLI併設対応: politenessのドメイン毎レート制御(直近リクエスト時刻)は元々プロセス内メモリ
|
|
20
|
+
* のみで保持していたが、CLIは1コマンド=1プロセスのためプロセスを跨いで状態を引き継げず、
|
|
21
|
+
* 短時間に連続実行すると同一ドメインへの最小間隔が守られない。host_requests テーブルへ
|
|
22
|
+
* 最終リクエスト時刻を永続化し、PolitenessManagerのstoreオプションとして注入することで
|
|
23
|
+
* MCPサーバー/CLIの複数プロセス間で共有する(再起動直後の連続アクセスも守れる副次効果あり)。
|
|
24
|
+
* journal_mode=WAL は複数プロセスからの同時アクセスを想定して設定している。
|
|
25
|
+
*/
|
|
26
|
+
import { createHash } from "node:crypto";
|
|
27
|
+
import { existsSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
|
|
28
|
+
import { homedir } from "node:os";
|
|
29
|
+
import { join } from "node:path";
|
|
30
|
+
import Database from "better-sqlite3";
|
|
31
|
+
const DEFAULT_TTL_MS = 15 * 60 * 1000; // 15分
|
|
32
|
+
/** Phase 4テンプレート学習: 定型ブロック判定に使う「直近ページ数」の既定値。 */
|
|
33
|
+
const DEFAULT_TEMPLATE_RECENT_PAGES = 3;
|
|
34
|
+
function resolveCacheDir() {
|
|
35
|
+
const dir = process.env.AMENBO_CACHE_DIR ?? join(homedir(), ".cache", "amenbo");
|
|
36
|
+
if (!existsSync(dir)) {
|
|
37
|
+
mkdirSync(dir, { recursive: true });
|
|
38
|
+
}
|
|
39
|
+
return dir;
|
|
40
|
+
}
|
|
41
|
+
export function hashContent(content) {
|
|
42
|
+
return createHash("sha256").update(content).digest("hex");
|
|
43
|
+
}
|
|
44
|
+
/** スクリーンショットのキャッシュキー(url+撮影パラメータの組)を計算する。 */
|
|
45
|
+
export function computeScreenshotCacheKey(url, width, scale, fullPage) {
|
|
46
|
+
return hashContent(`${url}|w=${width}|s=${scale}|full=${fullPage}`);
|
|
47
|
+
}
|
|
48
|
+
export class PageCache {
|
|
49
|
+
db;
|
|
50
|
+
cacheDir;
|
|
51
|
+
ttlMs;
|
|
52
|
+
now;
|
|
53
|
+
constructor(options = {}) {
|
|
54
|
+
this.cacheDir = options.cacheDir ?? resolveCacheDir();
|
|
55
|
+
if (!existsSync(this.cacheDir)) {
|
|
56
|
+
mkdirSync(this.cacheDir, { recursive: true });
|
|
57
|
+
}
|
|
58
|
+
this.db = new Database(options.dbPath ?? join(this.cacheDir, "cache.sqlite"));
|
|
59
|
+
this.ttlMs = options.ttlMs ?? DEFAULT_TTL_MS;
|
|
60
|
+
this.now = options.now ?? Date.now;
|
|
61
|
+
this.db.pragma("journal_mode = WAL");
|
|
62
|
+
this.db.exec(`
|
|
63
|
+
CREATE TABLE IF NOT EXISTS pages (
|
|
64
|
+
url TEXT PRIMARY KEY,
|
|
65
|
+
etag TEXT,
|
|
66
|
+
last_modified TEXT,
|
|
67
|
+
content_hash TEXT NOT NULL,
|
|
68
|
+
markdown TEXT NOT NULL,
|
|
69
|
+
metadata TEXT NOT NULL,
|
|
70
|
+
fetched_at INTEGER NOT NULL
|
|
71
|
+
)
|
|
72
|
+
`);
|
|
73
|
+
this.db.exec(`
|
|
74
|
+
CREATE TABLE IF NOT EXISTS screenshots (
|
|
75
|
+
cache_key TEXT PRIMARY KEY,
|
|
76
|
+
url TEXT NOT NULL,
|
|
77
|
+
tile_paths TEXT NOT NULL,
|
|
78
|
+
metadata TEXT NOT NULL,
|
|
79
|
+
fetched_at INTEGER NOT NULL
|
|
80
|
+
)
|
|
81
|
+
`);
|
|
82
|
+
this.db.exec(`
|
|
83
|
+
CREATE TABLE IF NOT EXISTS domain_pages (
|
|
84
|
+
domain TEXT NOT NULL,
|
|
85
|
+
url TEXT NOT NULL,
|
|
86
|
+
fetched_at INTEGER NOT NULL,
|
|
87
|
+
block_hashes TEXT NOT NULL,
|
|
88
|
+
PRIMARY KEY (domain, url)
|
|
89
|
+
)
|
|
90
|
+
`);
|
|
91
|
+
this.db.exec(`CREATE INDEX IF NOT EXISTS idx_domain_pages_domain_fetched_at ON domain_pages (domain, fetched_at DESC)`);
|
|
92
|
+
this.db.exec(`
|
|
93
|
+
CREATE TABLE IF NOT EXISTS host_requests (
|
|
94
|
+
host TEXT PRIMARY KEY,
|
|
95
|
+
last_request_at INTEGER NOT NULL
|
|
96
|
+
)
|
|
97
|
+
`);
|
|
98
|
+
// M3: TTL超過削除・サイズ上限が無くディスクが無制限に増加する問題への対応。
|
|
99
|
+
// 起動(コンストラクタ)のたびにTTL超過エントリを掃除する。
|
|
100
|
+
this.pruneExpired();
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* M3: TTL超過エントリを削除する(ディスク無制限増加の防止)。起動時に自動実行される。
|
|
104
|
+
* screenshotsはSQLite行に加え、対応するPNGファイル(タイル)もあわせて削除する。
|
|
105
|
+
*/
|
|
106
|
+
pruneExpired() {
|
|
107
|
+
const cutoff = this.now() - this.ttlMs;
|
|
108
|
+
this.db.prepare("DELETE FROM pages WHERE fetched_at < ?").run(cutoff);
|
|
109
|
+
this.db.prepare("DELETE FROM domain_pages WHERE fetched_at < ?").run(cutoff);
|
|
110
|
+
// code-reviewer指摘: host_requestsもM3の対象から漏れていた(訪問ホスト毎に1行増える
|
|
111
|
+
// 無制限増加テーブルという点でdomain_pages等と同じ性質を持つため、他テーブルと一貫させる)。
|
|
112
|
+
this.db.prepare("DELETE FROM host_requests WHERE last_request_at < ?").run(cutoff);
|
|
113
|
+
const expiredScreenshots = this.db.prepare("SELECT * FROM screenshots WHERE fetched_at < ?").all(cutoff);
|
|
114
|
+
for (const row of expiredScreenshots) {
|
|
115
|
+
const tilePaths = JSON.parse(row.tile_paths);
|
|
116
|
+
for (const filePath of tilePaths) {
|
|
117
|
+
try {
|
|
118
|
+
rmSync(filePath, { force: true });
|
|
119
|
+
}
|
|
120
|
+
catch {
|
|
121
|
+
// 既に削除済み等のファイルシステムエラーはベストエフォートで無視する
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
this.db.prepare("DELETE FROM screenshots WHERE fetched_at < ?").run(cutoff);
|
|
126
|
+
}
|
|
127
|
+
/** 保存済みエントリを取得する(TTL判定は行わない。判定は isFresh を使う)。 */
|
|
128
|
+
get(url) {
|
|
129
|
+
const row = this.db.prepare("SELECT * FROM pages WHERE url = ?").get(url);
|
|
130
|
+
if (!row)
|
|
131
|
+
return undefined;
|
|
132
|
+
return {
|
|
133
|
+
url: row.url,
|
|
134
|
+
etag: row.etag,
|
|
135
|
+
lastModified: row.last_modified,
|
|
136
|
+
contentHash: row.content_hash,
|
|
137
|
+
markdown: row.markdown,
|
|
138
|
+
metadata: JSON.parse(row.metadata),
|
|
139
|
+
fetchedAt: row.fetched_at,
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
/** エントリ(ページ/スクリーンショットいずれも可)がTTL内(再検証不要)かどうかを判定する。 */
|
|
143
|
+
isFresh(entry) {
|
|
144
|
+
return this.now() - entry.fetchedAt < this.ttlMs;
|
|
145
|
+
}
|
|
146
|
+
/** 保存済みスクリーンショットエントリを取得する(TTL判定は isFresh を使う)。 */
|
|
147
|
+
getScreenshot(cacheKey) {
|
|
148
|
+
const row = this.db.prepare("SELECT * FROM screenshots WHERE cache_key = ?").get(cacheKey);
|
|
149
|
+
if (!row)
|
|
150
|
+
return undefined;
|
|
151
|
+
return {
|
|
152
|
+
cacheKey: row.cache_key,
|
|
153
|
+
url: row.url,
|
|
154
|
+
tilePaths: JSON.parse(row.tile_paths),
|
|
155
|
+
metadata: JSON.parse(row.metadata),
|
|
156
|
+
fetchedAt: row.fetched_at,
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
/** スクリーンショットのタイル群をファイル保存し、パスをSQLiteへ記録する。 */
|
|
160
|
+
setScreenshot(input) {
|
|
161
|
+
const tileDir = join(this.cacheDir, "screenshots", input.cacheKey);
|
|
162
|
+
mkdirSync(tileDir, { recursive: true });
|
|
163
|
+
const tilePaths = input.tiles.map((buffer, index) => {
|
|
164
|
+
const filePath = join(tileDir, `tile-${index}.png`);
|
|
165
|
+
writeFileSync(filePath, buffer);
|
|
166
|
+
return filePath;
|
|
167
|
+
});
|
|
168
|
+
const fetchedAt = this.now();
|
|
169
|
+
this.db
|
|
170
|
+
.prepare(`INSERT INTO screenshots (cache_key, url, tile_paths, metadata, fetched_at)
|
|
171
|
+
VALUES (@cacheKey, @url, @tilePaths, @metadata, @fetchedAt)
|
|
172
|
+
ON CONFLICT(cache_key) DO UPDATE SET
|
|
173
|
+
url = excluded.url,
|
|
174
|
+
tile_paths = excluded.tile_paths,
|
|
175
|
+
metadata = excluded.metadata,
|
|
176
|
+
fetched_at = excluded.fetched_at`)
|
|
177
|
+
.run({
|
|
178
|
+
cacheKey: input.cacheKey,
|
|
179
|
+
url: input.url,
|
|
180
|
+
tilePaths: JSON.stringify(tilePaths),
|
|
181
|
+
metadata: JSON.stringify(input.metadata),
|
|
182
|
+
fetchedAt,
|
|
183
|
+
});
|
|
184
|
+
return { cacheKey: input.cacheKey, url: input.url, tilePaths, metadata: input.metadata, fetchedAt };
|
|
185
|
+
}
|
|
186
|
+
/** 新規取得結果を保存(既存があれば上書き)する。 */
|
|
187
|
+
set(input) {
|
|
188
|
+
const contentHash = hashContent(input.markdown);
|
|
189
|
+
const fetchedAt = this.now();
|
|
190
|
+
this.db
|
|
191
|
+
.prepare(`INSERT INTO pages (url, etag, last_modified, content_hash, markdown, metadata, fetched_at)
|
|
192
|
+
VALUES (@url, @etag, @lastModified, @contentHash, @markdown, @metadata, @fetchedAt)
|
|
193
|
+
ON CONFLICT(url) DO UPDATE SET
|
|
194
|
+
etag = excluded.etag,
|
|
195
|
+
last_modified = excluded.last_modified,
|
|
196
|
+
content_hash = excluded.content_hash,
|
|
197
|
+
markdown = excluded.markdown,
|
|
198
|
+
metadata = excluded.metadata,
|
|
199
|
+
fetched_at = excluded.fetched_at`)
|
|
200
|
+
.run({
|
|
201
|
+
url: input.url,
|
|
202
|
+
etag: input.etag,
|
|
203
|
+
lastModified: input.lastModified,
|
|
204
|
+
contentHash,
|
|
205
|
+
markdown: input.markdown,
|
|
206
|
+
metadata: JSON.stringify(input.metadata),
|
|
207
|
+
fetchedAt,
|
|
208
|
+
});
|
|
209
|
+
return {
|
|
210
|
+
url: input.url,
|
|
211
|
+
etag: input.etag,
|
|
212
|
+
lastModified: input.lastModified,
|
|
213
|
+
contentHash,
|
|
214
|
+
markdown: input.markdown,
|
|
215
|
+
metadata: input.metadata,
|
|
216
|
+
fetchedAt,
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
/** 304再検証成功時: 本文は変えず取得時刻のみ更新する(再変換しない)。 */
|
|
220
|
+
touch(url) {
|
|
221
|
+
this.db.prepare("UPDATE pages SET fetched_at = ? WHERE url = ?").run(this.now(), url);
|
|
222
|
+
}
|
|
223
|
+
/** Phase 4テンプレート学習: このページの(除去前・全)ブロックハッシュ集合をドメイン別に記録する。 */
|
|
224
|
+
recordDomainPageBlocks(domain, url, blockHashes) {
|
|
225
|
+
this.db
|
|
226
|
+
.prepare(`INSERT INTO domain_pages (domain, url, fetched_at, block_hashes)
|
|
227
|
+
VALUES (@domain, @url, @fetchedAt, @blockHashes)
|
|
228
|
+
ON CONFLICT(domain, url) DO UPDATE SET
|
|
229
|
+
fetched_at = excluded.fetched_at,
|
|
230
|
+
block_hashes = excluded.block_hashes`)
|
|
231
|
+
.run({ domain, url, fetchedAt: this.now(), blockHashes: JSON.stringify(blockHashes) });
|
|
232
|
+
}
|
|
233
|
+
/**
|
|
234
|
+
* 直近recentPages件のページ全てに共通して出現したブロックハッシュを定型ブロック
|
|
235
|
+
* (ヘッダ/フッタ/定型ナビ等)とみなして返す。まだrecentPages件分のページ履歴が
|
|
236
|
+
* 無い場合(コールドスタート)は誤除去を避けるため空集合を返す。
|
|
237
|
+
*/
|
|
238
|
+
getTemplateBlockHashes(domain, recentPages = DEFAULT_TEMPLATE_RECENT_PAGES) {
|
|
239
|
+
const rows = this.db
|
|
240
|
+
.prepare("SELECT * FROM domain_pages WHERE domain = ? ORDER BY fetched_at DESC LIMIT ?")
|
|
241
|
+
.all(domain, recentPages);
|
|
242
|
+
if (rows.length < recentPages)
|
|
243
|
+
return new Set();
|
|
244
|
+
const occurrenceCount = new Map();
|
|
245
|
+
for (const row of rows) {
|
|
246
|
+
const hashes = JSON.parse(row.block_hashes);
|
|
247
|
+
for (const hash of hashes) {
|
|
248
|
+
occurrenceCount.set(hash, (occurrenceCount.get(hash) ?? 0) + 1);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
const templateHashes = new Set();
|
|
252
|
+
for (const [hash, count] of occurrenceCount) {
|
|
253
|
+
if (count >= recentPages)
|
|
254
|
+
templateHashes.add(hash);
|
|
255
|
+
}
|
|
256
|
+
return templateHashes;
|
|
257
|
+
}
|
|
258
|
+
/**
|
|
259
|
+
* CLI併設対応: ホスト(例 "example.com")の最終リクエスト時刻を返す(未記録ならnull)。
|
|
260
|
+
* PolitenessManagerのstoreオプションとして注入し、プロセス間でレート制御状態を共有するために使う。
|
|
261
|
+
*/
|
|
262
|
+
getHostLastRequestAt(host) {
|
|
263
|
+
const row = this.db.prepare("SELECT * FROM host_requests WHERE host = ?").get(host);
|
|
264
|
+
return row ? row.last_request_at : null;
|
|
265
|
+
}
|
|
266
|
+
/**
|
|
267
|
+
* ホストの最終リクエスト時刻を記録する。
|
|
268
|
+
*
|
|
269
|
+
* code-reviewer指摘: MCPサーバー/CLIの複数プロセスが同一ホストへほぼ同時にwaitTurnを
|
|
270
|
+
* 実行すると、「読み取り→待機→書き込み」の3ステップがプロセス間でアトミックではないため
|
|
271
|
+
* 後勝ちの書き込みが古い時刻で新しい時刻を上書きし、最小間隔がすり抜けうる
|
|
272
|
+
* (完全な直列化はプロセス内のみ。プロセス間はベストエフォート。PolitenessManagerの
|
|
273
|
+
* store JSDoc参照)。せめて時刻が巻き戻らないよう、SQL側でMAXを取って単調増加を保証する。
|
|
274
|
+
*/
|
|
275
|
+
setHostLastRequestAt(host, at) {
|
|
276
|
+
this.db
|
|
277
|
+
.prepare(`INSERT INTO host_requests (host, last_request_at)
|
|
278
|
+
VALUES (@host, @lastRequestAt)
|
|
279
|
+
ON CONFLICT(host) DO UPDATE SET last_request_at = MAX(last_request_at, excluded.last_request_at)`)
|
|
280
|
+
.run({ host, lastRequestAt: at });
|
|
281
|
+
}
|
|
282
|
+
close() {
|
|
283
|
+
this.db.close();
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
//# sourceMappingURL=cache.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cache.js","sourceRoot":"","sources":["../src/cache.ts"],"names":[],"mappings":"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"}
|
package/dist/cli.d.ts
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { type ImageBlock, type TextBlock } from "./core.js";
|
|
3
|
+
/** 引数の使い方が不正な場合に投げる(cli.ts側でusageを表示しexit code 2にする)。 */
|
|
4
|
+
export declare class CliUsageError extends Error {
|
|
5
|
+
}
|
|
6
|
+
declare const FETCH_MODES: readonly ["auto", "markdown", "outline", "screenshot"];
|
|
7
|
+
export type CliFetchMode = (typeof FETCH_MODES)[number];
|
|
8
|
+
export type ParsedCommand = {
|
|
9
|
+
kind: "serve";
|
|
10
|
+
} | {
|
|
11
|
+
kind: "version";
|
|
12
|
+
} | {
|
|
13
|
+
kind: "help";
|
|
14
|
+
topic?: "fetch" | "links" | "screenshot";
|
|
15
|
+
} | ParsedFetchCommand | ParsedLinksCommand | ParsedScreenshotCommand;
|
|
16
|
+
export interface ParsedFetchCommand {
|
|
17
|
+
kind: "fetch";
|
|
18
|
+
url: string;
|
|
19
|
+
mode?: CliFetchMode;
|
|
20
|
+
selector?: string;
|
|
21
|
+
section?: string;
|
|
22
|
+
page?: number;
|
|
23
|
+
maxTokens?: number;
|
|
24
|
+
forceFull?: boolean;
|
|
25
|
+
outDir?: string;
|
|
26
|
+
}
|
|
27
|
+
export interface ParsedLinksCommand {
|
|
28
|
+
kind: "links";
|
|
29
|
+
url: string;
|
|
30
|
+
filter?: string;
|
|
31
|
+
}
|
|
32
|
+
export interface ParsedScreenshotCommand {
|
|
33
|
+
kind: "screenshot";
|
|
34
|
+
url: string;
|
|
35
|
+
viewportOnly?: boolean;
|
|
36
|
+
width?: number;
|
|
37
|
+
scale?: number;
|
|
38
|
+
outDir?: string;
|
|
39
|
+
}
|
|
40
|
+
/** CLI引数(process.argv.slice(2)相当)をコマンドへパースする純関数。 */
|
|
41
|
+
export declare function parseCliArgs(argv: string[]): ParsedCommand;
|
|
42
|
+
/** TextBlockは標準出力へ、ImageBlockはoutDir配下へ保存しパスを列挙する。テスト用export。 */
|
|
43
|
+
export declare function writeBlocks(blocks: Array<TextBlock | ImageBlock>, url: string, outDir: string): void;
|
|
44
|
+
/** CLIのメイン処理。テスト用export(引数を受け取り終了コードを返す純粋な形にしてある)。 */
|
|
45
|
+
export declare function run(argv: string[]): Promise<number>;
|
|
46
|
+
export {};
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* cli.ts — amenboのコマンドラインエントリ。MCPサーバー(server.ts)と同一のコア(core.ts)を
|
|
4
|
+
* 共有し、シェルスクリプト/CI/デバッグ用途で同じfetch/links/screenshotをコマンドとして使えるようにする。
|
|
5
|
+
*
|
|
6
|
+
* - 引数なし、または `amenbo serve` はMCPサーバーとして起動する(既存の.mcp.jsonの
|
|
7
|
+
* `"command": "amenbo"` との後方互換のため最重要)。
|
|
8
|
+
* - `amenbo fetch/links/screenshot <url> [options]` は対応するcore.tsのハンドラを呼び、
|
|
9
|
+
* TextBlockはそのまま標準出力へ、ImageBlock(スクリーンショット/スキャンPDF画像)は
|
|
10
|
+
* --out-dir配下へファイル保存しパスを列挙する(エージェント/ユーザーがそのまま開けるように)。
|
|
11
|
+
* - 引数パース(parseCliArgs)は純関数として切り出し、ユニットテストできるようにしている。
|
|
12
|
+
* - キャッシュ・差分応答・レート制御の状態はMCPサーバーと同じ ~/.cache/amenbo (cache.ts)を
|
|
13
|
+
* 共有する。CLIは1コマンド=1プロセスのため、politenessのドメイン毎レート制御は
|
|
14
|
+
* core.ts側でcacheをstoreとして注入したPolitenessManagerがプロセス間永続化する。
|
|
15
|
+
*/
|
|
16
|
+
import { mkdirSync, writeFileSync } from "node:fs";
|
|
17
|
+
import { join } from "node:path";
|
|
18
|
+
import { parseArgs } from "node:util";
|
|
19
|
+
import { fileURLToPath } from "node:url";
|
|
20
|
+
import { cache, formatLinksResponse, handleFetchTool, handleScreenshotTool, politeness, resolvePackageVersion, } from "./core.js";
|
|
21
|
+
import { AmenboError } from "./errors.js";
|
|
22
|
+
import { closeBrowser } from "./fetcher/browser.js";
|
|
23
|
+
import { discoverLinks } from "./links.js";
|
|
24
|
+
import { runServer } from "./server.js";
|
|
25
|
+
// ---- 引数パース(純関数。ユニットテスト対象) ----
|
|
26
|
+
/** 引数の使い方が不正な場合に投げる(cli.ts側でusageを表示しexit code 2にする)。 */
|
|
27
|
+
export class CliUsageError extends Error {
|
|
28
|
+
}
|
|
29
|
+
const FETCH_MODES = ["auto", "markdown", "outline", "screenshot"];
|
|
30
|
+
function parsePositiveInt(raw, flag) {
|
|
31
|
+
const n = Number(raw);
|
|
32
|
+
if (!Number.isInteger(n) || n <= 0) {
|
|
33
|
+
throw new CliUsageError(`${flag}は正の整数で指定してください: ${raw}`);
|
|
34
|
+
}
|
|
35
|
+
return n;
|
|
36
|
+
}
|
|
37
|
+
function parsePositiveFloat(raw, flag) {
|
|
38
|
+
const n = Number(raw);
|
|
39
|
+
if (!Number.isFinite(n) || n <= 0) {
|
|
40
|
+
throw new CliUsageError(`${flag}は正の数値で指定してください: ${raw}`);
|
|
41
|
+
}
|
|
42
|
+
return n;
|
|
43
|
+
}
|
|
44
|
+
function requireUrl(positionals, command) {
|
|
45
|
+
const url = positionals[0];
|
|
46
|
+
if (!url) {
|
|
47
|
+
throw new CliUsageError(`${command}: URLを指定してください`);
|
|
48
|
+
}
|
|
49
|
+
if (positionals.length > 1) {
|
|
50
|
+
throw new CliUsageError(`${command}: 不明な引数です: ${positionals[1]}`);
|
|
51
|
+
}
|
|
52
|
+
return url;
|
|
53
|
+
}
|
|
54
|
+
/** node:util parseArgsの例外(不明なオプション等)をCliUsageErrorへ変換する。 */
|
|
55
|
+
function safeParseArgs(args, command, options) {
|
|
56
|
+
try {
|
|
57
|
+
// node:util parseArgsは第2型引数を渡さなくてもoptions定義から値の型を推論するが、
|
|
58
|
+
// このジェネリックラッパー越しだと推論が効かないため戻り値側で明示キャストする。
|
|
59
|
+
const result = parseArgs({ args, allowPositionals: true, options });
|
|
60
|
+
return result;
|
|
61
|
+
}
|
|
62
|
+
catch (error) {
|
|
63
|
+
throw new CliUsageError(`${command}: ${error instanceof Error ? error.message : String(error)}`);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
function parseFetchArgs(args) {
|
|
67
|
+
const { values, positionals } = safeParseArgs(args, "fetch", {
|
|
68
|
+
mode: { type: "string" },
|
|
69
|
+
selector: { type: "string" },
|
|
70
|
+
section: { type: "string" },
|
|
71
|
+
page: { type: "string" },
|
|
72
|
+
"max-tokens": { type: "string" },
|
|
73
|
+
"force-full": { type: "boolean" },
|
|
74
|
+
"out-dir": { type: "string" },
|
|
75
|
+
help: { type: "boolean", short: "h" },
|
|
76
|
+
});
|
|
77
|
+
if (values.help)
|
|
78
|
+
return { kind: "help", topic: "fetch" };
|
|
79
|
+
const url = requireUrl(positionals, "fetch");
|
|
80
|
+
if (values.mode !== undefined && !FETCH_MODES.includes(values.mode)) {
|
|
81
|
+
throw new CliUsageError(`fetch: --modeは${FETCH_MODES.join("/")}のいずれかで指定してください: ${values.mode}`);
|
|
82
|
+
}
|
|
83
|
+
return {
|
|
84
|
+
kind: "fetch",
|
|
85
|
+
url,
|
|
86
|
+
...(values.mode !== undefined ? { mode: values.mode } : {}),
|
|
87
|
+
...(values.selector !== undefined ? { selector: values.selector } : {}),
|
|
88
|
+
...(values.section !== undefined ? { section: values.section } : {}),
|
|
89
|
+
...(values.page !== undefined ? { page: parsePositiveInt(values.page, "--page") } : {}),
|
|
90
|
+
...(values["max-tokens"] !== undefined ? { maxTokens: parsePositiveInt(values["max-tokens"], "--max-tokens") } : {}),
|
|
91
|
+
...(values["force-full"] ? { forceFull: true } : {}),
|
|
92
|
+
...(values["out-dir"] !== undefined ? { outDir: values["out-dir"] } : {}),
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
function parseLinksArgs(args) {
|
|
96
|
+
const { values, positionals } = safeParseArgs(args, "links", {
|
|
97
|
+
filter: { type: "string" },
|
|
98
|
+
help: { type: "boolean", short: "h" },
|
|
99
|
+
});
|
|
100
|
+
if (values.help)
|
|
101
|
+
return { kind: "help", topic: "links" };
|
|
102
|
+
const url = requireUrl(positionals, "links");
|
|
103
|
+
return {
|
|
104
|
+
kind: "links",
|
|
105
|
+
url,
|
|
106
|
+
...(values.filter !== undefined ? { filter: values.filter } : {}),
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
function parseScreenshotArgs(args) {
|
|
110
|
+
const { values, positionals } = safeParseArgs(args, "screenshot", {
|
|
111
|
+
"viewport-only": { type: "boolean" },
|
|
112
|
+
width: { type: "string" },
|
|
113
|
+
scale: { type: "string" },
|
|
114
|
+
"out-dir": { type: "string" },
|
|
115
|
+
help: { type: "boolean", short: "h" },
|
|
116
|
+
});
|
|
117
|
+
if (values.help)
|
|
118
|
+
return { kind: "help", topic: "screenshot" };
|
|
119
|
+
const url = requireUrl(positionals, "screenshot");
|
|
120
|
+
return {
|
|
121
|
+
kind: "screenshot",
|
|
122
|
+
url,
|
|
123
|
+
...(values["viewport-only"] ? { viewportOnly: true } : {}),
|
|
124
|
+
...(values.width !== undefined ? { width: parsePositiveInt(values.width, "--width") } : {}),
|
|
125
|
+
...(values.scale !== undefined ? { scale: parsePositiveFloat(values.scale, "--scale") } : {}),
|
|
126
|
+
...(values["out-dir"] !== undefined ? { outDir: values["out-dir"] } : {}),
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
/** CLI引数(process.argv.slice(2)相当)をコマンドへパースする純関数。 */
|
|
130
|
+
export function parseCliArgs(argv) {
|
|
131
|
+
if (argv.length === 0)
|
|
132
|
+
return { kind: "serve" };
|
|
133
|
+
const [first, ...rest] = argv;
|
|
134
|
+
if (first === "--version" || first === "-v")
|
|
135
|
+
return { kind: "version" };
|
|
136
|
+
if (first === "--help" || first === "-h")
|
|
137
|
+
return { kind: "help" };
|
|
138
|
+
if (first === "serve")
|
|
139
|
+
return { kind: "serve" };
|
|
140
|
+
if (first === "fetch")
|
|
141
|
+
return parseFetchArgs(rest);
|
|
142
|
+
if (first === "links")
|
|
143
|
+
return parseLinksArgs(rest);
|
|
144
|
+
if (first === "screenshot")
|
|
145
|
+
return parseScreenshotArgs(rest);
|
|
146
|
+
throw new CliUsageError(`不明なコマンドです: ${first}`);
|
|
147
|
+
}
|
|
148
|
+
// ---- ヘルプ/usage文言 ----
|
|
149
|
+
function usageText() {
|
|
150
|
+
return [
|
|
151
|
+
"amenbo [command] [options]",
|
|
152
|
+
"",
|
|
153
|
+
"Commands:",
|
|
154
|
+
" serve MCPサーバーとして起動する(既定。引数省略時と同じ)",
|
|
155
|
+
" fetch <url> [options] ページをMarkdownとして取得する",
|
|
156
|
+
" links <url> [options] ページ内のリンクを列挙する(sitemap/RSS優先)",
|
|
157
|
+
" screenshot <url> [options] ページのスクリーンショットを撮影する",
|
|
158
|
+
"",
|
|
159
|
+
"Options:",
|
|
160
|
+
" -h, --help ヘルプを表示する",
|
|
161
|
+
" -v, --version バージョンを表示する",
|
|
162
|
+
"",
|
|
163
|
+
"各コマンドの詳細は `amenbo <command> --help` を参照してください。",
|
|
164
|
+
].join("\n");
|
|
165
|
+
}
|
|
166
|
+
function fetchHelpText() {
|
|
167
|
+
return [
|
|
168
|
+
"amenbo fetch <url> [options]",
|
|
169
|
+
"",
|
|
170
|
+
"Options:",
|
|
171
|
+
" --mode <auto|markdown|outline|screenshot> 取得モード(既定 auto)",
|
|
172
|
+
" --selector <css> 本文を絞り込むCSSセレクタ",
|
|
173
|
+
" --section <id> outlineで得たsection ID。その節のMarkdownのみ返す",
|
|
174
|
+
" --page <n> ページ番号(既定 1)",
|
|
175
|
+
" --max-tokens <n> 1ページの概算トークン上限(既定 8000)",
|
|
176
|
+
" --force-full 差分応答(unchanged/diff)と定型ブロック除去を無効化し常に全文を返す",
|
|
177
|
+
" --out-dir <dir> screenshot切替時の画像保存先(既定 カレントディレクトリ)",
|
|
178
|
+
].join("\n");
|
|
179
|
+
}
|
|
180
|
+
function linksHelpText() {
|
|
181
|
+
return ["amenbo links <url> [options]", "", "Options:", " --filter <pattern> URL/リンクテキストの部分一致、または*を使ったglob"].join("\n");
|
|
182
|
+
}
|
|
183
|
+
function screenshotHelpText() {
|
|
184
|
+
return [
|
|
185
|
+
"amenbo screenshot <url> [options]",
|
|
186
|
+
"",
|
|
187
|
+
"Options:",
|
|
188
|
+
" --viewport-only 最初のビューポート分のみ撮影する(既定はfullPage)",
|
|
189
|
+
" --width <n> タイル幅px(既定 1280)",
|
|
190
|
+
" --scale <x> 解像度スケール(既定 1.0)。小さいほど画像トークンが減る",
|
|
191
|
+
" --out-dir <dir> 画像保存先(既定 カレントディレクトリ)",
|
|
192
|
+
].join("\n");
|
|
193
|
+
}
|
|
194
|
+
function helpText(topic) {
|
|
195
|
+
if (topic === "fetch")
|
|
196
|
+
return fetchHelpText();
|
|
197
|
+
if (topic === "links")
|
|
198
|
+
return linksHelpText();
|
|
199
|
+
if (topic === "screenshot")
|
|
200
|
+
return screenshotHelpText();
|
|
201
|
+
return usageText();
|
|
202
|
+
}
|
|
203
|
+
// ---- 出力(TextBlockはstdout、ImageBlockはファイル保存) ----
|
|
204
|
+
/** ファイル名として安全なホスト名(取れなければ"page")。amenbo-<hostname>-<連番>.pngの命名に使う。 */
|
|
205
|
+
function hostnameForFilename(url) {
|
|
206
|
+
try {
|
|
207
|
+
return new URL(url).hostname.replace(/[^A-Za-z0-9.-]/g, "_") || "page";
|
|
208
|
+
}
|
|
209
|
+
catch {
|
|
210
|
+
return "page";
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
/** TextBlockは標準出力へ、ImageBlockはoutDir配下へ保存しパスを列挙する。テスト用export。 */
|
|
214
|
+
export function writeBlocks(blocks, url, outDir) {
|
|
215
|
+
const hostname = hostnameForFilename(url);
|
|
216
|
+
const savedPaths = [];
|
|
217
|
+
let sequence = 0;
|
|
218
|
+
for (const block of blocks) {
|
|
219
|
+
if (block.type === "text") {
|
|
220
|
+
process.stdout.write(`${block.text}\n`);
|
|
221
|
+
continue;
|
|
222
|
+
}
|
|
223
|
+
sequence++;
|
|
224
|
+
if (savedPaths.length === 0) {
|
|
225
|
+
mkdirSync(outDir, { recursive: true });
|
|
226
|
+
}
|
|
227
|
+
const extension = block.mimeType === "image/png" ? "png" : "bin";
|
|
228
|
+
const filePath = join(outDir, `amenbo-${hostname}-${sequence}.${extension}`);
|
|
229
|
+
writeFileSync(filePath, Buffer.from(block.data, "base64"));
|
|
230
|
+
savedPaths.push(filePath);
|
|
231
|
+
}
|
|
232
|
+
if (savedPaths.length > 0) {
|
|
233
|
+
process.stdout.write(`\n画像を保存しました:\n${savedPaths.map((filePath) => `- ${filePath}`).join("\n")}\n`);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
// ---- 実行 ----
|
|
237
|
+
async function dispatch(command) {
|
|
238
|
+
if (command.kind === "fetch") {
|
|
239
|
+
const input = {
|
|
240
|
+
url: command.url,
|
|
241
|
+
...(command.mode !== undefined ? { mode: command.mode } : {}),
|
|
242
|
+
...(command.selector !== undefined ? { selector: command.selector } : {}),
|
|
243
|
+
...(command.section !== undefined ? { section: command.section } : {}),
|
|
244
|
+
...(command.page !== undefined ? { page: command.page } : {}),
|
|
245
|
+
...(command.maxTokens !== undefined ? { max_tokens: command.maxTokens } : {}),
|
|
246
|
+
...(command.forceFull !== undefined ? { force_full: command.forceFull } : {}),
|
|
247
|
+
};
|
|
248
|
+
const blocks = await handleFetchTool(input);
|
|
249
|
+
writeBlocks(blocks, command.url, command.outDir ?? process.cwd());
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
if (command.kind === "links") {
|
|
253
|
+
const result = await discoverLinks(command.url, politeness, command.filter !== undefined ? { filter: command.filter } : {});
|
|
254
|
+
process.stdout.write(`${formatLinksResponse(command.url, result)}\n`);
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
const input = {
|
|
258
|
+
url: command.url,
|
|
259
|
+
fullPage: !command.viewportOnly,
|
|
260
|
+
...(command.width !== undefined ? { width: command.width } : {}),
|
|
261
|
+
...(command.scale !== undefined ? { scale: command.scale } : {}),
|
|
262
|
+
};
|
|
263
|
+
const blocks = await handleScreenshotTool(input);
|
|
264
|
+
writeBlocks(blocks, command.url, command.outDir ?? process.cwd());
|
|
265
|
+
}
|
|
266
|
+
/** CLIのメイン処理。テスト用export(引数を受け取り終了コードを返す純粋な形にしてある)。 */
|
|
267
|
+
export async function run(argv) {
|
|
268
|
+
let command;
|
|
269
|
+
try {
|
|
270
|
+
command = parseCliArgs(argv);
|
|
271
|
+
}
|
|
272
|
+
catch (error) {
|
|
273
|
+
if (error instanceof CliUsageError) {
|
|
274
|
+
process.stderr.write(`${error.message}\n\n${usageText()}\n`);
|
|
275
|
+
return 2;
|
|
276
|
+
}
|
|
277
|
+
throw error;
|
|
278
|
+
}
|
|
279
|
+
if (command.kind === "help") {
|
|
280
|
+
process.stdout.write(`${helpText(command.topic)}\n`);
|
|
281
|
+
return 0;
|
|
282
|
+
}
|
|
283
|
+
if (command.kind === "version") {
|
|
284
|
+
process.stdout.write(`${resolvePackageVersion()}\n`);
|
|
285
|
+
return 0;
|
|
286
|
+
}
|
|
287
|
+
if (command.kind === "serve") {
|
|
288
|
+
// MCPサーバーはstdioで待受け続けるプロセスのため、closeBrowser/cache.closeは呼ばない
|
|
289
|
+
// (呼ぶとサーバー起動直後にキャッシュ/ブラウザが使えなくなる)。
|
|
290
|
+
await runServer();
|
|
291
|
+
return 0;
|
|
292
|
+
}
|
|
293
|
+
try {
|
|
294
|
+
await dispatch(command);
|
|
295
|
+
return 0;
|
|
296
|
+
}
|
|
297
|
+
catch (error) {
|
|
298
|
+
if (error instanceof AmenboError) {
|
|
299
|
+
process.stderr.write(`${error.message}\n`);
|
|
300
|
+
}
|
|
301
|
+
else {
|
|
302
|
+
console.error(error);
|
|
303
|
+
}
|
|
304
|
+
return 1;
|
|
305
|
+
}
|
|
306
|
+
finally {
|
|
307
|
+
// playwrightのブラウザが開いたままだとプロセスがハングするため、サブコマンド完了後は
|
|
308
|
+
// 確実にクローズしてから終了する(1実行=1プロセスのCLIではプロセス終了時のcloseに頼れない)。
|
|
309
|
+
await closeBrowser();
|
|
310
|
+
cache.close();
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
/** server.tsのisDirectlyExecutedと同様、テストからparseCliArgs等をimportした際にrun()が走らないようにするガード。 */
|
|
314
|
+
function isDirectlyExecuted() {
|
|
315
|
+
return typeof process.argv[1] === "string" && fileURLToPath(import.meta.url) === process.argv[1];
|
|
316
|
+
}
|
|
317
|
+
if (isDirectlyExecuted()) {
|
|
318
|
+
run(process.argv.slice(2))
|
|
319
|
+
.then((exitCode) => {
|
|
320
|
+
process.exitCode = exitCode;
|
|
321
|
+
})
|
|
322
|
+
.catch((error) => {
|
|
323
|
+
console.error(error);
|
|
324
|
+
process.exitCode = 1;
|
|
325
|
+
});
|
|
326
|
+
}
|
|
327
|
+
//# sourceMappingURL=cli.js.map
|