bibliocanvas 0.2.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.
@@ -0,0 +1,138 @@
1
+ ---
2
+ name: bibliocanvas
3
+ description: BiblioCanvasの書籍管理CLI。本の追加・更新・削除、本棚管理、Kindle本のASIN登録に対応。ユーザーが本・読書・本棚・BiblioCanvasに言及したら使用。
4
+ allowed-tools: Bash(bibliocanvas:*), Bash(curl:*), Bash(npx:*)
5
+ ---
6
+
7
+ # BiblioCanvas CLI
8
+
9
+ BiblioCanvasの書籍ライブラリをコマンドラインから管理する。
10
+
11
+ ## セットアップ
12
+
13
+ ```bash
14
+ npx bibliocanvas --version # インストール確認
15
+ npx bibliocanvas login # 初回: ブラウザ認証
16
+ ```
17
+
18
+ ## コマンド一覧
19
+
20
+ ### 書籍の一覧・検索
21
+
22
+ ```bash
23
+ npx bibliocanvas list # 全書籍
24
+ npx bibliocanvas list -q "キーワード" # タイトル・著者で検索
25
+ npx bibliocanvas list --status READ # ステータスで絞り込み(READ, READING, UNREAD, BACKLOG)
26
+ npx bibliocanvas list --shelf <shelfId> # 特定の本棚の書籍
27
+ npx bibliocanvas list --rating 5 # 最低評価で絞り込み
28
+ npx bibliocanvas list --limit 20 # 表示件数制限
29
+ npx bibliocanvas list --json # JSON出力
30
+ ```
31
+
32
+ ### 書籍の追加
33
+
34
+ **Kindle本(ASIN指定):**
35
+ ```bash
36
+ npx bibliocanvas add --title "タイトル" --authors "著者" --book-id <ASIN> --source kindle_import --image <画像URL>
37
+ ```
38
+
39
+ **ISBN指定:**
40
+ ```bash
41
+ npx bibliocanvas add --isbn 9784065371534
42
+ ```
43
+
44
+ **手動入力:**
45
+ ```bash
46
+ npx bibliocanvas add --title "タイトル" --authors "著者"
47
+ ```
48
+
49
+ ### 書籍の更新
50
+
51
+ ```bash
52
+ npx bibliocanvas update <bookId> --status READ
53
+ npx bibliocanvas update <bookId> --rating 5
54
+ npx bibliocanvas update <bookId> --memo "感想テキスト"
55
+ ```
56
+
57
+ ### 書籍の削除
58
+
59
+ ```bash
60
+ npx bibliocanvas delete <bookId>
61
+ ```
62
+
63
+ ### 本棚の管理
64
+
65
+ ```bash
66
+ npx bibliocanvas shelf list # 本棚一覧
67
+ npx bibliocanvas shelf books <shelfId> # 本棚の中身を表示
68
+ npx bibliocanvas shelf add-book <shelfId> <bookId> # 本棚に書籍追加
69
+ npx bibliocanvas shelf remove-book <shelfId> <bookId> # 本棚から書籍削除
70
+ npx bibliocanvas shelf create --name "本棚名" # 新規本棚作成
71
+ ```
72
+
73
+ ### 公開本棚の閲覧(ログイン不要)
74
+
75
+ ```bash
76
+ npx bibliocanvas public shelves # 公開本棚一覧
77
+ npx bibliocanvas public shelves --user <username> # ユーザーの公開本棚一覧
78
+ npx bibliocanvas public shelf <shelfId> # 公開本棚の詳細+書籍一覧
79
+ npx bibliocanvas public shelves --json # JSON出力
80
+ ```
81
+
82
+ ## Kindle本の登録手順
83
+
84
+ BiblioCanvasのKindleインポートで取り込めなかった本や、手動で追加したいKindle本をASINで登録する。
85
+
86
+ ### 1. ASINを特定する
87
+
88
+ AmazonのURLから取得: `https://www.amazon.co.jp/dp/{ASIN}`
89
+
90
+ ### 2. 表紙画像を取得する
91
+
92
+ Amazonの商品ページからOG画像を取得:
93
+ ```bash
94
+ curl -s -H "User-Agent: Mozilla/5.0" "https://www.amazon.co.jp/dp/{ASIN}" | grep -oP 'property="og:image"[^>]*content="([^"]+)"' | grep -oP 'https://[^"]+' | head -1
95
+ ```
96
+
97
+ ### 3. 登録する
98
+
99
+ ```bash
100
+ npx bibliocanvas add --title "タイトル" --authors "著者" --book-id {ASIN} --source kindle_import --image "{画像URL}"
101
+ ```
102
+
103
+ ### シリーズ全巻の一括登録
104
+
105
+ 1. 1巻のページからシリーズのASINを取得:
106
+ ```bash
107
+ curl -s -H "User-Agent: Mozilla/5.0" "https://www.amazon.co.jp/dp/{1巻のASIN}" | grep -oP 'dp/(B[A-Z0-9]{9,10})' | sort -u
108
+ ```
109
+
110
+ 2. 各ASINのタイトルを確認:
111
+ ```bash
112
+ curl -s -H "User-Agent: Mozilla/5.0" "https://www.amazon.co.jp/dp/{ASIN}" | grep -oP '<title>[^<]*' | sed 's/<title>//'
113
+ ```
114
+
115
+ 3. 各巻を表紙画像付きで登録する。
116
+
117
+ ### 重要: 追加前に重複チェック
118
+
119
+ ```bash
120
+ npx bibliocanvas list -q "タイトル"
121
+ ```
122
+
123
+ ## メモの書式(=== セパレータ)
124
+
125
+ ```
126
+ ひとこと感想(本棚ビューでポップ表示)
127
+ ===
128
+ 読書ログ(リストビューで全文表示)。
129
+ Markdown対応。
130
+ ```
131
+
132
+ `===` の前が「ひとこと」、後が「読書ログ」。`===` がない場合はメモ全体が「ひとこと」として扱われる。
133
+
134
+ ## 補足
135
+
136
+ - トークンは自動リフレッシュ。期限切れで再ログイン不要。
137
+ - `--dev` フラグで開発環境に接続。
138
+ - CLIからの変更は公開本棚にも自動同期される。
package/src/api.ts ADDED
@@ -0,0 +1,315 @@
1
+ /**
2
+ * API client for BiblioCanvas REST API
3
+ */
4
+
5
+ import { getIdToken, getApiBaseUrl } from './auth.js';
6
+
7
+ export interface Book {
8
+ bookId: string;
9
+ title: string;
10
+ authors: string;
11
+ acquiredTime: number;
12
+ readStatus: 'NONE' | 'UNREAD' | 'READING' | 'READ' | 'BACKLOG';
13
+ productImage: string;
14
+ source: string;
15
+ addedDate: number;
16
+ rating?: number;
17
+ memo?: string;
18
+ detailPageUrl?: string;
19
+ }
20
+
21
+ export interface Shelf {
22
+ id: string;
23
+ name: string;
24
+ description: string;
25
+ books: string[];
26
+ isPublic: boolean;
27
+ color: string;
28
+ }
29
+
30
+ export interface SearchResult {
31
+ volumeId: string;
32
+ title: string;
33
+ authors: string;
34
+ thumbnail: string;
35
+ isbn?: string;
36
+ }
37
+
38
+ type Env = 'production' | 'development';
39
+
40
+ async function apiRequest(
41
+ method: string,
42
+ path: string,
43
+ env: Env,
44
+ body?: Record<string, unknown>,
45
+ query?: Record<string, string>
46
+ ): Promise<unknown> {
47
+ const token = await getIdToken(env);
48
+ const baseUrl = getApiBaseUrl(env);
49
+ let url = `${baseUrl}${path}`;
50
+
51
+ if (query) {
52
+ const params = new URLSearchParams(
53
+ Object.entries(query).filter(([, v]) => v !== undefined)
54
+ );
55
+ if (params.toString()) {
56
+ url += `?${params.toString()}`;
57
+ }
58
+ }
59
+
60
+ const options: RequestInit = {
61
+ method,
62
+ headers: {
63
+ Authorization: `Bearer ${token}`,
64
+ 'Content-Type': 'application/json',
65
+ },
66
+ };
67
+
68
+ if (body && (method === 'POST' || method === 'PATCH')) {
69
+ options.body = JSON.stringify(body);
70
+ }
71
+
72
+ const response = await fetch(url, options);
73
+ const data = await response.json();
74
+
75
+ if (!response.ok) {
76
+ throw new Error(
77
+ (data as { error?: string }).error || `API error: ${response.status}`
78
+ );
79
+ }
80
+
81
+ return data;
82
+ }
83
+
84
+ async function publicApiRequest(
85
+ path: string,
86
+ env: Env,
87
+ query?: Record<string, string>
88
+ ): Promise<unknown> {
89
+ const baseUrl = getApiBaseUrl(env);
90
+ let url = `${baseUrl}${path}`;
91
+
92
+ if (query) {
93
+ const params = new URLSearchParams(
94
+ Object.entries(query).filter(([, v]) => v !== undefined)
95
+ );
96
+ if (params.toString()) {
97
+ url += `?${params.toString()}`;
98
+ }
99
+ }
100
+
101
+ const response = await fetch(url);
102
+ const data = await response.json();
103
+
104
+ if (!response.ok) {
105
+ throw new Error(
106
+ (data as { error?: string }).error || `API error: ${response.status}`
107
+ );
108
+ }
109
+
110
+ return data;
111
+ }
112
+
113
+ // ==================== Public Shelves (no auth required) ====================
114
+
115
+ export interface PublicShelf {
116
+ id: string;
117
+ name: string;
118
+ description: string;
119
+ ownerUsername: string;
120
+ ownerDisplayName: string;
121
+ slug: string;
122
+ bookCount: number;
123
+ likeCount: number;
124
+ commentCount: number;
125
+ updatedAt: string | null;
126
+ books?: PublicBook[];
127
+ }
128
+
129
+ export interface PublicBook {
130
+ bookId: string;
131
+ title: string;
132
+ authors: string;
133
+ productImage: string;
134
+ rating: number | null;
135
+ memo: string | null;
136
+ readStatus: string | null;
137
+ }
138
+
139
+ export async function listPublicShelves(
140
+ env: Env,
141
+ options?: { limit?: number }
142
+ ): Promise<{ shelves: PublicShelf[]; total: number }> {
143
+ const query: Record<string, string> = {};
144
+ if (options?.limit) query.limit = options.limit.toString();
145
+ return publicApiRequest('/public/shelves', env, query) as Promise<{
146
+ shelves: PublicShelf[];
147
+ total: number;
148
+ }>;
149
+ }
150
+
151
+ export async function listUserPublicShelves(
152
+ env: Env,
153
+ username: string
154
+ ): Promise<{ shelves: PublicShelf[]; total: number }> {
155
+ return publicApiRequest(`/public/users/${username}/shelves`, env) as Promise<{
156
+ shelves: PublicShelf[];
157
+ total: number;
158
+ }>;
159
+ }
160
+
161
+ export async function getPublicShelf(
162
+ env: Env,
163
+ shelfId: string,
164
+ options?: { books?: boolean }
165
+ ): Promise<PublicShelf> {
166
+ const query: Record<string, string> = {};
167
+ if (options?.books === false) query.books = 'false';
168
+ return publicApiRequest(`/public/shelves/${shelfId}`, env, query) as Promise<PublicShelf>;
169
+ }
170
+
171
+ // ==================== Books ====================
172
+
173
+ export async function listBooks(
174
+ env: Env,
175
+ options?: { q?: string; status?: string; sort?: string; dir?: string }
176
+ ): Promise<{ books: Book[]; total: number }> {
177
+ const query: Record<string, string> = {};
178
+ if (options?.q) query.q = options.q;
179
+ if (options?.status) query.status = options.status;
180
+ if (options?.sort) query.sort = options.sort;
181
+ if (options?.dir) query.dir = options.dir;
182
+
183
+ return apiRequest('GET', '/books', env, undefined, query) as Promise<{
184
+ books: Book[];
185
+ total: number;
186
+ }>;
187
+ }
188
+
189
+ export async function getBook(
190
+ env: Env,
191
+ bookId: string
192
+ ): Promise<Book> {
193
+ return apiRequest('GET', `/books/${bookId}`, env) as Promise<Book>;
194
+ }
195
+
196
+ export async function addBookByIsbn(
197
+ env: Env,
198
+ isbn: string
199
+ ): Promise<Book> {
200
+ return apiRequest('POST', '/books', env, { isbn }) as Promise<Book>;
201
+ }
202
+
203
+ export async function addBookBySearch(
204
+ env: Env,
205
+ search: string,
206
+ volumeId?: string
207
+ ): Promise<Book | { message: string; results: SearchResult[] }> {
208
+ const body: Record<string, unknown> = { search };
209
+ if (volumeId) body.volumeId = volumeId;
210
+ return apiRequest('POST', '/books', env, body) as Promise<
211
+ Book | { message: string; results: SearchResult[] }
212
+ >;
213
+ }
214
+
215
+ export async function addBookManual(
216
+ env: Env,
217
+ data: {
218
+ title: string;
219
+ authors?: string;
220
+ readStatus?: string;
221
+ rating?: number;
222
+ memo?: string;
223
+ bookId?: string;
224
+ source?: string;
225
+ productImage?: string;
226
+ }
227
+ ): Promise<Book> {
228
+ return apiRequest('POST', '/books', env, data) as Promise<Book>;
229
+ }
230
+
231
+ export async function updateBook(
232
+ env: Env,
233
+ bookId: string,
234
+ updates: Record<string, unknown>
235
+ ): Promise<Book> {
236
+ return apiRequest('PATCH', `/books/${bookId}`, env, updates) as Promise<Book>;
237
+ }
238
+
239
+ export async function deleteBook(
240
+ env: Env,
241
+ bookId: string
242
+ ): Promise<{ deleted: string }> {
243
+ return apiRequest('DELETE', `/books/${bookId}`, env) as Promise<{
244
+ deleted: string;
245
+ }>;
246
+ }
247
+
248
+ export async function searchBooks(
249
+ env: Env,
250
+ q: string,
251
+ type?: 'isbn' | 'title'
252
+ ): Promise<{ results: SearchResult[] }> {
253
+ const query: Record<string, string> = { q };
254
+ if (type) query.type = type;
255
+ return apiRequest('GET', '/books/search', env, undefined, query) as Promise<{
256
+ results: SearchResult[];
257
+ }>;
258
+ }
259
+
260
+ // ==================== Shelves ====================
261
+
262
+ export async function listShelves(
263
+ env: Env
264
+ ): Promise<{ shelves: Shelf[]; total: number }> {
265
+ return apiRequest('GET', '/shelves', env) as Promise<{
266
+ shelves: Shelf[];
267
+ total: number;
268
+ }>;
269
+ }
270
+
271
+ export async function createShelf(
272
+ env: Env,
273
+ data: { name: string; description?: string; color?: string }
274
+ ): Promise<Shelf> {
275
+ return apiRequest('POST', '/shelves', env, data) as Promise<Shelf>;
276
+ }
277
+
278
+ export async function updateShelf(
279
+ env: Env,
280
+ shelfId: string,
281
+ updates: Record<string, unknown>
282
+ ): Promise<Shelf> {
283
+ return apiRequest('PATCH', `/shelves/${shelfId}`, env, updates) as Promise<Shelf>;
284
+ }
285
+
286
+ export async function deleteShelf(
287
+ env: Env,
288
+ shelfId: string
289
+ ): Promise<{ deleted: string }> {
290
+ return apiRequest('DELETE', `/shelves/${shelfId}`, env) as Promise<{
291
+ deleted: string;
292
+ }>;
293
+ }
294
+
295
+ export async function addBookToShelf(
296
+ env: Env,
297
+ shelfId: string,
298
+ bookId: string
299
+ ): Promise<{ shelfId: string; bookId: string; added: boolean }> {
300
+ return apiRequest('POST', `/shelves/${shelfId}/books`, env, {
301
+ bookId,
302
+ }) as Promise<{ shelfId: string; bookId: string; added: boolean }>;
303
+ }
304
+
305
+ export async function removeBookFromShelf(
306
+ env: Env,
307
+ shelfId: string,
308
+ bookId: string
309
+ ): Promise<{ shelfId: string; bookId: string; removed: boolean }> {
310
+ return apiRequest(
311
+ 'DELETE',
312
+ `/shelves/${shelfId}/books/${bookId}`,
313
+ env
314
+ ) as Promise<{ shelfId: string; bookId: string; removed: boolean }>;
315
+ }