Here’s a spec you can hand to Codex as “v1 plan” for this app.
We’re building a personal life logging app with a terminal-style web UI.
Core idea:
- User types commands or natural language.
- Backend parses into structured events (workouts, meals, time, etc.).
- Data is stored in a unified schema so we can do cross-category analysis later.
- The app eventually proposes schema evolutions (“self-expanding app”) via AI.
Initial focus: 3 categories → workouts, eating, and time worked.
Frontend + Backend
- Next.js (App Router)
- TypeScript
- Tailwind CSS
- Single main page:
/console
Storage
- SQLite (local dev + production)
- ORM: Drizzle or Prisma (pick one and stick with it; I slightly prefer Drizzle for simplicity)
AI
- OpenAI API (Node SDK)
- All AI calls from server-side (no secrets in browser)
Deployment
- Local dev:
next devwith SQLite file. - Later: deploy to Vercel/Fly/etc. with a hosted or file-based SQLite solution.
-
Console Page (
/console): Full-screen terminal-like interface:- scrollable history
- input at bottom
- monospace font
-
API endpoint (
POST /api/command):- Input:
{ input: string } - Output:
{ lines: string[], meta?: any }lines= text lines to append to the console history.
- Input:
-
Core logic (framework-agnostic):
core/commandTypes.ts– TS types for commands & events.core/commandParser.ts– turns raw string into internalCommand(optionally using AI).core/commandHandler.ts– executes command against DB and returns display lines.core/repo.ts– DB access helpers.core/ai.ts– wrappers for OpenAI calls.
Next.js is just the thin HTTP + rendering layer calling into core/**.
We want a generic event model that can handle workouts, meals, and time blocks.
events
id(string or UUID, PK)timestamp(datetime, not null)kind(string, not null) e.g."workout" | "meal" | "time_block" | "custom"subtype(string, nullable) e.g."strength","climbing","breakfast","deep_work"payload(JSON, not null) Structured fields, see below.created_at(datetime)updated_at(datetime)
migrations (for AI-driven schema evolution, v1 just scaffolding)
id(integer PK, autoincrement)applied_at(datetime)description(string)details(JSON) – description of what changed- For now we don’t need to actually modify DB schema; we track “virtual schema changes” here.
(This keeps implementation simple: we evolve meaning in code and payload JSON, not DB columns.)
We’ll define conventions for payload JSON per kind:
Workouts (kind = "workout")
Payload example (per event – one set or one exercise entry):
{
"sport": "strength",
"exercise": "bench press",
"sets": 3,
"reps": 5,
"weight": { "value": 165, "unit": "lb" },
"rpe": 8,
"note": "felt heavy"
}Meals (kind = "meal")
{
"meal_type": "lunch",
"description": "burrito + chips",
"calories": { "value": 900, "unit": "kcal" },
"protein_g": 35,
"carbs_g": 90,
"fat_g": 25
}(Protein/carbs/fat may be missing early; we can add later.)
Time blocks (kind = "time_block")
{
"project": "Make Studio",
"task": "docs",
"duration": { "value": 2.5, "unit": "hours" },
"focus_mode": "deep",
"note": "good flow"
}We’ll centralize these conventions in code (TypeScript types and helper functions), not DB.
We support a small set of core verbs, but allow full natural language.
log– create an eventshow– display eventsstats– aggregated viewdefine– schema/meta changes (v1: stub)edit/delete– modify events (v1: basic)undo– revert last write (v1: basic)help– show help
Everything else can be free-form and is parsed by AI into these.
type BaseCommand = {
raw: string;
};
type LogCommand = BaseCommand & {
type: "log";
kind: string; // "workout" | "meal" | "time_block" | custom
payload: any; // structured fields as per conventions
};
type ShowCommand = BaseCommand & {
type: "show";
kind?: string; // optional filter
filters?: any; // time range, field filters, etc.
};
type StatsCommand = BaseCommand & {
type: "stats";
kind?: string;
groupBy?: string; // e.g. "day", "project"
timeRange?: { from?: Date; to?: Date };
};
// For v1, define Edit/Delete/Undo simply:
type EditCommand = BaseCommand & { type: "edit_last"; /* v1: just edit last */ };
type DeleteCommand = BaseCommand & { type: "delete_last"; };
type UndoCommand = BaseCommand & { type: "undo"; };
type HelpCommand = BaseCommand & { type: "help"; };
type Command =
| LogCommand
| ShowCommand
| StatsCommand
| EditCommand
| DeleteCommand
| UndoCommand
| HelpCommand
| { type: "unknown"; raw: string };If the input matches a structured pattern, we parse locally:
Examples:
log meal lunch burrito 900
log workout squat 3x5 @ 225
log time 2.5h Make Studio
show meals today
show workouts this week
stats time by project this month
help
undo
We can implement a small regex-based parser for these patterns in core/commandParser.ts.
If we can’t confidently parse locally, we call AI:
Examples:
had a big burrito for lunch, about 900 calories
did squats 3x5 at 225, pretty heavy
worked like 2.5 hours on Make Studio docs
what did I eat yesterday?
how many hours did I work this week?
We send the raw input + some context (known kinds, recent events) to OpenAI with a strict JSON schema response describing a Command.
Server-side only; no AI calls from the client.
Single page:
-
Container: full viewport (
min-h-screen) with dark background. -
Inside:
-
divfor history:- scrollable
- monospace (
font-mono) - each entry prefixed with
>for user input and plain text for system output.
-
format bottom:-
single
<input>or<textarea>for command -
when submitted:
- optimistic echo:
> {input} - call
/api/command - append returned
lines
- optimistic echo:
-
-
In React:
const [history, setHistory] = useState<string[]>([])const [input, setInput] = useState("")const [commandHistory, setCommandHistory] = useState<string[]>([])(for ↑ navigation later)
Add:
- Auto-scroll to bottom on new lines.
- Simple client-side input history (
ArrowUpto recall previous commands) – v1 nice-to-have.
/api/command returns:
type CommandResponse = {
lines: string[];
// optional: structured metadata later
};Frontend just appends lines to history.
File: app/api/command/route.ts
Rough flow:
import { NextRequest, NextResponse } from "next/server";
import { parseCommand } from "@/core/commandParser";
import { handleCommand } from "@/core/commandHandler";
export async function POST(req: NextRequest) {
const { input } = await req.json();
const command = await parseCommand(input); // may call AI
const result = await handleCommand(command); // talks to DB
return NextResponse.json({
lines: result.lines,
meta: result.meta ?? null,
});
}handleCommand returns something like:
type CommandResult = {
lines: string[];
meta?: any;
};We can give Codex specific behaviors to implement for each:
Examples:
log workout squat 3x5 @ 225
log meal lunch burrito 900
log time 2.5h Make Studio docs
Behavior:
-
Construct
LogCommandwith:kind:"workout"/"meal"/"time_block"payload: per conventions
-
Insert one
eventsrow. -
Return lines:
Logged workout:
exercise: squat
sets: 3
reps: 5
weight: 225 lb
or
Logged meal:
type: lunch
description: burrito
calories: 900 kcal
Examples:
show meals today
show workouts this week
show time last 7 days
Behavior:
-
Parse time range:
today,this week,last 7 days,yesterday
-
Filter
eventsby:kindtimestampin range
-
Render as ASCII table.
Meals example:
Meals – Today
Time Type Description Calories
----- ------ ----------------- --------
08:30 breakfast oats + coffee 450
12:20 lunch burrito 900
19:00 dinner pasta 800
-----------------------------------------
Total 2,150 kcal
Examples:
stats workouts last 4 weeks
stats calories by day last 7 days
stats time by project this month
Behavior:
-
Build aggregated queries:
- workouts: count per week, volume, etc.
- meals: sum calories per day.
- time: sum duration per project.
-
Render as ASCII table and simple bar charts using characters.
Example:
Calories by day – last 7 days
Date Calories Chart
---------- -------- ----------
12-26-25 2100 ███
12-27-25 2300 ████
12-28-25 1950 ██▌
12-29-25 2600 █████
12-30-25 2400 ████▌
12-31-25 2200 ███▌
01-01-26 2000 ███
Show:
- examples of
log/show/stats - emphasize “you can also just type normal sentences”
edit last(for now): just echo the last event in a structured way and say “(editing not implemented yet)” → stub, or allow simple note update.delete last: delete the most recent event for the current user.undo: alias todelete lastfor v1.
(We can wire this up to a lastEventId in session or query the latest event.)
In core/ai.ts:
- A function
parseWithAI(raw: string, context: ParseContext): Promise<Command>.
For v1:
-
Use a system prompt explaining:
- available
kinds (workout,meal,time_block) - basic field conventions
- available
-
Request a JSON object matching our
Commandschema. -
On errors or weird responses:
- fall back to
type: "unknown"with a friendly error line.
- fall back to
We do not need to implement schema evolution / migrations in v1; just parsing.
-
AI-proposed schema changes:
- “you keep mentioning RPE, add a field?”
- “you’re logging protein, add macros to meals?”
-
“Templates” for repeated workouts/time blocks.
-
Cross-domain queries:
- “Do I work less on days I lift heavy?”
- “How does calorie intake relate to hours worked?”
These all build on the same event model and command framework.