feat(cli): add nerve agent inject/update/remove/status commands
Phase 2 of #289: - nerve agent inject hermes [--profile <name>] - nerve agent update (updates all injected skills) - nerve agent remove hermes [--profile <name>] - nerve agent status (version check across profiles) - Include skills/ in npm package files Ref: #289, #293
This commit is contained in:
@@ -10,7 +10,7 @@
|
||||
},
|
||||
"main": "dist/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
"files": ["dist"],
|
||||
"files": ["dist", "skills"],
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
|
||||
@@ -3,6 +3,7 @@ import "@uncaged/nerve-daemon/experimental-warning-suppression.js";
|
||||
import { defineCommand, runMain } from "citty";
|
||||
|
||||
import { consumeGlobalDaemonCliFlags } from "./cli-global.js";
|
||||
import { agentCommand } from "./commands/agent.js";
|
||||
import { createCommand } from "./commands/create.js";
|
||||
import { daemonCommand } from "./commands/daemon.js";
|
||||
import { devCommand } from "./commands/dev.js";
|
||||
@@ -42,6 +43,7 @@ const main = defineCommand({
|
||||
"Nerve — an AI agent kernel. Global options: --host <host:port> (remote HTTP), --api-token <secret> (Bearer auth).",
|
||||
},
|
||||
subCommands: {
|
||||
agent: agentCommand,
|
||||
init: initCommand,
|
||||
create: createCommand,
|
||||
daemon: daemonCommand,
|
||||
|
||||
@@ -0,0 +1,225 @@
|
||||
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";
|
||||
|
||||
const CLI_VERSION = "0.5.0";
|
||||
|
||||
function getSkillSourceDir(): string {
|
||||
const thisFile = fileURLToPath(import.meta.url);
|
||||
let dir = dirname(thisFile);
|
||||
for (let i = 0; i < 5; i++) {
|
||||
if (existsSync(join(dir, "skills"))) return join(dir, "skills");
|
||||
dir = dirname(dir);
|
||||
}
|
||||
throw new Error("Cannot locate skills directory. Is the CLI package intact?");
|
||||
}
|
||||
|
||||
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 === CLI_VERSION) {
|
||||
const loc = profile !== null ? ` (profile: ${profile})` : "";
|
||||
process.stdout.write(`✅ Hermes nerve skill is already up to date (v${CLI_VERSION})${loc}\n`);
|
||||
return;
|
||||
}
|
||||
|
||||
mkdirSync(targetDir, { recursive: true });
|
||||
cpSync(sourceDir, targetDir, { recursive: true });
|
||||
writeVersionFile(targetDir, CLI_VERSION);
|
||||
|
||||
const action = existing !== null ? "Updated" : "Installed";
|
||||
const loc = profile !== null ? ` (profile: ${profile})` : "";
|
||||
process.stdout.write(`✅ ${action} Hermes nerve skill v${CLI_VERSION}${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${CLI_VERSION})\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 === CLI_VERSION) {
|
||||
process.stdout.write(` ${label}: ✅ v${version}\n`);
|
||||
} else {
|
||||
process.stdout.write(
|
||||
` ${label}: ⚠️ v${version} → v${CLI_VERSION} 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,
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user