Subzero Labs

Free code · Subzero Labs

AI OS starter: memory, scheduled agents and a home screen (free code)

From the Subzero Labs video "Your AI Has Amnesia. This Free Code Makes It Remember".

Free, no sign-up. Copy it, use it, share it.

Give your AI a memory that lasts. The three parts of an AI OS, in plain Python on your own computer, with a local model (Ollama) by default:

1. Memory per project (aios/memory.py) - your notes in a small SQLite full-text database; python -m aios ask <project> "question" answers only from them and names the note.

2. Agents on a schedule (aios/scheduler.py) - one file per job, one line of config, your computer's own scheduler runs python -m aios run-due every 15 minutes.

3. One home screen (aios/home.py) - what needs you today + every agent's latest result.

Plus 10 example agents for real business jobs (one page each below). The agents read files you export or paste in; they never log in to your accounts and never send anything. The AI only writes drafts; plain code does every number and checks every draft.

Download the AI OS starter (zip) 65 KB

MIT licence. Python 3.10+, no other installs. Unzip, then follow README.md.

Run it

  1. Install Python 3.10+ (Windows: tick 'Add python.exe to PATH') and Ollama (ollama.com/download).
  2. In a terminal: ollama pull qwen2.5:7b-instruct
  3. Unzip, then: cd ai_os_starter and python -m aios check (prints 'model says: ready')
  4. python -m aios ask brand "What do I charge for one Short?" - memory: answers from the sample rate card
  5. python -m aios run all - runs the 10 example agents on the sample data
  6. python -m aios home --open - opens the home screen
  7. python -m unittest discover -s tests -v - the tests (no model needed)
  8. Point config.json at your own exports, then python -m aios schedule for the Task Scheduler / cron line.

The 10 use cases (one page each)

1. Creators: Brand-deal inbox: scam check + rate-card replies - https://subzero.bodymindbeing.me/free/ai-os-brand-deal-inbox/

2. Freelancers and creators: Unpaid-invoice chaser - https://subzero.bodymindbeing.me/free/ai-os-invoice-chaser/

3. Anyone self-employed: Bookkeeping sorter for bank, Stripe and PayPal exports - https://subzero.bodymindbeing.me/free/ai-os-bookkeeping-sorter/

4. Local service businesses: Google review reply drafts for local businesses - https://subzero.bodymindbeing.me/free/ai-os-review-replies/

5. Etsy and Payhip sellers: Etsy + Payhip profit digest (what you kept after fees) - https://subzero.bodymindbeing.me/free/ai-os-etsy-profit-digest/

6. Etsy sellers: Etsy seasonal listing refresher - https://subzero.bodymindbeing.me/free/ai-os-etsy-listing-refresher/

7. Etsy and print-on-demand sellers: Trademark pre-screen for Etsy and print-on-demand - https://subzero.bodymindbeing.me/free/ai-os-trademark-check/

8. Etsy and Payhip sellers: Etsy + Payhip customer message drafts - https://subzero.bodymindbeing.me/free/ai-os-customer-messages/

9. Online stores (Shopify and others): Reorder alerts from Shopify exports - https://subzero.bodymindbeing.me/free/ai-os-reorder-alerts/

10. KDP authors: KDP royalty digest from the report export - https://subzero.bodymindbeing.me/free/ai-os-kdp-royalty-digest/

aios/scheduler.py (part 2: agents on a schedule)

"""Part 2 - agents that run on a schedule.
Each agent is agents/<name>.py with run(cfg, home, settings) -> {"summary", "items", "attention"}.
config.json says when each one runs: "daily 07:00", "weekdays 08:30", "every 30m" or "every 6h".
Your computer's own scheduler (Task Scheduler / cron) calls `python -m aios run-due` every 15 minutes."""
import importlib
import json
import re
import time
import traceback
from datetime import datetime, timedelta

from .config import data_dir


def is_due(when, last, now):
    """when = the schedule text, last = datetime of the last run (or None), now = datetime."""
    m = re.fullmatch(r"every (\d+)\s*(m|h)", when.strip())
    if m:
        step = timedelta(minutes=int(m.group(1)) * (60 if m.group(2) == "h" else 1))
        return last is None or now - last >= step
    m = re.fullmatch(r"(daily|weekdays) (\d{1,2}):(\d{2})", when.strip())
    if not m:
        raise ValueError(f"unknown schedule '{when}'")
    if m.group(1) == "weekdays" and now.weekday() >= 5:
        return False
    slot = now.replace(hour=int(m.group(2)), minute=int(m.group(3)), second=0, microsecond=0)
    return now >= slot and (last is None or last < slot)


def _state(home):
    f = home / "state.json"
    return f, (json.loads(f.read_text(encoding="utf-8")) if f.exists() else {})


def run_agent(cfg, name, now=None):
    """Run one agent now, save its result for the home screen. -> the result."""
    home = data_dir(cfg)
    settings = cfg["agents"][name]
    t0 = time.time()
    try:
        mod = importlib.import_module(f"agents.{name}")
        res = mod.run(cfg, home, settings)
        res.setdefault("title", getattr(mod, "TITLE", name))
    except Exception as e:  # a failed agent shows up on the home screen instead of failing silently
        res = {"title": name, "summary": "", "items": [], "attention": [f"{name} failed: {e}"],
               "error": traceback.format_exc(limit=3)}
    res.update(agent=name, ran=(now or datetime.now()).strftime("%Y-%m-%d %H:%M"), seconds=round(time.time() - t0, 1))
    out = home / "results"
    out.mkdir(exist_ok=True)
    (out / f"{name}.json").write_text(json.dumps(res, indent=1, ensure_ascii=False), encoding="utf-8")
    f, st = _state(home)
    st[name] = res["ran"]
    f.write_text(json.dumps(st, indent=1), encoding="utf-8")
    with open(home / "log.txt", "a", encoding="utf-8") as log:
        log.write(f"{res['ran']} {name}: {len(res.get('items', []))} items, "
                  f"{len(res.get('attention', []))} need attention, {res['seconds']} s\n")
    return res


def run_due(cfg, now=None):
    """Run every agent whose time has come. -> names that ran."""
    now = now or datetime.now()
    _f, st = _state(data_dir(cfg))
    ran = []
    for name, settings in cfg["agents"].items():
        if not settings.get("on", True):
            continue
        last = datetime.strptime(st[name], "%Y-%m-%d %H:%M") if st.get(name) else None
        if is_due(settings["when"], last, now):
            run_agent(cfg, name, now)
            ran.append(name)
    return ran

aios/memory.py (part 1: memory per project)

"""Part 1 - memory per project. Each project is one SQLite file with full-text search.
The AI reads the best-matching notes BEFORE it answers, so it does not start from zero."""
import re
import sqlite3
import time
from contextlib import contextmanager
from html.parser import HTMLParser
from pathlib import Path
from urllib.request import Request, urlopen

from . import llm

STOP = set("a an and are as at be by for from has have how i in is it its of on or that the this to was what "
           "when where which who why will with you your do does did can our we my me".split())


def _db(home, project):
    d = Path(home) / "memory"
    d.mkdir(parents=True, exist_ok=True)
    con = sqlite3.connect(d / f"{re.sub(r'[^a-z0-9_-]+', '-', project.lower())}.db")
    con.execute("CREATE VIRTUAL TABLE IF NOT EXISTS notes USING fts5(title, body, source, added)")
    return con


@contextmanager
def _open(home, project):
    con = _db(home, project)
    try:
        with con:
            yield con
    finally:
        con.close()


def remember(home, project, text, source="", title=""):
    """Save text into a project, split into ~1,000-character chunks. -> number of chunks."""
    parts = [p.strip() for p in re.split(r"\n\s*\n", text) if p.strip()]
    chunks, buf = [], ""
    for p in parts:
        if buf and len(buf) + len(p) > 1000:
            chunks.append(buf)
            buf = ""
        buf = (buf + "\n\n" + p).strip()
    if buf:
        chunks.append(buf)
    with _open(home, project) as con:
        con.execute("DELETE FROM notes WHERE source = ?", (source,))  # saving again replaces the old copy
        for c in chunks:
            con.execute("INSERT INTO notes VALUES (?,?,?,?)", (title, c, source, time.strftime("%Y-%m-%d %H:%M")))
    return len(chunks)


class _Text(HTMLParser):
    def __init__(self):
        super().__init__()
        self.out, self.title, self._skip, self._in_title = [], "", 0, False

    def handle_starttag(self, tag, attrs):
        self._skip += tag in ("script", "style", "nav", "footer")
        self._in_title = tag == "title"

    def handle_endtag(self, tag):
        self._skip -= tag in ("script", "style", "nav", "footer")
        if tag in ("p", "h1", "h2", "h3", "li", "div", "br", "tr"):
            self.out.append("\n\n")
        self._in_title = False

    def handle_data(self, data):
        if self._in_title:
            self.title += data.strip()
        elif not self._skip:
            self.out.append(data)


def read_source(src):
    """A web page, an .html file or a text/markdown file -> (title, plain text)."""
    if re.match(r"https?://", src):
        with urlopen(Request(src, headers={"User-Agent": "ai-os-starter/1.0"}), timeout=30) as r:
            raw = r.read().decode("utf-8", "replace")
    else:
        raw = Path(src).read_text(encoding="utf-8", errors="replace")
    if "<html" in raw.lower() or str(src).lower().endswith((".html", ".htm")):
        p = _Text()
        p.feed(raw)
        text = re.sub(r"[ \t]+", " ", "".join(p.out))
        return p.title or Path(src).name, re.sub(r"\n\s*\n\s*\n+", "\n\n", text).strip()
    return Path(src).stem, raw


def remember_source(home, project, src, label=None):
    """label = how the source is shown later (e.g. a short relative path instead of the full one)."""
    title, text = read_source(src)
    return title, remember(home, project, text, source=label or str(src), title=title)


def search(home, project, question, k=5):
    words = [w for w in re.findall(r"[a-z0-9]+", question.lower()) if w not in STOP and len(w) > 1]
    if not words:
        return []
    q = " OR ".join(f'"{w}"' for w in words)
    with _open(home, project) as con:
        return con.execute("SELECT title, body, source FROM notes WHERE notes MATCH ? ORDER BY bm25(notes) LIMIT ?",
                           (q, k)).fetchall()


def ask(home, project, question, k=5):
    """Answer from the project's own notes only, with the sources it used. -> (answer, sources)."""
    hits = search(home, project, question, k)
    if not hits:
        return "Nothing about that in this project yet.", []
    context = "\n\n".join(f"[{i + 1}] ({t or s})\n{b}" for i, (t, b, s) in enumerate(hits))
    answer = llm.ask(f"Notes from the project '{project}':\n\n{context}\n\nQuestion: {question}\n\n"
                     "Answer ONLY from these notes in 2-4 sentences and cite them like [1]. "
                     "If the notes do not say, answer: The notes don't say.")
    return answer, list(dict.fromkeys(s for _t, _b, s in hits))


def count(home, project):
    with _open(home, project) as con:
        return con.execute("SELECT count(*) FROM notes").fetchone()[0]


def sync_folder(home, project, folder):
    """Keep a folder of notes (Markdown / text, e.g. an Obsidian vault) in a project's memory: only new or edited
    files are read again. -> number of files (re)read."""
    import json
    sig_f = Path(home) / "memory" / f"{project}_files.json"
    sig_f.parent.mkdir(parents=True, exist_ok=True)
    sigs = json.loads(sig_f.read_text()) if sig_f.exists() else {}
    changed = 0
    for f in sorted(Path(folder).glob("*")):
        if f.suffix.lower() not in (".md", ".txt", ".html", ".htm"):
            continue
        sig = f"{f.stat().st_size}-{int(f.stat().st_mtime)}"
        if sigs.get(f.name) != sig:
            title, text = read_source(str(f))
            remember(home, project, text, source=f"{Path(folder).name}/{f.name}", title=f.stem)
            sigs[f.name] = sig
            changed += 1
    sig_f.write_text(json.dumps(sigs, indent=1))
    return changed


def notes_text(home, project):
    """Everything saved in a project (for small projects: a brand's rate card, a shop's FAQ)."""
    with _open(home, project) as con:
        return "\n\n".join(f"[{s}]\n{b}" for b, s in con.execute("SELECT body, source FROM notes"))

aios/llm.py (the model: local Ollama, any OpenAI-compatible server, or the Claude API)

"""One function the whole starter uses: ask(prompt) -> text.

Pick the model with environment variables (keys are never written in a file):
  AIOS_BACKEND = ollama (default) | openai | claude
  AIOS_MODEL   = model name (default per backend, see DEFAULTS)
  OLLAMA_HOST  = Ollama address (default http://127.0.0.1:11434 - your own computer)
  OPENAI_API_KEY (+ OPENAI_BASE_URL)  any OpenAI-compatible server (OpenAI, LM Studio, llama.cpp server, OpenRouter...)
  ANTHROPIC_API_KEY                   the Claude API (needs:  pip install anthropic)

The AI only ever writes TEXT here: drafts, groupings, suggestions. Every number you see on the home screen is
counted by plain Python from your own files, and nothing is ever sent anywhere by this starter.
"""
import json
import os
import re
import urllib.request

DEFAULTS = {"ollama": "qwen2.5:7b-instruct", "openai": "gpt-4o-mini", "claude": "claude-opus-5-5"}
SYSTEM = "You are a helpful assistant for a busy solo business owner. Be brief and concrete."


def _post(url, body, headers):
    req = urllib.request.Request(url, json.dumps(body).encode("utf-8"),
                                 {"Content-Type": "application/json", **headers})
    with urllib.request.urlopen(req, timeout=int(os.environ.get("AIOS_TIMEOUT", "300"))) as r:
        return json.loads(r.read().decode("utf-8"))


def backend():
    return os.environ.get("AIOS_BACKEND", "ollama").strip().lower()


def model():
    return os.environ.get("AIOS_MODEL") or DEFAULTS.get(backend(), "")


def _ollama(system, prompt, m):
    host = os.environ.get("OLLAMA_HOST", "http://127.0.0.1:11434").rstrip("/")
    if not host.startswith("http"):
        host = "http://" + host
    r = _post(host + "/api/chat", {"model": m, "stream": False, "think": False,
                                   "messages": [{"role": "system", "content": system}, {"role": "user", "content": prompt}],
                                   "options": {"temperature": 0.2}}, {})
    return r["message"]["content"]


def _openai(system, prompt, m):
    url = os.environ.get("OPENAI_BASE_URL", "https://api.openai.com/v1").rstrip("/")
    r = _post(url + "/chat/completions", {"model": m, "temperature": 0.2, "messages": [
        {"role": "system", "content": system}, {"role": "user", "content": prompt}]},
        {"Authorization": "Bearer " + os.environ["OPENAI_API_KEY"]})
    return r["choices"][0]["message"]["content"]


def _claude(system, prompt, m):
    try:
        import anthropic  # official SDK, MIT licence - only needed for this backend
    except ImportError:
        raise RuntimeError("the Claude backend needs the official SDK: pip install anthropic")
    client = anthropic.Anthropic()  # reads ANTHROPIC_API_KEY from the environment
    r = client.beta.messages.create(model=m, max_tokens=16000, system=system,
                                    messages=[{"role": "user", "content": prompt}],
                                    output_config={"effort": "low"},
                                    betas=["server-side-fallback-2026-07-01"], fallbacks="default")
    if r.stop_reason == "refusal":
        raise RuntimeError("the model declined this request")
    return "".join(b.text for b in r.content if b.type == "text")


def ask(prompt, system=SYSTEM):
    b, m = backend(), model()
    fn = {"ollama": _ollama, "openai": _openai, "claude": _claude}.get(b)
    if not fn:
        raise ValueError(f"unknown AIOS_BACKEND '{b}' (use ollama, openai or claude)")
    text = fn(system, prompt, m)
    return re.sub(r"<think>.*?</think>", "", text or "", flags=re.S).strip()


def ask_json(prompt, system=SYSTEM):
    """ask() for a JSON answer; one retry if the model wraps it in text."""
    text = ""
    for _ in range(2):
        text = ask(prompt + "\n\nAnswer with JSON only, no other text.", system)
        m = re.search(r"(\{.*\}|\[.*\])", text, re.S)
        if m:
            try:
                return json.loads(m.group(1))
            except json.JSONDecodeError:
                pass
    raise ValueError("the model did not return valid JSON: " + text[:200])

README.md (also in the zip)

# AI OS starter - 10 business agents that run on your own computer

Free and open source (MIT licence). Plain Python, no paid apps, no account logins.

It has three parts:

1. **Memory per project.** Each business or brand gets its own small search database (SQLite). Your rate card, shop policies and payment terms live there. An agent reads the right notes before it writes anything.
2. **Agents on a schedule.** One small Python file per job. Your computer's own scheduler runs them (Task Scheduler on Windows, cron on Mac and Linux).
3. **One home screen.** A local web page with what needs you today, your to-do list and every agent's latest result.

The agents work on **files you export or paste in**: CSV and Excel exports, mailbox (.mbox) exports and pasted text. They never log in to your accounts and never send, post, order or pay for anything. Replies come out as draft text files that you check and send yourself.

**The AI only writes text** (drafts, groupings, suggestions). Plain code counts every number you see, and checks every AI draft before you get it. For example, a price in a reply must really be on your rate card, an invoice reminder must name the right invoice and amount, and a category must be on your list.

## The 10 use cases

| # | Who it's for | Agent | What you give it | What you get |
|---|---|---|---|---|
| 1 | Creators | `brand_deals` | Sponsor emails (.mbox export or pasted .txt files) | Scam warnings (code rules), fit score, reply draft quoting your rate card |
| 2 | Freelancers and creators | `invoice_chaser` | Invoice list (CSV/xlsx) | Overdue days, reminder step 1-4, reminder drafts, total owed |
| 3 | Anyone self-employed | `bookkeeping` | Bank, Stripe and PayPal CSV exports | Every line sorted into your categories, totals, lines to review |
| 4 | Local service businesses | `review_replies` | New Google reviews, pasted into one text file | Checked reply drafts, low ratings flagged |
| 5 | Etsy and Payhip sellers | `profit_digest` | Etsy order + monthly statement CSVs, Payhip sales export | Sales vs last week, best sellers, refunds, what you kept after Etsy fees and ads |
| 6 | Etsy sellers | `listing_refresher` | Etsy listings export + numbers copied from Stats | Out-of-date years, upcoming seasons, new titles and tags within Etsy's limits |
| 7 | Etsy and print-on-demand | `trademark_check` | Etsy listings export + your watchlist | Names to check before a listing gets reported |
| 8 | Etsy and Payhip sellers | `customer_messages` | Customer messages (pasted .txt files or .mbox) | Sorted messages and reply drafts that follow your shop policies |
| 9 | Online stores (Shopify and others) | `reorder_alerts` | Inventory export, orders export, suppliers.csv | Days of stock left, order-by date and quantity, purchase-order drafts |
| 10 | KDP authors | `royalty_digest` | KDP royalties report + Kindle Unlimited (KENP) pages report | Royalties per book and per currency, this period vs the last, drops and refunds flagged |

Every agent comes with made-up sample data in `sample_data/`, so you can run all ten before you touch your own files.

---

## 1. Install (once)

You need **Python 3.10 or newer** and **Ollama** (it runs the AI model on your own computer).

### Windows
1. Install Python from https://www.python.org/downloads/. In the installer, tick **"Add python.exe to PATH"**.
2. Install Ollama from https://ollama.com/download.
3. Open **PowerShell** (Start menu, type PowerShell) and get a model:
   ```
   ollama pull qwen2.5:7b-instruct
   ```
4. Unzip `ai_os_starter.zip` to a folder, for example `C:\ai-os\`, then:
   ```
   cd C:\ai-os\ai_os_starter
   python -m aios check
   ```
   It should print `model says: ready`.

### Mac / Linux
1. Python: Mac has `python3` once you install the Xcode command line tools (`xcode-select --install`) or Python from python.org. On Linux use your package manager (`sudo apt install python3`).
2. Ollama: https://ollama.com/download (Linux: follow the install command on that page).
3. In a terminal:
   ```
   ollama pull qwen2.5:7b-instruct
   cd ~/ai-os/ai_os_starter
   python3 -m aios check
   ```

**Which model?** `qwen2.5:7b-instruct` (about 5 GB) runs on most computers with 16 GB of RAM. Drafts are better with a bigger model. With 32 GB of RAM or a large graphics card, try `ollama pull qwen3.6:35b` (about 23 GB), then set `AIOS_MODEL=qwen3.6:35b` (see section 6).

No other installs are needed. The starter uses only Python's standard library.

## 2. Run the sample data

```
python -m aios run all        (Mac/Linux: python3 -m aios run all)
python -m aios home --open
```

`run all` runs the ten agents and prints what each found. `home --open` opens the home screen (`data/home.html`) in your browser.

- Drafts are in `data/drafts/<agent>/`.
- Tables are in `data/` (`bookkeeping.csv`, `listing_suggestions.csv`).
- A log of every run is in `data/log.txt`.

The sample data is pinned to the date 2026-11-24 (`"today"` in `config.json`), so everyone gets the same numbers. Delete that line when you use your own files.

Run a single agent: `python -m aios run invoice_chaser`. List them all: `python -m aios list`.

## 3. Check it yourself (each use case)

Nothing here asks you to trust the AI. Here is how to check each agent against your own files.

1. **brand_deals**
   - Run it, then open `data/drafts/brand_deals/`.
   - Every price in a draft must be on your rate card. If one is not, the draft starts with `[CHECK BEFORE SENDING: ...]`.
   - The two scam emails in the sample (asks for a fee in USDT, a short link plus "sign in with Google") get no draft at all.
   - The rules are the `FLAGS` list in `agents/brand_deals.py`. You can read and edit them.
2. **invoice_chaser**
   - Open `sample_data/freelance/invoices.csv` in a spreadsheet and work out *today - due date* for each unpaid row.
   - The days late and the total owed in the output must match.
   - Each draft names the invoice number and the exact amount. If the AI's draft misses either one, the code uses a plain template.
3. **bookkeeping**
   - Open `data/bookkeeping.csv` and sort by Category. The "Decided by" column says whether your rule, the AI or nobody decided each line.
   - Add up a category in a spreadsheet. It must match the home screen.
   - Money coming in that the AI filed as an expense is sent to "Needs review".
4. **review_replies**
   - Read each draft in `data/drafts/reviews/`.
   - Each reply uses the reviewer's name and is 20-90 words, with no refund or discount promise in public.
   - Each 1-3 star reply asks the customer to call or email.
   - The checks are in `review_replies.check()`, which tells you exactly what a draft got wrong.
5. **profit_digest**
   - Open the two Etsy statement CSVs and filter the last 30 days.
   - The sum of the Net column must equal "you kept". Sales plus fees, marketing and refunds must equal "kept".
6. **listing_refresher**
   - Check `data/listing_suggestions.csv`: titles of 140 characters at most, 13 tags at most, each tag 20 characters at most.
   - No watchlist name appears in a suggested tag. A title with one is marked in the `check` column.
   - Season dates are calculated in `events()`. For example, Mother's Day (US) is the 2nd Sunday of May.
7. **trademark_check**
   - Every flagged name really appears in the listing text. A name the AI "finds" that is not in your text is dropped.
   - Look each one up yourself at https://tmsearch.uspto.gov (or your country's trademark office).
   - This is a pre-screen, not legal advice.
8. **customer_messages**
   - Compare each draft with `sample_data/shop/store_notes/faq.md`. A price that is not in your policies is flagged at the top of the draft.
9. **reorder_alerts**
   - Pick one SKU and count its units in `orders_export.csv` for the last 28 days.
   - Sold per day = units / 28.
   - Days left = on hand / sold per day.
   - Order-by date = today + days left - lead time - 3 safety days.
   - Quantity = sold per day x (lead time + 30) - on hand, but at least the supplier's minimum.
10. **royalty_digest**
    - Filter the KDP report to the last 30 days and add up the Royalty column per currency. It must match.
    - Currencies are never converted or mixed.

Run the automatic tests too (no model or internet needed; a stand-in AI gives some wrong answers on purpose and the tests check that the code catches them):
```
python -m unittest discover -s tests -v
```

## 4. Use your own files

Edit `config.json` and point each agent at your files. Paths can be relative to `config.json` or full paths.

| Agent | Where to get the file |
|---|---|
| brand_deals | Gmail: Google Takeout > Mail gives an .mbox file (you can pick one label). Thunderbird and Apple Mail can also export .mbox. Or paste each email into a .txt file (`From:` line, `Subject:` line, empty line, text) and point `mailbox` at the folder. |
| invoice_chaser | Your invoicing tool's export, or your own sheet with the columns Invoice, Client, Email, Amount, Currency, Due, Paid. |
| bookkeeping | Your bank's CSV download; Stripe: Payments > Export; PayPal: Activity > Statements > Activity report (CSV). Add your own rules to `my_rules.csv`. |
| review_replies | Copy new reviews from your Google Business Profile into `reviews.txt` (`name \| stars \| date`, the text, then `---`). |
| profit_digest | Etsy: Shop Manager > Settings > Options > Download Data (orders) and Finances > Monthly statements (CSV); Payhip: your sales export. |
| listing_refresher, trademark_check | Etsy: Shop Manager > Settings > Options > Download Data > listings CSV. Copy visits, orders and search terms from Stats into `listing_stats.csv`. |
| customer_messages | Paste each new message into a .txt file in the messages folder (Etsy has no message export). |
| reorder_alerts | Shopify: Products > Inventory > Export, and Orders > Export. Fill in `suppliers.csv` yourself. |
| royalty_digest | KDP Reports: download the royalties report and the KENP report (CSV or .xlsx as they come). |

Menus change sometimes. If a column has another name in your export, the agents try common names (see `col()` calls in each agent). You can also rename the column in your file.

Your business notes are plain Markdown files: rate card, shop FAQ, payment terms, "about us". Each agent copies them into its memory project, and an edited note is read again on the next run. You can also ask a project a question:

```
python -m aios ask store "Can a teacher print the habit tracker for a paid class?"
python -m aios remember store my_new_policy.md
```

## 5. Put it on a schedule

`python -m aios schedule` prints the exact line for your computer.

- **Windows:** it prints a `schtasks` command that runs `python -m aios run-due` every 15 minutes. Paste it into PowerShell once.
- **Mac / Linux:** it prints a crontab line. Run `crontab -e` and paste it in.

Each agent's `"when"` in `config.json` says when it is due. The options are `daily 07:00`, `weekdays 08:00`, `every 30m` and `every 6h`.

## 6. Pick the model (local or cloud)

Set these as environment variables. Keys are never written in a file.

| Setting | Meaning |
|---|---|
| `AIOS_BACKEND` | `ollama` (default, your computer), `openai` (any OpenAI-compatible server: OpenAI, LM Studio, llama.cpp server, OpenRouter...) or `claude` (the Claude API) |
| `AIOS_MODEL` | Model name; the default depends on the backend (see `aios/llm.py`) |
| `OLLAMA_HOST` | Address of Ollama, if it runs on another computer on your network |
| `OPENAI_API_KEY`, `OPENAI_BASE_URL` | For the `openai` backend |
| `ANTHROPIC_API_KEY` | For the `claude` backend (also run `pip install anthropic` once) |

Windows (PowerShell, for this window only): `$env:AIOS_MODEL = "qwen3.6:35b"`
Mac / Linux: `export AIOS_MODEL=qwen3.6:35b`

With the `ollama` backend, your files never leave your computer. With a cloud backend, the text an agent asks about is sent to that provider.

## 7. Add your own agent

Copy any file in `agents/`. An agent is one function: `run(cfg, data_folder, settings)`. It returns `{"summary": "...", "items": [...], "attention": [...]}`. Add it to `config.json` and it shows up on the home screen.

## Safety

- There is no code that sends email, posts, orders or pays. A test checks for it.
- No logins and no scraping. Amazon/KDP, Etsy and Google are only read through the files you export or paste.
- Money numbers come from code, never from the AI.
- Treat drafts as drafts. The bookkeeping and trademark agents help you prepare and are not tax or legal advice.

## Credits (open source used)

| Part | What it does here | Licence |
|---|---|---|
| Python standard library (csv, sqlite3, mailbox, urllib, zipfile, unittest...) | Everything in the starter | PSF License |
| SQLite with FTS5 full-text search | Memory per project (via Python's sqlite3) | Public domain |
| Ollama (github.com/ollama/ollama) | Runs the model locally (default backend) | MIT |
| Qwen2.5-7B-Instruct and Qwen3.6-35B-A3B (Alibaba Qwen team) | Suggested local models (the Subzero Labs demo ran on Qwen3.6-35B-A3B) | Apache 2.0 |
| anthropic (github.com/anthropics/anthropic-sdk-python) | Only for the optional `claude` backend | MIT |

The AI OS starter itself is MIT licensed (see LICENSE). Made by Subzero Labs.

Shared as shown in the video. Replace any <PLACEHOLDERS> with your own values.