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)**, in the **cloud (Claude, API key)**, or
through the **Claude Code CLI on your own machine** (billed to your Claude subscription
rather than API tokens); whichever you pick,
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 (API key) ├── live cart panel
├── Claude Code CLI ◄─http─ MCP proxy │
└── 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.
- **The Claude Code brain is the exception**: the CLI runs its *own* agent loop, so it
bypasses `loop.ts`. It gets the grocery tools back through a loopback MCP proxy
([`claudeCodeBridge.ts`](src/main/agent/claudeCodeBridge.ts)) that forwards every call
into this process — so tool execution, the approval gate and the session log stay
exactly where they are, and no second `trundler-mcp` is spawned.
- **Tools come from MCP**, so every brain inherits 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.
- **Quantity and pricing unit are explicit**: weight-sold items (loose produce, deli,
butchery) are flagged `⚖ priced per kg`, and every card, favourite and cart line
carries a typeable quantity plus a **kg / ea** switch, so "1" is never ambiguous.
- **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).
- **A brain** — any one of these is enough:
- **[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
```
- **An Anthropic API key**, for the cloud Claude backend.
- **The [Claude Code CLI](https://claude.com/claude-code)**, to run Claude on your
existing subscription instead of API tokens:
```
npm i -g @anthropic-ai/claude-code
claude # run once to sign in, then quit
```
No API key is involved — the CLI carries its own credentials. Settings → Models
shows whether the app can find it.
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.
### What the Claude Code brain is allowed to do
It is deliberately boxed in, since Claude Code can otherwise read files and run commands:
- `--tools ""` removes every built-in tool, so there is **no file, shell or web access**.
- `--strict-mcp-config` means it sees *only* the grocery tools — none of your own MCP
servers are loaded. (This matters: without it, a developer's connectors get pulled into
every prompt — measured at 211k tokens of cache on a one-word reply.)
- `--setting-sources ""` and `--disable-slash-commands` keep your Claude Code setup,
skills and `CLAUDE.md` out of a grocery session; it runs in an empty scratch directory.
- Cart changes still raise the app's own **approval modal** — the CLI never reaches the
grocery layer directly, only the proxy.
## 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 / Claude Code) and provider are quick switches in the top bar. The
⚙︎ drawer holds the rest — model, Ollama host, Claude API key, Claude Code CLI command,
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)
claudeCodeAgent.ts Claude Code CLI (spawn, containment, NDJSON stream)
claudeCodeBridge.ts loopback MCP proxy so the CLI reuses our tools/gate
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 Claude Code path has no smoke test — it needs the signed-in CLI, so it can't run in
CI. It was validated by hand end-to-end: tool call → approval → cart write, the
approval-denied path, `--resume` continuity across messages, and cancel taking the CLI
and its MCP proxy down together (no orphans).
- 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
ActivityMaintained
ResponsivenessUnresponsive