Force ntn to authenticate as the same integration that writes the page by
injecting NOTION_API_TOKEN=$NOTION_API_KEY into every ntn subprocess env.
Notion scopes file uploads to the creating integration, so a mismatch caused
"Could not find file_upload with ID …" when attaching uploaded images.
Added test_upload_passes_token_env (9 total in test_ntn_files.py) and updated
code/CLAUDE.md to reflect that a separate ntn login is no longer required.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
FIX 1: _content_has_local_images and extract_local_images now skip lines inside
``` fences so a local-image ref inside a code block is not treated as real.
FIX 2: md_translate.translate() tracks fence state; callout/columns/mention
transforms are suppressed for lines inside ``` ... ``` blocks (pass verbatim).
FIX 3: main() catches NtnUploadError and prints a clean error + e.stderr before
sys.exit(1); body moved to _main_body(parser, args).
FIX 4: ntn_files.upload() raises NtnUploadError on empty stdout instead of
IndexError, giving a clean message combined with FIX 3.
FIX 5: Removed unused write_to_page() (both page paths are inlined).
FIX 6: create_page_markdown() omits the 'markdown' key when value is falsy,
avoiding empty-markdown sends on DB rows created without --file/--stdin.
FIX 7: CLAUDE.md documents --allow-deleting-content scope (markdown engine only).
TDD: Added test_fence_passthrough_no_transform (md_translate, suite now 8) and
test_local_image_inside_fence_ignored (engine_routing, suite now 6).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Mirror the blocks path's error handling in the markdown engine:
- page + DB markdown two-phase image append now check append_to_page's
return and exit(1) on failure instead of printing success silently
- markdown DB upsert guards update_page_properties return
- restore blocks-engine page progress print + replace-failure print
- move unused parent computation into the create branch
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
notion.blocks.children.list returns at most 100 blocks per call.
Without pagination, --replace only cleared the first 100 blocks
of a page, leaving the rest behind and producing surprising
partial-replace behavior.
Loop with next_cursor until has_more is false so the entire
page is cleared.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Plaintext .env storage of NOTION_API_KEY is unnecessary risk — Notion
integration tokens grant write access to every connected page/database
and have no expiry. Document the 1Password CLI fetch pattern as the
preferred path; .env becomes a discouraged fallback for environments
without `op`.
SKILL.md adds a Credential handling section covering:
- One-shot inline fetch via `op read 'op://Development/Notion - Claude Agent/api-key'`
- Subshell-scoped export pattern for repeated use without shell-history exposure
- 1Password field-name conventions (api-key for the token)
- Anti-patterns to avoid (echo, CLI args, git commits, chat paste)
- Token rotation procedure
notion_writer.py error message updated to point at the 1Password recipe
when NOTION_API_KEY is missing, instead of telling users to create a .env.
Tested end-to-end: pushed a 22 KB markdown report (16 tables, 120 nested
checkboxes) using the inline 1Password fetch. Bot identity verified, all
markdown blocks rendered as native Notion blocks.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Code review flagged two implicit invariants worth pinning with comments:
- 'mention' must precede 'link' in INLINE_PATTERNS so the @[X](Y) prefix
takes priority over plain link parsing
- format_id_with_dashes is required because the Notion mention API expects
a dashed UUID while extract_notion_id returns dashless
32/32 tests still pass.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Inline rich-text gets a new 'mention' pattern matching @[Title](target).
Target can be a raw 32-hex page ID, a dashed UUID, or a Notion URL —
all resolved via the existing extract_notion_id helper. Invalid targets
degrade to plain text '@Title' with no warning (avoids false-positive
spam from unrelated @[x](y) patterns).
Pattern placed BEFORE 'link' in INLINE_PATTERNS so the @ prefix takes
priority over plain link parsing.
Also widens extract_notion_id's URL regex from [^-]+- to [^/]+?- so
multi-hyphen Notion URLs (e.g. notion.so/My-Cool-Page-{id}) resolve
correctly — previous regex only matched single-segment titles.
+3 tests, 32 passing total.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Pandoc-style ::: columns / ::: column / ::: blocks emit a Notion
column_list with column children. Each column's body recurses through
_parse_lines so any block type (lists, code, nested columns) works
inside. Single-column wrappers degrade to plain paragraphs because
Notion requires at least two columns.
Single-depth-counter design: depth=1 on enter, increments on `::: columns`
or `::: column`, decrements on `:::`. depth=0 closes the wrapper.
+3 tests, 29 passing total.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Code review caught a redundant `import sys as _sys` inside the unclosed-
<details> branch. The module already imports `sys` at the top, so the
inline import was dead weight. Use the module-level `sys.stderr` directly.
26/26 tests still pass.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Multi-line <details>...</details> with optional <summary> emits a Notion
toggle block. Body recurses through _parse_lines so any block type
(lists, code, nested toggles) is supported inside. Depth tracking lets
<details> nest inside <details>. Unclosed <details> at EOF degrades to
plain paragraphs with a stderr warning instead of crashing.
+4 tests, 26 passing total.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Code review caught that all 9 other create_*_block factories include
"object": "block" as the first key, but the new callout factory was
missing it. The Notion API accepts both forms, but the inconsistency
would compound; align with the existing convention.
22/22 tests still pass.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds support for > [!NOTE] / > [!TIP] / > [!IMPORTANT] / > [!WARNING] /
> [!CAUTION] callouts. Each alert type maps to a Notion callout block
with an emoji icon and matching colored background. Unknown alert types
(e.g. > [!BOGUS]) fall through to the existing quote handler with the
marker preserved.
Also restores the "must come before generic bullet match" comment on
the todo branch (load-bearing ordering note).
+6 tests, 22 passing total.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Split the entry function into a public reentrant entry that accepts
either string or List[str], and a private _parse_lines engine that
container detectors will recurse into. No behavior change for existing
callers; all 16 parser tests still pass.
Prep for Phase 3c: callout/toggle/columns/page-mention block coverage.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Extend markdown_to_notion_blocks with GFM table support and rewrite
parse_rich_text to emit Notion annotations for bold/italic/code/link/strike
instead of dropping them as literal text. Move checkbox match ahead of the
bullet match so "- [ ]" lines become to_do blocks, not bullets.
Add test_parser.py with 13 regression tests covering rich-text spans,
block types, and table cell formatting.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The --replace flag failed with "Can't edit block that is archived" when
a page contained previously archived blocks. Now skips them during clear.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>