|
|
|
@@ -0,0 +1,244 @@
|
|
|
|
|
import {
|
|
|
|
|
cpSync,
|
|
|
|
|
existsSync,
|
|
|
|
|
mkdirSync,
|
|
|
|
|
readFileSync,
|
|
|
|
|
readdirSync,
|
|
|
|
|
rmSync,
|
|
|
|
|
writeFileSync,
|
|
|
|
|
} from "node:fs";
|
|
|
|
|
import { homedir } from "node:os";
|
|
|
|
|
import { dirname, join } from "node:path";
|
|
|
|
|
import { fileURLToPath } from "node:url";
|
|
|
|
|
|
|
|
|
|
import { defineCommand } from "citty";
|
|
|
|
|
|
|
|
|
|
function getPackageRootDir(): string {
|
|
|
|
|
const thisFile = fileURLToPath(import.meta.url);
|
|
|
|
|
let dir = dirname(thisFile);
|
|
|
|
|
for (let i = 0; i < 5; i++) {
|
|
|
|
|
if (existsSync(join(dir, "package.json"))) return dir;
|
|
|
|
|
dir = dirname(dir);
|
|
|
|
|
}
|
|
|
|
|
throw new Error("Cannot locate package root. Is the CLI package intact?");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function getCliVersion(): string {
|
|
|
|
|
const pkgPath = join(getPackageRootDir(), "package.json");
|
|
|
|
|
const pkg = JSON.parse(readFileSync(pkgPath, "utf8")) as { version: string };
|
|
|
|
|
return pkg.version;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let _cachedVersion: string | null = null;
|
|
|
|
|
function cliVersion(): string {
|
|
|
|
|
if (_cachedVersion === null) _cachedVersion = getCliVersion();
|
|
|
|
|
return _cachedVersion;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function getSkillSourceDir(): string {
|
|
|
|
|
const root = getPackageRootDir();
|
|
|
|
|
const skillsDir = join(root, "skills");
|
|
|
|
|
if (!existsSync(skillsDir)) {
|
|
|
|
|
throw new Error("Cannot locate skills directory. Is the CLI package intact?");
|
|
|
|
|
}
|
|
|
|
|
return skillsDir;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function getHermesSkillDir(profile: string | null): string {
|
|
|
|
|
const hermesHome = join(homedir(), ".hermes");
|
|
|
|
|
if (profile !== null) {
|
|
|
|
|
return join(hermesHome, "profiles", profile, "skills", "nerve");
|
|
|
|
|
}
|
|
|
|
|
return join(hermesHome, "skills", "nerve");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function readVersionFile(skillDir: string): string | null {
|
|
|
|
|
const versionPath = join(skillDir, ".nerve-version");
|
|
|
|
|
if (!existsSync(versionPath)) return null;
|
|
|
|
|
return readFileSync(versionPath, "utf8").trim();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function writeVersionFile(skillDir: string, version: string): void {
|
|
|
|
|
writeFileSync(join(skillDir, ".nerve-version"), `${version}\n`, "utf8");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function injectHermes(profile: string | null): void {
|
|
|
|
|
const sourceDir = join(getSkillSourceDir(), "hermes");
|
|
|
|
|
const targetDir = getHermesSkillDir(profile);
|
|
|
|
|
const existing = readVersionFile(targetDir);
|
|
|
|
|
|
|
|
|
|
if (existing === cliVersion()) {
|
|
|
|
|
const loc = profile !== null ? ` (profile: ${profile})` : "";
|
|
|
|
|
process.stdout.write(`✅ Hermes nerve skill is already up to date (v${cliVersion()})${loc}\n`);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
mkdirSync(targetDir, { recursive: true });
|
|
|
|
|
cpSync(sourceDir, targetDir, { recursive: true });
|
|
|
|
|
writeVersionFile(targetDir, cliVersion());
|
|
|
|
|
|
|
|
|
|
const action = existing !== null ? "Updated" : "Installed";
|
|
|
|
|
const loc = profile !== null ? ` (profile: ${profile})` : "";
|
|
|
|
|
process.stdout.write(`✅ ${action} Hermes nerve skill v${cliVersion()}${loc}\n`);
|
|
|
|
|
process.stdout.write(` → ${targetDir}/SKILL.md\n`);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function removeHermes(profile: string | null): void {
|
|
|
|
|
const targetDir = getHermesSkillDir(profile);
|
|
|
|
|
if (!existsSync(targetDir)) {
|
|
|
|
|
process.stdout.write("ℹ️ Hermes nerve skill is not installed.\n");
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
rmSync(targetDir, { recursive: true, force: true });
|
|
|
|
|
const loc = profile !== null ? ` (profile: ${profile})` : "";
|
|
|
|
|
process.stdout.write(`✅ Removed Hermes nerve skill${loc}\n`);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function printStatus(): void {
|
|
|
|
|
process.stdout.write(`nerve agent skills (CLI v${cliVersion()})\n\n`);
|
|
|
|
|
|
|
|
|
|
// Default profile
|
|
|
|
|
const defaultDir = getHermesSkillDir(null);
|
|
|
|
|
const defaultVer = readVersionFile(defaultDir);
|
|
|
|
|
printAgentLine("Hermes (default)", defaultVer);
|
|
|
|
|
|
|
|
|
|
// Named profiles
|
|
|
|
|
const profilesDir = join(homedir(), ".hermes", "profiles");
|
|
|
|
|
if (existsSync(profilesDir)) {
|
|
|
|
|
const profiles = readdirSync(profilesDir, { withFileTypes: true })
|
|
|
|
|
.filter((d) => d.isDirectory())
|
|
|
|
|
.map((d) => d.name);
|
|
|
|
|
|
|
|
|
|
for (const profile of profiles) {
|
|
|
|
|
const dir = getHermesSkillDir(profile);
|
|
|
|
|
const ver = readVersionFile(dir);
|
|
|
|
|
if (ver !== null) {
|
|
|
|
|
printAgentLine(`Hermes (${profile})`, ver);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
process.stdout.write("\n");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function printAgentLine(label: string, version: string | null): void {
|
|
|
|
|
if (version === null) {
|
|
|
|
|
process.stdout.write(` ${label}: ❌ not installed\n`);
|
|
|
|
|
} else if (version === cliVersion()) {
|
|
|
|
|
process.stdout.write(` ${label}: ✅ v${version}\n`);
|
|
|
|
|
} else {
|
|
|
|
|
process.stdout.write(
|
|
|
|
|
` ${label}: ⚠️ v${version} → v${cliVersion()} available (run \`nerve agent update\`)\n`,
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const injectCommand = defineCommand({
|
|
|
|
|
meta: {
|
|
|
|
|
name: "inject",
|
|
|
|
|
description: "Inject nerve skill into an AI agent",
|
|
|
|
|
},
|
|
|
|
|
args: {
|
|
|
|
|
target: {
|
|
|
|
|
type: "positional",
|
|
|
|
|
description: "Agent target: hermes",
|
|
|
|
|
},
|
|
|
|
|
profile: {
|
|
|
|
|
type: "string",
|
|
|
|
|
description: "Hermes profile name (default: main profile)",
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
run({ args }) {
|
|
|
|
|
if (args.target !== "hermes") {
|
|
|
|
|
process.stderr.write(`❌ Unknown agent target: ${args.target}\n`);
|
|
|
|
|
process.stderr.write(" Supported targets: hermes\n");
|
|
|
|
|
process.exit(1);
|
|
|
|
|
}
|
|
|
|
|
injectHermes(args.profile ?? null);
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const updateCommand = defineCommand({
|
|
|
|
|
meta: {
|
|
|
|
|
name: "update",
|
|
|
|
|
description: "Update all injected nerve skills to current CLI version",
|
|
|
|
|
},
|
|
|
|
|
run() {
|
|
|
|
|
let updated = 0;
|
|
|
|
|
|
|
|
|
|
// Default profile
|
|
|
|
|
const defaultDir = getHermesSkillDir(null);
|
|
|
|
|
if (existsSync(defaultDir)) {
|
|
|
|
|
injectHermes(null);
|
|
|
|
|
updated++;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Named profiles
|
|
|
|
|
const profilesDir = join(homedir(), ".hermes", "profiles");
|
|
|
|
|
if (existsSync(profilesDir)) {
|
|
|
|
|
const profiles = readdirSync(profilesDir, { withFileTypes: true })
|
|
|
|
|
.filter((d) => d.isDirectory())
|
|
|
|
|
.map((d) => d.name);
|
|
|
|
|
|
|
|
|
|
for (const profile of profiles) {
|
|
|
|
|
const dir = getHermesSkillDir(profile);
|
|
|
|
|
if (existsSync(dir)) {
|
|
|
|
|
injectHermes(profile);
|
|
|
|
|
updated++;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (updated === 0) {
|
|
|
|
|
process.stdout.write("ℹ️ No injected skills found. Run `nerve agent inject hermes` first.\n");
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const removeCommand = defineCommand({
|
|
|
|
|
meta: {
|
|
|
|
|
name: "remove",
|
|
|
|
|
description: "Remove injected nerve skill from an AI agent",
|
|
|
|
|
},
|
|
|
|
|
args: {
|
|
|
|
|
target: {
|
|
|
|
|
type: "positional",
|
|
|
|
|
description: "Agent target: hermes",
|
|
|
|
|
},
|
|
|
|
|
profile: {
|
|
|
|
|
type: "string",
|
|
|
|
|
description: "Hermes profile name (default: main profile)",
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
run({ args }) {
|
|
|
|
|
if (args.target !== "hermes") {
|
|
|
|
|
process.stderr.write(`❌ Unknown agent target: ${args.target}\n`);
|
|
|
|
|
process.stderr.write(" Supported targets: hermes\n");
|
|
|
|
|
process.exit(1);
|
|
|
|
|
}
|
|
|
|
|
removeHermes(args.profile ?? null);
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const statusCommand = defineCommand({
|
|
|
|
|
meta: {
|
|
|
|
|
name: "status",
|
|
|
|
|
description: "Show injection status of nerve skills across agents",
|
|
|
|
|
},
|
|
|
|
|
run() {
|
|
|
|
|
printStatus();
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
export const agentCommand = defineCommand({
|
|
|
|
|
meta: {
|
|
|
|
|
name: "agent",
|
|
|
|
|
description: "Manage nerve skill injection for AI agents",
|
|
|
|
|
},
|
|
|
|
|
subCommands: {
|
|
|
|
|
inject: injectCommand,
|
|
|
|
|
update: updateCommand,
|
|
|
|
|
remove: removeCommand,
|
|
|
|
|
status: statusCommand,
|
|
|
|
|
},
|
|
|
|
|
});
|