trundler
README.md
<p align="center">
<img src=".github/social-preview.png" alt="Trundler — local grocery-shopping companion" width="720" />
</p>
A desktop app that lets you **have a dialogue with an agent** while it shops for you.
The agent's brain runs either **locally (Ollama)** or in the **cloud (Claude)**; either
way, all the actual grocery I/O happens on your own machine and residential connection
via [`trundler-mcp`](https://www.npmjs.com/package/@auckland-ai-collective/trundler-mcp)
([source](https://github.com/auckland-ai-collective/trundler-mcp)), so there's no
bot-detection or hosting problem to solve. It's a normal npm dependency of this app —
you don't install it separately.
> **Cloud brain, local hands.** Bot detection only cares about the requests hitting the
> grocery sites — those are made by trundler-mcp on your machine. The model that decides
> _what_ to fetch can run wherever you like.
## Architecture
```
Electron main (Node) Renderer (React)
├── TrundlerMcp ──stdio──► trundler-mcp ──► grocery sites (residential IP)
├── Agent loop (shared) ├── chat + streamed tokens
│ ├── OllamaBackend (local) ├── product grid (from tool results)
│ └── AnthropicBackend (cloud) ├── live cart panel
└── IPC + approval gate ◄──── approve/deny ──┤ approval modal (cart mutations)
```
- **One shared agent loop** ([`src/main/agent/loop.ts`](src/main/agent/loop.ts)) drives
the conversation. It's parameterised by a `Backend`, so switching Ollama ↔ Claude is a
dropdown, not a rewrite.
- **Tools come from MCP**, so both brains inherit trundler's own product-listing
instructions (letter labels, price-per-unit, cheapest-first).
- **Cart mutations require approval** — `cart_add` / `cart_update` / `cart_remove` pop an
approval modal before they run.
- **Structured tool results become UI**: product results render as a card grid; cart
tools refresh the live cart panel.
- **Auth is first-class**: a status chip shows whether you're signed in to Countdown,
with **Log in** / **Log out** buttons in the top bar (no need to discover it via an error).
## Prerequisites
- **Node ≥ 20** (dev has 22).
- **[Ollama](https://ollama.com)** running locally with a tool-capable model:
```
ollama pull llama3.1:8b # fast, good enough for dev
ollama pull qwen3:14b # stronger tool use, slower
```
That's it — the grocery data layer (`@auckland-ai-collective/trundler-mcp`) is a
dependency and installs automatically. No second repo to clone or build.
## Setup
```bash
npm install # also pulls trundler-mcp (no browser download; that's lazy — see below)
```
### Verify the plumbing (no GUI)
```bash
npm run smoke -- "find jasmine rice on special" # stdio round-trip via the MCP + Ollama
npm run libcheck # in-process (buildServer) round-trip
```
Both resolve the MCP from the installed package, run a real tool call, and print the
result — use them to confirm your model works before launching the app.
### Run the app
```bash
npm run dev # launches the Electron app with HMR
```
Build a distributable:
```bash
npm run build # compile main/preload/renderer into out/
npm run dist # + package a Windows installer (electron-builder)
```
## Signing in (Countdown)
Countdown/Woolworths needs a login for cart and order history. Two ways:
- **In the app:** click **Log in** in the top bar — a real browser window opens; sign in
there and the app captures the session. The **first** sign-in downloads a browser
(~150 MB, one-time) — the app shows a banner while that happens.
- **From the CLI:** `npx trundler login` (the package ships a `trundler` bin).
**Log out** (in the app) clears the stored session and forces re-authentication. New
World and Pak'nSave need no login (anonymous, read-only).
## Configuration
Brain (Ollama/Claude) and provider are quick switches in the top bar. The ⚙︎ drawer
holds the rest — model, Ollama host, Claude API key, MCP server path, and the
cart-approval and debug-logging toggles — and edits there apply only when you press
**Save** (Cancel discards). Defaults can also come from env vars — see
[`.env.example`](.env.example). Settings persist to `config.json` in the app's userData
directory.
**MCP path:** by default the app resolves the server from the installed
`@auckland-ai-collective/trundler-mcp` package automatically — no path to set. Override
with `TRUNDLER_MCP_PATH` (or the ⚙︎ field) only if you're pointing at a local checkout.
### Providers
| Provider | id | Cart | Notes |
| ------------ | ----------- | :--: | ------------------------------------------------- |
| Countdown | `countdown` | ✅ | Needs login — use the **Log in** button. |
| New World | `newworld` | ❌ | Read-only; pick a store first (agent does this). |
| Pak'nSave | `paknsave` | ❌ | Read-only; per-store pricing. |
## Debug logging / telemetry
Trundler writes a structured **JSONL session log** capturing the whole interaction:
the user's prompt, **the model/backend in use**, every MCP tool call and its result,
cart state, approvals, and errors. One file per app run in the app's
`userData/logs/` directory (use the **open logs** button in the debug footer, or
`shell` reveal).
- **Off by default.** Turn it on with the **Debug logging** toggle in ⚙︎ Settings; the
footer line (with an **open logs** button) appears only while it's on.
- **`--debug` or `TRUNDLER_DEBUG=1`** force it on regardless of the setting, so a user can
capture and send you logs without touching Settings:
```
Trundler.exe --debug
```
Each line is one JSON event, e.g.:
```json
{"t":"2026-07-05T…","type":"user-message","text":"add A to cart","backend":"ollama","model":"llama3.1:8b","provider":"countdown"}
{"t":"2026-07-05T…","type":"mcp-call","name":"cart_add","args":{"sku":"601342","quantity":1}}
{"t":"2026-07-05T…","type":"mcp-result","name":"cart_add","ok":true,"provider":"countdown","data":{…}}
{"t":"2026-07-05T…","type":"cart-state","provider":"countdown","itemCount":1,"detailedItems":0,"total":null}
```
The debug log is what let us pin down real bugs — e.g. `detailedItems: 0` with a
non-zero `itemCount` was the fingerprint of a cart-detail mapping bug in trundler-mcp
([#1](https://github.com/auckland-ai-collective/trundler-mcp/issues/1), now fixed).
The cart panel still tolerates sparse data defensively in case a provider returns it.
## Project layout
```
src/
shared/types.ts shared types (domain types re-exported from the MCP package)
main/
index.ts window, IPC, orchestration, approval + auth gate
config.ts config + MCP path resolution (from the installed package)
mcpClient.ts trundler-mcp stdio client (spawn + reconnect)
logger.ts JSONL session logger
agent/
loop.ts shared tool loop
ollamaBackend.ts local model (streaming + tools)
anthropicBackend.ts Claude (streaming SSE + tools)
system.ts system prompt (+ MCP instructions)
preload/index.ts contextBridge API
renderer/ React chat UI
scripts/smoke.mjs headless stdio MCP + Ollama check
scripts/lib-check.mjs headless in-process (buildServer) check
```
## Notes / next steps
- Only the Ollama path is exercised by the smoke test; the Claude path shares the same
loop and is wired but needs an API key to try.
- The app spawns the MCP as a subprocess with Electron's bundled Node
(`ELECTRON_RUN_AS_NODE`). The package also exposes a library entry (`buildServer`), so a
packaged build can instead mount the MCP **in-process** (in-memory transport) to avoid
subprocess/path issues — validated by `npm run libcheck`.
## License
MIT © 2026 Michael Wells <mike@aaic.nz> — see [LICENSE](LICENSE).
An open-source project. Contributions welcome.
TDQS
A3.5/5.0
Scored across 14 tools
Disambiguation5/5
Each tool has a clearly distinct purpose: browsing vs searching, separate cart operations, distinct login and order history tools. No overlap.
Naming Consistency5/5
All tools follow a consistent snake_case verb_noun pattern (e.g., browse_products, cart_add, list_past_orders).
Tool Count5/5
14 tools is well-scoped for a shopping service, covering browsing, cart management, login, and order history without being excessive.
Completeness2/5
Notably missing a place_order or checkout tool, which is critical for the shopping domain. Agents can build a cart but cannot complete a purchase, leaving a dead end.
Maintenance
ActivitySlowing
ResponsivenessUnresponsive