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.
Files changed (57) hide show
  1. {muninn_cli-0.2.0 → muninn_cli-0.3.0}/.github/workflows/ci.yml +4 -0
  2. muninn_cli-0.3.0/.github/workflows/deploy-docs.yml +52 -0
  3. {muninn_cli-0.2.0 → muninn_cli-0.3.0}/.gitignore +6 -0
  4. muninn_cli-0.3.0/PKG-INFO +71 -0
  5. muninn_cli-0.3.0/README.md +51 -0
  6. muninn_cli-0.3.0/README_zh.md +51 -0
  7. muninn_cli-0.3.0/docs/api/base-plugin.md +60 -0
  8. muninn_cli-0.3.0/docs/api/data-plugin.md +90 -0
  9. muninn_cli-0.3.0/docs/api/flashcard.md +60 -0
  10. muninn_cli-0.3.0/docs/api/matchers.md +79 -0
  11. muninn_cli-0.3.0/docs/commands.md +97 -0
  12. muninn_cli-0.3.0/docs/getting-started.md +48 -0
  13. muninn_cli-0.3.0/docs/index.md +17 -0
  14. muninn_cli-0.3.0/docs/pack-spec.md +67 -0
  15. muninn_cli-0.3.0/docs/plugin-dev.md +50 -0
  16. {muninn_cli-0.2.0 → muninn_cli-0.3.0}/pyproject.toml +4 -1
  17. {muninn_cli-0.2.0 → muninn_cli-0.3.0}/src/cli/manager.py +206 -9
  18. {muninn_cli-0.2.0 → muninn_cli-0.3.0}/src/cli/runner.py +2 -1
  19. {muninn_cli-0.2.0 → muninn_cli-0.3.0}/src/main.py +33 -0
  20. {muninn_cli-0.2.0 → muninn_cli-0.3.0}/src/ui.py +27 -0
  21. {muninn_cli-0.2.0 → muninn_cli-0.3.0}/tests/cli/test_manager.py +131 -105
  22. muninn_cli-0.3.0/uv.lock +351 -0
  23. muninn_cli-0.3.0/zensical.toml +52 -0
  24. muninn_cli-0.2.0/.agent/api.md +0 -193
  25. muninn_cli-0.2.0/.agent/plan.md +0 -124
  26. muninn_cli-0.2.0/PKG-INFO +0 -203
  27. muninn_cli-0.2.0/README.md +0 -183
  28. muninn_cli-0.2.0/README_zh.md +0 -181
  29. muninn_cli-0.2.0/uv.lock +0 -108
  30. {muninn_cli-0.2.0 → muninn_cli-0.3.0}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
  31. {muninn_cli-0.2.0 → muninn_cli-0.3.0}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
  32. {muninn_cli-0.2.0 → muninn_cli-0.3.0}/.github/workflows/publish.yml +0 -0
  33. {muninn_cli-0.2.0 → muninn_cli-0.3.0}/.python-version +0 -0
  34. {muninn_cli-0.2.0 → muninn_cli-0.3.0}/.vscode/settings.json +0 -0
  35. {muninn_cli-0.2.0 → muninn_cli-0.3.0}/LICENSE +0 -0
  36. {muninn_cli-0.2.0 → muninn_cli-0.3.0}/example/chemistry/elements.json +0 -0
  37. {muninn_cli-0.2.0 → muninn_cli-0.3.0}/example/chemistry/manifest.json +0 -0
  38. {muninn_cli-0.2.0 → muninn_cli-0.3.0}/example/chemistry/plugin.py +0 -0
  39. {muninn_cli-0.2.0 → muninn_cli-0.3.0}/example/flashcard/manifest.json +0 -0
  40. {muninn_cli-0.2.0 → muninn_cli-0.3.0}/example/flashcard/plugin.py +0 -0
  41. {muninn_cli-0.2.0 → muninn_cli-0.3.0}/example/flashcard/words.csv +0 -0
  42. {muninn_cli-0.2.0 → muninn_cli-0.3.0}/main.py +0 -0
  43. {muninn_cli-0.2.0 → muninn_cli-0.3.0}/scripts/bump_version.py +0 -0
  44. {muninn_cli-0.2.0 → muninn_cli-0.3.0}/src/__init__.py +0 -0
  45. {muninn_cli-0.2.0 → muninn_cli-0.3.0}/src/cli/__init__.py +0 -0
  46. {muninn_cli-0.2.0 → muninn_cli-0.3.0}/src/core/__init__.py +0 -0
  47. {muninn_cli-0.2.0 → muninn_cli-0.3.0}/src/core/base_plugin.py +0 -0
  48. {muninn_cli-0.2.0 → muninn_cli-0.3.0}/src/core/helpers.py +0 -0
  49. {muninn_cli-0.2.0 → muninn_cli-0.3.0}/src/core/scheduler.py +0 -0
  50. {muninn_cli-0.2.0 → muninn_cli-0.3.0}/src/core/state.py +0 -0
  51. {muninn_cli-0.2.0 → muninn_cli-0.3.0}/tests/__init__.py +0 -0
  52. {muninn_cli-0.2.0 → muninn_cli-0.3.0}/tests/cli/__init__.py +0 -0
  53. {muninn_cli-0.2.0 → muninn_cli-0.3.0}/tests/conftest.py +0 -0
  54. {muninn_cli-0.2.0 → muninn_cli-0.3.0}/tests/core/__init__.py +0 -0
  55. {muninn_cli-0.2.0 → muninn_cli-0.3.0}/tests/core/test_helpers.py +0 -0
  56. {muninn_cli-0.2.0 → muninn_cli-0.3.0}/tests/core/test_scheduler.py +0 -0
  57. {muninn_cli-0.2.0 → muninn_cli-0.3.0}/tests/core/test_state.py +0 -0
@@ -1,5 +1,9 @@
1
1
  name: CI
2
2
 
3
+ concurrency:
4
+ group: ${{ github.workflow }}-${{ github.ref }}
5
+ cancel-in-progress: true
6
+
3
7
  on:
4
8
  push:
5
9
  branches: ["main"]
@@ -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
@@ -216,3 +216,9 @@ __marimo__/
216
216
 
217
217
  # Streamlit
218
218
  .streamlit/secrets.toml
219
+
220
+ # Zensical
221
+ site/
222
+
223
+ # Agent files
224
+ .agent/
@@ -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) &nbsp;|&nbsp;[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) &nbsp;|&nbsp;[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) &nbsp;|&nbsp;[完整文档](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.