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:
parent
27e7d908ed
commit
27f9849507
34
.gitignore
vendored
Normal file
34
.gitignore
vendored
Normal 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
|
||||||
35
README.md
35
README.md
@ -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
29
bun.lock
Normal 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
19
package.json
Normal 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
37
src/cli.ts
Normal 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
273
src/commands/personality.ts
Normal 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
30
tsconfig.json
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user