<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Parth Sharma</title>
    <description>The latest articles on DEV Community by Parth Sharma (@darkvelocity_).</description>
    <link>https://dev.to/darkvelocity_</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4157139%2F76578a27-e00d-4805-8868-23f709638946.png</url>
      <title>DEV Community: Parth Sharma</title>
      <link>https://dev.to/darkvelocity_</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/darkvelocity_"/>
    <language>en</language>
    <item>
      <title>SlothSort: a local AI that tidies my Downloads folder while I sleep</title>
      <dc:creator>Parth Sharma</dc:creator>
      <pubDate>Mon, 19 Oct 2026 21:17:38 +0000</pubDate>
      <link>https://dev.to/darkvelocity_/slothsort-a-local-ai-that-tidies-my-downloads-folder-while-i-sleep-1h0i</link>
      <guid>https://dev.to/darkvelocity_/slothsort-a-local-ai-that-tidies-my-downloads-folder-while-i-sleep-1h0i</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for the &lt;a href="https://dev.to/challenges/hacktoberfest-week3-2026-10-19"&gt;Hacktoberfest Open-Source AI Challenge Week 3: Slow and Steady&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Built
&lt;/h2&gt;

&lt;p&gt;SlothSort is a tiny local agent that cleans up my Downloads folder so I never have to. Every night it looks at each new file, reads a snippet of it, decides what it is, renames it to something sensible (&lt;code&gt;Final_FINAL(2).pdf&lt;/code&gt; becomes &lt;code&gt;electricity-bill-oct-2026.pdf&lt;/code&gt;), files it into a category folder, and writes a short digest I can read over morning chai. Every move is journaled, so one command undoes the whole run.&lt;/p&gt;

&lt;p&gt;It is about 120 lines of Python with no framework and no API keys. The only dependency is Ollama, plus optional &lt;code&gt;pypdf&lt;/code&gt; for reading PDFs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Demo
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Try it yourself in 5 minutes
&lt;/h3&gt;

&lt;p&gt;You don't need an API key, an account or internet (after the one-time model download). This uses a &lt;strong&gt;sandbox folder with fake messy files&lt;/strong&gt;, so your real files are never touched.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Get the code and the model&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/[[REPLACE-YOUR-USERNAME]]/slothsort.git
&lt;span class="nb"&gt;cd &lt;/span&gt;slothsort
ollama pull qwen2.5:7b      &lt;span class="c"&gt;# one-time download, then fully offline&lt;/span&gt;
pip &lt;span class="nb"&gt;install &lt;/span&gt;pypdf           &lt;span class="c"&gt;# optional, only needed to read PDFs&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;2. Create a messy sandbox&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; /tmp/messy &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;cd&lt;/span&gt; /tmp/messy

&lt;span class="nb"&gt;cat&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"Final_FINAL(2).txt"&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="no"&gt;EOF&lt;/span&gt;&lt;span class="sh"&gt;'
Electricity Bill - October 2026
Account No: 48201933
Amount due: Rs 2,340
Due date: 28 Oct 2026
&lt;/span&gt;&lt;span class="no"&gt;EOF

&lt;/span&gt;&lt;span class="nb"&gt;cat&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"notes (3).txt"&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="no"&gt;EOF&lt;/span&gt;&lt;span class="sh"&gt;'
Operating Systems - lecture notes
Round robin vs shortest job first scheduling.
Context switch overhead, time quantum, starvation.
&lt;/span&gt;&lt;span class="no"&gt;EOF

&lt;/span&gt;&lt;span class="nb"&gt;cat&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"script_old.py"&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="no"&gt;EOF&lt;/span&gt;&lt;span class="sh"&gt;'
import requests
def fetch(url):
    return requests.get(url, timeout=10).json()
&lt;/span&gt;&lt;span class="no"&gt;EOF

&lt;/span&gt;&lt;span class="nb"&gt;cat&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"doc1.txt"&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="no"&gt;EOF&lt;/span&gt;&lt;span class="sh"&gt;'
Hi team, the Diwali party is on Friday at 6 PM.
Please bring a snack and confirm your headcount.
&lt;/span&gt;&lt;span class="no"&gt;EOF

&lt;/span&gt;&lt;span class="nb"&gt;cat&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"download.csv"&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="no"&gt;EOF&lt;/span&gt;&lt;span class="sh"&gt;'
date,item,amount
2026-10-01,groceries,1200
2026-10-03,petrol,800
&lt;/span&gt;&lt;span class="no"&gt;EOF

&lt;/span&gt;&lt;span class="nb"&gt;cd&lt;/span&gt; -
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;3. Dry run (nothing moves yet)&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python3 sloth.py run &lt;span class="nt"&gt;--src&lt;/span&gt; /tmp/messy &lt;span class="nt"&gt;--dest&lt;/span&gt; /tmp/sorted &lt;span class="nt"&gt;--min-age&lt;/span&gt; 0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;--min-age 0&lt;/code&gt; is only needed here because the files were just created. Normally SlothSort skips anything edited in the last 2 minutes, in case it is still downloading.&lt;/p&gt;

&lt;p&gt;You should see lines like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;WOULD MOVE: Final_FINAL(2).txt -&amp;gt; Receipts/electricity-bill-oct-2026.txt
WOULD MOVE: notes (3).txt -&amp;gt; Study/os-scheduling-notes.txt
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Exact names will vary a little, since it is a small local model.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Do it for real&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python3 sloth.py run &lt;span class="nt"&gt;--src&lt;/span&gt; /tmp/messy &lt;span class="nt"&gt;--dest&lt;/span&gt; /tmp/sorted &lt;span class="nt"&gt;--min-age&lt;/span&gt; 0 &lt;span class="nt"&gt;--apply&lt;/span&gt;
&lt;span class="nb"&gt;ls&lt;/span&gt; &lt;span class="nt"&gt;-R&lt;/span&gt; /tmp/sorted
&lt;span class="nb"&gt;cat&lt;/span&gt; ~/.slothsort/digests/&lt;span class="k"&gt;*&lt;/span&gt;.md     &lt;span class="c"&gt;# your morning digest&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;5. Changed your mind? Undo it&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python3 sloth.py undo
&lt;span class="nb"&gt;ls&lt;/span&gt; /tmp/messy                     &lt;span class="c"&gt;# everything is back, original names&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;6. Go full sloth (optional)&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Point it at your real folder and let cron do it every night at 2 AM:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;crontab &lt;span class="nt"&gt;-e&lt;/span&gt;
&lt;span class="c"&gt;# add this line:&lt;/span&gt;
0 2 &lt;span class="k"&gt;*&lt;/span&gt; &lt;span class="k"&gt;*&lt;/span&gt; &lt;span class="k"&gt;*&lt;/span&gt; /usr/bin/python3 ~/slothsort/sloth.py run &lt;span class="nt"&gt;--apply&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Want to experiment?&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Try another open model: &lt;code&gt;ollama pull llama3.2&lt;/code&gt;, then add &lt;code&gt;--model llama3.2&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Edit &lt;code&gt;CATEGORIES&lt;/code&gt; in &lt;code&gt;sloth.py&lt;/code&gt; to match your own life (&lt;code&gt;Invoices&lt;/code&gt;, &lt;code&gt;Memes&lt;/code&gt;, &lt;code&gt;Thesis&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Turn off your Wi-Fi before step 3. It still works, because the model runs on your machine.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Code
&lt;/h2&gt;

&lt;p&gt;`#!/usr/bin/env python3&lt;br&gt;
"""SlothSort: a local, open-weight file tidier. Nothing leaves your machine."""&lt;br&gt;
import argparse, json, re, shutil, time, urllib.request&lt;br&gt;
from datetime import datetime&lt;br&gt;
from pathlib import Path&lt;/p&gt;

&lt;p&gt;OLLAMA = "&lt;a href="http://localhost:11434/api/chat" rel="noopener noreferrer"&gt;http://localhost:11434/api/chat&lt;/a&gt;"&lt;br&gt;
MODEL = "qwen2.5:7b"&lt;br&gt;
CATEGORIES = ["Documents", "Study", "Receipts", "Images", "Code", "Media", "Archives", "Misc"]&lt;br&gt;
TEXT_EXT = {".txt", ".md", ".csv", ".json", ".py", ".js", ".html", ".log"}&lt;br&gt;
SKIP_EXT = {".crdownload", ".part", ".tmp", ".download"}&lt;br&gt;
HOME = Path.home() / ".slothsort"&lt;br&gt;
JOURNAL = HOME / "journal.jsonl"&lt;br&gt;
DIGESTS = HOME / "digests"&lt;/p&gt;

&lt;p&gt;SYSTEM = (&lt;br&gt;
    "You tidy files. Given a filename and a text snippet, reply ONLY with JSON: "&lt;br&gt;
    '{"category": one of %s, "new_name": "short-descriptive-kebab-case-name-without-extension", '&lt;br&gt;
    '"summary": "max 12 words"}. If unsure, use category "Misc" and keep the original name.' % CATEGORIES&lt;br&gt;
)&lt;/p&gt;

&lt;p&gt;def snippet(path, limit=1500):&lt;br&gt;
    ext = path.suffix.lower()&lt;br&gt;
    try:&lt;br&gt;
        if ext in TEXT_EXT:&lt;br&gt;
            return path.read_text(errors="ignore")[:limit]&lt;br&gt;
        if ext == ".pdf":&lt;br&gt;
            from pypdf import PdfReader  # optional: pip install pypdf&lt;br&gt;
            return (PdfReader(str(path)).pages[0].extract_text() or "")[:limit]&lt;br&gt;
    except Exception:&lt;br&gt;
        pass&lt;br&gt;
    return ""&lt;/p&gt;

&lt;p&gt;def ask(name, text, model):&lt;br&gt;
    body = json.dumps({&lt;br&gt;
        "model": model, "stream": False, "format": "json",&lt;br&gt;
        "options": {"temperature": 0},&lt;br&gt;
        "messages": [&lt;br&gt;
            {"role": "system", "content": SYSTEM},&lt;br&gt;
            {"role": "user", "content": f"Filename: {name}\nSnippet:\n{text or '(no text available)'}"},&lt;br&gt;
        ],&lt;br&gt;
    }).encode()&lt;br&gt;
    req = urllib.request.Request(OLLAMA, body, {"Content-Type": "application/json"})&lt;br&gt;
    with urllib.request.urlopen(req, timeout=180) as r:&lt;br&gt;
        return json.loads(json.loads(r.read())["message"]["content"])&lt;/p&gt;

&lt;p&gt;def clean(s, fallback):&lt;br&gt;
    s = re.sub(r"[^\w- ]", "", str(s)).strip().replace(" ", "-").lower()[:60]&lt;br&gt;
    return s or fallback&lt;/p&gt;

&lt;p&gt;def unique(path):&lt;br&gt;
    if not path.exists():&lt;br&gt;
        return path&lt;br&gt;
    i = 2&lt;br&gt;
    while path.with_name(f"{path.stem}-{i}{path.suffix}").exists():&lt;br&gt;
        i += 1&lt;br&gt;
    return path.with_name(f"{path.stem}-{i}{path.suffix}")&lt;/p&gt;

&lt;p&gt;def run(args):&lt;br&gt;
    src, dest = Path(args.src).expanduser(), Path(args.dest).expanduser()&lt;br&gt;
    HOME.mkdir(exist_ok=True); DIGESTS.mkdir(exist_ok=True)&lt;br&gt;
    run_id = datetime.now().strftime("%Y%m%d-%H%M%S")&lt;br&gt;
    moved = []&lt;br&gt;
    for f in sorted(src.iterdir()):&lt;br&gt;
        if not f.is_file() or f.name.startswith(".") or f.suffix.lower() in SKIP_EXT:&lt;br&gt;
            continue&lt;br&gt;
        if time.time() - f.stat().st_mtime &amp;lt; args.min_age * 60:&lt;br&gt;
            continue  # probably still downloading&lt;br&gt;
        try:&lt;br&gt;
            r = ask(f.name, snippet(f), args.model)&lt;br&gt;
        except Exception as e:&lt;br&gt;
            print(f"! skipped {f.name}: {e}")&lt;br&gt;
            continue&lt;br&gt;
        cat = r.get("category") if r.get("category") in CATEGORIES else "Misc"&lt;br&gt;
        new = unique(dest / cat / (clean(r.get("new_name"), f.stem) + f.suffix.lower()))&lt;br&gt;
        summary = str(r.get("summary", ""))[:100]&lt;br&gt;
        print(f"{'MOVE' if args.apply else 'WOULD MOVE'}: {f.name} -&amp;gt; {cat}/{new.name}")&lt;br&gt;
        if args.apply:&lt;br&gt;
            new.parent.mkdir(parents=True, exist_ok=True)&lt;br&gt;
            shutil.move(str(f), str(new))&lt;br&gt;
            with JOURNAL.open("a") as j:&lt;br&gt;
                j.write(json.dumps({"run": run_id, "from": str(f), "to": str(new)}) + "\n")&lt;br&gt;
        moved.append((f.name, cat, new.name, summary))&lt;br&gt;
    lines = [f"# SlothSort digest {run_id}", f"Tidied &lt;strong&gt;{len(moved)}&lt;/strong&gt; files while you slept.\n"]&lt;br&gt;
    lines += [f"- &lt;code&gt;{a}&lt;/code&gt; -&amp;gt; &lt;strong&gt;{b}/{c}&lt;/strong&gt;: {d}" for a, b, c, d in moved]&lt;br&gt;
    digest = DIGESTS / f"{run_id}.md"&lt;br&gt;
    digest.write_text("\n".join(lines) + "\n")&lt;br&gt;
    print(f"\nDigest: {digest}")&lt;/p&gt;

&lt;p&gt;def undo(_):&lt;br&gt;
    if not JOURNAL.exists():&lt;br&gt;
        return print("Nothing to undo.")&lt;br&gt;
    rows = [json.loads(l) for l in JOURNAL.read_text().splitlines() if l.strip()]&lt;br&gt;
    last = rows[-1]["run"]&lt;br&gt;
    keep = [r for r in rows if r["run"] != last]&lt;br&gt;
    for r in reversed([r for r in rows if r["run"] == last]):&lt;br&gt;
        if Path(r["to"]).exists():&lt;br&gt;
            shutil.move(r["to"], unique(Path(r["from"])))&lt;br&gt;
            print(f"restored {r['from']}")&lt;br&gt;
    JOURNAL.write_text("".join(json.dumps(r) + "\n" for r in keep))&lt;/p&gt;

&lt;p&gt;if &lt;strong&gt;name&lt;/strong&gt; == "&lt;strong&gt;main&lt;/strong&gt;":&lt;br&gt;
    p = argparse.ArgumentParser(description="Local AI file tidier")&lt;br&gt;
    sub = p.add_subparsers(required=True)&lt;br&gt;
    r = sub.add_parser("run")&lt;br&gt;
    r.add_argument("--src", default="~/Downloads")&lt;br&gt;
    r.add_argument("--dest", default="~/Sorted")&lt;br&gt;
    r.add_argument("--model", default=MODEL)&lt;br&gt;
    r.add_argument("--min-age", type=int, default=2, help="minutes since last edit")&lt;br&gt;
    r.add_argument("--apply", action="store_true", help="actually move files (default is dry-run)")&lt;br&gt;
    r.set_defaults(fn=run)&lt;br&gt;
    u = sub.add_parser("undo"); u.set_defaults(fn=undo)&lt;br&gt;
    a = p.parse_args(); a.fn(a)`&lt;/p&gt;

&lt;h2&gt;
  
  
  How I Built It
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Scan. The script lists files in &lt;code&gt;~/Downloads&lt;/code&gt;, skipping hidden files, partial downloads (&lt;code&gt;.crdownload&lt;/code&gt;, &lt;code&gt;.part&lt;/code&gt;) and anything edited in the last 2 minutes.&lt;/li&gt;
&lt;li&gt;Peek. For text and PDF files it grabs the first ~1,500 characters. For other files it just uses the filename.&lt;/li&gt;
&lt;li&gt;Ask the local model. It sends the filename and snippet to Ollama's &lt;code&gt;/api/chat&lt;/code&gt; with &lt;code&gt;format: "json"&lt;/code&gt; and &lt;code&gt;temperature: 0&lt;/code&gt;. The model returns &lt;code&gt;{category, new_name, summary}&lt;/code&gt;. Constraining the categories to a fixed list keeps the output predictable.&lt;/li&gt;
&lt;li&gt;Move safely. Names are sanitised, collisions get &lt;code&gt;-2&lt;/code&gt;, &lt;code&gt;-3&lt;/code&gt; suffixes, and the default mode is a &lt;strong&gt;dry run&lt;/strong&gt; that only prints what would happen. &lt;code&gt;--apply&lt;/code&gt; does the real move and writes each move to a journal.&lt;/li&gt;
&lt;li&gt;Digest. A markdown summary of the night's work is saved to &lt;code&gt;~/.slothsort/digests/&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Undo. &lt;code&gt;sloth.py undo&lt;/code&gt; reads the journal and puts everything from the last run back.&lt;/li&gt;
&lt;li&gt;Schedule. One cron line makes it fully hands-off.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The hardest part was getting the small model to reliably return valid JSON with a category from my list. JSON mode, a strict system prompt and falling back to &lt;code&gt;Misc&lt;/code&gt; on anything unexpected fixed it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Does Open Innovation Matter?
&lt;/h2&gt;

&lt;p&gt;A file tidier has to read my files: bills, bank statements, ID scans, college documents. I did not want to upload any of that to a server I don't control just to avoid dragging files into folders.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Privacy: an open-weight model running on my own machine means the contents of my files never leave my laptop.&lt;/li&gt;
&lt;li&gt;Offline: it works with no internet, including during a power or connectivity cut, which is common enough where I live.&lt;/li&gt;
&lt;li&gt;Cost: it runs on hardware I already own. A closed API would charge per token every night, forever, for a task I wanted to stop thinking about.&lt;/li&gt;
&lt;li&gt;Control: the model is one config value. I can swap Qwen for Llama or Gemma with &lt;code&gt;--model&lt;/code&gt;, or fine-tune on my own naming style, without waiting for a vendor.&lt;/li&gt;
&lt;li&gt;Verifiable: the code is MIT, the model weights are open, and anyone can read exactly what touches their files.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A closed model could probably sort slightly better, but for a task this small, a local 7B model is good enough, and "good enough, private and free" beats "perfect, remote and metered" here.&lt;/p&gt;

&lt;h2&gt;
  
  
  My Agent Session
&lt;/h2&gt;

&lt;p&gt;I used Claude for the code and at some other areas which needed improvement.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Lazy Payoff
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Before: 140 unsorted files and a search-by-panic habit.&lt;/li&gt;
&lt;li&gt;After: zero minutes spent sorting, with one cron line and a digest I skim in 10 seconds.&lt;/li&gt;
&lt;li&gt;Setup took about 7 minutes, and it has now saved me from the "where did I download that?" hunt every week.&lt;/li&gt;
&lt;li&gt;Because it defaults to dry-run and keeps an undo journal, I trust it enough to ignore it, which is the whole point of a sloth.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Prize Categories
&lt;/h2&gt;

&lt;p&gt;Best Open-Source AI Project (overall): SlothSort is a new project built around an open-weight model running locally through Ollama, published in a public MIT-licensed repo with the model and its license named in the README.&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>hf26challenge</category>
    </item>
    <item>
      <title>Local Receipt Sorter: a bookkeeping assistant whose data never leaves your laptop</title>
      <dc:creator>Parth Sharma</dc:creator>
      <pubDate>Sun, 18 Oct 2026 18:28:43 +0000</pubDate>
      <link>https://dev.to/darkvelocity_/local-receipt-sorter-a-bookkeeping-assistant-whose-data-never-leaves-your-laptop-2k3</link>
      <guid>https://dev.to/darkvelocity_/local-receipt-sorter-a-bookkeeping-assistant-whose-data-never-leaves-your-laptop-2k3</guid>
      <description>&lt;p&gt;Liquid syntax error: Variable '{{&lt;br&gt;
  "vendor": "business name",&lt;br&gt;
  "date": "YYYY-MM-DD",&lt;br&gt;
  "subtotal": number or null,&lt;br&gt;
  "tax": number or null,&lt;br&gt;
  "total": number,&lt;br&gt;
  "currency": "ISO code such as USD, INR, EUR",&lt;br&gt;
  "invoice_number": "string or null",&lt;br&gt;
  "category": one of {categories}' was not properly terminated with regexp: /\}\}/&lt;/p&gt;
</description>
      <category>devchallenge</category>
      <category>hf26challenge</category>
    </item>
  </channel>
</rss>
