How to Build Your Own AI Reading System with OpenClaw
Since my last post about using AI to finally read the classics, a few people asked how to set it up themselves. This is that guide.
What You're Building
The system has four parts:
- A book library: plain text files typically from Project Gutenberg
- A state tracker: JSON files that remember where you left off
- A reader script: Node.js script that serves chunks and updates progress
- A cron job: OpenClaw automation that delivers chunks on a schedule
The magic is in part 4. The script is simple. The cron job is what makes it automatic.
Total setup time: about 30 minutes if you're comfortable in a terminal working back and forth with your assistant!
Prerequisites
- OpenClaw installed and running: if not, start here
- A delivery channel: Telegram (easiest) or email via himalaya
If you already have OpenClaw talking to you via Telegram, you're 95% of the way there. Again, you can just work with your assistant by treating it like a competent entity to set this up.
Something like: "I need you to set up a reading system to help me read old and dense books. For example, The Art of War. The system should chunk the book into bite-sized chunks so that I can read it on the go in 2–3 minutes. For each chunk, please offer a modern interpretation in an easy to read format. Each day, I'd like you to send me the chunk, my progress and the interpretation at 8am so I can read it on my commute to work" Then work with your assistant to set it up the best way for you.
Now that said, if you want to learn about the internals of how I got it set up for me, here's the deep dive.
Step 1: Set Up the Directory Structure
Create the folders:
cd ~/.openclaw/workspace
mkdir -p books/library books/reading books/finished books/summaries scripts
Your structure should look like this:
~/.openclaw/workspace/
├── books/
│ ├── library/ # Raw book .txt files
│ ├── reading/ # JSON state files (one per book)
│ ├── finished/ # Completed books (move here when done)
│ └── summaries/ # AI-generated summaries (optional)
├── scripts/
│ └── read-book.js # Core reader script
Step 2: Get Your First Book from Project Gutenberg
Go to gutenberg.org and find a book. I started with The Art of War.
For example:
cd ~/.openclaw/workspace/books/library
curl -o art-of-war.txt "https://www.gutenberg.org/cache/epub/132/pg132.txt"
Use a simple filename — lowercase, hyphens, no spaces. This becomes your bookId.
Step 3: Create the Book State File
Each book needs a JSON file in books/reading/ that tracks your progress. Create one:
cat > ~/.openclaw/workspace/books/reading/art-of-war.json << 'EOF'
{
"bookId": "art-of-war",
"title": "The Art of War",
"author": "Sun Tzu (translated by Lionel Giles)",
"filePath": "books/library/art-of-war.txt",
"startedAt": "2026-02-19T00:00:00.000Z",
"status": "active",
"progress": {
"currentLine": 0,
"totalLines": null,
"lastChunkSent": null,
"chunkSize": 800,
"estimatedPosition": "0%"
},
"deliverySchedule": {
"auto": true,
"cronTime": "30 8 * * 1-5",
"timezone": "America/Toronto"
}
}
EOF
Adjust these fields:
bookId— matches the filename (without the suffix .txt or .json)titleandauthor— whatever you want displayedfilePath— relative path from workspace to the .txt filechunkSize— characters per chunk (800 ≈ 2–3 minutes of reading)timezone— your timezone
The script will fill in totalLines and update currentLine as you read.
Step 4: Install the Reader Script
Create the script at scripts/read-book.js:
(disclaimer, please treat the below as pseudo-code! It may not work as-is!)
cat > ~/.openclaw/workspace/scripts/read-book.js << 'SCRIPT'
#!/usr/bin/env node
/**
* Personal Librarian - Book Reader
* Usage:
* node read-book.js next [bookId] - Get next chunk
* node read-book.js recap [bookId] - Summarize progress
* node read-book.js status - Show all reading progress
* node read-book.js start <bookId> - Start a new book
*/
const fs = require('fs');
const path = require('path');
const WORKSPACE = process.env.OPENCLAW_WORKSPACE || path.join(process.env.HOME, '.openclaw/workspace');
const BOOKS_DIR = path.join(WORKSPACE, 'books');
function loadBookState(bookId) {
const statePath = path.join(BOOKS_DIR, 'reading', `${bookId}.json`);
if (!fs.existsSync(statePath)) {
throw new Error(`Book not found: ${bookId}`);
}
return JSON.parse(fs.readFileSync(statePath, 'utf8'));
}
function saveBookState(bookId, state) {
const statePath = path.join(BOOKS_DIR, 'reading', `${bookId}.json`);
fs.writeFileSync(statePath, JSON.stringify(state, null, 2));
}
function getActiveBook() {
const readingDir = path.join(BOOKS_DIR, 'reading');
if (!fs.existsSync(readingDir)) return null;
const files = fs.readdirSync(readingDir).filter(f => f.endsWith('.json'));
if (files.length === 0) return null;
let newest = null;
let newestTime = 0;
for (const file of files) {
const state = JSON.parse(fs.readFileSync(path.join(readingDir, file), 'utf8'));
if (state.status === 'active') {
const mtime = fs.statSync(path.join(readingDir, file)).mtimeMs;
if (mtime > newestTime) {
newestTime = mtime;
newest = state;
}
}
}
return newest;
}
function readNextChunk(bookId) {
const state = loadBookState(bookId);
const bookPath = path.join(WORKSPACE, state.filePath);
if (!fs.existsSync(bookPath)) {
throw new Error(`Book file not found: ${bookPath}`);
}
const content = fs.readFileSync(bookPath, 'utf8');
const lines = content.split('\n');
// Initialize total lines on first read
if (state.progress.totalLines === null) {
state.progress.totalLines = lines.length;
}
const start = state.progress.currentLine;
const chunkSize = state.progress.chunkSize || 800;
let charCount = 0;
let endLine = start;
let chunk = '';
// Collect lines until we hit the character limit
while (endLine < lines.length && charCount < chunkSize) {
const line = lines[endLine];
chunk += line + '\n';
charCount += line.length + 1;
endLine++;
}
// Skip Gutenberg header on first chunk
if (start === 0) {
const startMarker = '*** START OF THE PROJECT GUTENBERG EBOOK';
const startIdx = chunk.indexOf(startMarker);
if (startIdx !== -1) {
const afterMarker = chunk.substring(startIdx);
const nextPara = afterMarker.indexOf('\n\n') + 2;
chunk = chunk.substring(startIdx + nextPara);
}
}
// Update state
state.progress.currentLine = endLine;
state.progress.lastChunkSent = new Date().toISOString();
state.progress.estimatedPosition = `${Math.round((endLine / lines.length) * 100)}%`;
saveBookState(bookId, state);
return {
chunk: chunk.trim(),
progress: state.progress,
title: state.title,
author: state.author,
finished: endLine >= lines.length
};
}
function getRecap(bookId) {
const state = loadBookState(bookId);
const bookPath = path.join(WORKSPACE, state.filePath);
const content = fs.readFileSync(bookPath, 'utf8');
const lines = content.split('\n');
const readSoFar = lines.slice(0, state.progress.currentLine).join('\n');
return {
title: state.title,
author: state.author,
progress: state.progress,
contentSoFar: readSoFar.substring(0, 2000) + (readSoFar.length > 2000 ? '...' : '')
};
}
function showStatus() {
const readingDir = path.join(BOOKS_DIR, 'reading');
if (!fs.existsSync(readingDir)) return [];
const files = fs.readdirSync(readingDir).filter(f => f.endsWith('.json'));
return files.map(f => {
const state = JSON.parse(fs.readFileSync(path.join(readingDir, f), 'utf8'));
return {
bookId: state.bookId,
title: state.title,
author: state.author,
status: state.status,
progress: state.progress.estimatedPosition,
lastRead: state.progress.lastChunkSent
};
});
}
// CLI handling
const command = process.argv[2];
const arg = process.argv[3];
try {
switch (command) {
case 'next': {
const bookId = arg || getActiveBook()?.bookId;
if (!bookId) {
console.error('No active book. Specify a bookId or mark one as active.');
process.exit(1);
}
console.log(JSON.stringify(readNextChunk(bookId), null, 2));
break;
}
case 'recap': {
const bookId = arg || getActiveBook()?.bookId;
if (!bookId) {
console.error('No active book. Specify a bookId or mark one as active.');
process.exit(1);
}
console.log(JSON.stringify(getRecap(bookId), null, 2));
break;
}
case 'status': {
console.log(JSON.stringify(showStatus(), null, 2));
break;
}
case 'start': {
if (!arg) {
console.error('Usage: read-book.js start <bookId>');
process.exit(1);
}
const state = loadBookState(arg);
console.log(`Started: ${state.title} by ${state.author}`);
break;
}
default:
console.error('Usage: read-book.js {next|recap|status|start} [bookId]');
process.exit(1);
}
} catch (err) {
console.error('Error:', err.message);
process.exit(1);
}
SCRIPT
chmod +x ~/.openclaw/workspace/scripts/read-book.js
Step 5: Test It Manually
Run the script to make sure everything works:
cd ~/.openclaw/workspace
node scripts/read-book.js status
You should see your book listed with 0% progress.
Now get the first chunk:
node scripts/read-book.js next
You'll see JSON output with the first ~800 characters of actual book content (the script automatically skips the Gutenberg header).
Run it again — you'll get the next chunk. The state file updates automatically.
If you want to reset, just set currentLine back to 0 in the JSON file.
Step 6: Set Up Automated Delivery via OpenClaw Cron
This is where the magic happens. You're going to tell OpenClaw to run the script every morning and deliver the chunk to you.
Open the OpenClaw CLI:
openclaw cron create
Configure it with:
- Schedule:
30 8 * * 1-5(8:30 AM, Monday through Friday) - Timezone: Your timezone (e.g., America/Toronto)
- Session: isolated (runs separately from your main chat)
For the message payload, use something like this:
Send the next book chunk for morning reading:
- Run:
node scripts/read-book.js next - Extract the chunk, title, author, and progress from the JSON output
- Send me the chunk with:
- The book title and author
- Progress indicator (e.g., "24% complete")
- The VERBATIM text from the book (this is important — show the actual text first)
- Your interpretation: what it means, why it matters, modern context
- Keep it concise — this should take 2–3 minutes to read
Format it nicely for [Telegram/email].
If you're using Telegram, that's it. The message goes straight to your chat.
Alternative: Telegram Commands
If you don't want scheduled delivery, you can just tell your AI these commands:
/read-next— get the next chunk right now/read-recap— summarize everything you've read so far/read-status— see all books and their progress
Add these to your TOOLS.md so the AI knows what they mean:
### /read-next
Get next chunk of current book.
- **Script:** `scripts/read-book.js next`
- **Output:** ~800 character chunk with progress indicator
- **Format:** Show verbatim text first, then add interpretation
### /read-recap
Summarize what's happened in the book so far.
- **Script:** `scripts/read-book.js recap`
- **Output:** AI-generated summary of content read to date
### /read-status
Show all books and reading progress.
- **Script:** `scripts/read-book.js status`
- **Output:** List of books with progress percentages
Step 7: Configure Email Delivery (Optional)
If you prefer email over Telegram, you'll need himalaya (or gog or another email integration) set up
with your email account.
Update the cron message payload to include email formatting:
Send next book chunk for morning reading:
- Run 'node scripts/read-book.js next' to get raw chunk JSON
- Extract: bookId, title, author, chunk (verbatim text), progress
- Format as HTML email with:
- Subject: 📖 Reading: [Title] — [Today's Date]
- Header with date and "Morning Reading"
- The verbatim book text in a styled block
- Your interpretation in a separate styled block
- Footer with progress (e.g., "Line 1734 of 7138")
- Send via:
himalaya message send -a [account-name]
The AI handles the HTML formatting. You can provide a CSS file in config/email-style.css if you want
consistent styling.
What the Output Looks Like
Every morning, you get something like this:
The verbatim text grounds you in the source. The interpretation connects it to things you actually deal with.
Adding More Books
To add another book:
- Download the .txt from Project Gutenberg to
books/library/ - Create a state file in
books/reading/[bookId].json - Set
"status": "active"if you want it to be the current book
The script picks the most recently modified active book by default. Or specify explicitly:
node scripts/read-book.js next meditations.
When you finish a book, move its state file to books/finished/ and start the next one.
Ideas to Extend This
The reading system is just one application of the pattern. The same approach works for:
- Language learning — daily vocabulary with spaced repetition
- Research papers — drip-feed sections of papers in your field
- Writing practice — daily prompts at your peak creative time
- News digest — curated summaries from sources you trust
The common thread: consistency beats intensity. And a well-configured cron job maintains consistency better than willpower.
That's it. A personal librarian that never forgets to show up.
The individual pieces are simple: a script, a JSON file, a cron job. The compound effect is what matters: showing up every day, automatically, with the next piece of whatever you're learning.