diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml index 7a84acf..dbd7da9 100644 --- a/.gitea/workflows/ci.yml +++ b/.gitea/workflows/ci.yml @@ -37,14 +37,14 @@ jobs: - name: Build TinyCC run: bash scripts/ci_tcc.sh - name: Build - run: make CC=tcc AR=ar CFLAGS="-std=c11 -Wall -O2 -g" -j"$(nproc)" + run: make CC=tcc AR=ar CFLAGS="-std=c11 -Wall -O2 -g" -j"$(nproc)" hermit - name: Unit tests run: make CC=tcc AR=ar CFLAGS="-std=c11 -Wall -O2 -g" check - name: Smoke test run: | - ./cst-agent --help - ./cst-agent --workspace "$PWD" --list-sessions - ./cst-agent --demo-ctl --workspace . + ./hermit --help + ./hermit --workspace "$PWD" --list-sessions + ./hermit --demo-ctl --workspace . - name: WebUI end-to-end (recorded stream, no API key) run: sh scripts/e2e_webui.sh @@ -103,14 +103,14 @@ jobs: run: bash scripts/package_linux.sh ${{ matrix.arch }} - name: Smoke test run: | - d=$(ls -d dist/cst-agent-*-linux-${{ matrix.arch }}) - file "$d/cst-agent" + d=$(ls -d dist/hermit-*-linux-${{ matrix.arch }}) + file "$d/hermit" run="" [ "${{ matrix.arch }}" = aarch64 ] && run=qemu-aarch64-static - $run "$d/cst-agent" --version - $run "$d/cst-agent" --help >/dev/null - $run "$d/cst-agent" --workspace "$PWD" --list-sessions - cat dist/cst-agent-*-linux-${{ matrix.arch }}.tar.gz.sha256 + $run "$d/hermit" --version + $run "$d/hermit" --help >/dev/null + $run "$d/hermit" --workspace "$PWD" --list-sessions + cat dist/hermit-*-linux-${{ matrix.arch }}.tar.gz.sha256 # Release vVERSION (from include/cst_version.h) on Gitea. Prefer repo # secret RELEASE_TOKEN (write:repository); fall back to the job token. - name: Publish Gitea Release @@ -121,7 +121,7 @@ jobs: CST_GITEA: ${{ github.server_url }} CST_REPO: ${{ github.repository }} GITHUB_REF: ${{ github.ref }} - run: bash scripts/ci_publish.sh dist/cst-agent-*-linux-${{ matrix.arch }}.tar.gz + run: bash scripts/ci_publish.sh dist/hermit-*-linux-${{ matrix.arch }}.tar.gz dist/cst-agent-*-linux-${{ matrix.arch }}.tar.gz # Release: mingw-w64 UCRT gcc (Debian trixie) with -Werror. # dist/cst-agent-VERSION-windows-x86_64.zip and -setup.exe (NSIS) diff --git a/.gitignore b/.gitignore index 69f3019..aaa7a68 100644 --- a/.gitignore +++ b/.gitignore @@ -58,3 +58,6 @@ libcstcore.a # nix-build result +hermit +.peer_probe.out +reports/ diff --git a/CMakeLists.txt b/CMakeLists.txt index 8bb87ae..890701b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -152,6 +152,7 @@ set(CST_CORE_SOURCES src/models.c src/model_registry.c src/metrics.c + src/hermit_glyph.c src/net.c src/http_srv.c src/web_md.c @@ -257,6 +258,7 @@ else() endif() add_executable(cst-agent ${CST_SOURCES}) +set_target_properties(cst-agent PROPERTIES OUTPUT_NAME hermit) add_dependencies(cst-agent sashidown) cst_win_exe(cst-agent) diff --git a/Makefile b/Makefile index 5941492..a00b32b 100644 --- a/Makefile +++ b/Makefile @@ -136,6 +136,7 @@ CORE_SRC = \ src/mm_tool.c \ src/models.c src/model_registry.c \ src/metrics.c \ + src/hermit_glyph.c \ $(WEB_SRC) SRC = $(CORE_SRC) src/textsel.c src/tui.c src/main.c @@ -206,18 +207,23 @@ TEST_LIBS = -L$(SD_DIR) -lsashidown $(CURL_LIBS) $(RPATH_LIBS) $(TEST_SYS_LIBS) TEST_OS_SRC = src/os_compat.c src/os_win.c src/util.c .PHONY: all clean check demo sashidown-lib cmake-gcc cmake-clang asan install \ - uninstall install-service help + uninstall install-service help hermit-font cst-agent -all: sashidown-lib cst-agent $(CST_CORE_LIB) +all: sashidown-lib hermit $(CST_CORE_LIB) + +# Compat alias: many scripts still say `make cst-agent`. +cst-agent: hermit help: - @echo "CST-Agent build targets (prefer: make -j\$$(nproc))" - @echo " all / cst-agent incremental build (ccache auto if installed)" + @echo "Hermit build targets (prefer: make -j\$$(nproc))" + @echo " all / hermit incremental build (ccache auto if installed)" @echo " check build + run unit tests" @echo " asan AddressSanitizer rebuild + check" - @echo " install PREFIX=$(PREFIX) (default /usr/local)" + @echo " hermit-font optional SVG→color font (needs rsvg + fonttools)" + @echo " install PREFIX=$(PREFIX); hermit + cst-agent symlink" @echo " install-service systemd --user parliament unit" @echo "Env: USE_CCACHE=0|1|auto STATIC=1 CST_WEBUI=0 CC=tcc" + @echo " HERMIT_ICONS=auto|mini|font HERMIT_*/CST_* env" @echo "Release: scripts/package_linux.sh ARCH | scripts/ci_windows_cross.sh" @echo "Fast install: scripts/bootstrap_linux.sh --release" @@ -238,8 +244,13 @@ cmake-clang: sashidown-lib: $(MAKE) -C $(SD_DIR) libsashidown.a -cst-agent: $(OBJ) sashidown-lib +hermit: $(OBJ) sashidown-lib $(CC) $(CFLAGS) -o $@ $(OBJ) $(LDFLAGS) $(LDLIBS) + @ln -sfr $@ cst-agent 2>/dev/null || ln -sf hermit cst-agent + +hermit-font: + @python3 scripts/build_hermit_font.py || \ + echo "hermit-font: skipped (install rsvg-convert + fonttools to build)" # Built-in model data as a C byte list (included by model_registry.c). .gen/models_builtin.inc: src/models_builtin.json @@ -303,32 +314,38 @@ check: sashidown-lib $(TESTS) asan: $(MAKE) clean $(MAKE) CFLAGS="-std=c11 -Wall -Wextra -O1 -g -fsanitize=address,undefined -fno-sanitize-recover=undefined -fno-omit-frame-pointer" \ - sashidown-lib cst-agent check + sashidown-lib hermit check -demo: cst-agent - ./cst-agent --demo-ctl --workspace . +demo: hermit + ./hermit --demo-ctl --workspace . clean: - rm -f $(OBJ) $(DEPFILES) cst-agent $(CST_CORE_LIB) $(TESTS) \ + rm -f $(OBJ) $(DEPFILES) hermit cst-agent $(CST_CORE_LIB) $(TESTS) \ .gen/models_builtin.inc .gen/web_assets.inc -install: cst-agent +install: hermit install -d "$(DESTDIR)$(PREFIX)/bin" - install -m 0755 cst-agent "$(DESTDIR)$(PREFIX)/bin/cst-agent" + install -m 0755 hermit "$(DESTDIR)$(PREFIX)/bin/hermit" + ln -sfr "$(DESTDIR)$(PREFIX)/bin/hermit" "$(DESTDIR)$(PREFIX)/bin/cst-agent" 2>/dev/null || \ + ln -sf hermit "$(DESTDIR)$(PREFIX)/bin/cst-agent" + @if [ -f res/hermit-icons.ttf ]; then \ + install -d "$(DESTDIR)$(PREFIX)/share/hermit"; \ + install -m 0644 res/hermit-icons.ttf "$(DESTDIR)$(PREFIX)/share/hermit/"; \ + fi # Optional systemd --user unit for the parliament seat service. install-service: install install -d "$(DESTDIR)$(HOME)/.config/systemd/user" - sed -e 's|@BIN@|$(PREFIX)/bin/cst-agent|g' \ + sed -e 's|@BIN@|$(PREFIX)/bin/hermit|g' \ scripts/cst-agent-parliament.service.in \ > "$(DESTDIR)$(HOME)/.config/systemd/user/cst-agent-parliament.service" @echo "Installed user unit. Enable with:" @echo " systemctl --user daemon-reload" @echo " systemctl --user enable --now cst-agent-parliament.service" - @echo "(set CST_REALM_PASSWORD / CST_INSTANCE_ID in the drop-in or environment)" + @echo "(set HERMIT_/CST_ REALM_PASSWORD / INSTANCE_ID in the drop-in or environment)" uninstall: - rm -f "$(DESTDIR)$(PREFIX)/bin/cst-agent" + rm -f "$(DESTDIR)$(PREFIX)/bin/hermit" "$(DESTDIR)$(PREFIX)/bin/cst-agent" rm -f "$(HOME)/.config/systemd/user/cst-agent-parliament.service" -include $(DEPFILES) diff --git a/README.md b/README.md index f3ba88e..5c40c83 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ -# CST-Agent +# Hermit -本地 coding agent:终端 TUI、可选 WebUI / 桌面 GUI,在工作区里读改文件、跑命令、管会话。 +本地 coding agent(原 CST-Agent):终端 TUI、可选 WebUI / 桌面 GUI,在工作区里读改文件、跑命令、管会话。 C11 实现,OpenAI 兼容网关(DeepSeek / Kimi / MiniMax / OpenAI 等)。 **适合**:有监督的本地改动与仓库探索(权限确认、diff、`/undo`、沙箱)。 @@ -8,188 +8,80 @@ C11 实现,OpenAI 兼容网关(DeepSeek / Kimi / MiniMax / OpenAI 等)。 文档索引:[docs/README.md](docs/README.md)。 +兼容:安装后提供 `cst-agent` → `hermit` 软链;环境变量 `HERMIT_*` 优先,仍接受 `CST_*`;配置目录优先 `~/.config/hermit`,若无则回退 `~/.config/cst-agent`。 + --- ## 安装 -### Linux / WSL / macOS(推荐) +### Linux / WSL / macOS ```bash -# 预编译包(快;失败则自动回退源码编译) +# 预编译包(快;失败则回退源码) curl -fsSL https://git.whcself.xyz/WHC/CST-agent/raw/branch/main/scripts/bootstrap_linux.sh | bash -s -- --release -# 或始终从源码增量编译安装到 ~/.local/bin +# 源码增量编译 → ~/.local/bin/hermit(+ cst-agent 软链) curl -fsSL https://git.whcself.xyz/WHC/CST-agent/raw/branch/main/scripts/bootstrap_linux.sh | bash ``` -已有仓库目录: - ```bash -./scripts/bootstrap_linux.sh . # 源码 -./scripts/bootstrap_linux.sh . --release # 只装 Release 二进制 -cst-agent-update # 之后升级 -cst-agent --version +./scripts/bootstrap_linux.sh . +hermit --version # hermit 0.10.0 +cst-agent-update # 升级助手仍可用 ``` -[Release](https://git.whcself.xyz/WHC/CST-agent/releases) 也提供静态 musl 包 -`cst-agent-VERSION-linux-{x86_64,x86,aarch64}.tar.gz`(旁有 `.sha256`),解压后把 `cst-agent` 放进 `PATH` 即可。 +[Release](https://git.whcself.xyz/WHC/CST-agent/releases):`hermit-VERSION-linux-{x86_64,x86,aarch64}.tar.gz`。 -### Windows 10 1903+ +### Windows -```powershell -irm https://git.whcself.xyz/WHC/CST-agent/raw/branch/main/scripts/install_windows.ps1 | iex -``` - -或下载 `cst-agent-VERSION-windows-x86_64-setup.exe` / 便携 zip。详见 [docs/README-windows.md](docs/README-windows.md)。 - -### Termux - -```bash -curl -fsSL https://git.whcself.xyz/WHC/CST-agent/raw/branch/main/scripts/bootstrap_termux.sh | bash -``` +见 [docs/README-windows.md](docs/README-windows.md)(产物名逐步改为 `hermit-…`;旧 `cst-agent-…` 包过渡期可能仍有)。 ### 第一次用 ```bash -cd ~/my-project && cst-agent +cd ~/my-project && hermit ``` -首次选择 provider 并输入 API key(遮罩输入,按 provider 存到配置目录 `credentials`,权限 0600)。 -之后用 `/provider`、`/model`、`/effort`、`/key` 切换;选择会记在 `settings.json`。 - -完整环境变量:[docs/env_openai.md](docs/env_openai.md)。 -多设备议会:[docs/parliament.md](docs/parliament.md)。 - -| 平台 | 脚本 | -|---|---| -| Linux / macOS | [`scripts/bootstrap_linux.sh`](scripts/bootstrap_linux.sh) | -| Termux | [`scripts/bootstrap_termux.sh`](scripts/bootstrap_termux.sh) | -| Windows 安装 | [`scripts/install_windows.ps1`](scripts/install_windows.ps1) | -| Linux 静态包 | [`scripts/package_linux.sh`](scripts/package_linux.sh) `x86_64\|x86\|aarch64` | -| Windows 交叉构建 | [`scripts/bootstrap_windows.sh`](scripts/bootstrap_windows.sh) | -| Windows 原生(MSYS2) | [`scripts/bootstrap_msys2.sh`](scripts/bootstrap_msys2.sh) | - --- ## 构建 ```bash -make -j$(nproc) # 增量;有 ccache 会自动用;make help 看目标 -make -j$(nproc) check # 单元测试 -make asan # ASan + UBSan -make CC=tcc AR=ar check # TinyCC 快校验(CI 同款) +make -j$(nproc) # 产出 hermit(并链 cst-agent) +make -j$(nproc) check +make hermit-font # 可选:SVG→PUA 字体(需 rsvg/fonttools) make install PREFIX=$HOME/.local -make install-service PREFIX=$HOME/.local # 可选:systemd --user 议会单元 ``` -NixOS:`nix-shell --run 'make -j$(nproc)'`。macOS:`brew install curl ncurses pkg-config` 后直接 `make`。 -平台细节:[docs/compat.md](docs/compat.md)。 - -```bash -# CMake(多编译器 / 无 TUI) -cmake -B build -DCMAKE_C_COMPILER=gcc -DCST_BUILD_TUI=ON -cmake --build build -j && ctest --test-dir build --output-on-failure -``` - -可选 Qt 桌面前端:[app/README.md](app/README.md)。 -Android APK:[docs/android.md](docs/android.md)。 - -### CI 与发布 - -推送 `main` / `v*` 时 [`.gitea/workflows/ci.yml`](.gitea/workflows/ci.yml) 会:TinyCC `check` → 打 Linux/Windows 发行包 → `scripts/ci_publish.sh` 上传 Gitea Release。 -共用逻辑:[`scripts/lib_release.sh`](scripts/lib_release.sh)。本地上传:`GITEA_TOKEN=... scripts/release.sh dist/cst-agent-*`。 +TUI 图标:`HERMIT_ICONS=auto|mini|font`(默认极简字符帧;`font` 需安装 `share/hermit/hermit-icons.ttf`)。 --- ## 用法速览 ```bash -cst-agent # TUI,工作区=当前目录 -cst-agent --web # 浏览器 http://127.0.0.1:8787/ -cst-agent --headless --prompt '…' # 无人值守(默认自动批准) -cst-agent --ask-key --provider deepseek +hermit # TUI +hermit --web # http://127.0.0.1:8787/ +hermit --headless --prompt '…' ``` -| 参数 | 作用 | -|---|---| -| `-c` / `-N` / `-R ID` / `-L` | 继续上次 / 新会话 / 恢复 / 列表 | -| `-A ask\|auto\|yolo` / `-y` | 审批模式 | -| `-T default\|minimal` | 工具集(`minimal` = 读写 shell 四件套) | -| `-M coding\|general` | 工作模式 | -| `-P` / `-m` / `-e` | provider / 模型 / 推理强度 | -| `-w DIR` | 工作区 | +首页品牌 **HERMIT** + 寄居蟹色块动效;色板:brand `#277F94`、crab `#F68042`、eye `#9957AD`。 -会话目录:`/.agent/sessions//`(自带 `.gitignore`)。WebUI 协议:[docs/webui.md](docs/webui.md)。 - -### Provider 默认 - -| provider | API Base | 默认模型 | Key | -|---|---|---|---| -| `minimax`(默认) | `https://api.minimax.cn/v1` | `MiniMax-M2.7` | `MINIMAX_API_KEY` | -| `deepseek` | `https://api.deepseek.com/v1` | `deepseek-flash` | `DEEPSEEK_API_KEY` | -| `kimi` | `https://api.moonshot.ai/v1` | `kimi-k3` | `MOONSHOT_API_KEY` | -| `kimi-code` | `https://api.kimi.ai/coding/v1` | `k3` | `KIMI_API_KEY` | -| `openai` | `https://api.openai.com/v1` | `gpt-4.1-mini` | `OPENAI_API_KEY` | -| `teamorouter` | `https://api.teamorouter.com/v1` | `/models` 首个 | `TEAMOROUTER_API_KEY` | - -优先级:CLI → 已保存 credentials → 环境变量 → TUI 询问。详见 [docs/env_openai.md](docs/env_openai.md)。 - -### 内置能力(摘要) - -- **文件**:`read_file` / `list_dir` / `glob` / `grep` / `edit_file` / `apply_patch` / `write_file` / `delete_file` / `move_file` -- **命令**:`run_shell`(沙箱 + 可中断;超时 `CST_SHELL_TIMEOUT`) -- **网络**:`web_fetch`;Kimi/DeepSeek 另有官方 `web_search` -- **任务**:`todo_write`、`ask_user`、`task` 子 agent、后台 `job_*`(完成会推 TUI / 下一回合笔记) -- **扩展**:Skills、插件、MCP([docs/plugins.md](docs/plugins.md));worker 编排([docs/workers.md](docs/workers.md)) - -同一步里连续只读调用会自动并行(默认最多 8,`CST_PAR_MAX` 可调)。截断结果带 `[truncated … evidence=ID]`,用 `evidence_read` 续读。 - -### 审批与沙箱 - -Shift+Tab / `/approval` 在 `ask` → `auto` → `yolo` 间切换。`auto` 下 Linux 优先 Landlock(否则 seccomp),macOS `sandbox_init`,Windows Low IL;详见源码与 [docs/compat.md](docs/compat.md)。 -写文件前自动快照,`/undo` 可回滚;工作区 `AGENTS.md` 会注入系统提示。 - -### TUI 常用 - -| 按键 / 命令 | 说明 | -|---|---| -| Enter / Esc | 发送(busy 时入队)/ 取消当前回合 | -| Tab / Shift+Tab | 补全或 Build↔Plan / 审批模式 | -| Ctrl+P / Ctrl+T | 命令面板 / 任务面板 | -| Ctrl+X 再按字母 | Leader 快捷键(`/keys` 或 F1 查看) | -| `/provider` `/model` `/effort` `/key` | 切换后端 | -| `/realm` `/motion` | 议会登录提示、Seat Deck 动效 | -| `/web` `/quit` | 起 WebUI / 退出 | - -完整快捷键与斜杠命令仍以 TUI 内 `/help`、`/keys` 为准。 - -### 工具协议(可选) - -默认 `native-eos`(OpenAI `tools[]`,流结束后执行)。也可 `native-inc`、文本帧 `ctl` / `ctl-heredoc`、`xml`(`CST_PROTOCOL` / `/protocol`)。日常使用保持默认即可。 +更多:Provider / 工具 / 议会 / WebUI 见 [docs/](docs/)。 --- -## 平台 +## CI 与发布 -| OS | Arch | TUI | 备注 | -|---|---|---|---| -| Linux glibc/musl | x86_64 / x86 / aarch64 | ncursesw | 静态 Release 为 musl | -| WSL2 | x86_64 | ncursesw | 工作区放在 Linux 文件系统 | -| macOS | arm64 / x86_64 | Homebrew ncurses | | -| Termux | aarch64 等 | Termux ncurses | | -| Windows 10 1903+ | x86_64 | 捆绑 ncurses | shell 为 busybox-w32 | +`.gitea/workflows/ci.yml`:TinyCC check → 打 Linux/Windows 包 → `scripts/ci_publish.sh`。 +共用逻辑:[scripts/lib_release.sh](scripts/lib_release.sh)。 ---- - -## 开发者备忘 +## 开发者 ```bash make check -scripts/prompt_smoke.sh # 系统提示契约(无 API) -scripts/e2e_webui.sh # WebUI 录制流(无 API) -scripts/e2e_fixture.sh mini_calc --provider deepseek # 可选:带 key 的夹具 +scripts/prompt_smoke.sh +scripts/e2e_webui.sh ``` -`fixtures/` 下是可重复的本地任务夹具(`TASK.md` + `check.sh`),用于手工或脚本验收,不是产品依赖。 - -许可证与第三方:`vendor/sashidown`、`vendor/cjson`(MIT)、`vendor/monocypher` 随仓库提供。 +`icons/agent-motion/` 为 SVG 资产;TUI 默认用字体序列极简轨,`make hermit-font` 生成 PUA 字体轨。 diff --git a/docs/README.md b/docs/README.md index 6bd4f3b..7a16ac7 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,16 +1,15 @@ -# 文档索引 +# Hermit 文档索引 -面向日常使用的说明。二进制名仍是 `cst-agent`,配置目录默认 `~/.config/cst-agent/`。 +二进制:`hermit`(兼容软链 `cst-agent`)。配置目录:`~/.config/hermit`(若无则回退 `~/.config/cst-agent`)。 | 文档 | 内容 | |---|---| -| [env_openai.md](env_openai.md) | Provider、模型、API Key、环境变量 | -| [parliament.md](parliament.md) | 多设备议会(Realm / 席位 / 同步) | +| [env_openai.md](env_openai.md) | Provider、模型、API Key、环境变量(`HERMIT_*` / `CST_*`) | +| [parliament.md](parliament.md) | 多设备议会 | | [workers.md](workers.md) | 后台任务、worker、job 完成报告 | | [webui.md](webui.md) | 内置 HTTP + SSE WebUI | | [plugins.md](plugins.md) | Skills、插件、MCP | -| [compat.md](compat.md) | 平台兼容与验收清单 | -| [README-windows.md](README-windows.md) | Windows 安装、沙箱、构建 | +| [compat.md](compat.md) | 平台兼容 | +| [README-windows.md](README-windows.md) | Windows | | [android.md](android.md) | Android / APK | -| [windows_selftest.md](windows_selftest.md) | Windows 自测步骤 | | [archive/](archive/) | 历史设计笔记(非产品文档) | diff --git a/icons/agent-motion/README.md b/icons/agent-motion/README.md new file mode 100644 index 0000000..f0c4044 --- /dev/null +++ b/icons/agent-motion/README.md @@ -0,0 +1,21 @@ +# Hermit / 寄居蟹 · 扁平 SVG 动图包 + +设计原则:遵循原始草图。纯色块、橙色方形身体、紫色眼睛、短脚;无渐变、无阴影、无高光。蓝色终端方块就是寄居蟹的壳。 + +6 个状态 × 2 个版本,共 12 个 SVG: +- idle:等待输入 +- question:请求问题 +- thinking:思考,黄色小钳子托腮 +- coding:写码,>_ 眼睛 +- success:完成 +- error:遇到问题 + +icons/{状态}-shell.svg 是大的带壳图标;icons/{状态}.svg 是小的无壳图标。透明背景、CSS 循环动画,支持系统减少动态效果。 + +直接打开 index.html 查看,支持深浅背景、暂停、速度调整、自动轮播以及两个版本的 SVG 下载。无联网依赖。 + +接入示例:等待输入。大图标建议 64px 及以上,小图标建议 24–32px;16px 时状态细节较弱。使用内联 SVG 时可通过 --speed 设置速度。 + +普通字符终端不直接渲染 SVG;图标适用于支持图形的终端界面、Webview 或 CLI 伴随应用。 + +更新:橘杏色身体、莓紫眼睛、低饱和蓝绿终端壳。预览主角色以 620ms 缓入缓出插值连续改变眼睛路径与身体姿态,支持中途切换;不重建主角色。状态符号淡入淡出。独立 SVG 保留各状态循环动效;跨状态变形逻辑位于 index.html。 diff --git a/icons/agent-motion/icons/coding-shell.svg b/icons/agent-motion/icons/coding-shell.svg new file mode 100644 index 0000000..029ef7f --- /dev/null +++ b/icons/agent-motion/icons/coding-shell.svg @@ -0,0 +1 @@ +Hermit · 写码 \ No newline at end of file diff --git a/icons/agent-motion/icons/coding.svg b/icons/agent-motion/icons/coding.svg new file mode 100644 index 0000000..678ad0b --- /dev/null +++ b/icons/agent-motion/icons/coding.svg @@ -0,0 +1 @@ +Hermit · 写码 \ No newline at end of file diff --git a/icons/agent-motion/icons/error-shell.svg b/icons/agent-motion/icons/error-shell.svg new file mode 100644 index 0000000..5aee776 --- /dev/null +++ b/icons/agent-motion/icons/error-shell.svg @@ -0,0 +1 @@ +Hermit · 遇到问题 \ No newline at end of file diff --git a/icons/agent-motion/icons/error.svg b/icons/agent-motion/icons/error.svg new file mode 100644 index 0000000..171bd65 --- /dev/null +++ b/icons/agent-motion/icons/error.svg @@ -0,0 +1 @@ +Hermit · 遇到问题 \ No newline at end of file diff --git a/icons/agent-motion/icons/idle-shell.svg b/icons/agent-motion/icons/idle-shell.svg new file mode 100644 index 0000000..6f12497 --- /dev/null +++ b/icons/agent-motion/icons/idle-shell.svg @@ -0,0 +1 @@ +Hermit · 等待输入 \ No newline at end of file diff --git a/icons/agent-motion/icons/idle.svg b/icons/agent-motion/icons/idle.svg new file mode 100644 index 0000000..627b6ca --- /dev/null +++ b/icons/agent-motion/icons/idle.svg @@ -0,0 +1 @@ +Hermit · 等待输入 \ No newline at end of file diff --git a/icons/agent-motion/icons/question-shell.svg b/icons/agent-motion/icons/question-shell.svg new file mode 100644 index 0000000..7abb533 --- /dev/null +++ b/icons/agent-motion/icons/question-shell.svg @@ -0,0 +1 @@ +Hermit · 请求问题 \ No newline at end of file diff --git a/icons/agent-motion/icons/question.svg b/icons/agent-motion/icons/question.svg new file mode 100644 index 0000000..bb1624a --- /dev/null +++ b/icons/agent-motion/icons/question.svg @@ -0,0 +1 @@ +Hermit · 请求问题 \ No newline at end of file diff --git a/icons/agent-motion/icons/success-shell.svg b/icons/agent-motion/icons/success-shell.svg new file mode 100644 index 0000000..0a9a7ab --- /dev/null +++ b/icons/agent-motion/icons/success-shell.svg @@ -0,0 +1 @@ +Hermit · 完成 \ No newline at end of file diff --git a/icons/agent-motion/icons/success.svg b/icons/agent-motion/icons/success.svg new file mode 100644 index 0000000..ffd07f1 --- /dev/null +++ b/icons/agent-motion/icons/success.svg @@ -0,0 +1 @@ +Hermit · 完成 \ No newline at end of file diff --git a/icons/agent-motion/icons/thinking-shell.svg b/icons/agent-motion/icons/thinking-shell.svg new file mode 100644 index 0000000..ca4e350 --- /dev/null +++ b/icons/agent-motion/icons/thinking-shell.svg @@ -0,0 +1 @@ +Hermit · 思考 \ No newline at end of file diff --git a/icons/agent-motion/icons/thinking.svg b/icons/agent-motion/icons/thinking.svg new file mode 100644 index 0000000..ddcd992 --- /dev/null +++ b/icons/agent-motion/icons/thinking.svg @@ -0,0 +1 @@ +Hermit · 思考 \ No newline at end of file diff --git a/icons/agent-motion/index.html b/icons/agent-motion/index.html new file mode 100644 index 0000000..ed0377b --- /dev/null +++ b/icons/agent-motion/index.html @@ -0,0 +1,15 @@ +Hermit · Flat Motion Kit
ııHermit / 寄居蟹
AGENT MOTION KIT · VOL. 03
A LITTLE PRESENCE IN YOUR TERMINAL

一个壳,一个小伙伴。

终端是它的壳。橙色小寄居蟹,简单而有表情。

06 EXPRESSIONS
FLAT SVG / TWO SIZES
FULL ICON / 带壳
HERMIT / TERMINAL●
~/your-next-idea
❯ hermit agent

小图标 / 无壳状态

CLICK TO EXPLORE ↓

大图标带终端壳,小图标只保留寄居蟹。纯色扁平,柔和配色,表情连续变换。

原生终端通常不渲染 SVG;适用于 CLI 的图形界面、Webview 和文档。

HERMIT — SMALL FRIEND, BIG IDEAS.CRAFTED IN SVG · 2026
\ No newline at end of file diff --git a/icons/hermit-flat-motion-kit.zip b/icons/hermit-flat-motion-kit.zip new file mode 100644 index 0000000..d94e7fe Binary files /dev/null and b/icons/hermit-flat-motion-kit.zip differ diff --git a/include/cst_hermit_glyph.h b/include/cst_hermit_glyph.h new file mode 100644 index 0000000..2e6a18b --- /dev/null +++ b/include/cst_hermit_glyph.h @@ -0,0 +1,63 @@ +#ifndef CST_HERMIT_GLYPH_H +#define CST_HERMIT_GLYPH_H + +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/* Product display name (user-visible). Binary install name is "hermit". */ +#define HERMIT_NAME "Hermit" +#define HERMIT_BIN "hermit" +#define HERMIT_LEGACY_BIN "cst-agent" +#define HERMIT_CONFIG_DIR "hermit" +#define HERMIT_LEGACY_CONFIG_DIR "cst-agent" + +/* Icon states mirror icons/agent-motion SVG set. */ +typedef enum { + HERMIT_ST_IDLE = 0, + HERMIT_ST_QUESTION, + HERMIT_ST_THINKING, + HERMIT_ST_CODING, + HERMIT_ST_SUCCESS, + HERMIT_ST_ERROR, + HERMIT_ST_N +} hermit_state_t; + +typedef enum { + HERMIT_ICON_AUTO = 0, + HERMIT_ICON_MINI, + HERMIT_ICON_FONT +} hermit_icon_mode_t; + +/* HERMIT_ then CST_. Never returns NULL (may be ""). */ +const char *hermit_getenv(const char *suffix); + +hermit_icon_mode_t hermit_icon_mode(void); +/* Effective mode after AUTO resolution (ascii forces MINI). */ +hermit_icon_mode_t hermit_icon_mode_eff(int ascii); + +/* Small badge UTF-8 (or ASCII). frame advances animation. */ +const char *hermit_mini_badge(hermit_state_t st, int frame, int ascii); + +/* Home shell crab: up to *nlines strings (not owned). Returns line count. */ +int hermit_shell_art(hermit_state_t st, int with_shell, int ascii, + const char **lines, int max_lines); + +/* Color roles for art cells: 0=body(crab) 1=shell 2=eye 3=leg 4=prompt 5=mark */ +int hermit_shell_cell_role(hermit_state_t st, int with_shell, int row, int col); + +/* PUA base U+E700; layout: shell*HERMIT_ST_N*HERMIT_FONT_FRAMES + st*FRAMES + frame */ +#define HERMIT_FONT_PUA_BASE 0xE700 +#define HERMIT_FONT_FRAMES 8 +wchar_t hermit_font_codepoint(hermit_state_t st, int frame, int with_shell); + +/* Map agent UI signals → icon state. */ +hermit_state_t hermit_state_from_ui(int busy, int ask, int error); + +#ifdef __cplusplus +} +#endif +#endif diff --git a/include/cst_version.h b/include/cst_version.h index 6c54555..217835a 100644 --- a/include/cst_version.h +++ b/include/cst_version.h @@ -2,8 +2,8 @@ #define CST_VERSION_H #define CST_VERSION_MAJOR 0 -#define CST_VERSION_MINOR 9 -#define CST_VERSION_PATCH 34 +#define CST_VERSION_MINOR 10 +#define CST_VERSION_PATCH 0 #define CST_VERSION_STR_(x) #x #define CST_VERSION_STR(x) CST_VERSION_STR_(x) diff --git a/scripts/bootstrap_linux.sh b/scripts/bootstrap_linux.sh index 8ea74eb..f485520 100755 --- a/scripts/bootstrap_linux.sh +++ b/scripts/bootstrap_linux.sh @@ -44,7 +44,9 @@ run_make() { install_bin() { local prefix="$1" bin="$2" mkdir -p "$prefix/bin" - install -m 0755 "$bin" "$prefix/bin/cst-agent" + install -m 0755 "$bin" "$prefix/bin/hermit" + ln -sfr "$prefix/bin/hermit" "$prefix/bin/cst-agent" 2>/dev/null || \ + ln -sf hermit "$prefix/bin/cst-agent" } install_helpers() { @@ -60,7 +62,7 @@ EOF #!$(command -v bash) # Manage the parliament seat service (foreground under systemd, or --service). set -euo pipefail -BIN="$prefix/bin/cst-agent" +BIN="$prefix/bin/hermit" cmd="\${1:-status}" case "\$cmd" in status) @@ -102,19 +104,24 @@ install_from_release() { if [[ -z "$ver" ]]; then ver="$(cst_version)" fi - local name="cst-agent-$ver-linux-$arch.tar.gz" + local name="hermit-$ver-linux-$arch.tar.gz" + # One-cycle legacy asset name fallback handled below if download fails. local tmp tmp="$(mktemp -d /tmp/cst-rel.XXXXXX)" # shellcheck disable=SC2046 + local legacy="cst-agent-$ver-linux-$arch.tar.gz" if ! cst_fetch "$tmp/$name" $(cst_release_asset_urls "$ver" "$name"); then - echo "release download failed for $name; falling back to source build" >&2 - rm -rf "$tmp" - return 1 + if ! cst_fetch "$tmp/$legacy" $(cst_release_asset_urls "$ver" "$legacy"); then + echo "release download failed; falling back to source build" >&2 + rm -rf "$tmp" + return 1 + fi + name="$legacy" fi tar -C "$tmp" -xzf "$tmp/$name" local bin - bin="$(find "$tmp" -type f -name cst-agent | head -1)" - [[ -x "$bin" ]] || { echo "tarball missing cst-agent" >&2; rm -rf "$tmp"; return 1; } + bin="$(find "$tmp" -type f \( -name hermit -o -name cst-agent \) | head -1)" + [[ -x "$bin" ]] || { echo "tarball missing hermit/cst-agent" >&2; rm -rf "$tmp"; return 1; } install_bin "$prefix" "$bin" rm -rf "$tmp" echo "==> installed release $ver ($arch) -> $prefix/bin/cst-agent" @@ -177,7 +184,7 @@ main() { if install_from_release "$prefix" "$dir"; then install_helpers "$prefix" "$dir" echo - echo "OK: $("$prefix/bin/cst-agent" --version) (release install)" + echo "OK: $("$prefix/bin/hermit" --version) (release install)" case ":$PATH:" in *":$prefix/bin:"*) ;; *) echo "Add to PATH: export PATH=\"$prefix/bin:\$PATH\"" ;; @@ -209,14 +216,14 @@ main() { install_helpers "$prefix" "$dir" echo - echo "OK: $("$prefix/bin/cst-agent" --version)" + echo "OK: $("$prefix/bin/hermit" --version)" case ":$PATH:" in *":$prefix/bin:"*) ;; *) echo "Add to PATH: export PATH=\"$prefix/bin:\$PATH\"" ;; esac - echo "Run 'cst-agent' inside a project directory; upgrade with 'cst-agent-update'." + echo "Run 'hermit' (or 'cst-agent') in a project; upgrade with 'cst-agent-update'." echo "Fast upgrade from Release: 'cst-agent-update --release'." - echo "Parliament seat service helper: 'cst-agent-service status|install-unit'." + echo "Parliament helper: 'cst-agent-service status|install-unit'." echo "The first start asks for the provider and its API key and saves them." } diff --git a/scripts/build_hermit_font.py b/scripts/build_hermit_font.py new file mode 100755 index 0000000..82483e1 --- /dev/null +++ b/scripts/build_hermit_font.py @@ -0,0 +1,202 @@ +#!/usr/bin/env python3 +"""Batch-convert icons/agent-motion SVG states into a color emoji-style font. + +Outputs: + res/hermit-icons.ttf (CBDT/CBLC bitmap glyphs when fonttools works) + .gen/hermit_font_map.inc (PUA codepoint table for the TUI) + +PUA layout matches cst_hermit_glyph.h: + U+E700 + (shell * N_STATE + state) * N_FRAME + frame + +Requires (optional): rsvg-convert or cairosvg, pillow, fonttools. +Without them, still writes hermit_font_map.inc so FONT mode can emit codepoints. +""" +from __future__ import annotations + +import struct +import subprocess +import sys +import tempfile +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[1] +ICONS = ROOT / "icons" / "agent-motion" / "icons" +OUT_TTF = ROOT / "res" / "hermit-icons.ttf" +OUT_INC = ROOT / ".gen" / "hermit_font_map.inc" + +STATES = ["idle", "question", "thinking", "coding", "success", "error"] +N_FRAME = 8 +PUA_BASE = 0xE700 + + +def codepoint(shell: int, state: int, frame: int) -> int: + return PUA_BASE + (shell * len(STATES) + state) * N_FRAME + frame + + +def write_map_inc() -> None: + OUT_INC.parent.mkdir(parents=True, exist_ok=True) + lines = [ + "/* Generated by scripts/build_hermit_font.py — do not edit. */", + f"#define HERMIT_FONT_PUA_BASE_GEN 0x{PUA_BASE:X}", + f"#define HERMIT_FONT_FRAMES_GEN {N_FRAME}", + f"#define HERMIT_FONT_STATES_GEN {len(STATES)}", + "/* state names: " + ", ".join(STATES) + " */", + "", + ] + OUT_INC.write_text("\n".join(lines) + "\n", encoding="utf-8") + print(f"wrote {OUT_INC}") + + +def render_svg(svg: Path, png: Path, size: int) -> bool: + png.parent.mkdir(parents=True, exist_ok=True) + try: + if subprocess.call( + ["rsvg-convert", "-w", str(size), "-h", str(size), "-o", str(png), str(svg)], + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + ) == 0: + return png.is_file() + except FileNotFoundError: + pass + try: + import cairosvg # type: ignore + + cairosvg.svg2png(url=str(svg), write_to=str(png), output_width=size, output_height=size) + return png.is_file() + except Exception: + return False + + +def build_ttf(png_by_cp: dict[int, Path]) -> bool: + try: + from fontTools.fontBuilder import FontBuilder # type: ignore + from fontTools.ttLib import TTFont # type: ignore + from fontTools.ttLib.tables.E_B_D_T_ import table_E_B_D_T_ # noqa: F401 + from fontTools.ttLib.tables.C_B_D_T_ import table_C_B_D_T_ # noqa: F401 + from fontTools.ttLib.tables.C_B_L_C_ import table_C_B_L_C_ # noqa: F401 + from fontTools.ttLib.tables._g_l_y_f import Glyph # type: ignore + from fontTools.pens.ttGlyphPen import TTGlyphPen # type: ignore + except Exception as e: + print(f"fonttools CBDT path unavailable: {e}", file=sys.stderr) + return False + + # Minimal TTF with empty glyf + Unicode cmap to PUA; color bitmaps via nanoemoji-less stub: + # Prefer shipping PNGs + cmap so terminals that map PUA→font face still get something. + try: + from PIL import Image # type: ignore + except Exception as e: + print(f"pillow required for font pack: {e}", file=sys.stderr) + return False + + # Simplified: write a placeholder TTF that only has cmap entries; bitmaps + # need full CBDT authoring. For now embed as sbix-like custom is heavy — + # create a valid empty-outline font with correct cmap so install works, and + # document that full color CBDT needs `nanoemoji` when available. + glyph_order = [".notdef"] + cmap = {} + for cp, png in sorted(png_by_cp.items()): + name = f"u{cp:04X}" + glyph_order.append(name) + cmap[cp] = name + + fb = FontBuilder(1024, isTTF=True) + fb.setupGlyphOrder(glyph_order) + fb.setupCharacterMap(cmap) + pen = TTGlyphPen(None) + empty = pen.glyph() + glyphs = {".notdef": empty} + for name in glyph_order[1:]: + glyphs[name] = empty + fb.setupGlyf(glyphs) + fb.setupHorizontalMetrics({n: (1024, 0) for n in glyph_order}) + fb.setupHorizontalHeader(ascent=1024, descent=0) + fb.setupNameTable( + { + "familyName": "Hermit Icons", + "styleName": "Regular", + "uniqueFontIdentifier": "Hermit Icons", + "fullName": "Hermit Icons", + "psName": "HermitIcons", + "version": "0.10", + } + ) + fb.setupOS2() + fb.setupPost() + OUT_TTF.parent.mkdir(parents=True, exist_ok=True) + fb.save(str(OUT_TTF)) + print(f"wrote {OUT_TTF} (cmap + empty outlines; color CBDT via nanoemoji optional)") + # Touch PNGs kept beside for tooling + dump = ROOT / "res" / "hermit-icon-frames" + dump.mkdir(parents=True, exist_ok=True) + for cp, png in png_by_cp.items(): + target = dump / f"u{cp:04X}.png" + target.write_bytes(png.read_bytes()) + print(f"wrote {len(png_by_cp)} frames under {dump}") + return True + + +def try_nanoemoji(svgs: list[Path]) -> bool: + """If nanoemoji is installed, build a proper color font from SVGs.""" + if not svgs: + return False + try: + with tempfile.TemporaryDirectory() as td: + td = Path(td) + for i, s in enumerate(svgs): + (td / f"{i:03d}.svg").write_bytes(s.read_bytes()) + out = td / "HermitIcons.ttf" + rc = subprocess.call( + ["nanoemoji", f"--output_file={out}", *sorted(str(p) for p in td.glob("*.svg"))], + cwd=str(td), + ) + if rc == 0 and out.is_file(): + OUT_TTF.parent.mkdir(parents=True, exist_ok=True) + OUT_TTF.write_bytes(out.read_bytes()) + print(f"wrote {OUT_TTF} via nanoemoji") + return True + except FileNotFoundError: + pass + except Exception as e: + print(f"nanoemoji failed: {e}", file=sys.stderr) + return False + + +def main() -> int: + write_map_inc() + if not ICONS.is_dir(): + print(f"missing {ICONS}", file=sys.stderr) + return 1 + + svg_list: list[Path] = [] + png_by_cp: dict[int, Path] = {} + with tempfile.TemporaryDirectory() as td: + td = Path(td) + for shell, suffix in ((0, ""), (1, "-shell")): + for si, st in enumerate(STATES): + svg = ICONS / f"{st}{suffix}.svg" + if not svg.is_file(): + print(f"skip missing {svg.name}", file=sys.stderr) + continue + svg_list.append(svg) + # One master frame per state for now; duplicate across N_FRAME. + png = td / f"{shell}_{st}.png" + if not render_svg(svg, png, 64): + print(f"render failed: {svg.name}", file=sys.stderr) + continue + for fr in range(N_FRAME): + png_by_cp[codepoint(shell, si, fr)] = png + + if try_nanoemoji(svg_list): + return 0 + if png_by_cp and build_ttf(png_by_cp): + return 0 + print( + "font binary not built (install rsvg-convert/cairosvg + pillow + fonttools " + "or nanoemoji). Map .inc is ready for PUA emission.", + file=sys.stderr, + ) + return 0 # map is enough for compile + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/scripts/cst-agent-parliament.service.in b/scripts/cst-agent-parliament.service.in index e4b86c2..de80176 100644 --- a/scripts/cst-agent-parliament.service.in +++ b/scripts/cst-agent-parliament.service.in @@ -1,5 +1,5 @@ [Unit] -Description=CST-Agent parliament seat service +Description=Hermit parliament seat service Documentation=file://%h/CST-agent/docs/parliament.md After=network-online.target Wants=network-online.target @@ -15,6 +15,8 @@ Environment=CST_INSTANCE_ID=%H # ~/.config/systemd/user/cst-agent-parliament.service.d/override.conf # [Service] # Environment=CST_REALM_PASSWORD=… +# Also accepts HERMIT_* and config under ~/.config/hermit/ +EnvironmentFile=-%h/.config/hermit/parliament.env EnvironmentFile=-%h/.config/cst-agent/parliament.env [Install] diff --git a/scripts/package_linux.sh b/scripts/package_linux.sh index 1fe03ce..71975bc 100755 --- a/scripts/package_linux.sh +++ b/scripts/package_linux.sh @@ -29,7 +29,7 @@ mk=(make CC="${T}gcc" AR="${T}ar" STATIC=1 stamp=".gen/release-linux-$ARCH.stamp" want="$(cst_build_stamp "${T}gcc")" -if [[ "${CST_FORCE_REBUILD:-0}" == 1 || ! -f cst-agent || ! -f "$stamp" || +if [[ "${CST_FORCE_REBUILD:-0}" == 1 || ! -f hermit || ! -f "$stamp" || "$(cat "$stamp" 2>/dev/null || true)" != "$want" ]]; then echo "==> clean rebuild ($ARCH)" make clean >/dev/null @@ -37,12 +37,12 @@ if [[ "${CST_FORCE_REBUILD:-0}" == 1 || ! -f cst-agent || ! -f "$stamp" || else echo "==> incremental rebuild ($ARCH, stamp match)" fi -"${mk[@]}" -j"$(nproc)" cst-agent +"${mk[@]}" -j"$(nproc)" hermit mkdir -p .gen printf '%s\n' "$want" >"$stamp" -if "${T}readelf" -l cst-agent | grep -q 'Requesting program interpreter'; then - echo "cst-agent is dynamically linked" >&2 +if "${T}readelf" -l hermit | grep -q 'Requesting program interpreter'; then + echo "hermit is dynamically linked" >&2 exit 1 fi if [[ "${CST_LINUX_CHECK:-0}" == 1 ]]; then @@ -50,14 +50,18 @@ if [[ "${CST_LINUX_CHECK:-0}" == 1 ]]; then fi ver="$(cst_version)" -name="cst-agent-$ver-linux-$ARCH" +name="hermit-$ver-linux-$ARCH" rel="dist/$name" rm -rf "$rel" mkdir -p "$rel" -cp -f cst-agent "$rel/cst-agent" -"${T}strip" "$rel/cst-agent" +cp -f hermit "$rel/hermit" +ln -sfr "$rel/hermit" "$rel/cst-agent" 2>/dev/null || ln -sf hermit "$rel/cst-agent" +"${T}strip" "$rel/hermit" cp -f README.md "$rel/README.md" echo "$ver" >"$rel/VERSION" tgz="$(cst_pack_tgz dist "$name")" +# Compat alias package name for one release cycle +cp -f "$tgz" "dist/cst-agent-$ver-linux-$ARCH.tar.gz" +cst_sha256_write "dist/cst-agent-$ver-linux-$ARCH.tar.gz" ls -la "$rel" echo "$ROOT/$tgz" diff --git a/scripts/release.sh b/scripts/release.sh index cb817e5..a77f496 100755 --- a/scripts/release.sh +++ b/scripts/release.sh @@ -59,9 +59,13 @@ json() { # CODE [ARGS...]: d = the JSON on stdin, ARGS in sys.argv[1:] python3 -c "import json,sys; d=json.load(sys.stdin); $code" "$@" } -BODY="Linux (static, musl, any distro): cst-agent-$ver-linux-{x86_64,x86,aarch64}.tar.gz -- extract and run ./cst-agent, or copy it onto PATH. +BODY="Hermit $ver -Windows x86_64: cst-agent-$ver-windows-x86_64-setup.exe installs per user (no admin rights) and adds PATH; cst-agent-$ver-windows-x86_64.zip is the portable build (install_windows.ps1 inside). +Linux (static, musl): hermit-$ver-linux-{x86_64,x86,aarch64}.tar.gz — extract and run ./hermit (cst-agent symlink included). Legacy cst-agent-*.tar.gz aliases may ship for one cycle. + +Windows x86_64: hermit-$ver-windows-x86_64-setup.exe / .zip (cst-agent.exe symlink or copy when present). + +Env: HERMIT_* preferred; CST_* still accepted. Config: ~/.config/hermit (falls back to ~/.config/cst-agent). Every file has a .sha256 next to it." diff --git a/src/hermit_glyph.c b/src/hermit_glyph.c new file mode 100644 index 0000000..9c7df97 --- /dev/null +++ b/src/hermit_glyph.c @@ -0,0 +1,167 @@ +#define _POSIX_C_SOURCE 200809L +#include "cst_hermit_glyph.h" + +#include +#include +#include + +const char *hermit_getenv(const char *suffix) { + char hk[96], ck[96]; + if (!suffix || !suffix[0]) + return ""; + snprintf(hk, sizeof(hk), "HERMIT_%s", suffix); + const char *e = getenv(hk); + if (e && e[0]) + return e; + snprintf(ck, sizeof(ck), "CST_%s", suffix); + e = getenv(ck); + return e ? e : ""; +} + +hermit_icon_mode_t hermit_icon_mode(void) { + const char *e = hermit_getenv("ICONS"); + if (!e[0] || !strcmp(e, "auto")) + return HERMIT_ICON_AUTO; + if (!strcmp(e, "font") || !strcmp(e, "ttf")) + return HERMIT_ICON_FONT; + return HERMIT_ICON_MINI; +} + +hermit_icon_mode_t hermit_icon_mode_eff(int ascii) { + if (ascii) + return HERMIT_ICON_MINI; + hermit_icon_mode_t m = hermit_icon_mode(); + if (m != HERMIT_ICON_AUTO) + return m; + /* AUTO: font only when forced path exists or HERMIT_ICON_FONT=1. */ + if (hermit_getenv("ICON_FONT")[0] == '1') + return HERMIT_ICON_FONT; + return HERMIT_ICON_MINI; +} + +hermit_state_t hermit_state_from_ui(int busy, int ask, int error) { + if (error) + return HERMIT_ST_ERROR; + if (ask) + return HERMIT_ST_QUESTION; + if (busy) + return HERMIT_ST_CODING; + return HERMIT_ST_IDLE; +} + +wchar_t hermit_font_codepoint(hermit_state_t st, int frame, int with_shell) { + if (st < 0 || st >= HERMIT_ST_N) + st = HERMIT_ST_IDLE; + if (frame < 0) + frame = 0; + frame %= HERMIT_FONT_FRAMES; + int shell = with_shell ? 1 : 0; + return (wchar_t)(HERMIT_FONT_PUA_BASE + + (shell * HERMIT_ST_N + (int)st) * HERMIT_FONT_FRAMES + + frame); +} + +/* --- minimal badge ------------------------------------------------------- */ + +const char *hermit_mini_badge(hermit_state_t st, int frame, int ascii) { + int f = frame < 0 ? 0 : frame; + if (ascii) { + switch (st) { + case HERMIT_ST_QUESTION: + return "||?"; + case HERMIT_ST_ERROR: + return "\\/!"; + case HERMIT_ST_SUCCESS: + return "||*"; + case HERMIT_ST_THINKING: + return (f % 2) ? "~|" : "|~"; + case HERMIT_ST_CODING: + case HERMIT_ST_IDLE: + default: { + static const char *const spin[] = {"||", ">_", "||", "-"}; + return spin[f % 4]; + } + } + } + switch (st) { + case HERMIT_ST_QUESTION: + return "║║?"; + case HERMIT_ST_ERROR: + return "╲╱!"; + case HERMIT_ST_SUCCESS: + return "║║✦"; + case HERMIT_ST_THINKING: + return (f % 2) ? "║~" : "~║"; + case HERMIT_ST_CODING: + case HERMIT_ST_IDLE: + default: { + static const char *const spin[] = {"║║", ">_", "║║", "─"}; + return spin[f % 4]; + } + } +} + +/* --- home shell art (fixed grid) ----------------------------------------- */ +/* 6 rows x 14 cols. Roles: 0 body 1 shell 2 eye 3 leg 4 prompt 5 empty/mark */ + +#define ART_H 6 +#define ART_W 14 + +static const char *const ART_SHELL[ART_H] = { + " ######## ", /* shell top */ + " ## >_ ## ", /* shell + prompt */ + " ########## ", /* shell bottom */ + " ###### ", /* body */ + " #::## ", /* eyes :: on body */ + " | | ", /* legs */ +}; + +static const char *const ART_NOSHELL[ART_H] = { + " ", + " ", + " ###### ", + " #::## ", + " ###### ", + " | | ", +}; + +static const char *const ART_SHELL_ASC[ART_H] = { + " ######## ", + " ## >_ ## ", + " ########## ", + " ###### ", + " #::## ", + " | | ", +}; + +int hermit_shell_art(hermit_state_t st, int with_shell, int ascii, + const char **lines, int max_lines) { + (void)st; + if (!lines || max_lines < 1) + return 0; + const char *const *src = + with_shell ? (ascii ? ART_SHELL_ASC : ART_SHELL) : ART_NOSHELL; + int n = ART_H < max_lines ? ART_H : max_lines; + for (int i = 0; i < n; i++) + lines[i] = src[i]; + return n; +} + +int hermit_shell_cell_role(hermit_state_t st, int with_shell, int row, int col) { + (void)st; + if (row < 0 || row >= ART_H || col < 0 || col >= ART_W) + return 5; + const char *const *src = with_shell ? ART_SHELL : ART_NOSHELL; + char c = src[row][col]; + if (c == ' ' || !c) + return 5; + if (c == '>' || c == '_') + return 4; + if (c == ':') + return 2; + if (c == '|') + return 3; + if (with_shell && row <= 2) + return 1; /* shell */ + return 0; /* body */ +} diff --git a/src/main.c b/src/main.c index 93ccfc5..ab47b6b 100644 --- a/src/main.c +++ b/src/main.c @@ -671,7 +671,7 @@ int main(int argc, char **argv) { do_migrate = 1; break; case 'V': - printf("cst-agent %s\n", CST_VERSION); + printf("hermit %s\n", CST_VERSION); cst_curl_cleanup(); return 0; default: diff --git a/src/os_compat.c b/src/os_compat.c index b74759b..c68b17e 100644 --- a/src/os_compat.c +++ b/src/os_compat.c @@ -230,6 +230,7 @@ int os_rename_replace(const char *from, const char *to) { int os_config_dir(char *out, size_t out_sz) { if (!out || out_sz < 8) return -1; + char neu[OS_PATH_MAX], old[OS_PATH_MAX]; #ifdef _WIN32 const char *base = getenv("XDG_CONFIG_HOME"); if (!base || !os_path_is_abs(base)) @@ -238,20 +239,42 @@ int os_config_dir(char *out, size_t out_sz) { base = getenv("LOCALAPPDATA"); if (!base || !base[0]) return -1; - int n = snprintf(out, out_sz, "%s/cst-agent", base); - os_path_norm_slashes(out); + int n = snprintf(neu, sizeof(neu), "%s/hermit", base); + int o = snprintf(old, sizeof(old), "%s/cst-agent", base); + if (n < 0 || o < 0 || (size_t)n >= sizeof(neu) || (size_t)o >= sizeof(old)) + return -1; + os_path_norm_slashes(neu); + os_path_norm_slashes(old); #else const char *xdg = getenv("XDG_CONFIG_HOME"); const char *home = getenv("HOME"); - int n; - if (xdg && xdg[0] == '/') - n = snprintf(out, out_sz, "%s/cst-agent", xdg); - else if (home && home[0]) - n = snprintf(out, out_sz, "%s/.config/cst-agent", home); - else + int n, o; + if (xdg && xdg[0] == '/') { + n = snprintf(neu, sizeof(neu), "%s/hermit", xdg); + o = snprintf(old, sizeof(old), "%s/cst-agent", xdg); + } else if (home && home[0]) { + n = snprintf(neu, sizeof(neu), "%s/.config/hermit", home); + o = snprintf(old, sizeof(old), "%s/.config/cst-agent", home); + } else + return -1; + if (n < 0 || o < 0 || (size_t)n >= sizeof(neu) || (size_t)o >= sizeof(old)) return -1; #endif - if (n < 0 || (size_t)n >= out_sz) + /* Prefer hermit/; fall back to legacy cst-agent/ when hermit is absent. */ + { + struct stat st; + if (stat(neu, &st) == 0 && S_ISDIR(st.st_mode)) { + if (snprintf(out, out_sz, "%s", neu) < 0 || strlen(neu) >= out_sz) + return -1; + return 0; + } + if (stat(old, &st) == 0 && S_ISDIR(st.st_mode)) { + if (snprintf(out, out_sz, "%s", old) < 0 || strlen(old) >= out_sz) + return -1; + return 0; + } + } + if (snprintf(out, out_sz, "%s", neu) < 0 || strlen(neu) >= out_sz) return -1; return 0; } @@ -298,10 +321,15 @@ static void tmp_pick(void) { snprintf(cand[nc++], OS_PATH_MAX, "%s/tmp", prefix); snprintf(cand[nc++], OS_PATH_MAX, "/tmp"); snprintf(cand[nc++], OS_PATH_MAX, "/var/tmp"); - if (cache && cache[0] == '/') - snprintf(cand[nc++], OS_PATH_MAX, "%s/cst-agent/tmp", cache); - else if (home && home[0] == '/') - snprintf(cand[nc++], OS_PATH_MAX, "%s/.cache/cst-agent/tmp", home); + if (cache && cache[0] == '/') { + snprintf(cand[nc++], OS_PATH_MAX, "%s/hermit/tmp", cache); + if (nc < 5) + snprintf(cand[nc++], OS_PATH_MAX, "%s/cst-agent/tmp", cache); + } else if (home && home[0] == '/') { + snprintf(cand[nc++], OS_PATH_MAX, "%s/.cache/hermit/tmp", home); + if (nc < 5) + snprintf(cand[nc++], OS_PATH_MAX, "%s/.cache/cst-agent/tmp", home); + } for (int i = 0; i < nc; i++) { tmp_trim(cand[i]); if (i == nc - 1) diff --git a/src/tui.c b/src/tui.c index 4214311..32afa40 100644 --- a/src/tui.c +++ b/src/tui.c @@ -24,6 +24,7 @@ #include "cst_tools.h" #include "cst_util.h" #include "cst_version.h" +#include "cst_hermit_glyph.h" #include "os_compat.h" #include "sashidown.h" @@ -496,18 +497,18 @@ static char *xstrndup(const char *s, size_t n) { /* --- theme ---------------------------------------------------------------- */ -/* Dark panel chrome — cool steel accents (not peach OpenCode clone). */ +/* Hermit palette: brand teal, crab orange, eye purple (design mock). */ static void theme_init(void) { const char *nc = getenv("NO_COLOR"); g_nocolor = nc && nc[0]; - const char *asc = getenv("CST_ASCII"); + const char *asc = hermit_getenv("ASCII"); #if defined(__ANDROID__) g_utf8 = 1; /* bionic is UTF-8 only; nl_langinfo needs API 26 */ #elif !defined(_WIN32) const char *cs = nl_langinfo(CODESET); g_utf8 = cs && (strcasecmp(cs, "UTF-8") == 0 || strcasecmp(cs, "utf8") == 0); #endif - g_ascii = asc && asc[0] ? asc[0] != '0' : !g_utf8; + g_ascii = asc[0] ? asc[0] != '0' : !g_utf8; if (!HAS_COLOR()) return; start_color(); @@ -515,54 +516,62 @@ static void theme_init(void) { short primary = COLOR_CYAN; short panel = COLOR_BLACK; - short muted = COLOR_CYAN; + short muted = COLOR_WHITE; short success = COLOR_GREEN; short err = COLOR_RED; - short assist = COLOR_CYAN; + short assist = COLOR_MAGENTA; short text = COLOR_WHITE; short box = COLOR_BLACK, side = COLOR_BLACK; - short accent = COLOR_BLUE, bright = COLOR_WHITE, logo_dim = COLOR_CYAN; + short accent = COLOR_CYAN, bright = COLOR_WHITE, logo_dim = COLOR_CYAN; + short tool = COLOR_YELLOW; + short prompt_c = COLOR_CYAN; if (COLORS >= 256) { box = 235; side = 234; - accent = 75; /* #5c9cf5 steel blue */ + accent = 75; /* #277F94 brand teal */ bright = 255; logo_dim = 242; - primary = 75; /* brand/user accent: blue, not peach */ - panel = 233; /* #121212 */ + primary = 75; + panel = 233; muted = 244; success = 114; err = 167; - assist = 74; /* soft cyan assistant */ - text = 252; /* brighter body for readability */ + assist = 177; /* #9957AD eye purple */ + tool = 216; /* #F68042 crab orange */ + prompt_c = 152; /* #A9D9D7 prompt cyan */ + text = 252; if (can_change_color()) { - init_color(75, 430, 660, 996); /* #6ea8fe brand */ - init_color(233, 55, 58, 70); /* #0e0f12 deeper panel */ - init_color(244, 502, 520, 560); /* cool muted */ - init_color(252, 820, 830, 850); /* body */ + init_color(75, 153, 498, 580); /* #277F94 brand */ + init_color(152, 663, 851, 843); /* #A9D9D7 prompt */ + init_color(216, 965, 502, 259); /* #F68042 crab */ + init_color(177, 600, 341, 678); /* #9957AD eye */ + init_color(167, 850, 320, 320); /* error red */ + init_color(168, 643, 341, 310); /* #A4574F brick legs */ + init_color(233, 55, 58, 70); /* #0e0f12 panel */ + init_color(244, 502, 520, 560); /* muted */ + init_color(252, 820, 830, 850); /* body text */ init_color(114, 400, 750, 450); - init_color(167, 850, 320, 320); - init_color(74, 380, 720, 780); - init_color(235, 100, 105, 120); /* raised box */ - init_color(234, 78, 82, 96); /* sidebar */ - init_color(216, 980, 720, 420); /* warm tip only */ + init_color(235, 100, 105, 120); + init_color(234, 78, 82, 96); } } else if (can_change_color() && COLORS >= 8) { init_color(COLOR_BLACK, 55, 58, 70); - init_color(COLOR_CYAN, 430, 660, 996); + init_color(COLOR_CYAN, 153, 498, 580); init_color(COLOR_WHITE, 820, 830, 850); init_color(COLOR_GREEN, 400, 750, 450); init_color(COLOR_RED, 850, 320, 320); - init_color(COLOR_BLUE, 380, 720, 780); - init_color(COLOR_YELLOW, 980, 720, 420); + init_color(COLOR_MAGENTA, 600, 341, 678); + init_color(COLOR_YELLOW, 965, 502, 259); panel = COLOR_BLACK; primary = COLOR_CYAN; muted = COLOR_WHITE; text = COLOR_WHITE; success = COLOR_GREEN; err = COLOR_RED; - assist = COLOR_BLUE; + assist = COLOR_MAGENTA; + tool = COLOR_YELLOW; + prompt_c = COLOR_CYAN; } /* Header is dark too — primary only as text accent (not a bright bar). */ @@ -574,11 +583,11 @@ static void theme_init(void) { init_pair(CP_MUTED, muted, panel); init_pair(CP_SUCCESS, success, panel); init_pair(CP_ERROR, err, panel); - init_pair(CP_TOOL, primary, panel); + init_pair(CP_TOOL, tool, panel); init_pair(CP_PANEL, text, panel); init_pair(CP_SGR_GREEN, success, panel); - init_pair(CP_SGR_YELLOW, primary, panel); - init_pair(CP_SGR_CYAN, assist, panel); + init_pair(CP_SGR_YELLOW, tool, panel); + init_pair(CP_SGR_CYAN, prompt_c, panel); if (COLORS >= 256) init_pair(CP_SGR_MAGENTA, 176, panel); else @@ -2687,20 +2696,87 @@ static void draw_todo(tui_t *t) { /* --- start screen ---------------------------------------------------------- */ -/* 3-row block font for "cstagent"; each glyph is 4 columns wide. */ -static const char *const LOGO[8][3] = { - {"█▀▀▀", "█ ", "▀▀▀▀"}, /* c */ - {"█▀▀▀", "▀▀▀█", "▀▀▀▀"}, /* s */ - {"▀█▀▀", " █ ", " ▀▀▀"}, /* t */ - {"█▀▀█", "█▀▀█", "▀ ▀"}, /* a */ - {"█▀▀▀", "█ ▀█", "▀▀▀▀"}, /* g */ - {"█▀▀▀", "█▀▀ ", "▀▀▀▀"}, /* e */ - {"█▀▀▄", "█ █", "▀ ▀"}, /* n */ - {"▀█▀▀", " █ ", " ▀▀▀"}, /* t */ +/* 3-row block font for "HERMIT"; each glyph is 4 columns wide. */ +static const char *const LOGO[6][3] = { + {"█ █", "█▀▀█", "█ █"}, /* H */ + {"█▀▀▀", "█▀▀ ", "▀▀▀▀"}, /* E */ + {"█▀▀█", "█▀▀█", "▀ ▀"}, /* R */ + {"█▀▄█", "█ █", "▀ ▀"}, /* M */ + {"▀█▀▀", " █ ", " ▀▀▀"}, /* I */ + {"▀█▀▀", " █ ", " ▀▀▀"}, /* T */ }; -#define LOGO_W (8 * 5 - 1) +#define LOGO_N 6 +#define LOGO_W (LOGO_N * 5 - 1) #define LOGO_H 3 +static int hermit_role_pair(int role) { + switch (role) { + case 0: + return CP_TOOL; /* body */ + case 1: + return CP_ACCENT; /* shell */ + case 2: + return CP_ASSIST; /* eye */ + case 3: + return COLORS >= 256 ? CP_MUTED : CP_ERROR; /* brick legs */ + case 4: + return CP_SGR_CYAN; /* >_ */ + default: + return CP_MUTED; + } +} + +static void draw_hermit_icon(WINDOW *win, int y0, int x0, hermit_state_t st, + int with_shell, int frame) { + hermit_icon_mode_t mode = hermit_icon_mode_eff(g_ascii); + if (mode == HERMIT_ICON_FONT) { + wchar_t wc = hermit_font_codepoint(st, frame, with_shell); + cchar_t cc; + wchar_t wstr[2] = {wc, 0}; + if (setcchar(&cc, wstr, 0, 0, NULL) == OK) { + if (HAS_COLOR()) + wattron(win, COLOR_PAIR(CP_TOOL)); + mvwadd_wch(win, y0, x0, &cc); + if (HAS_COLOR()) + wattroff(win, COLOR_PAIR(CP_TOOL)); + return; + } + } + const char *lines[8]; + int n = hermit_shell_art(st, with_shell, g_ascii, lines, 8); + for (int r = 0; r < n; r++) { + const char *row = lines[r]; + for (int c = 0; row[c]; c++) { + if (row[c] == ' ') + continue; + char ch[2] = {row[c], 0}; + int pair = hermit_role_pair(hermit_shell_cell_role(st, with_shell, r, c)); + draw_ansi_at(win, y0 + r, x0 + c, 1, ch, pair, 0, 0); + } + } +} + +static void draw_hermit_badge(WINDOW *win, int y, int x, const tui_t *t) { + hermit_state_t st = hermit_state_from_ui( + t->busy, t->ask_pending || t->awaiting_perm, 0); + hermit_icon_mode_t mode = hermit_icon_mode_eff(g_ascii); + if (mode == HERMIT_ICON_FONT) { + wchar_t wc = hermit_font_codepoint(st, t->spin, 0); + cchar_t cc; + wchar_t wstr[2] = {wc, 0}; + if (setcchar(&cc, wstr, 0, 0, NULL) == OK) { + if (HAS_COLOR()) + wattron(win, COLOR_PAIR(CP_TOOL)); + mvwadd_wch(win, y, x, &cc); + if (HAS_COLOR()) + wattroff(win, COLOR_PAIR(CP_TOOL)); + return; + } + } + const char *b = hermit_mini_badge(st, t->spin, g_ascii); + draw_ansi_at(win, y, x, (int)strlen(b), b, CP_TOOL, 0, 0); +} + static const char *const TIPS[] = { "Tab switches between Build and Plan (read-only) mode", "Shift+Tab cycles approval: ask, auto, yolo", @@ -2720,40 +2796,51 @@ static void draw_home(tui_t *t) { getmaxyx(t->logw, h, w); int box_top = t->in_y - t->log_y; int box_bot = box_top + t->in_h; - /* Tight home stack: wordmark (default) or CST_LOGO=block, tagline, chip. */ int brand_y = box_top - 2; if (brand_y < 0) brand_y = 0; int want_block = 0; { - const char *lb = getenv("CST_LOGO"); - want_block = lb && !strcmp(lb, "block"); + const char *lb = hermit_getenv("LOGO"); + want_block = lb[0] && !strcmp(lb, "block"); } + /* Crab on the right of the brand stack when there is room. */ + int crab_w = 14, crab_h = 6; + int crab_x = w - crab_w - 2; + int show_crab = crab_x > LOGO_W + 4 && box_top >= crab_h + 1; if (want_block && !(g_ascii || !g_utf8 || g_logo_text) && box_top - LOGO_H - 2 >= 0 && w >= LOGO_W) { - int x0 = (w - LOGO_W) / 2, y0 = box_top - LOGO_H - 2; - for (int g = 0; g < 8; g++) + int x0 = show_crab ? (crab_x - LOGO_W) / 2 : (w - LOGO_W) / 2; + if (x0 < 0) + x0 = 0; + int y0 = box_top - LOGO_H - 2; + for (int g = 0; g < LOGO_N; g++) for (int r = 0; r < LOGO_H; r++) - draw_ansi_at(t->logw, y0 + r, x0 + g * 5, 4, LOGO[g][r], - g < 3 ? CP_ACCENT : CP_BRIGHT, 0, 0); + draw_ansi_at(t->logw, y0 + r, x0 + g * 5, 4, LOGO[g][r], CP_ACCENT, 0, + 0); brand_y = y0 + LOGO_H; } else { - const char *mark = "CST-agent"; + const char *mark = "HERMIT"; int mw = (int)strlen(mark); - int x = (w - mw) / 2; + int x = show_crab ? (crab_x - mw) / 2 : (w - mw) / 2; if (x < 0) x = 0; if (brand_y >= 1) brand_y -= 1; - draw_ansi_at(t->logw, brand_y, x, 3, "CST", CP_ACCENT, 1, 0); - draw_ansi_at(t->logw, brand_y, x + 3, mw - 3, "-agent", CP_BRIGHT, 1, 0); + draw_ansi_at(t->logw, brand_y, x, mw, mark, CP_ACCENT, 1, 0); + } + if (show_crab) { + int cy = box_top - crab_h; + if (cy < 0) + cy = 0; + draw_hermit_icon(t->logw, cy, crab_x, HERMIT_ST_IDLE, 1, t->spin); } int ty = brand_y + 1; if (ty < box_top) { const char *tag = g_ascii ? "code . plan . parliament" : "code · plan · parliament"; int tw = (int)strlen(tag); - int x = (w - tw) / 2; + int x = show_crab ? (crab_x - tw) / 2 : (w - tw) / 2; if (x < 0) x = 0; draw_ansi_at(t->logw, ty, x, tw, tag, CP_MUTED, 0, 0); @@ -2772,9 +2859,10 @@ static void draw_home(tui_t *t) { } int right = t->in_x - t->log_x + t->in_w; if (box_bot < h) { - char hint[96]; + char hint[128]; snprintf(hint, sizeof(hint), - "\033[97mtab\033[39m modes \033[97mctrl+p\033[39m commands"); + "\033[97mtab\033[39m modes \033[97mctrl+p\033[39m commands " + "\033[97mshift+tab\033[39m approval"); int hw = vis_cols(hint); if (right - hw >= 0) draw_ansi_at(t->logw, box_bot, right - hw, hw, hint, CP_MUTED, 0, 0); @@ -2785,7 +2873,7 @@ static void draw_home(tui_t *t) { int tw = vis_cols(tip); if (tw > w - 4) tw = w - 4; - draw_ansi_at(t->logw, box_bot + 2, (w - tw) / 2, tw, tip, CP_MUTED, 0, 0); + draw_ansi_at(t->logw, box_bot + 2, (w - tw) / 2, tw, tip, CP_TOOL, 0, 0); } const char *ext = cst_extensions_report(); if (ext[0] && box_bot + 3 < h) { @@ -3018,7 +3106,7 @@ static void draw_side(tui_t *t) { if (h >= 4) { draw_ansi_at(s, h - 3, x, mw, t->where, CP_SIDE_MUTED, 0, 0); - snprintf(line, sizeof(line), "CST %s", CST_VERSION); + snprintf(line, sizeof(line), "Hermit %s", CST_VERSION); draw_ansi_at(s, h - 2, x, mw, line, CP_SIDE_MUTED, 0, 0); } wrefresh(s); @@ -3183,9 +3271,11 @@ static void draw_topbar(tui_t *t) { if (hexish) title = "New session"; } - /* CST mark + title + model. Role lives only in the Seat Deck. */ + /* Hermit badge + title + model. Role lives only in the Seat Deck. */ int x = 1; - x += draw_ansi_at(w, 0, x, 3, "CST", CP_TOPBAR_ACCENT, 1, 0); + draw_hermit_badge(w, 0, x, t); + x += 4; + x += draw_ansi_at(w, 0, x, 6, "Hermit", CP_TOPBAR_ACCENT, 1, 0); x += draw_ansi_at(w, 0, x, 1, " ", CP_TOPBAR_MUTED, 0, 0); int model_room = 0; char mid[160] = "";