note-connector 0.2.5 → 0.2.6

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.
Files changed (46) hide show
  1. package/dist/paths.js +4 -0
  2. package/dist/setup-dependencies.js +56 -13
  3. package/package.json +3 -2
  4. package/py/pyproject.toml +86 -0
  5. package/py/src/note_mcp/__init__.py +7 -0
  6. package/py/src/note_mcp/__main__.py +65 -0
  7. package/py/src/note_mcp/api/__init__.py +31 -0
  8. package/py/src/note_mcp/api/articles.py +1395 -0
  9. package/py/src/note_mcp/api/client.py +318 -0
  10. package/py/src/note_mcp/api/embeds.py +482 -0
  11. package/py/src/note_mcp/api/images.py +456 -0
  12. package/py/src/note_mcp/api/preview.py +142 -0
  13. package/py/src/note_mcp/api/public_notes.py +150 -0
  14. package/py/src/note_mcp/auth/__init__.py +9 -0
  15. package/py/src/note_mcp/auth/browser.py +574 -0
  16. package/py/src/note_mcp/auth/file_session.py +145 -0
  17. package/py/src/note_mcp/auth/session.py +240 -0
  18. package/py/src/note_mcp/browser/__init__.py +10 -0
  19. package/py/src/note_mcp/browser/config.py +21 -0
  20. package/py/src/note_mcp/browser/manager.py +182 -0
  21. package/py/src/note_mcp/browser/preview.py +68 -0
  22. package/py/src/note_mcp/browser/url_helpers.py +18 -0
  23. package/py/src/note_mcp/chatgpt/__init__.py +1 -0
  24. package/py/src/note_mcp/chatgpt/__main__.py +63 -0
  25. package/py/src/note_mcp/chatgpt/access_log.py +25 -0
  26. package/py/src/note_mcp/chatgpt/auth.py +52 -0
  27. package/py/src/note_mcp/chatgpt/images.py +92 -0
  28. package/py/src/note_mcp/chatgpt/login_once.py +26 -0
  29. package/py/src/note_mcp/chatgpt/middleware.py +31 -0
  30. package/py/src/note_mcp/chatgpt/tools.py +255 -0
  31. package/py/src/note_mcp/chatgpt/widgets.py +121 -0
  32. package/py/src/note_mcp/decorators.py +113 -0
  33. package/py/src/note_mcp/investigator/__init__.py +33 -0
  34. package/py/src/note_mcp/investigator/__main__.py +11 -0
  35. package/py/src/note_mcp/investigator/cli.py +313 -0
  36. package/py/src/note_mcp/investigator/core.py +653 -0
  37. package/py/src/note_mcp/investigator/mcp_tools.py +225 -0
  38. package/py/src/note_mcp/models.py +557 -0
  39. package/py/src/note_mcp/py.typed +0 -0
  40. package/py/src/note_mcp/server.py +905 -0
  41. package/py/src/note_mcp/utils/__init__.py +7 -0
  42. package/py/src/note_mcp/utils/file_parser.py +314 -0
  43. package/py/src/note_mcp/utils/html_to_markdown.py +477 -0
  44. package/py/src/note_mcp/utils/logging.py +119 -0
  45. package/py/src/note_mcp/utils/markdown.py +12 -0
  46. package/py/src/note_mcp/utils/markdown_to_html.py +826 -0
@@ -0,0 +1,557 @@
1
+ """Pydantic data models for note-mcp.
2
+
3
+ This module defines all data models used throughout the note-mcp server,
4
+ including session management, article handling, and error types.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import time
10
+ from enum import Enum
11
+
12
+ from pydantic import BaseModel
13
+
14
+
15
+ class Session(BaseModel):
16
+ """User authentication session.
17
+
18
+ Stores authentication state including cookies, user information,
19
+ and session expiration.
20
+
21
+ Attributes:
22
+ cookies: note.com authentication cookies (note_gql_auth_token, _note_session_v5)
23
+ user_id: note.com user ID
24
+ username: note.com username (used in URL paths)
25
+ expires_at: Session expiration timestamp (Unix timestamp), None if no expiry
26
+ created_at: Session creation timestamp (Unix timestamp)
27
+ """
28
+
29
+ cookies: dict[str, str]
30
+ user_id: str
31
+ username: str
32
+ expires_at: int | None = None
33
+ created_at: int
34
+
35
+ def is_expired(self) -> bool:
36
+ """Check if the session has expired.
37
+
38
+ Returns:
39
+ True if session has expired, False otherwise.
40
+ Returns False if expires_at is None (no expiry set).
41
+ """
42
+ if self.expires_at is None:
43
+ return False
44
+ return time.time() > self.expires_at
45
+
46
+
47
+ class ArticleStatus(str, Enum):
48
+ """Article publication status."""
49
+
50
+ DRAFT = "draft"
51
+ PUBLISHED = "published"
52
+ PRIVATE = "private"
53
+ DELETED = "deleted"
54
+
55
+
56
+ class ImageType(str, Enum):
57
+ """Image upload type.
58
+
59
+ Determines which note.com API endpoint to use for image upload.
60
+ """
61
+
62
+ EYECATCH = "eyecatch" # Header/eyecatch image (見出し画像)
63
+ BODY = "body" # Inline/body image (記事内埋め込み画像)
64
+
65
+
66
+ class Article(BaseModel):
67
+ """A note.com article.
68
+
69
+ Represents an article with all its metadata as stored on note.com.
70
+
71
+ Attributes:
72
+ id: Article ID (note.com internal ID)
73
+ key: Article key (used in URL path)
74
+ title: Article title
75
+ body: Article body content (HTML format)
76
+ status: Publication status
77
+ tags: List of hashtags (without # prefix)
78
+ eyecatch_image_key: Eyecatch image key (if set)
79
+ prev_access_key: Preview access key for draft articles
80
+ created_at: Creation timestamp (ISO 8601)
81
+ updated_at: Last update timestamp (ISO 8601)
82
+ published_at: Publication timestamp (ISO 8601)
83
+ url: Full article URL
84
+ """
85
+
86
+ id: str
87
+ key: str
88
+ title: str
89
+ body: str
90
+ status: ArticleStatus
91
+ tags: list[str] = []
92
+ eyecatch_image_key: str | None = None
93
+ prev_access_key: str | None = None
94
+ created_at: str | None = None
95
+ updated_at: str | None = None
96
+ published_at: str | None = None
97
+ url: str | None = None
98
+
99
+
100
+ class ArticleInput(BaseModel):
101
+ """Input data for creating or updating an article.
102
+
103
+ Attributes:
104
+ title: Article title
105
+ body: Article body content (Markdown format)
106
+ tags: List of hashtags (# prefix optional, will be normalized)
107
+ eyecatch_image_path: Local path to eyecatch image (optional)
108
+ """
109
+
110
+ title: str
111
+ body: str
112
+ tags: list[str] = []
113
+ eyecatch_image_path: str | None = None
114
+
115
+
116
+ class Image(BaseModel):
117
+ """An uploaded image.
118
+
119
+ Attributes:
120
+ key: note.com image key (None for eyecatch images as API doesn't return it)
121
+ url: Image URL on note.com
122
+ original_path: Original local file path
123
+ size_bytes: File size in bytes (optional)
124
+ uploaded_at: Upload timestamp (Unix timestamp)
125
+ image_type: Type of image (eyecatch or body)
126
+ """
127
+
128
+ key: str | None = None
129
+ url: str
130
+ original_path: str
131
+ size_bytes: int | None = None
132
+ uploaded_at: int
133
+ image_type: ImageType = ImageType.EYECATCH
134
+
135
+
136
+ class Tag(BaseModel):
137
+ """A hashtag for articles.
138
+
139
+ Attributes:
140
+ name: Tag name (without # prefix)
141
+ """
142
+
143
+ name: str
144
+
145
+ @classmethod
146
+ def normalize(cls, tag: str) -> str:
147
+ """Normalize a tag by removing leading # characters.
148
+
149
+ Args:
150
+ tag: Tag string, possibly with # prefix
151
+
152
+ Returns:
153
+ Tag string without # prefix
154
+ """
155
+ return tag.lstrip("#")
156
+
157
+
158
+ class ArticleListResult(BaseModel):
159
+ """Result of listing articles.
160
+
161
+ Attributes:
162
+ articles: List of articles
163
+ total: Total number of articles matching the query
164
+ page: Current page number (1-indexed)
165
+ has_more: Whether there are more articles to fetch
166
+ """
167
+
168
+ articles: list[Article]
169
+ total: int
170
+ page: int
171
+ has_more: bool
172
+
173
+
174
+ class BrowserArticleResult(BaseModel):
175
+ """Result of browser-based article creation/update.
176
+
177
+ Includes the article and optional TOC/alignment/embed/image insertion results for user notification.
178
+
179
+ Attributes:
180
+ article: The created/updated article
181
+ toc_inserted: True if TOC was successfully inserted, False if failed, None if not attempted
182
+ toc_error: Error message if TOC insertion failed
183
+ alignments_applied: Number of text alignments successfully applied, None if not attempted
184
+ alignment_error: Error message if text alignment application failed
185
+ embeds_inserted: Number of embeds successfully inserted, None if not attempted
186
+ embed_error: Error message if embed insertion failed
187
+ images_inserted: Number of images successfully inserted, None if not attempted
188
+ image_error: Error message if image insertion failed
189
+ debug_info: Debug information for troubleshooting (temporary)
190
+ """
191
+
192
+ article: Article
193
+ toc_inserted: bool | None = None
194
+ toc_error: str | None = None
195
+ alignments_applied: int | None = None
196
+ alignment_error: str | None = None
197
+ embeds_inserted: int | None = None
198
+ embed_error: str | None = None
199
+ images_inserted: int | None = None
200
+ image_error: str | None = None
201
+ debug_info: str | None = None
202
+
203
+
204
+ class ErrorCode(str, Enum):
205
+ """Error codes for note-mcp API errors."""
206
+
207
+ NOT_AUTHENTICATED = "not_authenticated"
208
+ SESSION_EXPIRED = "session_expired"
209
+ ARTICLE_NOT_FOUND = "article_not_found"
210
+ RATE_LIMITED = "rate_limited"
211
+ API_ERROR = "api_error"
212
+ UPLOAD_FAILED = "upload_failed"
213
+ INVALID_INPUT = "invalid_input"
214
+
215
+
216
+ class NoteAPIError(Exception):
217
+ """Exception for note.com API errors.
218
+
219
+ Attributes:
220
+ code: Error code
221
+ message: Human-readable error message
222
+ details: Additional error details
223
+ """
224
+
225
+ def __init__(
226
+ self,
227
+ code: ErrorCode,
228
+ message: str,
229
+ details: dict[str, object] | None = None,
230
+ ) -> None:
231
+ """Initialize the error.
232
+
233
+ Args:
234
+ code: Error code from ErrorCode enum
235
+ message: Human-readable error message
236
+ details: Additional context (e.g., status code, response body)
237
+ """
238
+ self.code = code
239
+ self.message = message
240
+ self.details = details or {}
241
+ super().__init__(message)
242
+
243
+
244
+ class LoginError(Exception):
245
+ """ログイン処理でのエラー。
246
+
247
+ reCAPTCHA検出、2FA要求、認証情報エラー時に送出される。
248
+ 手動ログインへのフォールバックは行わず、明確なエラーで通知する。
249
+
250
+ Attributes:
251
+ code: エラーコード(RECAPTCHA_DETECTED, TWO_FACTOR_REQUIRED,
252
+ INVALID_CREDENTIALS, LOGIN_TIMEOUT)
253
+ message: エラーメッセージ
254
+ resolution: 推奨される対処法
255
+ """
256
+
257
+ def __init__(
258
+ self,
259
+ code: str,
260
+ message: str,
261
+ resolution: str | None = None,
262
+ ) -> None:
263
+ """Initialize the error.
264
+
265
+ Args:
266
+ code: エラーコード
267
+ message: エラーメッセージ
268
+ resolution: 推奨される対処法(オプション)
269
+ """
270
+ self.code = code
271
+ self.message = message
272
+ self.resolution = resolution
273
+ super().__init__(message)
274
+
275
+
276
+ # =============================================================================
277
+ # Issue #141: Delete Draft Models
278
+ # =============================================================================
279
+
280
+ # Delete operation error messages (T020)
281
+ DELETE_ERROR_PUBLISHED_ARTICLE = "公開済み記事は削除できません。下書きのみ削除可能です。"
282
+ DELETE_ERROR_NO_ACCESS = "この記事へのアクセス権がありません。"
283
+ DELETE_ERROR_NOT_FOUND = "記事が見つかりません。キー: {article_key}"
284
+ DELETE_CONFIRM_REQUIRED = "削除を実行するには confirm=True を指定してください。"
285
+
286
+
287
+ class ArticleSummary(BaseModel):
288
+ """Summary information of an article.
289
+
290
+ Used in bulk delete preview/result to show article information
291
+ without the full body content.
292
+
293
+ Attributes:
294
+ article_id: Article ID (note.com internal ID)
295
+ article_key: Article key (used in URL path)
296
+ title: Article title
297
+ """
298
+
299
+ article_id: str
300
+ article_key: str
301
+ title: str
302
+
303
+
304
+ class FailedArticle(BaseModel):
305
+ """Information about a failed deletion.
306
+
307
+ Used in BulkDeleteResult to provide details about articles
308
+ that could not be deleted and why.
309
+
310
+ Attributes:
311
+ article_id: Article ID
312
+ article_key: Article key
313
+ title: Article title
314
+ error: Error message explaining the failure
315
+ """
316
+
317
+ article_id: str
318
+ article_key: str
319
+ title: str
320
+ error: str
321
+
322
+
323
+ class DeleteResult(BaseModel):
324
+ """Result of a single delete operation.
325
+
326
+ Returned when a delete operation completes (success or failure).
327
+
328
+ Attributes:
329
+ success: Whether the deletion was successful
330
+ article_id: ID of the deleted article
331
+ article_key: Key of the deleted article
332
+ article_title: Title of the deleted article
333
+ message: Result message for the user
334
+ """
335
+
336
+ success: bool
337
+ article_id: str
338
+ article_key: str
339
+ article_title: str
340
+ message: str
341
+
342
+
343
+ class DeletePreview(BaseModel):
344
+ """Preview information before deletion.
345
+
346
+ Returned when confirm=False to show what will be deleted
347
+ and prompt for confirmation.
348
+
349
+ Attributes:
350
+ article_id: ID of the article to be deleted
351
+ article_key: Key of the article to be deleted
352
+ article_title: Title of the article to be deleted
353
+ status: Current status of the article
354
+ message: Confirmation prompt message
355
+ """
356
+
357
+ article_id: str
358
+ article_key: str
359
+ article_title: str
360
+ status: ArticleStatus
361
+ message: str
362
+
363
+
364
+ class BulkDeletePreview(BaseModel):
365
+ """Preview information for bulk deletion.
366
+
367
+ Returned when confirm=False to show all drafts that will be deleted.
368
+
369
+ Attributes:
370
+ total_count: Total number of drafts to be deleted
371
+ articles: List of articles to be deleted
372
+ message: Confirmation prompt message
373
+ """
374
+
375
+ total_count: int
376
+ articles: list[ArticleSummary]
377
+ message: str
378
+
379
+
380
+ class BulkDeleteResult(BaseModel):
381
+ """Result of bulk delete operation.
382
+
383
+ Provides detailed information about the bulk deletion,
384
+ including counts and lists of successful/failed deletions.
385
+
386
+ Attributes:
387
+ success: Whether all deletions were successful
388
+ total_count: Total number of articles targeted
389
+ deleted_count: Number of successfully deleted articles
390
+ failed_count: Number of failed deletions
391
+ deleted_articles: List of successfully deleted articles
392
+ failed_articles: List of articles that failed to delete
393
+ message: Summary message
394
+ """
395
+
396
+ success: bool
397
+ total_count: int
398
+ deleted_count: int
399
+ failed_count: int
400
+ deleted_articles: list[ArticleSummary]
401
+ failed_articles: list[FailedArticle]
402
+ message: str
403
+
404
+
405
+ class DeleteDraftInput(BaseModel):
406
+ """Input for note_delete_draft MCP tool.
407
+
408
+ Attributes:
409
+ article_key: Key of the article to delete (format: nXXXXXXXXXXXX)
410
+ confirm: Confirmation flag (must be True to execute deletion)
411
+ """
412
+
413
+ article_key: str
414
+ confirm: bool = False
415
+
416
+
417
+ class PublicArticleSummary(BaseModel):
418
+ """Summary of a public note.com article in search results."""
419
+
420
+ key: str
421
+ title: str
422
+ author_username: str
423
+ author_nickname: str | None = None
424
+ url: str
425
+ published_at: str | None = None
426
+
427
+
428
+ class PublicSearchResult(BaseModel):
429
+ """Public note search results."""
430
+
431
+ items: list[PublicArticleSummary]
432
+ query: str
433
+ is_last_page: bool | None = None
434
+
435
+
436
+ class PublicArticle(BaseModel):
437
+ """A fetched public note.com article."""
438
+
439
+ key: str
440
+ title: str
441
+ body_markdown: str
442
+ author_username: str
443
+ author_nickname: str | None = None
444
+ url: str
445
+ status: str
446
+
447
+
448
+ class DeleteAllDraftsInput(BaseModel):
449
+ """Input for note_delete_all_drafts MCP tool.
450
+
451
+ Attributes:
452
+ confirm: Confirmation flag (must be True to execute deletion)
453
+ """
454
+
455
+ confirm: bool = False
456
+
457
+
458
+ def from_api_response(data: dict[str, object]) -> Article:
459
+ """Create an Article from note.com API response.
460
+
461
+ Article 6 (Data Accuracy Mandate) compliant:
462
+ - Required fields (id, key, status) must be present, no implicit fallbacks
463
+ - Missing required fields raise NoteAPIError
464
+
465
+ Args:
466
+ data: Raw API response dictionary
467
+
468
+ Returns:
469
+ Article instance
470
+
471
+ Raises:
472
+ NoteAPIError: If required fields (id, key, status) are missing or invalid
473
+ """
474
+ # Article 6: Validate required field 'id' - no fallback
475
+ article_id = data.get("id")
476
+ if not article_id:
477
+ raise NoteAPIError(
478
+ code=ErrorCode.API_ERROR,
479
+ message="API response missing required field: id",
480
+ details={"response": data},
481
+ )
482
+
483
+ # Article 6: Validate required field 'key' - no fallback
484
+ article_key = data.get("key")
485
+ if not article_key:
486
+ raise NoteAPIError(
487
+ code=ErrorCode.API_ERROR,
488
+ message="API response missing required field: key",
489
+ details={"response": data},
490
+ )
491
+
492
+ # Article 6: Validate required field 'status' - no fallback or guessing
493
+ status_str = data.get("status")
494
+ if not isinstance(status_str, str) or not status_str:
495
+ raise NoteAPIError(
496
+ code=ErrorCode.API_ERROR,
497
+ message="API response missing or invalid required field: status",
498
+ details={"response": data, "status_value": status_str},
499
+ )
500
+
501
+ # Extract hashtag names from the hashtags array
502
+ # Empty hashtags list is valid - this is not a fallback
503
+ hashtags = data.get("hashtags", [])
504
+ tags: list[str] = []
505
+ if isinstance(hashtags, list):
506
+ for ht in hashtags:
507
+ if isinstance(ht, dict):
508
+ hashtag_obj = ht.get("hashtag", {})
509
+ if isinstance(hashtag_obj, dict):
510
+ # Skip hashtags without name - no fallback to empty string
511
+ name = hashtag_obj.get("name")
512
+ if name:
513
+ tags.append(str(name))
514
+
515
+ # Extract title: use "name" field, fallback to "noteDraft.name" for drafts
516
+ title = data.get("name")
517
+ if not title:
518
+ note_draft = data.get("noteDraft")
519
+ if isinstance(note_draft, dict):
520
+ title = note_draft.get("name")
521
+ title_str = str(title) if title else ""
522
+
523
+ # body can be empty string - this is a valid value, not a missing field
524
+ body = data.get("body")
525
+ body_str = str(body) if body is not None else ""
526
+
527
+ return Article(
528
+ id=str(article_id),
529
+ key=str(article_key),
530
+ title=title_str,
531
+ body=body_str,
532
+ status=ArticleStatus(status_str),
533
+ tags=tags,
534
+ eyecatch_image_key=str(data.get("eyecatch_image_key")) if data.get("eyecatch_image_key") else None,
535
+ prev_access_key=str(data.get("prev_access_key")) if data.get("prev_access_key") else None,
536
+ created_at=str(data.get("created_at")) if data.get("created_at") else None,
537
+ updated_at=str(data.get("updated_at")) if data.get("updated_at") else None,
538
+ published_at=str(data.get("publish_at")) if data.get("publish_at") else None,
539
+ url=str(data.get("noteUrl")) if data.get("noteUrl") else None,
540
+ )
541
+
542
+
543
+ def to_api_request(article_input: ArticleInput, html_body: str) -> dict[str, object]:
544
+ """Convert ArticleInput to note.com API request format.
545
+
546
+ Args:
547
+ article_input: Input data from user
548
+ html_body: HTML-converted body content
549
+
550
+ Returns:
551
+ Dictionary suitable for note.com API request
552
+ """
553
+ return {
554
+ "name": article_input.title,
555
+ "body": html_body,
556
+ "status": "draft",
557
+ }
File without changes