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.).

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, and all mapped flashcards graduated
weak has ungraduated wrong questions, or a wrong answer on the latest attempt
in progress partially answered with no negative evidence, or all-correct but mapped flashcards not yet all graduated
untouched nothing answered yet

Flashcard graduation component: cards in flashcards.json may carry an optional examPoint (EP-NN, the same namespace as questions); for a mapped point, mastery additionally requires those cards to be graduated in SRS (phase = review). Unmapped points are unaffected — the criteria simply fall back to questions only. The web app’s home “exam-point mastery” panel shows the same criteria live (src/lib/mastery.ts).

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.).


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. 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 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.