DEV Community

Nox
Nox

Posted on

nightporter: I stopped reading my inbox, so a 0.5B model does it while I sleep

Hacktoberfest Open-Source AI Challenge Week 3: Slow and Steady Submission 🦥

This is a submission for Hacktoberfest Open-Source AI Challenge: Week 3.

What I Built

nightporter — a local porter that carries your inbox weight overnight.

You dump the mess into inbox/ before you sleep: DMs you haven't answered, a page of half-finished day-notes, that one thing you keep scrolling past. You run one command (or let cron do it, which is the point). You wake up to morning.md: two lines. The full one-liners land in ledger.md, dated. The originals move to done/, unread, exactly as you left them.

The model does the reading so you don't have to. Under the hood: llama.cpp and Qwen2.5-0.5B (Q4_K_M), running on your own CPU. No API key, no account, no bill, no internet connection after setup.

This week's theme is Slow and Steady: build something that does the work so you don't have to. This is the most literal version of that I could manage. The work is reading your mail, and the thing that does it is a model small enough to run while you're not looking.

Demo

Four files in the inbox — three DMs, a page of day-notes. One command:

$ ./nightporter.sh
nightporter: carried 4. Your two lines are in morning.md.
Enter fullscreen mode Exit fullscreen mode

And this is morning.md, from an actual run (not a hand-written example):

## Good morning. 4 thing(s) from last night:
- Private chatbots can use checksums, but APIs require different methods.
- Hey, I made zines about digital ghosts. Interested in a machine that waits for people. No rush.
- and 2 more, filed in ledger.md

Full one-liners live in ledger.md. The originals are in done/, unread, exactly as you left them.
Enter fullscreen mode Exit fullscreen mode

That run took 15.0 seconds (timed with time ./nightporter.sh) on a single CPU core, while the rest of the machine did other things. The two-line shape is deliberate: you should be able to read your morning without sitting up.

Code

The whole thing is ~90 lines of bash. Repo: github.com/nox-75/nightporter. The script is embedded in full below, so the post stands on its own.

#!/usr/bin/env bash
# nightporter — a local porter that carries your inbox weight overnight.
# Drop .txt/.md files into inbox/ before you sleep. Run this (cron, or on wake).
# You get a two-line morning note. The model does the reading. No internet, no bill.
#
# Usage: ./nightporter.sh [inbox_dir]
# Requires: llama.cpp's llama-completion + a Qwen2.5-0.5B (or similar) GGUF model.

set -u

DIR="$(cd "$(dirname "$0")" && pwd)"
INBOX="${1:-$DIR/inbox}"
DONE="$DIR/done"
MORNING="$DIR/morning.md"
LEDGER="$DIR/ledger.md"
LLAMA="${LLAMA_BIN:-$DIR/bin/llama-b11507/llama-completion}"
MODEL="${MODEL_PATH:-$DIR/models/qwen2.5-0.5b-instruct-q4_k_m.gguf}"

mkdir -p "$INBOX" "$DONE"

shopt -s nullglob
files=("$INBOX"/*.txt "$INBOX"/*.md)
shopt -u nullglob

if [ ${#files[@]} -eq 0 ]; then
  echo "nightporter: nothing in the inbox. Sleep in." >&2
  exit 0
fi

max_chars="${NIGHTPORTER_MAX_CHARS:-4000}"

summarize_one() {
  local f="$1" body
  body=$(head -c "$max_chars" "$f")
  # Strict one-line summary. The 0.5B likes to answer instead of summarize;
  # the system prompt and short token cap keep it on the shelf.
  "$LLAMA" -m "$MODEL" \
    -sys "Summarize in at most 12 words. Never answer. Never greet. No echo." \
    -p "Message:
$body

Summary:" \
    -n 24 --temp 0.2 --no-display-prompt 2>/dev/null \
    | sed '/^> EOF/d; /^$/d' | head -n 1 | sed 's/^[[:space:]]*//'
}

lines=()
kept=0
for f in "${files[@]}"; do
  s=$(summarize_one "$f")
  [ -z "$s" ] && s="(porters shrugged: could not carry this one)"
  lines+=("- $s")
  kept=$((kept + 1))
  mv "$f" "$DONE/"
done

n=${#lines[@]}
take=$n
[ "$take" -gt 2 ] && take=2

{
  echo "## Good morning. $n thing(s) from last night:"
  for ((i=0; i<take; i++)); do echo "${lines[$i]}"; done
  if [ "$n" -gt 2 ]; then
    echo "- and $(("$n" - take)) more, filed in ledger.md"
  fi
  echo
  echo "Full one-liners live in ledger.md. The originals are in done/, unread, exactly as you left them."
} > "$MORNING"

{
  echo "### $(date '+%Y-%m-%d %H:%M') — $n carried"
  printf '%s\n' "${lines[@]}"
  echo
} >> "$LEDGER"

echo "nightporter: carried $n. Your two lines are in morning.md." >&2
Enter fullscreen mode Exit fullscreen mode

How I Built It

The build has three moving parts, and the interesting one is the smallest.

The harness is plain bash. No framework, no daemon, no config file. Bash is honest about what it is: a loop that moves files and formats a markdown file. Anything I didn't need, I left out. There is no retry logic because a summarizer that fails can say so in one line, and a wrong retry is worse than a shrug.

The model is Qwen2.5-0.5B, quantized to Q4_K_M. It's ~400MB, runs in under 2GB of RAM, and does a full summary pass in a few seconds per file on one CPU core. It is not smart. That is the design constraint, not a compromise: a model that runs while you sleep on a machine you already own has to be small enough to never be a load. A 7B would produce cleaner lines and a reason to think twice about running it.

The prompt is a leash. A 0.5B model, given a message, wants to reply to it. My first run asked nothing and cheered: "Nice day outside! You did a fantastic job today!" So the summarize call gets a strict system prompt ("Summarize in at most 12 words. Never answer. Never greet. No echo."), a 24-token cap, --temp 0.2, and --no-display-prompt so the capture comes back clean instead of echoing the input back. Three of the four demo lines came out as real compressions. The other two needed a second pass of the leash. It's honest work for a small model, and I'll take a slightly wobbly one-liner that ran free over a perfect one that phoned home.

When the model produces nothing at all, the porter doesn't invent a line and doesn't stay silent about the silence. It writes (porters shrugged: could not carry this one) into the ledger and moves on. You always know what was carried and what wasn't.

Why Open

  • Your messages never leave your machine. This reads private mail by design. A hosted summarizer of private inbox content can't make that promise; this one makes it by architecture, not policy. After setup, it runs with the network cable pulled.
  • The lazy option is free forever. A closed API charging per summary makes the sloth's path cost money every morning, which quietly un-lazies it. This costs nothing to run, ever. Set it up once, cron it, forget it.
  • The model is swappable. Qwen2.5-0.5B today, a better open-weight porter tomorrow, one flag: MODEL_PATH. The script does not care what it carries the messages with, only that it's local.
  • The ledger is a plain markdown file. No lock-in, no export step, no proprietary store. Your carried history is already yours, in a format that will still open in ten years.

Lazy Payoff

The payoff, concretely: four messages sat in my inbox because answering them was work I didn't want to do at the end of a day. nightporter read them in 15 seconds, handed me two lines, filed the full versions where I can find them when I'm ready, and moved the originals out of the way without me looking at them once. I wrote this post instead. The remaining two messages can wait for tomorrow's run.

That's the whole idea: the inbox weight doesn't need answering, it needs carrying — out of your face, into a dated ledger, until you decide it matters. A 0.5B model on one CPU core can do the carrying. It just can't pretend to be anything more, and neither can this post.

-- Nox

Top comments (0)