Initial commit

This commit is contained in:
Zhongwei Li
2025-11-30 08:37:58 +08:00
commit f0a4617f0c
38 changed files with 4166 additions and 0 deletions

View File

@@ -0,0 +1,9 @@
# Configuration containing API tokens
config/config.json
# Fetched data
data/
# Temporary files
*.tmp
*.bak

View File

@@ -0,0 +1,186 @@
# Weeknotes Blog Post Composer
A Claude Code skill for composing conversational weeknotes blog posts from multiple data sources.
## Overview
This skill automatically fetches content from Mastodon and Linkding, then composes it into a well-formatted Jekyll-style blog post with proper voice, tone, and narrative structure. No more copy-paste dumps—get readable, conversational weeknotes that sound like you.
## Features
- **Multi-source data fetching**: Mastodon posts and Linkding bookmarks
- **Conversational composition**: Claude reads your content and composes readable prose
- **Style matching**: Optionally reference your past weeknotes to maintain consistent voice
- **Smart tagging**: Automatically generates 3-7 contextually appropriate tags
- **Jekyll-ready output**: YAML frontmatter with proper filename conventions
- **Cross-platform**: Supports macOS (ARM64/Intel) and Linux (AMD64)
## Quick Start
### Installation
Install this skill as a Claude Code marketplace:
```bash
# Add to your Claude config
# ~/.claude/config/settings.json
{
"plugins": [
"/path/to/lmorchard-agent-skills-private"
]
}
```
Restart Claude Code to load the skill.
### First-Time Setup
The first time you use the skill, Claude will guide you through configuration:
```
User: Draft weeknotes for this week
Claude: I need to configure the skill first. I'll need:
- Your Mastodon server URL and access token
- Your Linkding instance URL and API token
- (Optional) URL to your past weeknotes for style reference
```
**Getting API credentials:**
- **Mastodon**: Settings → Development → New Application (read permissions)
- **Linkding**: Settings → Integrations → Create Token
### Basic Usage
```
# Default: last 7 days (rolling 7-day window)
Draft weeknotes for this week
# Specific date range
Create weeknotes from November 4-10
```
Claude will:
1. Fetch your Mastodon posts and Linkding bookmarks
2. Analyze the content for themes and topics
3. Compose conversational prose that sounds like you
4. Generate contextually appropriate tags
5. Save to your blog directory (if detected) or offer to save elsewhere
## Configuration
After initial setup, your config lives in `config/config.json`:
```json
{
"mastodon": {
"server": "https://your-instance.social",
"token": "your-access-token"
},
"linkding": {
"url": "https://your-linkding.com",
"token": "your-api-token"
},
"weeknotes_archive": "https://yourblog.com/tag/weeknotes/"
}
```
To reconfigure:
```bash
./scripts/setup.sh
```
## Project Structure
```
weeknotes-blog-post-composer/
├── SKILL.md # Detailed documentation for Claude
├── README.md # This file
├── bin/ # Platform-specific Go CLI binaries
│ ├── darwin-arm64/
│ ├── darwin-amd64/
│ └── linux-amd64/
├── scripts/
│ ├── setup.sh # First-time configuration
│ ├── fetch-sources.sh # Fetch data from sources
│ ├── prepare-sources.py # Verify fetched data
│ └── download-binaries.sh # Update CLI binaries
├── config/
│ └── config.json # API credentials (gitignored)
└── data/ # Fetched markdown files (gitignored)
```
## Data Sources
Currently supported:
- **Mastodon**: Posts from specified date range
- **Linkding**: Bookmarks from specified date range
The architecture supports adding additional data sources in the future.
## Output Format
Generated blog posts include:
- **Jekyll YAML frontmatter** with title, date, tags, and layout
- **Conversational prose** composed from your content
- **Short contextual links** (3-5 words) for readability
- **Inline images** from Mastodon posts
- **3-7 tags** including "weeknotes" plus contextual tags
- **Proper structure** with TL;DR, main sections, Miscellanea, and conclusion
## Filename Convention
When run from your blog directory, posts are saved to:
```
content/posts/{YYYY}/{YYYY-MM-DD-wWW}.md
```
Where:
- `{YYYY}` = 4-digit year of start date
- `{YYYY-MM-DD}` = Start date of the 7-day period
- `{wWW}` = ISO week number (e.g., w16, w45)
Example: `content/posts/2025/2025-11-07-w45.md`
## Manual Commands
You can run individual components if needed:
```bash
# Update binaries to latest releases
./scripts/download-binaries.sh
# Fetch data for specific date range
./scripts/fetch-sources.sh --start 2025-11-01 --end 2025-11-07
# Verify fetched data
./scripts/prepare-sources.py
```
## Security
- API credentials stored in `config/config.json` with 600 permissions
- Config file is gitignored
- Temporary config files cleaned up after use
- Fetched data is gitignored
## Documentation
For detailed documentation on how the skill works and how Claude uses it, see [SKILL.md](SKILL.md).
## Requirements
- **Claude Code**: Latest version
- **Bash**: For shell scripts
- **Python 3**: For Python scripts
- **curl**: For API testing (during setup)
- **jq** (optional): For better config parsing
Binaries for Mastodon and Linkding fetching are included for all supported platforms.
## License
This is a personal skill for use with Claude Code. Binaries for `mastodon-to-markdown` and `linkding-to-markdown` are subject to their respective licenses.

View File

@@ -0,0 +1,603 @@
---
name: weeknotes-blog-post-composer
description: Compose weeknotes blog posts in Jekyll-style Markdown from multiple data sources including Mastodon and Linkding. Use this skill when the user requests to create, draft, or generate weeknotes content for a blog post.
---
# Weeknotes Blog Post Composer
## Overview
This skill enables composing weeknotes blog posts by automatically fetching content from multiple sources (Mastodon posts and Linkding bookmarks) and combining them into a well-formatted Jekyll-style Markdown document with YAML frontmatter. The skill handles data collection, formatting, and composition into a ready-to-publish blog post. Optionally, the skill can reference past weeknotes to match the user's personal writing style and voice.
## Quick Start
When a user first requests to create weeknotes, check if the skill is configured:
```bash
cd /path/to/weeknotes-blog-post-composer
# Check if config exists
if [ ! -f "./config/config.json" ]; then
echo "First-time setup required."
./scripts/setup.sh
fi
```
If configuration doesn't exist:
1. Inform the user that first-time setup is needed
2. Ask for their Mastodon server URL and access token
3. Ask for their Linkding instance URL and API token
4. Optionally ask for their weeknotes archive URL for style reference
5. Run `scripts/setup.sh` with their inputs
### Getting API Credentials
**Mastodon Access Token:**
1. Log into the Mastodon instance
2. Go to Settings → Development → New Application
3. Give it a name (e.g., "Weeknotes Composer")
4. Grant "read" permissions
5. Copy the access token
**Linkding API Token:**
1. Log into the Linkding instance
2. Go to Settings → Integrations
3. Click "Create Token"
4. Copy the generated token
## Composing Weeknotes
The primary workflow for composing weeknotes follows these steps:
### Step 1: Determine Date Range
By default, use the last 7 days (from 7 days ago to today). If the user specifies a different timeframe, parse their request and extract start/end dates.
Examples of user requests:
- "Draft weeknotes for this week" → 7 days ago to today
- "Create weeknotes for last week" → 14 days ago to 7 days ago
- "Generate weeknotes from November 4-10" → 2025-11-04 to 2025-11-10
**Default date calculation:**
```python
from datetime import datetime, timedelta
today = datetime.now()
end_date = today.strftime("%Y-%m-%d")
start_date = (today - timedelta(days=7)).strftime("%Y-%m-%d")
```
So if today is Thursday November 14, 2025:
- Start date: Thursday November 7, 2025
- End date: Thursday November 14, 2025
### Step 2: Fetch Source Data
Run the fetch script to collect data from all configured sources:
```bash
cd /path/to/weeknotes-blog-post-composer
# For current week (automatic date calculation)
./scripts/fetch-sources.sh
# For specific date range
./scripts/fetch-sources.sh --start YYYY-MM-DD --end YYYY-MM-DD
# For custom output directory
./scripts/fetch-sources.sh --start YYYY-MM-DD --end YYYY-MM-DD --output-dir ./data/custom
```
This fetches:
- Mastodon posts from the specified date range
- Linkding bookmarks from the specified date range
Output files are saved to `data/latest/` (or specified directory):
- `mastodon.md` - Formatted Mastodon posts
- `linkding.md` - Formatted bookmarks
### Step 3: Read and Analyze Source Data
Verify the fetched data is ready and understand what content is available:
```bash
cd /path/to/weeknotes-blog-post-composer
./scripts/prepare-sources.py
```
This shows which source files are available and their sizes.
Then read the fetched markdown files to understand the content:
```bash
# Read Mastodon posts
cat data/latest/mastodon.md
# Read Linkding bookmarks
cat data/latest/linkding.md
```
### Step 3.5: Review Past Weeknotes for Style Reference (Optional)
**Check for configured style reference:**
```bash
# Check if weeknotes_archive URL is configured
cd /path/to/weeknotes-blog-post-composer
cat config/config.json
```
If the config contains a `weeknotes_archive` URL, fetch and review 1-2 of the user's past weeknotes to understand their writing style and voice. Use the WebFetch tool to analyze the archive page and individual posts.
If no `weeknotes_archive` is configured, skip this step and compose in a conversational blog post style.
**Key style elements to look for in past weeknotes:**
1. **Voice & Tone:**
- Conversational and self-deprecating
- Frequent parenthetical asides and tangents
- Playful language (e.g., "Ope", casual interjections)
- Self-aware meta-commentary about the writing process itself
2. **Structure:**
- Starts with an opening paragraph containing inline "TL;DR: ..." summary
- Followed by `<!--more-->` on its own line (marks intro for Jekyll excerpt)
- 2-3 deeper dives into specific projects or topics (main body)
- **"Miscellanea" section near the end** (just before conclusion) for brief observations and items that didn't fit elsewhere
- **CRITICAL:** Use bullet points for each item in Miscellanea
- **CRITICAL:** Include ALL bookmarks/links here as bullet points, not in a separate section
- **CRITICAL:** Wrap the Miscellanea bullet points in `<div class="weeknote-miscellanea">` tags
- Miscellanea is a catch-all grab bag for everything else: short observations, bookmarks, reading, random thoughts
- Concluding reflection on the week
3. **Content Balance:**
- Equal weighting of technical depth and personal reflection
- Mixed technical projects, personal observations, and humor
- Philosophy embedded in technical writing
- Comfortable with digression and associative thinking
4. **Transitions:**
- Uses bullet points and whitespace rather than formal prose bridges
- Ideas progress through thematic gravity or personal relevance
- Stream-of-consciousness feel ("notes accumulated throughout the week")
5. **Distinctive Elements:**
- Metaphorical thinking (uses analogies to explain technical challenges)
- Acknowledges when feeling scattered or self-doubting
- References to ongoing projects and past posts
- Comfortable admitting uncertainty or work-in-progress status
When composing, aim to match this voice rather than writing in a generic blog style.
### Step 4: Compose Conversational Weeknotes
**Important:** Do not use template substitution. Instead, read the source markdown and compose it into readable prose.
**Style guidance:** Match the user's voice from past weeknotes (see Step 3.5) - conversational, self-deprecating, with parenthetical asides and comfortable with tangents. Start with an opening paragraph containing an inline "TL;DR: ..." summary (not a header), followed by `<!--more-->` on its own line. Use a "Miscellanea" section near the end (just before the conclusion) as a grab-bag for brief observations and items that didn't fit under other thematic sections. **CRITICAL:** Format ALL Miscellanea items as bullet points, including bookmarks and links - do NOT create a separate "Bookmarks and Reading" section.
Analyze the fetched content and compose a conversational weeknotes post that:
1. **Summarizes Mastodon activity** - Don't just list every post. Instead:
- Identify themes and topics from the week
- Highlight interesting conversations or thoughts
- Group related posts together
- Write in a natural, conversational tone
- Include specific details that are interesting or noteworthy
- **Link to actual Mastodon posts** using the URLs from the source (e.g., `[posted about X](https://masto.hackers.town/@user/12345)`)
- **CRITICAL - AVOID PLAGIARISM:** Only use the user's own words from "My Posts" sections directly in prose. Content from "Posts I Boosted" or "Posts I Favorited" should ONLY be:
- Referenced/cited with attribution (e.g., "Someone on Mastodon pointed out that...")
- Summarized in your own words, not quoted verbatim as if the user wrote them
- Alternatively, include blocks of text using blockquotes where it seems interesting
- Linked to without incorporating their text into the narrative
- This is extremely important to avoid unintentional plagiarism
- **IMPORTANT: Embed images inline** when they add value (e.g., `![Alt text](image-url)`)
- **Look for posts with Media entries** in the mastodon.md file - these contain images that should be included
- Images are especially important for: cats, interesting screenshots, funny visuals, project photos, etc.
2. **Integrates bookmarks meaningfully** - Don't just list links. Instead:
- **CRITICAL: ALL bookmarks MUST go in the Miscellanea section as bullet points**
- Do NOT create a separate "Bookmarks and Reading" section
- Group related bookmarks together within Miscellanea bullets when possible
- Explain why things were interesting or relevant in the bullet text
- Connect bookmarks to larger thoughts or projects
- **Include actual bookmark URLs** with descriptive link text (e.g., `[Article title](https://example.com)`)
- Format as bullet points with links in the Miscellanea section
3. **Creates a cohesive narrative** - The post should read like a blog post, not a data dump:
- Write in first person
- Use conversational language
- Connect different activities together
- Add context and reflection
- Include section headings that make sense for the content
4. **Uses proper formatting**:
- Jekyll-style YAML frontmatter with title, date, tags ("weeknotes" should always be used, along with 3-7 additional tags relevant to the content), and layout
- **Opening paragraph** with inline "TL;DR: ..." summary (NOT a header)
- **`<!--more-->`** comment on its own line immediately after the opening paragraph (marks excerpt boundary)
- **Table of contents nav** on its own line after `<!--more-->` if there are multiple sections (2+ headings): `<nav role="navigation" class="table-of-contents"></nav>`
- Markdown headings (##, ###) for structure in the main body
- Links to interesting posts or bookmarks
- Inline images from Mastodon posts where relevant
- Code blocks or quotes where appropriate
**Example opening structure:**
```markdown
TL;DR: Our 15-year-old solar inverter died this week, which kicked off a lot of thinking about technology longevity and IoT device lifecycles. Also spent time tinkering with Claude Code skills and bookmarking way too many articles about AI coding tools.
<!--more-->
<nav role="navigation" class="table-of-contents"></nav>
## Technology Longevity
...
```
**Critical: Always include the actual URLs!**
When referencing content:
- **Mastodon posts**: Link to the post URL with **short link text (3-5 words)** for aesthetics (e.g., `This week I [posted](https://masto.hackers.town/@user/12345) about solar inverters...`)
- **Bookmarks**: Include the bookmark URL with descriptive text (e.g., `I found [this article about AI coding](https://example.com/article) particularly interesting...`)
- **Images**: Embed Mastodon images inline using `![Description](image-url)` when they're interesting or funny
- **For multiple consecutive images** (3+), wrap them in `<image-gallery>` tags with newlines before/after the opening and closing tags:
```markdown
<image-gallery>
![First image](url1)
![Second image](url2)
![Third image](url3)
</image-gallery>
```
**Example composition approach:**
Instead of listing every post, write something like:
> This week I [spent a lot](https://masto.hackers.town/@user/12345) of time thinking about technology longevity. Our 15-year-old solar inverter died, which [kicked off](https://masto.hackers.town/@user/12346) a whole thread about IoT devices and how frustrating it is when tech doesn't have a 15-20 year plan.
**CRITICAL - Only use the user's own posts this way!** If you want to reference a boosted/favorited post or bookmark:
> There's been this interesting [article making the rounds](https://example.com/article) about BBS-era communication patterns - explaining how those carefully drafted essay-like responses created a distinctive writing style. But nope, it's just how we learned to write when bandwidth was scarce.
Then for bookmarks in Miscellanea, reference them naturally wrapped in the `weeknote-miscellanea` div:
```markdown
## Miscellanea
<div class="weeknote-miscellanea">
* [*Thinking About Thinking With LLMs*](https://example.com/article) - explores how new tools make it easier to code with shallower understanding
* [Another piece](https://example.com/article2) argues that the best programmers still dig deep to understand what's happening underneath
</div>
```
**IMPORTANT: Always scan the mastodon.md for images!**
The mastodon.md file includes `Media:` entries with image URLs and descriptions. Look for these and include them in your weeknotes. Example from the source:
```
Media: [image](https://cdn.masto.host/.../image.jpg) - Description of the image
```
When you find these, embed them in the weeknotes like this:
**Single image:**
> Miss Biscuits [discovered a new perch](https://masto.hackers.town/@user/12347):
>
> ![Description of the image](https://cdn.masto.host/.../image.jpg)
**Multiple images (3+) - use image gallery:**
> I [shared some photos](https://masto.hackers.town/@user/12348) of my 3D printing projects:
>
> <image-gallery>
>
> ![3D printed dragon](https://cdn.example.com/image1.jpg)
>
> ![Flexible octopus](https://cdn.example.com/image2.jpg)
>
> ![Cat playing with prints](https://cdn.example.com/image3.jpg)
>
> </image-gallery>
### Step 5: Review and Revise the Draft
Before finalizing, review the composed weeknotes and make light revisions:
1. **Structure check:**
- Ensure Miscellanea section is at the end (just before the conclusion)
- Move any straggling bookmark bullets that didn't fit into main sections into Miscellanea
- Verify all sections flow logically
2. **Prose polish:**
- Tighten up verbose sentences
- Remove unnecessary repetition
- Ensure transitions between sections make sense
- Check that the voice remains conversational and natural
3. **Content verification:**
- All Mastodon post links are present (3-5 word link text)
- All bookmark URLs are included
- Images are properly embedded (single images inline, 3+ images in `<image-gallery>`)
- Opening has inline "TL;DR: ..." followed by `<!--more-->`
- Table of contents nav is present if there are multiple sections
4. **Final touches:**
- Verify 3-7 tags (including "weeknotes")
- Check that conclusion ties things together
- Ensure Miscellanea items are formatted as bullet points
### Step 6: Write the Final Blog Post
Create the Jekyll blog post file with:
1. **YAML frontmatter:**
```yaml
---
title: "[Date Range]"
date: YYYY-MM-DD
tags:
- weeknotes
- [contextual-tag-1]
- [contextual-tag-2]
- [contextual-tag-3]
layout: post
---
```
**Important - Title Format:** Use the date range format without the word "Weeknotes" (e.g., "2025 Week 48" or "November 22-26, 2025"). The "weeknotes" tag already categorizes the post, so the title should be concise.
**Important - Tags:** Always include "weeknotes" as the first tag, then add 2-6 additional contextually appropriate tags based on the content (3-7 tags total). Tags should reflect major themes, technologies, topics, or projects discussed in the post. Examples:
- Technical topics: `ai`, `javascript`, `golang`, `docker`, `apis`
- Project types: `side-projects`, `open-source`, `blogging`
- Activities: `learning`, `refactoring`, `debugging`
- Themes: `productivity`, `tools`, `workflows`
Analyze the composed content and choose tags that genuinely reflect what the post is about.
2. **Composed content** - The conversational weeknotes you composed in Step 4 and revised in Step 5
**CRITICAL:** Do NOT include "Generated with Claude Code" or similar AI attribution footer in weeknotes posts. These are personal blog posts that should maintain the author's authentic voice throughout.
3. **Save** to the appropriate location and filename:
**Detecting the blog directory:**
Check if the current working directory contains `content/posts/` - if so, you're in the blog directory.
```bash
if [ -d "content/posts" ]; then
echo "In blog directory - using blog naming convention"
fi
```
**If running from the user's blog directory**, use this naming convention:
```
content/posts/{YYYY}/{YYYY-MM-DD-wWW}.md
```
Where:
- `{YYYY}` = 4-digit year (of today's date)
- `{YYYY-MM-DD}` = Today's date (the publication date)
- `{wWW}` = ISO week number for today (e.g., w16, w17, w42)
Examples:
- `content/posts/2025/2025-04-18-w16.md` (Week 16, published April 18, 2025)
- `content/posts/2025/2025-11-13-w46.md` (Week 46, published November 13, 2025)
**To calculate the week number and filename**, use the helper script:
```bash
cd /path/to/weeknotes-blog-post-composer
./scripts/calculate-week.py
# Or for a specific date:
./scripts/calculate-week.py --date 2025-11-13
# Or get JSON output:
./scripts/calculate-week.py --json
```
This script uses **today's date** (not the start date) and calculates the ISO week number, generating the correct filename format: `content/posts/{year}/{date}-w{week}.md`
**Important:** Ensure the year directory exists before saving:
```python
import os
year_dir = f"content/posts/{start_date.year}"
os.makedirs(year_dir, exist_ok=True)
```
**If not in the blog directory**, save to a temporary location (e.g., `/tmp/weeknotes-YYYY-MM-DD.md`) and ask the user where they'd like to move it
### Step 7: Select Cover Image Thumbnail
Review the images already embedded in the post and select one to use as the cover thumbnail:
1. **Analyze embedded images:**
- Review all images included in the post (from Mastodon posts)
- Consider their alt text/descriptions
- Evaluate which image best represents the overall themes of the weeknotes
2. **Selection criteria:**
- **Thematic relevance**: Image should represent main topics/themes, not just incidental content
- **Visual interest**: Choose images that are visually distinct and engaging
- **Quality**: Avoid low-quality screenshots or purely text-based images
- **Context**: Consider the image's role in the narrative - is it central to a main section or just a side note?
3. **Priority order:**
- Images related to primary themes/topics in the post
- Project photos, interesting technical subjects
- Noteworthy screenshots or visual examples
- Cat photos (only if cats are a significant theme of the week)
- Last resort: use the first image in the post
4. **Add to frontmatter:**
- Update the YAML frontmatter to include the `thumbnail:` property
- Use the full URL of the selected image
```yaml
---
title: "Weeknotes: [Date Range]"
date: YYYY-MM-DD
thumbnail: "https://cdn.masto.host/.../selected-image.jpg"
tags:
- weeknotes
- [other-tags]
layout: post
---
```
5. **If no suitable images exist in the post:**
- Omit the `thumbnail:` property for now
- The blog software will use the first image as a fallback
- Note: Future enhancement will add public domain image search
### Step 8: User Feedback and Final Refinement
1. Present the composed weeknotes to the user
2. Ask if they want any adjustments:
- Different tone or style
- More/less detail in certain areas
- Additional context or reflection
- Restructuring of content
3. Make requested edits
4. Offer to add a final reflection section if desired
## Additional Operations
### Updating Binaries
To update the Go CLI binaries to the latest releases:
```bash
cd /path/to/weeknotes-blog-post-composer
./scripts/download-binaries.sh
```
This downloads the latest versions of:
- `mastodon-to-markdown`
- `linkding-to-markdown`
For all supported platforms (darwin-arm64, darwin-amd64, linux-amd64).
### Reconfiguring
To update API credentials, change data source settings, or add/update style reference URL:
```bash
cd /path/to/weeknotes-blog-post-composer
./scripts/setup.sh
```
The setup script will detect existing configuration and ask for confirmation before reconfiguring. This includes:
- Mastodon server URL and access token
- Linkding URL and API token
- Weeknotes archive URL for style reference (optional)
### Customizing the Output Style
The composition process is flexible and can be customized based on user preferences:
1. **Tone and Style:**
- More formal or casual
- Technical vs. personal
- Detailed vs. high-level summaries
2. **Structure:**
- Different section organization
- Thematic groupings vs. chronological
- Depth of technical detail
3. **Content Selection:**
- Which topics to emphasize
- What to skip or summarize briefly
- Which links/posts deserve more attention
Ask the user about their preferences for these aspects when composing weeknotes.
### Adding New Data Sources
To extend the skill with additional data sources:
1. Add the new Go CLI binary to `bin/{platform}-{arch}/`
2. Update `scripts/fetch-sources.sh` to fetch from the new source
3. Update the SKILL.md Step 3 to instruct Claude to read the new source files
4. Update Step 4 composition guidance to explain how to integrate the new content
## Platform Detection
All scripts automatically detect the current platform and use the appropriate binary:
- **macOS ARM64**: `bin/darwin-arm64/`
- **macOS Intel**: `bin/darwin-amd64/`
- **Linux AMD64**: `bin/linux-amd64/`
Platform detection is handled automatically via `uname` commands. No manual configuration needed.
## Resources
### scripts/
- `setup.sh` - First-time configuration for API credentials
- `fetch-sources.sh` - Fetch data from all configured sources
- `prepare-sources.py` - Verify fetched data and prepare for composition
- `calculate-week.py` - Calculate ISO week number and generate filename for weeknotes
- `download-binaries.sh` - Update Go CLI binaries to latest releases
### bin/
Pre-compiled Go CLI binaries organized by platform:
- `mastodon-to-markdown` - Fetch Mastodon posts as markdown
- `linkding-to-markdown` - Fetch Linkding bookmarks as markdown
Binaries are platform-specific and automatically selected at runtime.
### config/
- `config.json` - User configuration with API credentials and optional settings (created by setup.sh)
- Contains Mastodon server URL and access token
- Contains Linkding URL and API token
- Optionally contains weeknotes_archive URL for style reference
- This file contains sensitive tokens and is secured with 600 permissions
### data/
- `latest/` - Most recently fetched source data
- Other directories for historical or custom fetches
- Contains `mastodon.md` and `linkding.md` after fetching
## Troubleshooting
### Configuration Issues
If setup fails:
- Verify API credentials are correct
- Check that server URLs are accessible
- Ensure tokens have appropriate permissions
### Binary Not Found
If platform detection fails:
```bash
# Check current platform
uname -s # Should show: Darwin or Linux
uname -m # Should show: arm64, x86_64, etc.
# Verify binary exists
ls -la bin/darwin-arm64/ # Or appropriate platform directory
```
### Empty Content
If fetched data is empty:
- Verify the date range includes actual activity
- Check that API credentials have read permissions
- Run fetch scripts with `--verbose` flag for debugging
### Template Errors
If composition fails with template errors:
- Verify `assets/weeknotes-template.md` exists and is readable
- Check that all required placeholders are present
- Ensure no syntax errors in template YAML frontmatter

View File

@@ -0,0 +1,56 @@
#!/usr/bin/env python3
"""
Calculate the current ISO week number and generate the weeknotes filename.
Usage:
./scripts/calculate-week.py [--date YYYY-MM-DD]
If no date is provided, uses today's date.
"""
import argparse
import os
from datetime import datetime
def calculate_week_info(date=None):
"""Calculate week information for the given date (or today)."""
if date is None:
date = datetime.now()
elif isinstance(date, str):
date = datetime.strptime(date, '%Y-%m-%d')
week_number = date.isocalendar()[1]
year = date.year
date_str = date.strftime('%Y-%m-%d')
return {
'date': date_str,
'year': year,
'week': week_number,
'filename': f"content/posts/{year}/{date_str}-w{week_number:02d}.md",
'title': f"Weeknotes: {year} Week {week_number}"
}
def main():
parser = argparse.ArgumentParser(description='Calculate weeknotes week number and filename')
parser.add_argument('--date', type=str, help='Date in YYYY-MM-DD format (default: today)')
parser.add_argument('--json', action='store_true', help='Output as JSON')
args = parser.parse_args()
info = calculate_week_info(args.date)
if args.json:
import json
print(json.dumps(info, indent=2))
else:
print(f"Date: {info['date']}")
print(f"ISO Week: {info['week']}")
print(f"Title: {info['title']}")
print(f"Filename: {info['filename']}")
if __name__ == '__main__':
main()

View File

@@ -0,0 +1,89 @@
#!/bin/bash
set -e
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
SKILL_DIR="$(dirname "$SCRIPT_DIR")"
BIN_DIR="${SKILL_DIR}/bin"
echo "╔════════════════════════════════════════╗"
echo "║ Weeknotes Binary Downloader ║"
echo "╚════════════════════════════════════════╝"
echo ""
# Create bin directory structure
mkdir -p "${BIN_DIR}/darwin-arm64"
mkdir -p "${BIN_DIR}/darwin-amd64"
mkdir -p "${BIN_DIR}/linux-amd64"
# Function to download and extract a GitHub release
download_tool() {
local repo=$1
local tool_name=$2
local platform=$3
local arch=$4
echo "📦 Downloading ${tool_name} for ${platform}-${arch}..."
# Construct the asset name based on the naming convention
local archive_name="${tool_name}-${platform}-${arch}.tar.gz"
local asset_url="https://github.com/${repo}/releases/download/latest/${archive_name}"
local temp_archive="/tmp/${archive_name}"
local target_dir="${BIN_DIR}/${platform}-${arch}"
local target_binary="${target_dir}/${tool_name}"
# Download the archive
echo " Downloading from ${asset_url}..."
if curl -L -f -o "${temp_archive}" "${asset_url}"; then
echo " ✅ Downloaded archive"
# Extract the binary from the archive
echo " Extracting binary..."
tar -xzf "${temp_archive}" -C "${target_dir}" "${tool_name}" 2>/dev/null || {
# If extraction with specific file fails, extract all and find the binary
tar -xzf "${temp_archive}" -C /tmp/
find /tmp -name "${tool_name}" -type f -exec mv {} "${target_binary}" \;
}
# Make binary executable
chmod +x "${target_binary}"
# Cleanup
rm -f "${temp_archive}"
# Verify the binary exists
if [ -f "${target_binary}" ]; then
echo " ✅ Installed to ${target_binary}"
else
echo " ❌ Failed to extract binary"
return 1
fi
else
echo " ❌ Failed to download from ${asset_url}"
return 1
fi
echo ""
}
# Download mastodon-to-markdown
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo "Mastodon to Markdown"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
download_tool "lmorchard/mastodon-to-markdown" "mastodon-to-markdown" "darwin" "arm64"
download_tool "lmorchard/mastodon-to-markdown" "mastodon-to-markdown" "darwin" "amd64"
download_tool "lmorchard/mastodon-to-markdown" "mastodon-to-markdown" "linux" "amd64"
# Download linkding-to-markdown
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo "Linkding to Markdown"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
download_tool "lmorchard/linkding-to-markdown" "linkding-to-markdown" "darwin" "arm64"
download_tool "lmorchard/linkding-to-markdown" "linkding-to-markdown" "darwin" "amd64"
download_tool "lmorchard/linkding-to-markdown" "linkding-to-markdown" "linux" "amd64"
echo "╔════════════════════════════════════════╗"
echo "║ Download Complete! ║"
echo "╚════════════════════════════════════════╝"
echo ""
echo "Binary locations:"
tree "${BIN_DIR}" || ls -R "${BIN_DIR}"

View File

@@ -0,0 +1,178 @@
#!/bin/bash
set -e
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
SKILL_DIR="$(dirname "$SCRIPT_DIR")"
CONFIG_FILE="${SKILL_DIR}/config/config.json"
DATA_DIR="${SKILL_DIR}/data"
# Default to last 7 days (from 7 days ago through today)
# Note: The APIs treat end date as exclusive, so we use tomorrow's date
get_week_dates() {
if [[ "$OSTYPE" == "darwin"* ]]; then
# macOS date command
START_DATE=$(date -v-7d +%Y-%m-%d)
END_DATE=$(date -v+1d +%Y-%m-%d)
else
# Linux date command
START_DATE=$(date -d "7 days ago" +%Y-%m-%d)
END_DATE=$(date -d "tomorrow" +%Y-%m-%d)
fi
}
# Parse command line arguments
START_DATE=""
END_DATE=""
OUTPUT_DIR="${DATA_DIR}/latest"
while [[ $# -gt 0 ]]; do
case $1 in
--start)
START_DATE="$2"
shift 2
;;
--end)
END_DATE="$2"
shift 2
;;
--output-dir)
OUTPUT_DIR="$2"
shift 2
;;
-h|--help)
echo "Usage: fetch-sources.sh [options]"
echo ""
echo "Options:"
echo " --start DATE Start date (YYYY-MM-DD), defaults to Monday of current week"
echo " --end DATE End date (YYYY-MM-DD), defaults to Sunday of current week"
echo " --output-dir DIR Output directory (default: data/latest)"
echo " -h, --help Show this help message"
echo ""
echo "Examples:"
echo " fetch-sources.sh # Fetch this week"
echo " fetch-sources.sh --start 2025-11-01 --end 2025-11-07"
exit 0
;;
*)
echo "Unknown option: $1"
echo "Use --help for usage information"
exit 1
;;
esac
done
# If dates not provided, use current week
if [ -z "$START_DATE" ] || [ -z "$END_DATE" ]; then
get_week_dates
fi
echo "╔════════════════════════════════════════╗"
echo "║ Weeknotes Source Fetcher ║"
echo "╚════════════════════════════════════════╝"
echo ""
echo "Fetching data from ${START_DATE} to ${END_DATE}"
echo ""
# Check if configured
if [ ! -f "${CONFIG_FILE}" ]; then
echo "❌ Not configured yet. Running setup..."
echo ""
"${SCRIPT_DIR}/setup.sh"
echo ""
fi
# Detect platform
OS=$(uname -s | tr '[:upper:]' '[:lower:]')
ARCH=$(uname -m)
case $ARCH in
x86_64) ARCH="amd64" ;;
aarch64|arm64) ARCH="arm64" ;;
esac
BIN_DIR="${SKILL_DIR}/bin/${OS}-${ARCH}"
# Check if binaries exist
if [ ! -f "${BIN_DIR}/mastodon-to-markdown" ] || [ ! -f "${BIN_DIR}/linkding-to-markdown" ]; then
echo "❌ Binaries not found for platform: ${OS}-${ARCH}"
echo " Please run scripts/download-binaries.sh first"
exit 1
fi
# Load config using jq (if not available, use basic parsing)
if command -v jq &> /dev/null; then
MASTODON_SERVER=$(jq -r .mastodon.server "${CONFIG_FILE}")
MASTODON_TOKEN=$(jq -r .mastodon.token "${CONFIG_FILE}")
LINKDING_URL=$(jq -r .linkding.url "${CONFIG_FILE}")
LINKDING_TOKEN=$(jq -r .linkding.token "${CONFIG_FILE}")
else
echo "⚠️ Warning: jq not found. Using basic config parsing."
echo " Install jq for better config handling: brew install jq"
# Basic parsing fallback (not recommended for production)
MASTODON_SERVER=$(grep -o '"server"[[:space:]]*:[[:space:]]*"[^"]*"' "${CONFIG_FILE}" | cut -d'"' -f4 | head -1)
MASTODON_TOKEN=$(grep -o '"token"[[:space:]]*:[[:space:]]*"[^"]*"' "${CONFIG_FILE}" | cut -d'"' -f4 | head -1)
LINKDING_URL=$(grep -o '"url"[[:space:]]*:[[:space:]]*"[^"]*"' "${CONFIG_FILE}" | cut -d'"' -f4 | tail -1)
LINKDING_TOKEN=$(grep -o '"token"[[:space:]]*:[[:space:]]*"[^"]*"' "${CONFIG_FILE}" | cut -d'"' -f4 | tail -1)
fi
# Create output directory
mkdir -p "${OUTPUT_DIR}"
# Create config files for the tools
MASTODON_CONFIG="${OUTPUT_DIR}/mastodon-config.yaml"
LINKDING_CONFIG="${OUTPUT_DIR}/linkding-config.yaml"
cat > "${MASTODON_CONFIG}" <<EOF
mastodon:
server: "${MASTODON_SERVER}"
access_token: "${MASTODON_TOKEN}"
EOF
cat > "${LINKDING_CONFIG}" <<EOF
linkding:
url: "${LINKDING_URL}"
token: "${LINKDING_TOKEN}"
EOF
# Fetch from Mastodon
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo "📱 Fetching Mastodon posts..."
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
"${BIN_DIR}/mastodon-to-markdown" fetch \
--config "${MASTODON_CONFIG}" \
--start "${START_DATE}" \
--end "${END_DATE}" \
--output "${OUTPUT_DIR}/mastodon.md" \
--verbose
echo "✅ Mastodon posts saved to: ${OUTPUT_DIR}/mastodon.md"
echo ""
# Fetch from Linkding
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo "🔖 Fetching Linkding bookmarks..."
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
"${BIN_DIR}/linkding-to-markdown" fetch \
--config "${LINKDING_CONFIG}" \
--since "${START_DATE}" \
--until "${END_DATE}" \
--output "${OUTPUT_DIR}/linkding.md" \
--verbose
echo "✅ Linkding bookmarks saved to: ${OUTPUT_DIR}/linkding.md"
echo ""
# Cleanup config files (they contain secrets)
rm -f "${MASTODON_CONFIG}" "${LINKDING_CONFIG}"
echo "╔════════════════════════════════════════╗"
echo "║ Fetch Complete! ║"
echo "╚════════════════════════════════════════╝"
echo ""
echo "Output directory: ${OUTPUT_DIR}"
echo "Files:"
echo " - mastodon.md"
echo " - linkding.md"
echo ""

View File

@@ -0,0 +1,102 @@
#!/usr/bin/env python3
"""
Prepare fetched source data for composition.
This script reads the fetched markdown files and displays them for Claude
to read and compose into a cohesive weeknotes blog post.
"""
import argparse
import sys
from datetime import datetime, timedelta
from pathlib import Path
def get_current_week_dates():
"""Calculate dates for the last 7 days (7 days ago to today)."""
today = datetime.now()
seven_days_ago = today - timedelta(days=7)
return seven_days_ago.strftime("%Y-%m-%d"), today.strftime("%Y-%m-%d")
def main():
"""Main entry point."""
parser = argparse.ArgumentParser(
description="Prepare fetched source data for weeknotes composition"
)
parser.add_argument(
"--input-dir",
type=Path,
help="Input directory with fetched data (default: data/latest)",
)
parser.add_argument("--start", help="Start date (YYYY-MM-DD)")
parser.add_argument("--end", help="End date (YYYY-MM-DD)")
args = parser.parse_args()
# Set default input directory
if not args.input_dir:
args.input_dir = Path(__file__).parent.parent / "data" / "latest"
print("╔════════════════════════════════════════╗")
print("║ Weeknotes Source Preparation ║")
print("╚════════════════════════════════════════╝")
print()
# Check if input directory exists
if not args.input_dir.exists():
print(f"❌ Input directory not found: {args.input_dir}")
print(" Please run fetch-sources.sh first")
sys.exit(1)
# Determine dates
if not args.start or not args.end:
args.start, args.end = get_current_week_dates()
week_range = f"{args.start} to {args.end}"
print(f"📅 Date range: {week_range}")
print()
# Check for source files
mastodon_file = args.input_dir / "mastodon.md"
linkding_file = args.input_dir / "linkding.md"
has_mastodon = mastodon_file.exists()
has_linkding = linkding_file.exists()
if not has_mastodon and not has_linkding:
print("❌ No source data found!")
print(f" Expected files in: {args.input_dir}")
sys.exit(1)
print("📂 Available source data:")
if has_mastodon:
size = mastodon_file.stat().st_size
print(f" ✅ Mastodon posts: {mastodon_file} ({size:,} bytes)")
else:
print(f" ⚠️ No Mastodon data: {mastodon_file}")
if has_linkding:
size = linkding_file.stat().st_size
print(f" ✅ Linkding bookmarks: {linkding_file} ({size:,} bytes)")
else:
print(f" ⚠️ No Linkding data: {linkding_file}")
print()
print("╔════════════════════════════════════════╗")
print("║ Ready for Composition ║")
print("╚════════════════════════════════════════╝")
print()
print("Source files are ready to be read and composed into a weeknotes post.")
print()
print("Next steps:")
print(f"1. Read: {mastodon_file}")
if has_linkding:
print(f"2. Read: {linkding_file}")
print(f"3. Compose conversational weeknotes for {week_range}")
print("4. Write the composed post with Jekyll frontmatter")
print()
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,226 @@
#!/bin/bash
set -e
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
SKILL_DIR="$(dirname "$SCRIPT_DIR")"
CONFIG_FILE="${SKILL_DIR}/config/config.json"
DATA_DIR="${SKILL_DIR}/data"
echo "╔════════════════════════════════════════╗"
echo "║ Weeknotes Composer Setup ║"
echo "╚════════════════════════════════════════╝"
echo ""
# Check if config already exists
if [ -f "${CONFIG_FILE}" ]; then
echo "⚠️ Configuration already exists."
read -p "Do you want to reconfigure? (y/N): " RECONFIGURE
if [[ ! "$RECONFIGURE" =~ ^[Yy]$ ]]; then
echo "Setup cancelled."
exit 0
fi
echo ""
fi
echo "This setup will configure connections to your data sources."
echo ""
# ============================================================================
# Mastodon Configuration
# ============================================================================
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo "📱 Mastodon Configuration"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo ""
echo "Enter your Mastodon instance details."
echo "Example server: https://mastodon.social"
echo ""
read -p "Mastodon server URL: " MASTODON_SERVER
MASTODON_SERVER="${MASTODON_SERVER%/}"
# Validate URL format
if [[ ! "$MASTODON_SERVER" =~ ^https?:// ]]; then
echo "❌ Error: URL must start with http:// or https://"
exit 1
fi
echo ""
echo "To get your Mastodon access token:"
echo "1. Log into your Mastodon instance"
echo "2. Go to Settings → Development → New Application"
echo "3. Give it a name (e.g., 'Weeknotes Composer')"
echo "4. Grant 'read' permissions"
echo "5. Copy the access token"
echo ""
read -sp "Mastodon access token: " MASTODON_TOKEN
echo ""
# Validate token is not empty
if [ -z "$MASTODON_TOKEN" ]; then
echo "❌ Error: Access token cannot be empty"
exit 1
fi
# Test the Mastodon connection
echo ""
echo "🔍 Testing Mastodon connection..."
HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" \
"${MASTODON_SERVER}/api/v1/accounts/verify_credentials" \
-H "Authorization: Bearer ${MASTODON_TOKEN}")
if [ "$HTTP_CODE" -eq 200 ]; then
echo "✅ Mastodon connection successful!"
else
echo "❌ Mastodon connection failed (HTTP ${HTTP_CODE})"
echo " Please check your server URL and token."
exit 1
fi
echo ""
# ============================================================================
# Linkding Configuration
# ============================================================================
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo "🔖 Linkding Configuration"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo ""
echo "Enter your Linkding instance details."
echo "Example: https://linkding.example.com"
echo ""
read -p "Linkding URL: " LINKDING_URL
LINKDING_URL="${LINKDING_URL%/}"
# Validate URL format
if [[ ! "$LINKDING_URL" =~ ^https?:// ]]; then
echo "❌ Error: URL must start with http:// or https://"
exit 1
fi
echo ""
echo "To get your Linkding API token:"
echo "1. Log into your Linkding instance"
echo "2. Go to Settings → Integrations"
echo "3. Click 'Create Token'"
echo "4. Copy the generated token"
echo ""
read -sp "Linkding API token: " LINKDING_TOKEN
echo ""
# Validate token is not empty
if [ -z "$LINKDING_TOKEN" ]; then
echo "❌ Error: API token cannot be empty"
exit 1
fi
# Test the Linkding connection
echo ""
echo "🔍 Testing Linkding connection..."
HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" \
"${LINKDING_URL}/api/bookmarks/?limit=1" \
-H "Authorization: Token ${LINKDING_TOKEN}")
if [ "$HTTP_CODE" -eq 200 ]; then
echo "✅ Linkding connection successful!"
else
echo "❌ Linkding connection failed (HTTP ${HTTP_CODE})"
echo " Please check your URL and token."
exit 1
fi
echo ""
# ============================================================================
# Style Reference Configuration (Optional)
# ============================================================================
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo "🎨 Style Reference (Optional)"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo ""
echo "Enter a URL to your past weeknotes archive for style reference."
echo "This helps maintain consistent voice and tone in composed posts."
echo "Example: https://blog.example.com/tag/weeknotes/"
echo ""
echo "Leave blank to skip style reference."
echo ""
read -p "Weeknotes archive URL (optional): " WEEKNOTES_ARCHIVE_URL
# Remove trailing slash if present
WEEKNOTES_ARCHIVE_URL="${WEEKNOTES_ARCHIVE_URL%/}"
# Validate URL format if provided
if [ -n "$WEEKNOTES_ARCHIVE_URL" ] && [[ ! "$WEEKNOTES_ARCHIVE_URL" =~ ^https?:// ]]; then
echo "⚠️ Warning: URL should start with http:// or https://"
echo " Proceeding anyway..."
fi
if [ -n "$WEEKNOTES_ARCHIVE_URL" ]; then
echo "✅ Style reference URL configured"
else
echo "⏭️ Skipping style reference"
fi
echo ""
# ============================================================================
# Save Configuration
# ============================================================================
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo "💾 Saving Configuration"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo ""
# Create config directory if it doesn't exist
mkdir -p "$(dirname "${CONFIG_FILE}")"
# Create data directory if it doesn't exist
mkdir -p "${DATA_DIR}"
# Write config file with conditional weeknotes_archive
if [ -n "$WEEKNOTES_ARCHIVE_URL" ]; then
cat > "${CONFIG_FILE}" <<EOF
{
"mastodon": {
"server": "${MASTODON_SERVER}",
"token": "${MASTODON_TOKEN}"
},
"linkding": {
"url": "${LINKDING_URL}",
"token": "${LINKDING_TOKEN}"
},
"weeknotes_archive": "${WEEKNOTES_ARCHIVE_URL}",
"created_at": "$(date -u +"%Y-%m-%dT%H:%M:%SZ")"
}
EOF
else
cat > "${CONFIG_FILE}" <<EOF
{
"mastodon": {
"server": "${MASTODON_SERVER}",
"token": "${MASTODON_TOKEN}"
},
"linkding": {
"url": "${LINKDING_URL}",
"token": "${LINKDING_TOKEN}"
},
"created_at": "$(date -u +"%Y-%m-%dT%H:%M:%SZ")"
}
EOF
fi
# Secure the config file
chmod 600 "${CONFIG_FILE}"
echo "✅ Configuration saved to: ${CONFIG_FILE}"
echo ""
echo "╔════════════════════════════════════════╗"
echo "║ Setup Complete! ║"
echo "╚════════════════════════════════════════╝"
echo ""
echo "You can now use the weeknotes composer."
echo ""