feat: personality subcommand — list/show/add/remove/switch

CRUD for Hermes personality presets in ~/.hermes/config.yaml.
Supports --tg flag to switch via Telegram Bot API for live session updates.

Usage:
  hermes-har personality list
  hermes-har personality show <name>
  hermes-har personality add <name> <prompt>
  hermes-har personality remove <name>
  hermes-har personality switch <name> [--tg]
This commit is contained in:
团子 2026-04-20 00:10:28 +00:00
parent 27e7d908ed
commit 27f9849507
7 changed files with 456 additions and 1 deletions

34
.gitignore vendored Normal file
View File

@ -0,0 +1,34 @@
# dependencies (bun install)
node_modules
# output
out
dist
*.tgz
# code coverage
coverage
*.lcov
# logs
logs
_.log
report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
# dotenv environment variable files
.env
.env.development.local
.env.test.local
.env.production.local
.env.local
# caches
.eslintcache
.cache
*.tsbuildinfo
# IntelliJ based IDEs
.idea
# Finder (MacOS) folder config
.DS_Store

View File

@ -1,3 +1,36 @@
# hermes-harness # hermes-harness
Hermes Agent CLI harness tools — personality management, scheduling, and more Hermes Agent CLI harness tools — personality management, scheduling, and more.
## Install
```bash
bun install
bun link
```
## Usage
```bash
# List all personalities
hermes-har personality list
# Show personality details
hermes-har personality show 日常团子
# Add a personality
hermes-har personality add "测试人格" "你是一个测试用的人格..."
# Remove a personality
hermes-har personality remove "测试人格"
# Switch personality (local config only)
hermes-har personality switch 日常团子
# Switch personality via Telegram (live session update)
hermes-har personality switch 娇媚团子 --tg
```
## License
MIT

29
bun.lock Normal file
View File

@ -0,0 +1,29 @@
{
"lockfileVersion": 1,
"configVersion": 1,
"workspaces": {
"": {
"name": "hermes-harness",
"dependencies": {
"yaml": "^2.8.3",
},
"devDependencies": {
"@types/bun": "latest",
"typescript": "^5.9.0",
},
},
},
"packages": {
"@types/bun": ["@types/bun@1.3.12", "", { "dependencies": { "bun-types": "1.3.12" } }, "sha512-DBv81elK+/VSwXHDlnH3Qduw+KxkTIWi7TXkAeh24zpi5l0B2kUg9Ga3tb4nJaPcOFswflgi/yAvMVBPrxMB+A=="],
"@types/node": ["@types/node@25.6.0", "", { "dependencies": { "undici-types": "~7.19.0" } }, "sha512-+qIYRKdNYJwY3vRCZMdJbPLJAtGjQBudzZzdzwQYkEPQd+PJGixUL5QfvCLDaULoLv+RhT3LDkwEfKaAkgSmNQ=="],
"bun-types": ["bun-types@1.3.12", "", { "dependencies": { "@types/node": "*" } }, "sha512-HqOLj5PoFajAQciOMRiIZGNoKxDJSr6qigAttOX40vJuSp6DN/CxWp9s3C1Xwm4oH7ybueITwiaOcWXoYVoRkA=="],
"typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
"undici-types": ["undici-types@7.19.2", "", {}, "sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg=="],
"yaml": ["yaml@2.8.3", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-AvbaCLOO2Otw/lW5bmh9d/WEdcDFdQp2Z2ZUH3pX9U2ihyUY0nvLv7J6TrWowklRGPYbB/IuIMfYgxaCPg5Bpg=="],
}
}

19
package.json Normal file
View File

@ -0,0 +1,19 @@
{
"name": "hermes-harness",
"version": "0.1.0",
"description": "Hermes Agent CLI harness tools",
"type": "module",
"bin": {
"hermes-har": "./src/cli.ts"
},
"scripts": {
"dev": "bun run src/cli.ts"
},
"dependencies": {
"yaml": "^2.8.3"
},
"devDependencies": {
"@types/bun": "latest",
"typescript": "^5.9.0"
}
}

37
src/cli.ts Normal file
View File

@ -0,0 +1,37 @@
#!/usr/bin/env bun
import { personality } from "./commands/personality";
const COMMANDS: Record<string, (args: string[]) => Promise<void>> = {
personality,
};
async function main() {
const [command, ...args] = process.argv.slice(2);
if (!command || command === "help" || command === "--help") {
console.log(`hermes-har — Hermes Agent CLI harness tools
Usage: hermes-har <command> [options]
Commands:
personality Manage personality presets (list/add/remove/switch)
Run 'hermes-har <command> --help' for details.`);
process.exit(0);
}
const handler = COMMANDS[command];
if (!handler) {
console.error(`Unknown command: ${command}`);
console.error(`Run 'hermes-har help' for available commands.`);
process.exit(1);
}
await handler(args);
}
main().catch((err) => {
console.error(err.message);
process.exit(1);
});

273
src/commands/personality.ts Normal file
View File

@ -0,0 +1,273 @@
import { readFileSync, writeFileSync, existsSync } from "fs";
import { homedir } from "os";
import { join } from "path";
import { parse, stringify } from "yaml";
const CONFIG_PATH = join(homedir(), ".hermes", "config.yaml");
interface Config {
agent?: {
personalities?: Record<string, string | PersonalityDict>;
system_prompt?: string;
[key: string]: unknown;
};
[key: string]: unknown;
}
interface PersonalityDict {
description?: string;
system_prompt?: string;
tone?: string;
style?: string;
}
function loadConfig(): Config {
if (!existsSync(CONFIG_PATH)) {
throw new Error(`Config not found: ${CONFIG_PATH}`);
}
return parse(readFileSync(CONFIG_PATH, "utf-8")) || {};
}
function saveConfig(config: Config) {
writeFileSync(CONFIG_PATH, stringify(config, { lineWidth: 120 }), "utf-8");
}
function getPersonalities(config: Config): Record<string, string | PersonalityDict> {
return config?.agent?.personalities || {};
}
function resolvePrompt(value: string | PersonalityDict): string {
if (typeof value === "string") return value;
const parts = [value.system_prompt || ""];
if (value.tone) parts.push(`Tone: ${value.tone}`);
if (value.style) parts.push(`Style: ${value.style}`);
return parts.filter(Boolean).join("\n");
}
function getPreview(value: string | PersonalityDict): string {
if (typeof value === "string") {
return value.length > 60 ? value.slice(0, 60) + "..." : value;
}
return value.description || (value.system_prompt || "").slice(0, 60) + "...";
}
// --- Actions ---
function list() {
const config = loadConfig();
const personalities = getPersonalities(config);
const names = Object.keys(personalities);
if (names.length === 0) {
console.log("No personalities configured.");
return;
}
console.log("Available personalities:\n");
for (const name of names) {
console.log(` ${name}${getPreview(personalities[name])}`);
}
console.log(`\nTotal: ${names.length}`);
}
function show(name: string) {
const config = loadConfig();
const personalities = getPersonalities(config);
if (!(name in personalities)) {
console.error(`Personality not found: ${name}`);
console.error(`Available: ${Object.keys(personalities).join(", ")}`);
process.exit(1);
}
const value = personalities[name];
console.log(`[${name}]\n`);
if (typeof value === "string") {
console.log(value);
} else {
if (value.description) console.log(`Description: ${value.description}`);
if (value.system_prompt) console.log(`\n${value.system_prompt}`);
if (value.tone) console.log(`\nTone: ${value.tone}`);
if (value.style) console.log(`Style: ${value.style}`);
}
}
function add(name: string, prompt: string) {
const config = loadConfig();
if (!config.agent) config.agent = {};
if (!config.agent.personalities) config.agent.personalities = {};
const existed = name in config.agent.personalities;
config.agent.personalities[name] = prompt;
saveConfig(config);
console.log(`${existed ? "Updated" : "Added"}: ${name}`);
}
function remove(name: string) {
const config = loadConfig();
const personalities = getPersonalities(config);
if (!(name in personalities)) {
console.error(`Personality not found: ${name}`);
process.exit(1);
}
delete config.agent!.personalities![name];
saveConfig(config);
console.log(`Removed: ${name}`);
}
function switchLocal(name: string) {
const config = loadConfig();
const personalities = getPersonalities(config);
if (name === "none" || name === "default") {
if (!config.agent) config.agent = {};
config.agent.system_prompt = "";
saveConfig(config);
console.log("Personality cleared.");
return;
}
if (!(name in personalities)) {
console.error(`Personality not found: ${name}`);
console.error(`Available: ${Object.keys(personalities).join(", ")}`);
process.exit(1);
}
const prompt = resolvePrompt(personalities[name]);
if (!config.agent) config.agent = {};
config.agent.system_prompt = prompt;
saveConfig(config);
console.log(`Switched to: ${name}`);
}
async function switchTelegram(name: string) {
const config = loadConfig();
const personalities = getPersonalities(config);
if (name !== "none" && name !== "default" && !(name in personalities)) {
console.error(`Personality not found: ${name}`);
console.error(`Available: ${Object.keys(personalities).join(", ")}`);
process.exit(1);
}
// Find bot token
let token = process.env.TELEGRAM_BOT_TOKEN || "";
if (!token) {
const envPath = join(homedir(), ".hermes", ".env");
if (existsSync(envPath)) {
for (const line of readFileSync(envPath, "utf-8").split("\n")) {
if (line.startsWith("TELEGRAM_BOT_TOKEN=")) {
token = line.split("=")[1].trim();
}
}
}
}
if (!token) {
console.error("TELEGRAM_BOT_TOKEN not found");
process.exit(1);
}
const chatId =
process.env.TELEGRAM_HOME_CHANNEL ||
(config as Record<string, unknown>).TELEGRAM_HOME_CHANNEL ||
"";
if (!chatId) {
console.error("TELEGRAM_HOME_CHANNEL not configured");
process.exit(1);
}
const displayName = name === "none" || name === "default" ? "none" : name;
const url = `https://api.telegram.org/bot${token}/sendMessage`;
const resp = await fetch(url, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ chat_id: chatId, text: `/personality ${displayName}` }),
});
const result = await resp.json() as { ok: boolean; description?: string };
if (result.ok) {
console.log(`Sent /personality ${displayName} to chat ${chatId}`);
} else {
console.error(`Telegram API error: ${result.description}`);
process.exit(1);
}
}
// --- CLI ---
const HELP = `hermes-har personality — Manage personality presets
Usage:
hermes-har personality list List all personalities
hermes-har personality show <name> Show personality details
hermes-har personality add <name> <prompt> Add or update a personality
hermes-har personality remove <name> Remove a personality
hermes-har personality switch <name> Switch personality (writes config)
hermes-har personality switch <name> --tg Switch via Telegram /personality command`;
export async function personality(args: string[]) {
const action = args[0];
if (!action || action === "--help" || action === "help") {
console.log(HELP);
return;
}
switch (action) {
case "list":
case "ls":
list();
break;
case "show":
case "get":
if (!args[1]) {
console.error("Usage: hermes-har personality show <name>");
process.exit(1);
}
show(args[1]);
break;
case "add":
case "set":
if (!args[1] || !args[2]) {
console.error("Usage: hermes-har personality add <name> <prompt>");
process.exit(1);
}
add(args[1], args.slice(2).join(" "));
break;
case "remove":
case "rm":
case "delete":
if (!args[1]) {
console.error("Usage: hermes-har personality remove <name>");
process.exit(1);
}
remove(args[1]);
break;
case "switch":
case "use":
if (!args[1]) {
console.error("Usage: hermes-har personality switch <name> [--tg]");
process.exit(1);
}
if (args.includes("--tg") || args.includes("--telegram")) {
await switchTelegram(args[1]);
} else {
switchLocal(args[1]);
}
break;
default:
console.error(`Unknown action: ${action}`);
console.log(HELP);
process.exit(1);
}
}

30
tsconfig.json Normal file
View File

@ -0,0 +1,30 @@
{
"compilerOptions": {
// Environment setup & latest features
"lib": ["ESNext"],
"target": "ESNext",
"module": "Preserve",
"moduleDetection": "force",
"jsx": "react-jsx",
"allowJs": true,
"types": ["bun"],
// Bundler mode
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"verbatimModuleSyntax": true,
"noEmit": true,
// Best practices
"strict": true,
"skipLibCheck": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedIndexedAccess": true,
"noImplicitOverride": true,
// Some stricter flags (disabled by default)
"noUnusedLocals": false,
"noUnusedParameters": false,
"noPropertyAccessFromIndexSignature": false
}
}