<!-- Source: https://hermesbible.com/docs/developer-guide/extending-the-cli -->
Hermes 在 HermesCLI 上暴露受保護的擴充 hook,讓包裝 CLI 可以新增小工具、鍵綁定和版面自訂,而無需覆寫 1000 多行的 run() 方法。這使你的擴充與內部變更解耦。
擴充點
有五個可用的擴充接縫:
| Hook | 用途 | 何時覆寫… |
|---|---|---|
_get_extra_tui_widgets() | 將小工具注入到版面中 | 你需要持久的 UI 元素(面板、狀態列、迷你播放器) |
_register_extra_tui_keybindings(kb, *, input_area) | 新增鍵盤快捷鍵 | 你需要熱鍵(切換面板、傳輸控制、模態快捷鍵) |
_build_tui_layout_children(**widgets) | 完全控制小工具排序 | 你需要重新排序或包裝現有小工具(少見) |
process_command() | 新增自訂斜線命令 | 你需要 /mycommand 處理(已存在的 hook) |
_build_tui_style_dict() | 自訂 prompt_toolkit 樣式 | 你需要自訂顏色或樣式(已存在的 hook) |
前三個是新的受保護 hook。後兩個已存在。
快速開始:包裝 CLI
#!/usr/bin/env python3
"""my_cli.py — 擴展 Hermes 的範例包裝 CLI。"""
from cli import HermesCLI
from prompt_toolkit.layout import FormattedTextControl, Window
from prompt_toolkit.filters import Condition
class MyCLI(HermesCLI):
def __init__(self, **kwargs):
super().__init__(**kwargs)
self._panel_visible = False
def _get_extra_tui_widgets(self):
"""在狀態列上方新增一個可切換的資訊面板。"""
cli_ref = self
return [
Window(
FormattedTextControl(lambda: "📊 My custom panel content"),
height=1,
filter=Condition(lambda: cli_ref._panel_visible),
),
]
def _register_extra_tui_keybindings(self, kb, *, input_area):
"""F2 切換自訂面板。"""
cli_ref = self
@kb.add("f2")
def _toggle_panel(event):
cli_ref._panel_visible = not cli_ref._panel_visible
def process_command(self, cmd: str) -> bool:
"""新增 /panel 斜線命令。"""
if cmd.strip().lower() == "/panel":
self._panel_visible = not self._panel_visible
state = "visible" if self._panel_visible else "hidden"
print(f"Panel is now {state}")
return True
return super().process_command(cmd)
if __name__ == "__main__":
cli = MyCLI()
cli.run()
執行它:
cd ~/.hermes/hermes-agent
source .venv/bin/activate
python my_cli.py
Hook 參考
_get_extra_tui_widgets()
回傳要插入 TUI 版面的 prompt_toolkit 小工具列表。小工具出現在間距和狀態列之間——在輸入區域上方但主要輸出下方。
def _get_extra_tui_widgets(self) -> list:
return [] # 預設:無額外小工具
每個小工具應是一個 prompt_toolkit 容器(例如 Window、ConditionalContainer、HSplit)。使用 ConditionalContainer 或 filter=Condition(...) 使小工具可切換。
from prompt_toolkit.layout import ConditionalContainer, Window, FormattedTextControl
from prompt_toolkit.filters import Condition
def _get_extra_tui_widgets(self):
return [
ConditionalContainer(
Window(FormattedTextControl("Status: connected"), height=1),
filter=Condition(lambda: self._show_status),
),
]
_register_extra_tui_keybindings(kb, *, input_area)
在 Hermes 註冊自己的鍵綁定後、版面建構前呼叫。將你的鍵綁定加入 kb。
def _register_extra_tui_keybindings(self, kb, *, input_area):
pass # 預設:無額外鍵綁定
參數:
kb— prompt_toolkit 應用程式的KeyBindings實例input_area— 主TextArea小工具,若你需要讀取或操作使用者輸入
def _register_extra_tui_keybindings(self, kb, *, input_area):
cli_ref = self
@kb.add("f3")
def _clear_input(event):
input_area.text = ""
@kb.add("f4")
def _insert_template(event):
input_area.text = "/search "
避免與內建鍵綁定衝突:Enter(提交)、Escape Enter(換行)、Ctrl-C(中斷)、Ctrl-D(退出)、Tab(自動建議接受)。功能鍵 F2+ 和 Ctrl 組合通常安全。
_build_tui_layout_children(**widgets)
僅在你需要完全控制小工具排序時覆寫此方法。大多數擴充應改用 _get_extra_tui_widgets()。
def _build_tui_layout_children(self, *, sudo_widget, secret_widget,
approval_widget, clarify_widget, model_picker_widget=None,
spinner_widget=None, spacer, status_bar, input_rule_top,
image_bar, input_area, input_rule_bot, voice_status_bar,
completions_menu) -> list:
預設實作回傳(任何 None 小工具被過濾):
[
Window(height=0), # 錨點
sudo_widget, # sudo 密碼提示(條件式)
secret_widget, # 密鑰輸入提示(條件式)
approval_widget, # 危險命令審核(條件式)
clarify_widget, # clarify 問題 UI(條件式)
model_picker_widget, # 模型選擇器覆蓋(條件式)
spinner_widget, # 思考旋轉器(條件式)
spacer, # 填充剩餘垂直空間
*self._get_extra_tui_widgets(), # 你的小工具在這裡
status_bar, # 模型/token/上下文狀態列
input_rule_top, # ─── 輸入上方邊界
image_bar, # 附加圖片指示器
input_area, # 使用者文字輸入
input_rule_bot, # ─── 輸入下方邊界
voice_status_bar, # 語音模式狀態(條件式)
completions_menu, # 自動完成下拉選單
]
版面圖
預設版面從上到下:
- 輸出區域 — 捲動的對話歷史
- 間距
- 額外小工具 — 來自
_get_extra_tui_widgets() - 狀態列 — 模型、上下文 %、已用時間
- 圖片列 — 附加圖片計數
- 輸入區域 — 使用者提示
- 語音狀態 — 錄音指示器
- 自動完成選單 — 自動完成建議
提示
- 狀態變更後使顯示失效:呼叫
self._invalidate()以觸發 prompt_toolkit 重繪。 - 存取 Agent 狀態:
self.agent、self.model、self.conversation_history都可用。 - 自訂樣式:覆寫
_build_tui_style_dict()並為你的自訂樣式類別新增項目。 - 斜線命令:覆寫
process_command(),處理你的命令,並為其他所有呼叫super().process_command(cmd)。 - 不要覆寫
run()除非絕對必要——擴充 hook 就是為了避免那種耦合而存在的。