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 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