muninn-cli 0.2.0__tar.gz → 0.3.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {muninn_cli-0.2.0 → muninn_cli-0.3.0}/.github/workflows/ci.yml +4 -0
- muninn_cli-0.3.0/.github/workflows/deploy-docs.yml +52 -0
- {muninn_cli-0.2.0 → muninn_cli-0.3.0}/.gitignore +6 -0
- muninn_cli-0.3.0/PKG-INFO +71 -0
- muninn_cli-0.3.0/README.md +51 -0
- muninn_cli-0.3.0/README_zh.md +51 -0
- muninn_cli-0.3.0/docs/api/base-plugin.md +60 -0
- muninn_cli-0.3.0/docs/api/data-plugin.md +90 -0
- muninn_cli-0.3.0/docs/api/flashcard.md +60 -0
- muninn_cli-0.3.0/docs/api/matchers.md +79 -0
- muninn_cli-0.3.0/docs/commands.md +97 -0
- muninn_cli-0.3.0/docs/getting-started.md +48 -0
- muninn_cli-0.3.0/docs/index.md +17 -0
- muninn_cli-0.3.0/docs/pack-spec.md +67 -0
- muninn_cli-0.3.0/docs/plugin-dev.md +50 -0
- {muninn_cli-0.2.0 → muninn_cli-0.3.0}/pyproject.toml +4 -1
- {muninn_cli-0.2.0 → muninn_cli-0.3.0}/src/cli/manager.py +206 -9
- {muninn_cli-0.2.0 → muninn_cli-0.3.0}/src/cli/runner.py +2 -1
- {muninn_cli-0.2.0 → muninn_cli-0.3.0}/src/main.py +33 -0
- {muninn_cli-0.2.0 → muninn_cli-0.3.0}/src/ui.py +27 -0
- {muninn_cli-0.2.0 → muninn_cli-0.3.0}/tests/cli/test_manager.py +131 -105
- muninn_cli-0.3.0/uv.lock +351 -0
- muninn_cli-0.3.0/zensical.toml +52 -0
- muninn_cli-0.2.0/.agent/api.md +0 -193
- muninn_cli-0.2.0/.agent/plan.md +0 -124
- muninn_cli-0.2.0/PKG-INFO +0 -203
- muninn_cli-0.2.0/README.md +0 -183
- muninn_cli-0.2.0/README_zh.md +0 -181
- muninn_cli-0.2.0/uv.lock +0 -108
- {muninn_cli-0.2.0 → muninn_cli-0.3.0}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
- {muninn_cli-0.2.0 → muninn_cli-0.3.0}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
- {muninn_cli-0.2.0 → muninn_cli-0.3.0}/.github/workflows/publish.yml +0 -0
- {muninn_cli-0.2.0 → muninn_cli-0.3.0}/.python-version +0 -0
- {muninn_cli-0.2.0 → muninn_cli-0.3.0}/.vscode/settings.json +0 -0
- {muninn_cli-0.2.0 → muninn_cli-0.3.0}/LICENSE +0 -0
- {muninn_cli-0.2.0 → muninn_cli-0.3.0}/example/chemistry/elements.json +0 -0
- {muninn_cli-0.2.0 → muninn_cli-0.3.0}/example/chemistry/manifest.json +0 -0
- {muninn_cli-0.2.0 → muninn_cli-0.3.0}/example/chemistry/plugin.py +0 -0
- {muninn_cli-0.2.0 → muninn_cli-0.3.0}/example/flashcard/manifest.json +0 -0
- {muninn_cli-0.2.0 → muninn_cli-0.3.0}/example/flashcard/plugin.py +0 -0
- {muninn_cli-0.2.0 → muninn_cli-0.3.0}/example/flashcard/words.csv +0 -0
- {muninn_cli-0.2.0 → muninn_cli-0.3.0}/main.py +0 -0
- {muninn_cli-0.2.0 → muninn_cli-0.3.0}/scripts/bump_version.py +0 -0
- {muninn_cli-0.2.0 → muninn_cli-0.3.0}/src/__init__.py +0 -0
- {muninn_cli-0.2.0 → muninn_cli-0.3.0}/src/cli/__init__.py +0 -0
- {muninn_cli-0.2.0 → muninn_cli-0.3.0}/src/core/__init__.py +0 -0
- {muninn_cli-0.2.0 → muninn_cli-0.3.0}/src/core/base_plugin.py +0 -0
- {muninn_cli-0.2.0 → muninn_cli-0.3.0}/src/core/helpers.py +0 -0
- {muninn_cli-0.2.0 → muninn_cli-0.3.0}/src/core/scheduler.py +0 -0
- {muninn_cli-0.2.0 → muninn_cli-0.3.0}/src/core/state.py +0 -0
- {muninn_cli-0.2.0 → muninn_cli-0.3.0}/tests/__init__.py +0 -0
- {muninn_cli-0.2.0 → muninn_cli-0.3.0}/tests/cli/__init__.py +0 -0
- {muninn_cli-0.2.0 → muninn_cli-0.3.0}/tests/conftest.py +0 -0
- {muninn_cli-0.2.0 → muninn_cli-0.3.0}/tests/core/__init__.py +0 -0
- {muninn_cli-0.2.0 → muninn_cli-0.3.0}/tests/core/test_helpers.py +0 -0
- {muninn_cli-0.2.0 → muninn_cli-0.3.0}/tests/core/test_scheduler.py +0 -0
- {muninn_cli-0.2.0 → muninn_cli-0.3.0}/tests/core/test_state.py +0 -0
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
name: Deploy Docs
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: ["main"]
|
|
6
|
+
paths:
|
|
7
|
+
- "docs/**"
|
|
8
|
+
- "zensical.toml"
|
|
9
|
+
- ".github/workflows/deploy-docs.yml"
|
|
10
|
+
workflow_dispatch:
|
|
11
|
+
|
|
12
|
+
concurrency:
|
|
13
|
+
group: pages
|
|
14
|
+
cancel-in-progress: true
|
|
15
|
+
|
|
16
|
+
jobs:
|
|
17
|
+
deploy:
|
|
18
|
+
name: Deploy to GitHub Pages
|
|
19
|
+
runs-on: ubuntu-latest
|
|
20
|
+
permissions:
|
|
21
|
+
contents: read
|
|
22
|
+
pages: write
|
|
23
|
+
id-token: write
|
|
24
|
+
environment:
|
|
25
|
+
name: github-pages
|
|
26
|
+
url: ${{ steps.deployment.outputs.page_url }}
|
|
27
|
+
steps:
|
|
28
|
+
- name: Checkout code
|
|
29
|
+
uses: actions/checkout@v7
|
|
30
|
+
|
|
31
|
+
- name: Install uv
|
|
32
|
+
uses: astral-sh/setup-uv@v9.0.0
|
|
33
|
+
with:
|
|
34
|
+
version: "latest"
|
|
35
|
+
|
|
36
|
+
- name: Set up Python
|
|
37
|
+
run: uv python install
|
|
38
|
+
|
|
39
|
+
- name: Install dependencies
|
|
40
|
+
run: uv sync --group docs
|
|
41
|
+
|
|
42
|
+
- name: Build docs
|
|
43
|
+
run: uv run zensical build --clean
|
|
44
|
+
|
|
45
|
+
- name: Upload Pages artifact
|
|
46
|
+
uses: actions/upload-pages-artifact@v5
|
|
47
|
+
with:
|
|
48
|
+
path: site
|
|
49
|
+
|
|
50
|
+
- name: Deploy to GitHub Pages
|
|
51
|
+
id: deployment
|
|
52
|
+
uses: actions/deploy-pages@v5
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: muninn-cli
|
|
3
|
+
Version: 0.3.0
|
|
4
|
+
Summary: Muninn - The Extensible Reciting CLI
|
|
5
|
+
Project-URL: Repository, https://github.com/a1fredbao/muninn
|
|
6
|
+
Project-URL: Issues, https://github.com/a1fredbao/muninn/issues
|
|
7
|
+
Author-email: Alfred <your@email.com>
|
|
8
|
+
License: MIT
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Keywords: cli,flashcard,memorize,recite
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Environment :: Console
|
|
13
|
+
Classifier: Intended Audience :: Education
|
|
14
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
17
|
+
Classifier: Topic :: Education
|
|
18
|
+
Requires-Python: >=3.12
|
|
19
|
+
Description-Content-Type: text/markdown
|
|
20
|
+
|
|
21
|
+
# Muninn
|
|
22
|
+
|
|
23
|
+
Muninn (雾尼) - An Extensible Reciting CLI.
|
|
24
|
+
|
|
25
|
+
[中文文档](./README_zh.md) | [Documentation](https://a1fredbao.github.io/muninn/)
|
|
26
|
+
|
|
27
|
+
## What is Muninn?
|
|
28
|
+
|
|
29
|
+
Muninn is a highly extensible CLI application designed to help you memorize anything. Instead of hardcoding questions, Muninn relies on a **Plugin Architecture**. You can install "Reciting Packs" created by others (like chemistry elements, GRE vocabulary, or historical events) or develop your own packs using Python.
|
|
30
|
+
|
|
31
|
+
Muninn acts as a "host" that provides:
|
|
32
|
+
|
|
33
|
+
1. A **Smart Scheduling Algorithm** (focuses on your weak-points).
|
|
34
|
+
2. **Persistent State Management** (remembers your progress across sessions).
|
|
35
|
+
3. A clean, distraction-free **Terminal UI**.
|
|
36
|
+
|
|
37
|
+
## Installation & Usage
|
|
38
|
+
|
|
39
|
+
Install Muninn globally using `uv` (recommended) or `pip`:
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
# Using uv (Recommended)
|
|
43
|
+
uv tool install muninn-cli
|
|
44
|
+
|
|
45
|
+
# Or using pip
|
|
46
|
+
pip install muninn-cli
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## Quickstart
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
# Install a pack from GitHub
|
|
53
|
+
muninn install a1fredbao/muninn-chemistry-plugin
|
|
54
|
+
|
|
55
|
+
# Start reciting
|
|
56
|
+
muninn run muninn-chemistry-plugin
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## Commands
|
|
60
|
+
|
|
61
|
+
| Command | |
|
|
62
|
+
| ---------------------------- | ------------------------------------------------------------ |
|
|
63
|
+
| `muninn install <source>` | Install a pack (local dir, zip, GitHub URL, or `user/repo`). |
|
|
64
|
+
| `muninn uninstall <pack_id>` | Remove a pack. |
|
|
65
|
+
| `muninn list` | List installed packs. |
|
|
66
|
+
| `muninn run <pack_id>` | Start a reciting session. |
|
|
67
|
+
| `muninn new <name>` | Generate a plugin template. |
|
|
68
|
+
|
|
69
|
+
## Write a Plugin
|
|
70
|
+
|
|
71
|
+
See full documentation at: [a1fredbao.github.io/muninn](https://a1fredbao.github.io/muninn/)
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# Muninn
|
|
2
|
+
|
|
3
|
+
Muninn (雾尼) - An Extensible Reciting CLI.
|
|
4
|
+
|
|
5
|
+
[中文文档](./README_zh.md) | [Documentation](https://a1fredbao.github.io/muninn/)
|
|
6
|
+
|
|
7
|
+
## What is Muninn?
|
|
8
|
+
|
|
9
|
+
Muninn is a highly extensible CLI application designed to help you memorize anything. Instead of hardcoding questions, Muninn relies on a **Plugin Architecture**. You can install "Reciting Packs" created by others (like chemistry elements, GRE vocabulary, or historical events) or develop your own packs using Python.
|
|
10
|
+
|
|
11
|
+
Muninn acts as a "host" that provides:
|
|
12
|
+
|
|
13
|
+
1. A **Smart Scheduling Algorithm** (focuses on your weak-points).
|
|
14
|
+
2. **Persistent State Management** (remembers your progress across sessions).
|
|
15
|
+
3. A clean, distraction-free **Terminal UI**.
|
|
16
|
+
|
|
17
|
+
## Installation & Usage
|
|
18
|
+
|
|
19
|
+
Install Muninn globally using `uv` (recommended) or `pip`:
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
# Using uv (Recommended)
|
|
23
|
+
uv tool install muninn-cli
|
|
24
|
+
|
|
25
|
+
# Or using pip
|
|
26
|
+
pip install muninn-cli
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Quickstart
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
# Install a pack from GitHub
|
|
33
|
+
muninn install a1fredbao/muninn-chemistry-plugin
|
|
34
|
+
|
|
35
|
+
# Start reciting
|
|
36
|
+
muninn run muninn-chemistry-plugin
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Commands
|
|
40
|
+
|
|
41
|
+
| Command | |
|
|
42
|
+
| ---------------------------- | ------------------------------------------------------------ |
|
|
43
|
+
| `muninn install <source>` | Install a pack (local dir, zip, GitHub URL, or `user/repo`). |
|
|
44
|
+
| `muninn uninstall <pack_id>` | Remove a pack. |
|
|
45
|
+
| `muninn list` | List installed packs. |
|
|
46
|
+
| `muninn run <pack_id>` | Start a reciting session. |
|
|
47
|
+
| `muninn new <name>` | Generate a plugin template. |
|
|
48
|
+
|
|
49
|
+
## Write a Plugin
|
|
50
|
+
|
|
51
|
+
See full documentation at: [a1fredbao.github.io/muninn](https://a1fredbao.github.io/muninn/)
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# Muninn
|
|
2
|
+
|
|
3
|
+
Muninn (雾尼) - 高度可扩展的背诵/记忆命令行工具。
|
|
4
|
+
|
|
5
|
+
[English](./README.md) | [完整文档](https://a1fredbao.github.io/muninn/)
|
|
6
|
+
|
|
7
|
+
## Muninn 是什么?
|
|
8
|
+
|
|
9
|
+
Muninn 是一个高度可扩展的命令行背诵软件,名字取自北欧神话中代表"记忆"的乌鸦雾尼。它不包含任何硬编码的题目,而是采用了 **插件架构**。你可以安装别人编写的"题库包"(比如化学元素表、GRE 单词、历史事件),也可以自己使用 Python 开发题库。
|
|
10
|
+
|
|
11
|
+
Muninn 作为"宿主",为你提供了三大能力:
|
|
12
|
+
|
|
13
|
+
1. **智能出题调度算法**(根据历史正确率和耗时,专门盯着你的薄弱点出题)。
|
|
14
|
+
2. **状态与进度持久化**(你的每一次练习进度都会被保存下来,随时可以继续)。
|
|
15
|
+
3. 干净、无干扰的**终端 UI**。
|
|
16
|
+
|
|
17
|
+
## 安装与使用
|
|
18
|
+
|
|
19
|
+
使用 `uv`(推荐)或 `pip` 全局安装 Muninn:
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
# 使用 uv 安装 (推荐)
|
|
23
|
+
uv tool install muninn-cli
|
|
24
|
+
|
|
25
|
+
# 或者使用 pip 安装
|
|
26
|
+
pip install muninn-cli
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## 快速开始
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
# 从 GitHub 安装一个题库包
|
|
33
|
+
muninn install a1fredbao/muninn-chemistry-plugin
|
|
34
|
+
|
|
35
|
+
# 开始背诵
|
|
36
|
+
muninn run muninn-chemistry-plugin
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## 命令一览
|
|
40
|
+
|
|
41
|
+
| 命令 | |
|
|
42
|
+
| ---------------------------- | -------------------------------------------------------- |
|
|
43
|
+
| `muninn install <source>` | 安装题库包(本地目录、zip、GitHub URL 或 `user/repo`)。 |
|
|
44
|
+
| `muninn uninstall <pack_id>` | 卸载题库包。 |
|
|
45
|
+
| `muninn list` | 列出已安装的题库包。 |
|
|
46
|
+
| `muninn run <pack_id>` | 开始背诵。 |
|
|
47
|
+
| `muninn new <name>` | 生成插件开发模板。 |
|
|
48
|
+
|
|
49
|
+
## 开发插件
|
|
50
|
+
|
|
51
|
+
完整文档请见:[a1fredbao.github.io/muninn](https://a1fredbao.github.io/muninn/)
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# BaseRecitePlugin
|
|
2
|
+
|
|
3
|
+
`BaseRecitePlugin` is the lowest-level plugin interface. Implement all
|
|
4
|
+
five methods for full control over rendering, answer checking, and
|
|
5
|
+
metadata display.
|
|
6
|
+
|
|
7
|
+
``` python
|
|
8
|
+
from core.base_plugin import BaseRecitePlugin
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class Plugin(BaseRecitePlugin):
|
|
12
|
+
def load_data(self):
|
|
13
|
+
"""Load static data from ``self.workspace_dir``. Called once
|
|
14
|
+
at initialisation."""
|
|
15
|
+
pass
|
|
16
|
+
|
|
17
|
+
def get_all_problem_ids(self) -> list[str]:
|
|
18
|
+
"""Return every unique problem ID in this pack."""
|
|
19
|
+
pass
|
|
20
|
+
|
|
21
|
+
def render_statement(self, problem_id: str) -> str:
|
|
22
|
+
"""Return the question text shown to the user."""
|
|
23
|
+
pass
|
|
24
|
+
|
|
25
|
+
def check_answer(self, problem_id: str, user_input: str) -> bool:
|
|
26
|
+
"""Return ``True`` if *user_input* is correct."""
|
|
27
|
+
pass
|
|
28
|
+
|
|
29
|
+
def get_expected_display(self, problem_id: str) -> str:
|
|
30
|
+
"""Return the correct answer to show on failure."""
|
|
31
|
+
pass
|
|
32
|
+
|
|
33
|
+
def get_expand_info(self, problem_id: str) -> str:
|
|
34
|
+
"""Optional: return extra info to show on success."""
|
|
35
|
+
return ""
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Lifecycle
|
|
39
|
+
|
|
40
|
+
1. Muninn calls `__init__(workspace_dir)`, which calls `load_data()`.
|
|
41
|
+
2. Muninn calls `get_all_problem_ids()` to build the scheduler queue.
|
|
42
|
+
3. For each problem, Muninn calls `render_statement()` → captures user
|
|
43
|
+
input → calls `check_answer()`.
|
|
44
|
+
4. On correct answer: `get_expand_info()` is shown.
|
|
45
|
+
5. On wrong answer: `get_expected_display()` is shown.
|
|
46
|
+
|
|
47
|
+
## `workspace_dir`
|
|
48
|
+
|
|
49
|
+
`self.workspace_dir` points to the pack's private directory under
|
|
50
|
+
`~/.muninn/packs/<pack_id>/`. Use it to load static assets like CSV or
|
|
51
|
+
JSON files:
|
|
52
|
+
|
|
53
|
+
``` python
|
|
54
|
+
def load_data(self):
|
|
55
|
+
import json, os
|
|
56
|
+
|
|
57
|
+
path = os.path.join(self.workspace_dir, "data.json")
|
|
58
|
+
with open(path, encoding="utf-8") as f:
|
|
59
|
+
self.records = json.load(f)
|
|
60
|
+
```
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
# DataPlugin
|
|
2
|
+
|
|
3
|
+
`DataPlugin` is a higher-level wrapper around `BaseRecitePlugin` designed
|
|
4
|
+
for the common pattern *"a set of records × a set of question
|
|
5
|
+
directions"*. Declare your question types and Muninn generates all
|
|
6
|
+
problem variants automatically.
|
|
7
|
+
|
|
8
|
+
## Quick Example
|
|
9
|
+
|
|
10
|
+
``` python
|
|
11
|
+
import os, json
|
|
12
|
+
from typing import ClassVar
|
|
13
|
+
from core.helpers import DataPlugin, QuestionType, Matchers
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class Plugin(DataPlugin):
|
|
17
|
+
QUESTION_TYPES: ClassVar[list[QuestionType]] = [
|
|
18
|
+
QuestionType(
|
|
19
|
+
label="Symbol → Name",
|
|
20
|
+
statement=lambda el: f"Element: {el['sym']}",
|
|
21
|
+
answer=lambda el: el["name"],
|
|
22
|
+
matcher=Matchers.case_insensitive("name"),
|
|
23
|
+
),
|
|
24
|
+
QuestionType(
|
|
25
|
+
label="Name → Number",
|
|
26
|
+
statement=lambda el: f"Element: {el['name']}",
|
|
27
|
+
answer=lambda el: str(el["num"]),
|
|
28
|
+
matcher=Matchers.exact_integer("num"),
|
|
29
|
+
),
|
|
30
|
+
]
|
|
31
|
+
|
|
32
|
+
def load_records(self) -> list:
|
|
33
|
+
with open(
|
|
34
|
+
os.path.join(self.workspace_dir, "elements.json"), encoding="utf-8"
|
|
35
|
+
) as f:
|
|
36
|
+
return json.load(f)
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
With 36 elements and 2 question types, this automatically produces 72
|
|
40
|
+
problems — no manual ID generation, no dispatch tables.
|
|
41
|
+
|
|
42
|
+
## `QUESTION_TYPES`
|
|
43
|
+
|
|
44
|
+
A list of [`QuestionType` objects](#questiontype). Each represents one
|
|
45
|
+
question direction (e.g. "show symbol, ask name").
|
|
46
|
+
|
|
47
|
+
## `load_records()`
|
|
48
|
+
|
|
49
|
+
Override to return a list of data dicts from your workspace directory.
|
|
50
|
+
Each dict is passed to every `QuestionType`.
|
|
51
|
+
|
|
52
|
+
## `filter(record, q_type)`
|
|
53
|
+
|
|
54
|
+
Optional. Return `False` to skip a particular record × question-type
|
|
55
|
+
combination.
|
|
56
|
+
|
|
57
|
+
``` python
|
|
58
|
+
def filter(self, record, q_type):
|
|
59
|
+
if q_type.label == "Position → Element":
|
|
60
|
+
return record["group"] != "0" # skip noble gases
|
|
61
|
+
return True
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## `_resolve(problem_id)`
|
|
65
|
+
|
|
66
|
+
Returns `(record, QuestionType)` for a given problem ID. Useful when
|
|
67
|
+
overriding `get_expand_info()`:
|
|
68
|
+
|
|
69
|
+
``` python
|
|
70
|
+
def get_expand_info(self, problem_id: str) -> str:
|
|
71
|
+
el, qt = self._resolve(problem_id)
|
|
72
|
+
return f"{el['name']} — Period {el['period']}, Group {el['group']}"
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
## Problem ID Format
|
|
76
|
+
|
|
77
|
+
IDs are generated as `{record_index}__{question_label}`, e.g.
|
|
78
|
+
`0__Symbol → Name`. You generally don't need to interact with them
|
|
79
|
+
directly — they're used internally by the scheduler and state manager.
|
|
80
|
+
|
|
81
|
+
## `QuestionType`
|
|
82
|
+
|
|
83
|
+
`QuestionType` is a dataclass that encapsulates one question direction:
|
|
84
|
+
|
|
85
|
+
| Field | Type | Description |
|
|
86
|
+
| ----------- | --------------------- | ------------------------------------------------------------------------- |
|
|
87
|
+
| `label` | `str` | Human-readable name, shown in the problem header. |
|
|
88
|
+
| `statement` | `(dict) -> str` | Function that renders the question text from a record. |
|
|
89
|
+
| `answer` | `(dict) -> str` | Function that renders the expected answer from a record. |
|
|
90
|
+
| `matcher` | `(dict, str) -> bool` | Function that checks user input. Use [`Matchers`](matchers.md) factories. |
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# FlashcardPlugin
|
|
2
|
+
|
|
3
|
+
`FlashcardPlugin` is the simplest plugin type. It handles the entire
|
|
4
|
+
"front → back" flashcard pattern with zero boilerplate.
|
|
5
|
+
|
|
6
|
+
## Quickstart
|
|
7
|
+
|
|
8
|
+
Create a CSV with `front` and `back` columns:
|
|
9
|
+
|
|
10
|
+
``` csv
|
|
11
|
+
front,back
|
|
12
|
+
apple,苹果
|
|
13
|
+
dog,狗
|
|
14
|
+
cat,猫
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Then write a 3-line plugin:
|
|
18
|
+
|
|
19
|
+
``` python
|
|
20
|
+
from core.helpers import FlashcardPlugin
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class Plugin(FlashcardPlugin):
|
|
24
|
+
DATA_FILE = "words.csv"
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
That's it. `FlashcardPlugin` automatically:
|
|
28
|
+
|
|
29
|
+
- Loads `words.csv` from the workspace directory.
|
|
30
|
+
- Generates one problem per row.
|
|
31
|
+
- Renders `front` as the question, checks against `back`.
|
|
32
|
+
|
|
33
|
+
## Supported Formats
|
|
34
|
+
|
|
35
|
+
| Extension | Format |
|
|
36
|
+
| --------- | -------------------------------------------------------- |
|
|
37
|
+
| `.csv` | CSV with `front` and `back` columns. |
|
|
38
|
+
| `.json` | JSON array of `{"front": "...", "back": "..."}` objects. |
|
|
39
|
+
|
|
40
|
+
## Customisation
|
|
41
|
+
|
|
42
|
+
Override `get_expand_info()` to show extra context on correct answers:
|
|
43
|
+
|
|
44
|
+
``` python
|
|
45
|
+
class Plugin(FlashcardPlugin):
|
|
46
|
+
DATA_FILE = "words.csv"
|
|
47
|
+
|
|
48
|
+
def get_expand_info(self, problem_id: str) -> str:
|
|
49
|
+
record, _ = self._resolve(problem_id)
|
|
50
|
+
return record.get("example", "")
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## File Layout
|
|
54
|
+
|
|
55
|
+
``` bash
|
|
56
|
+
my-flashcards/
|
|
57
|
+
├── manifest.json
|
|
58
|
+
├── plugin.py # 3 lines
|
|
59
|
+
└── words.csv # front, back columns
|
|
60
|
+
```
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# Matchers
|
|
2
|
+
|
|
3
|
+
Matchers are reusable answer-checking functions. Instead of writing
|
|
4
|
+
custom regex for every question type, use the built-in `Matchers`
|
|
5
|
+
factories.
|
|
6
|
+
|
|
7
|
+
## Built-in Matchers
|
|
8
|
+
|
|
9
|
+
### `Matchers.exact(key)`
|
|
10
|
+
|
|
11
|
+
Exact match after stripping whitespace.
|
|
12
|
+
|
|
13
|
+
``` python
|
|
14
|
+
# record: {"name": "hydrogen"}
|
|
15
|
+
Matchers.exact("name")
|
|
16
|
+
# user types "hydrogen" → True
|
|
17
|
+
# user types "Hydrogen" → False
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
### `Matchers.exact_integer(key)`
|
|
21
|
+
|
|
22
|
+
Extract digits from the user input and compare as strings (after stripping non-digit characters from the input). Note: this is string-based comparison, so leading zeros are preserved (e.g., "007" != "7").
|
|
23
|
+
|
|
24
|
+
``` python
|
|
25
|
+
# record: {"num": 17}
|
|
26
|
+
Matchers.exact_integer("num")
|
|
27
|
+
# user types " 17 " → True
|
|
28
|
+
# user types "#17" → True
|
|
29
|
+
# user types "18" → False
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
### `Matchers.case_insensitive(key)`
|
|
33
|
+
|
|
34
|
+
Case-insensitive match after trimming whitespace.
|
|
35
|
+
|
|
36
|
+
``` python
|
|
37
|
+
# record: {"sym": "He"}
|
|
38
|
+
Matchers.case_insensitive("sym")
|
|
39
|
+
# user types "he" → True
|
|
40
|
+
# user types "HE" → True
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
### `Matchers.chinese_symbol_pair(key1, key2)`
|
|
44
|
+
|
|
45
|
+
Match "Chinese+symbol" or "symbol+Chinese" in either order, ignoring
|
|
46
|
+
whitespace.
|
|
47
|
+
|
|
48
|
+
``` python
|
|
49
|
+
# record: {"name": "氢", "sym": "H"}
|
|
50
|
+
Matchers.chinese_symbol_pair("name", "sym")
|
|
51
|
+
# user types "氢H" → True
|
|
52
|
+
# user types "H氢" → True
|
|
53
|
+
# user types "氢 H" → True
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
### `Matchers.any_order(*keys)`
|
|
57
|
+
|
|
58
|
+
Match when all field values appear somewhere in the input, ignoring
|
|
59
|
+
non-alphanumeric characters and case. Only ASCII letters and digits are retained during normalization; Unicode characters are discarded.
|
|
60
|
+
|
|
61
|
+
``` python
|
|
62
|
+
# record: {"period": 4, "group": "IVB"}
|
|
63
|
+
Matchers.any_order("period", "group")
|
|
64
|
+
# user types "4 IVB" → True
|
|
65
|
+
# user types "IVB4" → True
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
### `Matchers.custom(fn)`
|
|
69
|
+
|
|
70
|
+
Pass your own function `(data_item: dict, user_input: str) -> bool`.
|
|
71
|
+
|
|
72
|
+
``` python
|
|
73
|
+
def my_matcher(record, user_input):
|
|
74
|
+
# custom logic
|
|
75
|
+
return True
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
Matchers.custom(my_matcher)
|
|
79
|
+
```
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
# Command Reference
|
|
2
|
+
|
|
3
|
+
## `muninn install`
|
|
4
|
+
|
|
5
|
+
Install a reciting pack.
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
muninn install <source>
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
`<source>` accepts:
|
|
12
|
+
|
|
13
|
+
| Format | Example |
|
|
14
|
+
| ------------------------- | --------------------------------------- |
|
|
15
|
+
| Local directory | `./my-pack` or `/abs/path` |
|
|
16
|
+
| Local `.zip` file | `./my-pack.zip` |
|
|
17
|
+
| GitHub shorthand | `user/repo` (defaults to `main` branch) |
|
|
18
|
+
| GitHub shorthand + branch | `user/repo@dev` |
|
|
19
|
+
| Full GitHub URL | `https://github.com/user/repo` |
|
|
20
|
+
|
|
21
|
+
The pack is copied to `~/.muninn/packs/<pack_id>/`. Installing the same
|
|
22
|
+
`pack_id` again overwrites the previous version.
|
|
23
|
+
|
|
24
|
+
Muninn records the installation source in the pack's `manifest.json` so
|
|
25
|
+
that `muninn upgrade` knows where to check for updates later.
|
|
26
|
+
|
|
27
|
+
## `muninn upgrade`
|
|
28
|
+
|
|
29
|
+
Check for and install newer versions of installed packs.
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
# Upgrade all installed packs
|
|
33
|
+
muninn upgrade
|
|
34
|
+
|
|
35
|
+
# Upgrade a specific pack
|
|
36
|
+
muninn upgrade <pack_id>
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
How it works:
|
|
40
|
+
|
|
41
|
+
| Source type | Upgrade strategy |
|
|
42
|
+
| --------------------------- | ----------------------------------------------------------------------------------------------------------------------------------- |
|
|
43
|
+
| GitHub (`github:user/repo`) | Fetches `manifest.json` from the repository's default branch. If the remote version is newer, the pack is re-installed from GitHub. |
|
|
44
|
+
| Local path (`local:/path`) | Reads `manifest.json` from the source directory. If it no longer exists, the pack is skipped with a warning. |
|
|
45
|
+
|
|
46
|
+
Packs installed before Muninn 0.3.0 do not have a `source` field and
|
|
47
|
+
cannot be upgraded — reinstall them with `muninn install` to enable
|
|
48
|
+
upgrades.
|
|
49
|
+
|
|
50
|
+
## `muninn uninstall`
|
|
51
|
+
|
|
52
|
+
Remove a previously installed pack.
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
muninn uninstall <pack_id>
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Deletes `~/.muninn/packs/<pack_id>/`. Progress data in
|
|
59
|
+
`~/.muninn/states/<pack_id>.db` is **not** deleted — you can reinstall
|
|
60
|
+
later and pick up where you left off.
|
|
61
|
+
|
|
62
|
+
## `muninn list`
|
|
63
|
+
|
|
64
|
+
List all installed packs with their metadata (id, name, author, version).
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
muninn list
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
## `muninn run`
|
|
71
|
+
|
|
72
|
+
Start a reciting session for a pack.
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
muninn run <pack_id>
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
The scheduler presents problems based on your historical accuracy and
|
|
79
|
+
response time, favouring topics you struggle with.
|
|
80
|
+
|
|
81
|
+
During a session:
|
|
82
|
+
|
|
83
|
+
- Type your answer and press Enter.
|
|
84
|
+
- Type `q` or press `Ctrl+C` to quit and see your session summary.
|
|
85
|
+
|
|
86
|
+
## `muninn new`
|
|
87
|
+
|
|
88
|
+
Generate a new plugin template in the current directory.
|
|
89
|
+
|
|
90
|
+
```bash
|
|
91
|
+
muninn new <pack_name>
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
Creates a `<pack_name>/` folder containing:
|
|
95
|
+
|
|
96
|
+
- `manifest.json` — pack metadata
|
|
97
|
+
- `plugin.py` — a skeleton `DataPlugin` with import scaffolding
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# Getting Started
|
|
2
|
+
|
|
3
|
+
## Installation
|
|
4
|
+
|
|
5
|
+
Install Muninn globally with `uv` (recommended) or `pip`:
|
|
6
|
+
|
|
7
|
+
``` bash
|
|
8
|
+
uv tool install muninn-cli
|
|
9
|
+
# or
|
|
10
|
+
pip install muninn-cli
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Basic Commands
|
|
14
|
+
|
|
15
|
+
``` bash
|
|
16
|
+
# Install a pack from a local directory, zip file, or GitHub repo
|
|
17
|
+
muninn install ./my-pack
|
|
18
|
+
muninn install user/repo
|
|
19
|
+
muninn install user/repo@dev
|
|
20
|
+
muninn install https://github.com/user/repo
|
|
21
|
+
|
|
22
|
+
# List installed packs
|
|
23
|
+
muninn list
|
|
24
|
+
|
|
25
|
+
# Run a pack
|
|
26
|
+
muninn run <pack_id>
|
|
27
|
+
|
|
28
|
+
# Uninstall a pack
|
|
29
|
+
muninn uninstall <pack_id>
|
|
30
|
+
|
|
31
|
+
# Create a new plugin template
|
|
32
|
+
muninn new <pack_name>
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Data Directory
|
|
36
|
+
|
|
37
|
+
Muninn stores all user data under `~/.muninn/`:
|
|
38
|
+
|
|
39
|
+
``` bash
|
|
40
|
+
~/.muninn/
|
|
41
|
+
├── packs/ # Installed reciting packs
|
|
42
|
+
│ └── chemistry/
|
|
43
|
+
└── states/ # Learning progress (SQLite)
|
|
44
|
+
└── chemistry.db
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Progress is per-pack — uninstalling a pack keeps your data unless you
|
|
48
|
+
delete it manually.
|