Initial commit
This commit is contained in:
9
skills/weeknotes-blog-post-composer/.gitignore
vendored
Normal file
9
skills/weeknotes-blog-post-composer/.gitignore
vendored
Normal file
@@ -0,0 +1,9 @@
|
||||
# Configuration containing API tokens
|
||||
config/config.json
|
||||
|
||||
# Fetched data
|
||||
data/
|
||||
|
||||
# Temporary files
|
||||
*.tmp
|
||||
*.bak
|
||||
186
skills/weeknotes-blog-post-composer/README.md
Normal file
186
skills/weeknotes-blog-post-composer/README.md
Normal 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.
|
||||
603
skills/weeknotes-blog-post-composer/SKILL.md
Normal file
603
skills/weeknotes-blog-post-composer/SKILL.md
Normal 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., ``)
|
||||
- **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 `` 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>
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
</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):
|
||||
>
|
||||
> 
|
||||
|
||||
**Multiple images (3+) - use image gallery:**
|
||||
> I [shared some photos](https://masto.hackers.town/@user/12348) of my 3D printing projects:
|
||||
>
|
||||
> <image-gallery>
|
||||
>
|
||||
> 
|
||||
>
|
||||
> 
|
||||
>
|
||||
> 
|
||||
>
|
||||
> </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
|
||||
0
skills/weeknotes-blog-post-composer/config/.gitkeep
Normal file
0
skills/weeknotes-blog-post-composer/config/.gitkeep
Normal file
56
skills/weeknotes-blog-post-composer/scripts/calculate-week.py
Executable file
56
skills/weeknotes-blog-post-composer/scripts/calculate-week.py
Executable 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()
|
||||
89
skills/weeknotes-blog-post-composer/scripts/download-binaries.sh
Executable file
89
skills/weeknotes-blog-post-composer/scripts/download-binaries.sh
Executable 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}"
|
||||
178
skills/weeknotes-blog-post-composer/scripts/fetch-sources.sh
Executable file
178
skills/weeknotes-blog-post-composer/scripts/fetch-sources.sh
Executable 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 ""
|
||||
102
skills/weeknotes-blog-post-composer/scripts/prepare-sources.py
Executable file
102
skills/weeknotes-blog-post-composer/scripts/prepare-sources.py
Executable 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()
|
||||
226
skills/weeknotes-blog-post-composer/scripts/setup.sh
Executable file
226
skills/weeknotes-blog-post-composer/scripts/setup.sh
Executable 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 ""
|
||||
Reference in New Issue
Block a user