@nulfrog/sandglass
v0.1.3
Published
Real-time terminal monitor for the Sandcastle issue-processing runner.
Maintainers
Readme
⏳ Sandglass
A real-time terminal dashboard for the Sandcastle issue-processing runner
(the .sandcastle/ plan → implement → review → merge loop).
Sandglass watches your .sandcastle/logs/ directory live and shows you which
issue is being worked on, which phase is active, recent activity, and a tail of
the current agent's log — without you having to tail -f anything by hand.
┌ ⏳ Sandglass · brycebooth-at-m42/level-agentic-lesson-lab ● LIVE · iteration 3/10 · 4s ago ┐
Plan → Implement → Review → Merge
┌ Current work · 3 in parallel ─────────────────────────────────────────────────────────────────┐
#14 Add lesson editor autosave
phase: Implement · branch: sandcastle/issue-14
┌ Issues (6) ──────────────────┐┌ Live log · ...issue-14-implementer.log ────────────────────────┐
✓ #19 Merge Convex backend Expanding shell expressions done (0.3s)
✓ #10 Merge Repo scaffolding ...
• #14 Implement Add lesson edi Writing apps/teacher/src/editor/autosave.ts
[s] Start runner [x] Stop runner [o] Open issue [q] QuitInstall / run
Sandglass is a published CLI — no clone required:
# one-off, from inside the repo you want to monitor
npx @nulfrog/sandglass
# or install globally
npm i -g @nulfrog/sandglass
sandglassBy default it auto-detects the repo root (the nearest ancestor of your current
working directory containing .sandcastle/) and monitors
<repo>/.sandcastle/logs. Point it elsewhere with flags or config:
npx @nulfrog/sandglass --repo /path/to/another/repo
npx @nulfrog/sandglass --combined /tmp/sandcastle-real.log # also parse a stdout log
npx @nulfrog/sandglass --github owner/name # issue links
npx @nulfrog/sandglass --helpOn Windows, use a UTF-8-capable terminal (e.g. Windows Terminal) so the emoji/box-drawing render correctly. Requires Node 22+.
Why a TUI (and not Electron / a web app)?
- Cross-platform out of the box — pure Node, runs the same on Windows, macOS and Linux.
- Lives where Sandcastle lives — the terminal. No packaging, no browser.
- Built to extend — UI is Ink (React for the terminal) and every key-driven capability is an entry in a small action registry, so adding new actions is a few lines.
Config
Resolution precedence: CLI flags > env (SANDGLASS_REPO, SANDGLASS_LOGS,
SANDGLASS_COMBINED_LOG) > config file > defaults. Config files are read from
your current working directory (and the install dir): create
sandglass.config.json (or sandglass.local.json, gitignored):
| Key | Meaning |
| ----------------- | -------------------------------------------------------- |
| repoPath | Repo containing .sandcastle/ |
| logsDir | Logs dir (defaults to <repoPath>/.sandcastle/logs) |
| runnerCommand | Command the Start runner action launches |
| combinedLog | Optional combined stdout log to also parse |
| githubRepo | owner/name for issue links/header (auto-detected from the repo's git remote when unset) |
| liveThresholdMs | A log counts as "live" if touched within this window |
| tailLines | Lines of the active log to display |
Keys / actions
| Key | Action |
| --- | --------------------------------------------------- |
| s | Start the runner (runnerCommand) as a child |
| x | Stop a runner Sandglass launched |
| o | Open the active issue on GitHub |
| q | Quit |
How it works
src/monitor/logsScanner.tsdecodes log filenames (<branch>-<phase>[--<issue>].log) into structured records.src/monitor/store.ts(Monitor) watches the logs dir withchokidar, treats the most-recently-modified log as the live phase, and reduces everything into a singleMonitorState. It can also attach to a combined stdout log or to a runner it spawns itself, parsing the runner's high-level lines (=== Iteration … ===, the planner's<id>: <title> → <branch>list,[phase] Started on branch …,Branches merged.,All done.). The runner (.sandcastle/main.mts) works all unblocked issues in parallel each iteration, so the Issues panel holds the concurrent set while "Current work" highlights the most-recently-active one.src/ui/App.tsxrenders that state with Ink and re-renders on every update.
Develop
npm install
npm run build # tsup → dist/cli.js (the published bin)
node dist/cli.js --help
npm run typecheckExtending with new actions
Add to src/actions/builtins.ts:
.register({
key: "p",
label: "Push main",
run: ({ monitor }) => {
// e.g. spawn a git push, then monitor.pushEvent(...)
},
})Each action gets { monitor, exit }. Monitor exposes startRunner,
stopRunner, isRunnerSpawned, and the current state. Add new state by
extending MonitorState in src/types.ts and the reducers in store.ts.
Project layout
src/
cli.tsx entry point (arg parsing, render)
config.ts config + flag/env resolution
types.ts shared data model
monitor/
parser.ts filename + stdout-line parsing
logsScanner.ts scan .sandcastle/logs
tail.ts read file tails / appended bytes
store.ts Monitor: watch + reduce + emit "update"
actions/
registry.ts Action type + registry
builtins.ts default action set
ui/
App.tsx Ink dashboardLicense
MIT
