Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ The MiniMax Code Agent communicates through the host MCP client. Business reques
| [A/时差保安 / Timezone Guard](plugins/liuchen19960902/timezone-guard/) | Check the other side's local time before you message: the target city's clock turns into a green "now is fine", yellow "keep it short", or red "go to sleep" verdict. Requires MiniMax Code 3.1.2 or later | [FineCat](https://github.com/LiuChen19960902) |
| [AI Node Canvas](plugins/antianqi/ai-node-canvas/) | An infinite-canvas node graph for MiniMax Code: wire text, image, video and audio runs into a graph, let each result flow downstream, and run media tools on any result | [antianqi](https://github.com/antianqi) |
| [Agent Flow](plugins/huixin615/agent-flow/) | Import this plugin's Hook event file to inspect native task calls and sub-agent lifecycles, with exact task-to-child links when IDs are available. Choose a recorded chat or the latest imported record; no Host session files are opened. Requires MiniMax Code 3.1.4 or later | [Huixin615](https://github.com/Huixin615) |
| [蹬王之王 · Expiry Guardian](plugins/hylouis233/expiry-guardian/) | Track credit and reset-card expiry from a local ledger you maintain, and get a "spend the card or let the credits burn first" recommendation with the reasoning spelled out | [Hylouis](https://github.com/Hylouis233) |

<details>
<summary>Preview: Token Usage Board</summary>
Expand Down
1 change: 1 addition & 0 deletions README.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ MiniMax Code Agent 通过宿主 MCP 客户端与 MiniApp 协作。业务请求
| [A/时差保安](plugins/liuchen19960902/timezone-guard/README.zh-CN.md) | 发消息前先看对方那边几点:按对方城市当地时间给出「现在可以聊 / 短聊就好 / 先睡,别发」三档判定。需要 MiniMax Code 3.1.2 或更高版本 | [FineCat](https://github.com/LiuChen19960902) |
| [AI 节点画布](plugins/antianqi/ai-node-canvas/README.zh-CN.md) | MiniMax Code 里的无限画布节点图:把文本、图片、视频、音频的生成接成一张图,结果顺着连线往下传,任何一张结果都能直接做二次处理 | [antianqi](https://github.com/antianqi) |
| [Agent 协作透视](plugins/huixin615/agent-flow/README.zh-CN.md) | 手动导入插件 Hook 记录,查看原生 task 调用与子 Agent 运行区间;有明确 ID 时关联父子任务。可选择记录会话或一键查看最近导入的记录,不读取宿主会话文件。需要 MiniMax Code 3.1.4 或更高版本 | [Huixin615](https://github.com/Huixin615) |
| [蹬王之王](plugins/hylouis233/expiry-guardian/README.md) | 看你自己维护的本地账本,算出「该用卡还是该留积分」,并把理由说清楚 | [Hylouis](https://github.com/Hylouis233) |

<details>
<summary>预览:Token 用量看板</summary>
Expand Down
3 changes: 3 additions & 0 deletions plugins/hylouis233/expiry-guardian/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Python 字节码缓存(跑测试时会重新生成,不该进版本库)
__pycache__/
*.pyc
21 changes: 21 additions & 0 deletions plugins/hylouis233/expiry-guardian/.minimax-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{
"schemaVersion": 1,
"name": "expiry-guardian",
"displayName": "蹬王之王",
"version": "2.0.0",
"description": "基于你自己维护的本地账本追踪积分与重置卡的到期时间,并给出「先过期先用」的建议:用卡还是留积分,并把理由说清楚。只读,不做任何写操作 —— 不扣卡、不暂停目标,不读 MiniMax Code 的登录凭据或内部数据库,也不读其它插件的数据。",
"author": "User",
"icon": "icon.png",
"category": "Productivity",
"exampleQueries": [
"打开蹬王之王,看我的积分和重置卡什么时候过期。",
"我的额度快用完了,该用重置卡还是留着?",
"看看我有多少积分要过期了。"
],
"apps": [],
"mcpServers": [
"servers.mcp.json"
],
"skills": [],
"darkIcon": "icon-dark.png"
}
21 changes: 21 additions & 0 deletions plugins/hylouis233/expiry-guardian/LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
MIT License

Copyright (c) 2026 Hylouis

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
210 changes: 210 additions & 0 deletions plugins/hylouis233/expiry-guardian/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,210 @@
# 蹬王之王 · Local Expiry Ledger & Card Decision

Track when your credits and reset cards expire, and decide — from a **ledger you keep
yourself** — whether to spend a card or let the credits burn first.

**v2.0 (trimmed) — 2026-10-20.**

## What it does

| | |
| --- | --- |
| **Local ledger** | The only data source: `~/.config/mcode-reset-card/ledger.json`, which *you* maintain. Edit it in the panel or in any editor. |
| **"Expiring first" decision** | When the 5-hour quota crosses the trigger line, it compares expiry dates and says whether to spend a card or keep the credits — with the reasoning spelled out. |
| **Honest failures** | If the ledger can't be read, it says so plainly instead of quietly concluding "you have nothing". |
| **Backend daemon** | A Rust daemon under `launchd` re-runs the same decision on a timer and writes the conclusion to a status file. |

## What it deliberately does *not* do

Community plugins must not touch the host's private data. So this version:

| Boundary | v1.x (removed) | v2.0 |
| --- | --- | --- |
| **No reading MiniMax Code's login credentials** | Read the lease socket / `.cap` for an access token and called the account APIs | **Doesn't.** The ledger is the only data source |
| **No reading/writing the host's internal database** | `UPDATE`/`DELETE`/`INSERT` on goals and the execution queue, and read turn records | **Doesn't touch it.** Only reads its own directory |
| **No reading other plugins' data directories** | Read `minimax-bot`'s `state.json` for bot and session ids, then messaged those sessions | **Doesn't.** The Bot watchdog is gone |

⇒ **v2.0 never spends a card, never pauses a goal, never wakes a Bot.** It computes the
conclusion and explains it; **you** do the action.

⇒ The upside: v2.0 is code that can live in a community repository. It touches no
private host data and its behaviour is fully predictable.

## The ledger

```json
{
"schemaVersion": 1,
"updatedAtMs": 1792480000000,
"quota": {
"fiveHour": { "usedPercent": 3, "totalPercent": 100, "unlimited": false, "resetTimeMs": 1792497600000 },
"weekly": { "usedPercent": 0, "totalPercent": 100, "unlimited": true, "resetTimeMs": 1792944000000 }
},
"cards": [
{ "cardId": "card_1", "label": "Card A", "sourceType": "code",
"outBizNo": "biz_1", "expireTimeMs": 1795000000000 }
],
"credits": [
{ "label": "Granted", "source": "code", "granted": 400, "used": 0,
"balance": 400, "expireTimeMs": 1795017600000 }
]
}
```

A card counts as usable only if it has **both** `outBizNo` and `sourceType`.
A credit counts only if `balance > 0` and it has not expired.

> ⚠️ **A missing `credits` field does not mean "no credits".**
> A missing field is treated as *incomplete data* ⇒ the conclusion stays "hold off".
> It will **never** recommend burning a real card because it couldn't read the data.

The panel has a ledger editor at the bottom — edit the JSON there and save, or edit
the file in any editor and hit refresh.

## The decision

| Situation | Conclusion |
| --- | --- |
| Quota below the trigger line | Hold — nothing to do |
| At the line, no usable credits, card available | **Spend a card** |
| At the line, no usable credits, no card | Hold |
| At the line, credits available, no card | **Keep the credits** |
| At the line, both, card expires first | **Spend a card** |
| At the line, both, credits expire first | **Keep the credits** |
| At the line, but the quota refreshes within N minutes | Hold — spending a card would be pure waste |
| Data incomplete | Hold — better no advice than wrong advice |

## Three-way consistency

The decision logic has **three isomorphic implementations**:

| Implementation | Where | Role |
| --- | --- | --- |
| Rust | `daemon/rust/src/decision.rs` | The backend daemon |
| Mini App | `miniapp/node/server.mjs` (`decide()`) | The dashboard |
| Python | `daemon/reset_card_autouse.py` (`decide_background()`) | The **reference** (pure, no network) |

`daemon/verify-parity.mjs` feeds all three the *same* cases and compares the
action **and the reason text, character for character**:

```
$ node daemon/verify-parity.mjs
19/19 三方完全一致(动作 + 理由文本逐字)
```

The reason text has to match character for character — change one side and you must
change all three, or the checker goes red. That check is the most valuable guard in
this project: it stops the three implementations from slowly drifting apart until
they contradict each other.

## Backend daemon

The Rust daemon runs under `launchd` and is **read-only**: it reads the ledger, runs
the decision, and writes the conclusion to the log and to
`~/.config/mcode-reset-card/daemon-status.json`. It performs **no write operations**.

If the ledger can't be read it says so explicitly ("no conclusion this round") and
escalates the warning as consecutive failures pile up — it never pretends the answer
is "you have no cards and no credits".

```bash
cd daemon/rust && cargo test --release # 25/25
./reset-card-daemon once # one round, prints the conclusion
./reset-card-daemon status # print the current conclusion only
```

## Local MCP endpoint

The plugin declares one MCP endpoint (`server=expiry-guardian`,
`path=/mcp/expiry-guardian`) so other plugins can ask it directly:

| Tool | What it does |
| --- | --- |
| `watchdog_status` | Quota, cards, credits and the current conclusion from the ledger |
| `watchdog_decide` | Decide right now — **decides only, never spends a card** |

⚠️ **Spending a card is deliberately not exposed**: it is a real write operation, and
letting another plugin spend your card on a whim is too dangerous.

## Configuration

`~/.config/mcode-reset-card/config.json` (read by Rust, the Mini App and Python):

```json
{
"schemaVersion": 1,
"cardTriggerPercent": 99,
"cardSkipBeforeResetMinutes": 60,
"quotaLevels": { "tight": 80, "low": 95 }
}
```

Config fields are **rebuilt from a whitelist** (`normalizeConfig`): a missing field is
a silently dead channel, so the config shape has to be changed in **three** places
(Rust config, the Mini App's `normalizeConfig`, and the real `config.json`).

## Tests

| Suite | Result |
| --- | --- |
| Rust (`daemon/rust`) | **25/25** |
| Python decision unit tests | **20/20** |
| Mini App (`tests/`) | **102/102** |
| Three-way parity | **19/19** |
| Upstream `npm run validate` | **OK** for this plugin (0 errors, 0 warnings); `repository: OK` |
| Upstream `npm test` | 69/69 |

One Mini App test is a **static guard** (`tests/server-lint.test.mjs`): it catches
identifiers that are used but never declared. It exists because of a real incident —
a typo in the dashboard's data layer made the whole snapshot throw, so the panel
(quota, decision, cards) was **completely broken**, while the unit tests kept passing
because they only cover the pure `decide()` function. The data layer itself was never
executed. The guard makes that class of error fail loudly.

## What v1.x used to do

The full history is in git (the v1.1–v1.6 commits). v1.x spent cards, paused goals,
and revived crashed Bots by reading the account API and other plugins' data — all of
which a community plugin must not do.

The hardening v1.x added — tolerant JSON parsing, judging "is it broken?" by the
latest *turn* rather than a stale error code, per-instance request ids, failure
counters that reset only on success, the static guard — **survived into v2.0**; it
just guards the local ledger now instead of the host's private state.

## Tested environment

- **MiniMax Code 3.1.2 or later.**
- Verified on macOS; the upstream checks run on **Node 20**.
- The Rust daemon needs a Rust toolchain to build (`cargo build --release`).

## Data & access

This plugin reads and writes **only its own directory**:

| Path | What |
| --- | --- |
| `~/.config/mcode-reset-card/ledger.json` | The ledger **you** maintain (read by the panel, writable from the panel's editor) |
| `~/.config/mcode-reset-card/config.json` | Shared configuration (trigger lines) |
| `~/.config/mcode-reset-card/daemon-status.json` | The backend daemon's latest conclusion |

It deliberately does **not**:

- read MiniMax Code's **login credentials** (no lease socket, no `.cap` file, no token on disk)
- open or write **`~/.minimax/v2/sqlite/runtime-state.sqlite`** (the host's internal database)
- read **another plugin's data directory** (e.g. `~/.minimax/v2/plugin-data/liveboards/*/state.json`)

It performs **no write operations** outside its own directory: it never spends a
reset card, never pauses a goal, and never writes into another plugin's sessions.
The backend daemon is optional — the dashboard works on its own.

## Boundaries

- **No host credentials, no host database, no other plugins' data** (hard constraints)
- **No write operations** — spending a card and pausing a goal are yours to click
- The conclusion is advice only: it is based on a ledger *you* wrote
- Incomplete data yields *no* conclusion rather than a guess

## Documentation

- [docs/README.zh-CN.md](docs/README.zh-CN.md) — Chinese documentation
51 changes: 51 additions & 0 deletions plugins/hylouis233/expiry-guardian/check-test-parity.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
#!/usr/bin/env bash
# 校验「工作区源码」与「插件包」**测试文件**保持一致。
#
# 2026-10-20 反复踩到的坑:工作区(miniapps/expiry-guardian/)与插件包
# (plugins/hylouis233/reset-card-autouse/)各有一份测试,路径布局还不同
# (工作区是 `./x.mjs`,插件包是 `../miniapp/node/x.mjs`)。
# 改了一边忘了另一边 ⇒ 插件包跑的是旧测试,出现「17/18」这种看不懂的结果。
#
# ⇒ 这个脚本只比**测试文件的内容**(忽略 import 路径差异),
# 不一致就报错并指出是哪几个文件。
set -euo pipefail

WORKSPACE="${WORKSPACE:-$HOME/Desktop/mmx/miniapps/expiry-guardian}"
PLUGIN="${PLUGIN:-$(cd "$(dirname "$0")" && pwd)}"

# 插件包里测试在 tests/,工作区在 miniapp/node/。
PAIRS=(
"config-fidelity.test.mjs"
"decision.test.mjs"
"mcp.test.mjs"
"server-lint.test.mjs"
)

fail=0
for f in "${PAIRS[@]}"; do
a="$WORKSPACE/miniapp/node/$f"
b="$PLUGIN/tests/$f"
if [ ! -f "$a" ] || [ ! -f "$b" ]; then
echo " SKIP $f(缺文件)"
continue
fi
# 归一化:去掉 import 路径差异与空白差异。
na=$(sed -E "s#\.\./miniapp/node/##g; s#'\./#'#g" "$a" | tr -d ' \t\r\n')
nb=$(sed -E "s#\.\./miniapp/node/##g; s#'\./#'#g" "$b" | tr -d ' \t\r\n')
if [ "$na" = "$nb" ]; then
echo " OK $f"
else
echo " DIFF $f ← 工作区与插件包不一致"
diff <(sed -E "s#\.\./miniapp/node/##g; s#'\./#'#g" "$a") \
<(sed -E "s#\.\./miniapp/node/##g; s#'\./#'#g" "$b") | head -12 || true
fail=1
fi
done

if [ "$fail" -ne 0 ]; then
echo
echo "❌ 工作区与插件包的测试文件不一致 —— 改完记得两边都同步。"
exit 1
fi
echo
echo "✅ 工作区与插件包的测试文件一致"
Loading
Loading