Skip to content

AI CLI Guide · the three AI command-line tools

ai-study-kit ships three AI CLIs that turn study material into loop artifacts: teach-generate produces courses, grill-wrong produces wrong-question deep-dives, and podcast-generate produces review podcasts. All driven by your own LLM/TTS API keys, and all work with any OpenAI-compatible service (OpenAI / Zhipu GLM / DeepSeek / Kimi / Qwen / Doubao, etc.).

In day-to-day study you don’t run these commands yourself. To produce a course, deep-dive wrong answers, or make a podcast, just tell the /ask-coach coach — that layer is guided execution by the coach, and the artifacts land straight in the directories in the table below. This page is the manual option, written for people who want to produce content by hand, bypassing the coach, or who run a self-hosted deployment and need to tune flags and debug; for how to install and use the coach, see ai-study-kit.en.md.

Each CLI has a shortcut at the repo root; this guide uses the short forms (equivalent to node apps/quiz-app/scripts/<script>.mjs):

Shortcut Script Output
pnpm run ai:teach teach-generate.mjs course HTML (lessons/*.html)
pnpm run ai:grill grill-wrong.mjs deep-dive HTML (study/wrong-questions/*.html)
pnpm run ai:podcast podcast-generate.mjs podcast script + transcript + audio (podcast-out/)

Terminal window
cp .env.example .env
# edit .env — at minimum LLM_BASE_URL / LLM_API_KEY / LLM_MODEL

Full provider options: configuration.en.md.

2. Start the quiz-app backend (needed by grill)

Section titled “2. Start the quiz-app backend (needed by grill)”
Terminal window
pnpm run server # in another terminal, serves :8787
Terminal window
# A. generate a course (from course-spec.json)
pnpm run ai:teach -- --theme dev-intro
# B. generate wrong-question deep-dives (pulls wrong answers from the server)
pnpm run ai:grill -- --theme dev-intro
# C. generate a podcast (from any study material)
pnpm run ai:podcast -- --input examples/dev-intro/lessons/git-basics.html

All three AI CLIs support --json: human-readable logs drop to stderr and stdout carries a single result JSON (a manifest of produced-file paths) for other agents / script pipelines to consume (same convention as mastery-report --json). No-op paths (e.g. no wrong questions right now) also emit JSON (status: "noop") so pipelines can branch on it.


Turns a theme spec (mission + resources + audience) into a multi-lesson, self-contained HTML course.

examples/<theme>/course-spec.json:

{
"theme": "react-basics",
"mission": "Be able to independently build a React component library",
"audience": "Developers with JS basics, first time with React",
"depth": "beginner", // beginner | intermediate | advanced
"lessonsCount": 3, // how many lessons you want
"outline": ["Hooks basics", "State management", "Component design"], // optional; LLM splits automatically if omitted
"resources": [ // optional, authoritative material links
{ "title": "React official docs", "url": "https://react.dev" }
]
}

examples/<theme>/lessons/0001-<slug>.html, 0002-<slug>.html…:

  • each lesson is self-contained HTML (sharing ../assets/styles.css)
  • structure: h1 + meta + lead + multiple h2 + callouts (key point / warning / tip) + quiz-anchor
  • mechanism diagram: at least one inline SVG per lesson — big picture, few words, mechanism only (nodes + arrows for flow / hierarchy / contrast)
  • source backlinks: every lesson ends with a 📚 Sources block listing authoritative links — resources = course-spec.json’s resources merged with the theme’s RESOURCES.md (the teach-workflow’s authoritative resource list), deduplicated by URL; the merged list feeds both the LLM’s prep references and the footer display (the artifact side of the “concepts come from reference materials” principle)
  • reference body fetching (v0.13): at generation time the page body of those links is fetched into the LLM’s prep context (the “concepts come from reference materials” principle moves from the citation layer to the content layer — fetched text is the primary prep basis); a local cache dedupes by URL (apps/quiz-app/node_modules/.cache/teach-resources/) so reruns don’t refetch; a failing source degrades to URL-list citation without interrupting the run
  • prev/next links chain lessons together
Terminal window
pnpm run ai:teach -- --theme react-basics
pnpm run ai:teach -- --theme X --lessons 5 # override lessonsCount
pnpm run ai:teach -- --theme X --lang en # produce the course in English
pnpm run ai:teach -- --theme X --json # machine-readable output (agent consumption)

--theme defaults to dev-intro. Reference: examples/dev-intro/course-spec.json.


grill-wrong — generate wrong-question deep-dives

Section titled “grill-wrong — generate wrong-question deep-dives”

Pulls your wrong answers from the server, clusters them by exam point with an LLM, then deeply expands each cluster.

  1. GET /api/progress fetches your wrong-question list (SERVER env var selects the backend)
  2. joins examples/<theme>/questions.json for full question text
  3. the LLM clusters wrong questions by exam point (e.g. “git reset vs revert” ×3, “HTTP status codes” ×2)
  4. per cluster the LLM writes a deep-dive HTML (core-differences table + decision flowchart + pitfall warnings + variant drills)
  5. written to examples/<theme>/study/wrong-questions/cluster-NN-<slug>.html (output in the legacy wrong-questions/ location is recognized and migrated automatically)
  6. updates examples/<theme>/study/wrong-questions/index.html, the wrong-question hub
  7. also writes a learner profile: the LLM additionally records per-exam-point wrong reasons (wrongReasons / advice) into examples/<theme>/study/records/profile.json (machine-readable; clusters sharing any question id merge into the same point and accumulate). The profile is private learner data and never ships with the build; the next mastery-report run or /ask-coach probe picks it up automatically, so recommendations get specific — “EP-03 missed twice, reason: unfamiliar with permission-bit combinations”.
Terminal window
# prerequisite: the quiz-app backend must be running, and you must have drilled and gotten things wrong
pnpm run server # another terminal
pnpm run ai:grill -- --theme react-basics
pnpm run ai:grill -- --max-clusters 5 # at most 5 clusters
pnpm run ai:grill -- --lang es # deep-dives in Spanish
pnpm run ai:grill -- --json # machine-readable output (agent consumption)
SERVER=http://my-server:8787 pnpm run ai:grill # pull wrong answers from a remote server

Wrong-question graduation rules (same as quiz-app)

Section titled “Wrong-question graduation rules (same as quiz-app)”
wrongCount Threshold Meaning
1 1 correct answer new wrong question; one correct answer removes it
2 2 correct answers missed twice; needs 2 consecutive correct answers to graduate
3+ 3 correct answers high-frequency; needs 3 consecutive correct answers to graduate

mastery-report — exam-point mastery report (no AI)

Section titled “mastery-report — exam-point mastery report (no AI)”

Companion tool to grill: deterministically derives each exam point’s mastery (a question’s examPoint, EP-NN) from the question bank + answer progress — no LLM involved. Shared by humans and agents: humans read the table, agents consume --json (the /ask-coach probe’s “weak points” snapshot line comes from here).

State Criterion
mastered all questions in the point answered, all correct on the latest attempt, no ungraduated wrong questions, plus one beyond-questions proof: oral mastery (when the point has oral attempts) or all mapped flashcards graduated; with no oral data, all-correct alone masters the point
weak has ungraduated wrong questions, a wrong answer on the latest attempt, or oral judged weak (negative evidence first)
in progress partially answered with no negative evidence, or all-correct but oral not yet at the bar and mapped flashcards not all graduated
untouched nothing answered yet

Beyond-questions proof (v0.26 / criteria v1.2, #119): oral mastery — the oral four-state computed from the oral attempts ledger (criteria in the next section) judging “mastered” — is the primary channel; all mapped flashcards graduated (flashcards.json optional examPoint, EP-NN in the same namespace as questions, SRS phase = review) is an equivalent alternative channel. No oral data means no blocker — chat-first learners who never touch flashcards master a point by answering all its questions correctly (SRS stays available but fully optional); when oral data exists but is below the bar, full flashcard graduation still masters the point (existing behavior does not regress). The criteria are dual-implemented: scripts/lib/mastery.mjs (this report) and src/lib/mastery.ts (web panorama page) agree on the same fixtures (a cross-implementation assertion test keeps it that way); the web side’s oral evidence comes from the build-time coverage snapshot coverage.json (oralStatus, fresh as of the last build).

The report joins the learner profile (study/records/profile.json, written by grill) — each point’s row carries its wrong reasons and advice. Point names are parsed from the MISSION.md layout table.

Oral four states (v0.14, the oral field of --json)

Section titled “Oral four states (v0.14, the oral field of --json)”

Every oral target in the oral attempts ledger (study/records/oral-attempts.json, per-QA details appended by the chat layer) — all layout-table exam points ∪ bare knowledge points appearing in the ledger (new concepts learned in chat that have no questions yet) — gets a pure oral-channel four-state, judged with zero LLM:

Status Criteria
Mastered recent weighted accuracy ≥ 0.85 (last 5 attempts weighted 0.5/0.7/0.85/0.95/1.0, normalized by weight sum; caps of 0.5/0.8 after 1/2 answers make it unreachable — one lucky guess proves nothing)
Weak most recent attempt wrong (negative evidence first), or weighted score < 0.5
In progress has attempts, no negatives, weighted score below the mastery line (e.g. 2/2 correct = 0.8)
Untouched no ledger entries

oral.weakRanked lets agents name weak oral targets; when merged with the question-channel four-state, negative evidence wins (any weak → weak), the question channel rules once it has data, and with no answers yet the oral channel can lift a point to at most “in progress” (verification happens by answering questions). The existing exam-point criteria (table above) are unchanged.

Knowledge-graph projection (v0.14, --graph / --write-projection)

Section titled “Knowledge-graph projection (v0.14, --graph / --write-projection)”

With --graph <graph.json path> (or the KNOWFLOW_GRAPH_JSON environment variable) the report loads the external knowflow knowledge graph and, combined with the exam-point↔node mapping (study/records/graph-map.json, proposed by the agent, confirmed by the learner), produces a read-only projection file:

Terminal window
pnpm run mastery -- --graph /path/to/knowflow/graph/graph.json --write-projection
# → writes mastery-projection.json next to graph.json:
# { version: 1, generatedAt, source, nodes: [{ id, mastery, oral: { asked, correct } }] }

The graph field of --json carries the graph signals: per-node four-states (mapped nodes = question-channel state merged with the oral channel, negative evidence first; unmapped nodes = pure oral channel), mapped counts, and the projection result. No graph / no mapping = silent degradation to the pure exam-point view (graph.loaded = false) — not an error; graph.json itself and knowledge pages are never touched, and the knowledge base is never written back (the ADR-0005 projection bridge).

Prerequisites and recommendation order (graph.weakPrereqs / graph.weakOrdered): when graph edges carry relation labels (knowflow relation-labeler vocabulary), the report maps prerequisite-class relations (prerequisite/dependency/source/reference/basis/usage/part-of/derivation) to the learning order “learn to first” — weakPrereqs gives each weak exam point’s prerequisite chain (including each prerequisite’s mastery state) and weakOrdered gives a recommendation order that respects prerequisites (prerequisites first, transitively; cycles and edges without a relation label don’t participate). Recommendation reasons become “the prerequisite EP-01 is still weak, shore it up first” instead of “drill EP-12”. No graph / no mapping / no prerequisite edges → these fields are absent, silent degradation.

Terminal window
pnpm run mastery # human-readable table (defaults to dev-intro)
pnpm run mastery -- --theme react-basics # select a theme (external theme-pack paths work too)
pnpm run mastery -- --json # machine-readable (for agent probing)
pnpm run mastery -- --progress /tmp/p.json # select a progress file (default apps/quiz-app/progress.json;
# for server progress first run curl -sf $SERVER/api/progress -o /tmp/p.json)
pnpm run mastery -- --panorama # exam-point panorama (v0.13): taught/practiced/mastered
# signals grouped by day + summary lines (taught = study records
# ∪ lessons completed; practiced = answers or the oral attempts ledger;
# mastered = the four-state criteria). Add --json for agents;
# the skill's "report progress" card consumes it

A missing progress file simply means empty progress (everything untouched) — not an error.


podcast-generate — generate a review podcast

Section titled “podcast-generate — generate a review podcast”

Turns any study material (course HTML / questions / deep-dives) into a two-host dialogue podcast.

--input takes one file; the script auto-detects the format:

Format Handling
.html tags stripped, title and body extracted
.md as-is
.json (questions.json) each question formatted as “stem + options + answer + analysis”
.txt as-is

Output (three pieces, written to podcast-out/)

Section titled “Output (three pieces, written to podcast-out/)”
File Contents
<slug>-script.json dialogue script (structured: title / source / generatedAt / script array)
<slug>-transcript.md Markdown transcript (👩 female host / 👨 male host markers)
<slug>.wav synthesized two-host audio (unless --no-tts)
Terminal window
# basic usage
pnpm run ai:podcast -- --input examples/dev-intro/lessons/git-basics.html
# control segments and style
pnpm run ai:podcast -- --input examples/dev-intro/questions.json \
--segments 15 --style interview
# script only, skip audio synthesis (saves TTS cost)
pnpm run ai:podcast -- \
--input examples/dev-intro/study/wrong-questions/cluster-01-*.html --no-tts
# produce the dialogue in another language (verify the script first with --no-tts, see "Output language")
pnpm run ai:podcast -- --input examples/dev-intro/questions.json --lang ru --no-tts
# machine-readable output (agent consumption)
pnpm run ai:podcast -- --input examples/dev-intro/questions.json --no-tts --json
Value Style
conversational (default) relaxed two-person chat, complementing / questioning / giving examples
lecture one host leads, the other asks follow-ups and summarizes
interview one plays the expert, the other the interviewer

Audio synthesis needs a TTS provider configured (GLM-TTS by default) — see configuration.en.md. --no-tts produces only script + transcript without calling TTS — cheaper, or synthesize later with other tools (NotebookLM etc.).


Voice discipline (how generated content reads)

Section titled “Voice discipline (how generated content reads)”

Each of the three CLIs embeds a voice discipline block in its generation prompt (since v0.21.0) — courses, wrong-question deep-dives, and podcast scripts should read “like speech face-to-face, not like desk writing”. The discipline text lives in one five-language source, scripts/lib/voice.mjs: whichever language --lang selects, the discipline block enters the prompt in that language.

The structure is a shared base + a shape per product:

Product Shape
teach course Written form: the first <h2> goes straight to what this lesson is for and where to start; rows ordered by dependency (claim first, then mechanism and examples, then boundaries and pitfalls); no textbook voice — no uniform “Definition / Key Features / Use Cases” filler sections
grill wrong-question deep-dive Diagnostic form: open with one sentence naming the cluster’s common error root; every pitfall warning points at a concrete question (question id + actual wrong pick + accumulated wrong count — quoting only records the input actually gives, never inventing); root causes land on concrete concepts or operations — “careless” is not a cause
podcast Spoken form: write for the ear (a sentence is as long as one breath), pick up the thread before moving on, answer the next question, topic return (every detour exits with a return signal); when the target segment count exceeds 12 (--segments > 12), a “mid-show recap” clause is appended — a line or two of dialogue around the halfway point, no separate segment

The base is shared by all three products, five rules: open on substance / anchor every word to a thing / tie every verdict to evidence / keep the hedges / delete before rewriting.

The skill-layer source of the coach’s voice is skills/references/voice.md (distilled and rewritten from cida; ADR-0008, no runtime dependency). voice.mjs is the same discipline applied to CLI generation prompts; the two sides are maintained in sync.


All three CLIs accept an output language for generated content:

Terminal window
pnpm run ai:teach -- --theme X --lang en # English course
pnpm run ai:grill -- --theme X --lang es # Spanish deep-dives
pnpm run ai:podcast -- --input Y --lang ru # Russian podcast dialogue
# or uniformly via env var (settable in .env)
STUDY_LANG=en pnpm run ai:teach -- --theme X

Supported: zh (default) / en / es / ru / ja. The registry lives in scripts/lib/langs.mjs; adding a language is one registry entry.

Behavior notes:

  • --lang affects generated content (course body, outline, deep-dive body, podcast dialogue/titles) and the fixed strings of generated HTML (prev/next navigation, footer, the <html lang> attribute, host names in transcripts);
  • the CLIs’ own logs/errors stay in Chinese (the operator is the maintainer);
  • question text (stems/options) is never translated — quotes inside deep-dives stay verbatim, deliberately: questions must match what you drilled;
  • podcast caveat: TTS currently only integrates GLM-TTS; whether non-Chinese dialogue can be synthesized depends on the provider’s multilingual support. Verify the script first with --lang X --no-tts, then synthesize.

The quiz-app UI’s languages (top-bar zh/EN/ES/RU/JA switch) are a separate mechanism — see the README’s “Multilingual” section.


The three CLIs are incremental capability, not a requirement. To just use ai-study-kit as a quiz + flashcard app, skip the LLM entirely — pnpm dev is enough. Courses, wrong-answer analysis and review podcasts unlock with one API key.


Decision Choice Rationale
LLM provider OpenAI-compatible protocol + baseURL one codebase covers 95% of providers (OpenAI/GLM/DeepSeek/Kimi/Qwen/Doubao)
Configuration three .env vars (LLM_BASE_URL + LLM_API_KEY + LLM_MODEL) minimal, single-file management
Robustness parseJsonLoose + 3 retries with exponential backoff + clear errors LLMs return fake JSON and rate-limit; must tolerate
Testing pure functions extracted to lib/, unit-tested with node:test LLM calls aren’t unit-testable, everything around them is
No AI-client lock-in CLIs rather than an agent skill ZCode / Claude Code / Cursor users all work, even CI

The theme workspace structure (MISSION.md / RESOURCES.md / lessons/) and part of the question-writing discipline (equal-length options, no format giveaways) come from the teach skill workflow — with thanks.

Full methodology background: methodology.en.md; the three CLIs are its engineering embodiment.


Q: Running a CLI errors with “LLM 配置不完整” (incomplete LLM config) A: .env is missing fields. Copy .env.example to .env and fill the three: LLM_BASE_URL / LLM_API_KEY / LLM_MODEL. See configuration.en.md.

Q: The LLM’s JSON fails to parse A: parseJsonLoose already tolerates a lot (extracts {...}, strips markdown fences). If it still fails, the output is severely off — try another model (gpt-4o-mini / glm-4.6 / deepseek-chat are all stable).

Q: TTS synthesis is slow A: GLM-TTS takes ~5-10 s per segment; a 12-segment dialogue ~2 min. For speed, use --no-tts and synthesize elsewhere.

Q: Generated course/deep-dive quality is poor A: tune course-spec.json’s audience / depth / resources — the more specific the audience and resources, the better the output. --segments (podcast) and --lessons (teach) control granularity.

Q: I want Claude / Gemini / another non-OpenAI provider A: The current abstraction only supports OpenAI-compatible protocols. Claude and Gemini both have OpenAI-compatible proxies (LiteLLM Proxy, OpenRouter) — connect through one. Native adapters may come later.