feat(agent): enhance interactive CLI with colors, spinner, and status bar (#43)

* feat(agent): improve interactive CLI with colors, spinner, and status bar

- Add colors.ts module with ANSI terminal color utilities
- Add spinner animation for tool execution feedback
- Add persistent status bar showing session/provider/model
- Apply colors to welcome banner, prompts, commands, and suggestions
- Support NO_COLOR env for accessibility

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix(agent): correct cursor position with ANSI-colored prompts

Strip ANSI escape codes when calculating visual length of prompt
to ensure cursor is positioned correctly after colored text.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix(agent): prevent duplicate input echo in interactive CLI

Lazy-initialize readline.Interface only when multiline mode is active.
This prevents readline from interfering with autocomplete's raw mode,
which was causing user input to be echoed twice.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* refactor(agent): move CLI files to dedicated cli/ directory

Reorganize CLI-related files into src/agent/cli/ for better separation:
- interactive.ts (was interactive-cli.ts)
- non-interactive.ts (was cli.ts)
- profile.ts, skills.ts, tools.ts (was *-cli.ts)
- autocomplete.ts, colors.ts, output.ts (CLI utilities)

Update all imports, package.json scripts, and build configuration.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Jiayuan Zhang 2026-01-31 21:07:07 +08:00 committed by GitHub
parent b7e1063b3a
commit b1d80f29ae
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 437 additions and 87 deletions

View file

@ -1,205 +0,0 @@
#!/usr/bin/env node
/**
* Agent Profile CLI
*
* Commands:
* new <id> Create a new profile with default templates
* list List all profiles
* show <id> Show profile contents
* edit <id> Open profile directory
*/
import { existsSync, readdirSync } from "node:fs";
import { join } from "node:path";
import {
createAgentProfile,
loadAgentProfile,
getProfileDir,
profileExists,
} from "./profile/index.js";
import { DATA_DIR } from "../shared/index.js";
const DEFAULT_BASE_DIR = join(DATA_DIR, "agent-profiles");
type Command = "new" | "list" | "show" | "edit" | "help";
function printUsage() {
console.log("Usage: pnpm profile <command> [options]");
console.log("");
console.log("Commands:");
console.log(" new <id> Create a new profile with default templates");
console.log(" list List all profiles");
console.log(" show <id> Show profile contents");
console.log(" edit <id> Open profile directory in Finder/file manager");
console.log(" help Show this help");
console.log("");
console.log("Examples:");
console.log(" pnpm profile new my-agent");
console.log(" pnpm profile list");
console.log(" pnpm profile show my-agent");
}
function cmdNew(profileId: string | undefined) {
if (!profileId) {
console.error("Error: Profile ID is required");
console.error("Usage: pnpm profile new <id>");
process.exit(1);
}
// Validate profile ID
if (!/^[a-zA-Z0-9_-]+$/.test(profileId)) {
console.error("Error: Profile ID can only contain letters, numbers, hyphens, and underscores");
process.exit(1);
}
if (profileExists(profileId)) {
console.error(`Error: Profile "${profileId}" already exists`);
console.error(`Location: ${getProfileDir(profileId)}`);
process.exit(1);
}
const profile = createAgentProfile(profileId);
const dir = getProfileDir(profileId);
console.log(`Created profile: ${profile.id}`);
console.log(`Location: ${dir}`);
console.log("");
console.log("Files created:");
console.log(" - soul.md (personality and constraints)");
console.log(" - identity.md (name and role)");
console.log(" - tools.md (tool usage instructions)");
console.log(" - memory.md (persistent knowledge)");
console.log(" - bootstrap.md (initial context)");
console.log("");
console.log("Edit these files to customize your agent, then run:");
console.log(` pnpm agent:cli --profile ${profileId} "Hello"`);
}
function cmdList() {
if (!existsSync(DEFAULT_BASE_DIR)) {
console.log("No profiles found.");
console.log(`Create one with: pnpm profile new <id>`);
return;
}
const entries = readdirSync(DEFAULT_BASE_DIR, { withFileTypes: true });
const profiles = entries.filter((e) => e.isDirectory()).map((e) => e.name);
if (profiles.length === 0) {
console.log("No profiles found.");
console.log(`Create one with: pnpm profile new <id>`);
return;
}
console.log("Available profiles:");
console.log("");
for (const id of profiles) {
const dir = getProfileDir(id);
console.log(` ${id}`);
console.log(` ${dir}`);
}
console.log("");
console.log(`Total: ${profiles.length} profile(s)`);
}
function cmdShow(profileId: string | undefined) {
if (!profileId) {
console.error("Error: Profile ID is required");
console.error("Usage: pnpm profile show <id>");
process.exit(1);
}
const profile = loadAgentProfile(profileId);
if (!profile) {
console.error(`Error: Profile "${profileId}" not found`);
console.error(`Create it with: pnpm profile new ${profileId}`);
process.exit(1);
}
console.log(`Profile: ${profile.id}`);
console.log(`Location: ${getProfileDir(profileId)}`);
console.log("");
if (profile.identity) {
console.log("=== identity.md ===");
console.log(profile.identity.trim());
console.log("");
}
if (profile.soul) {
console.log("=== soul.md ===");
console.log(profile.soul.trim());
console.log("");
}
if (profile.tools) {
console.log("=== tools.md ===");
console.log(profile.tools.trim());
console.log("");
}
if (profile.memory) {
console.log("=== memory.md ===");
console.log(profile.memory.trim());
console.log("");
}
if (profile.bootstrap) {
console.log("=== bootstrap.md ===");
console.log(profile.bootstrap.trim());
console.log("");
}
}
async function cmdEdit(profileId: string | undefined) {
if (!profileId) {
console.error("Error: Profile ID is required");
console.error("Usage: pnpm profile edit <id>");
process.exit(1);
}
if (!profileExists(profileId)) {
console.error(`Error: Profile "${profileId}" not found`);
console.error(`Create it with: pnpm profile new ${profileId}`);
process.exit(1);
}
const dir = getProfileDir(profileId);
const { spawn } = await import("node:child_process");
// Open in default file manager
const cmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "explorer" : "xdg-open";
spawn(cmd, [dir], { detached: true, stdio: "ignore" }).unref();
console.log(`Opened: ${dir}`);
}
async function main() {
const args = process.argv.slice(2);
const command = (args[0] || "help") as Command;
const arg1 = args[1];
switch (command) {
case "new":
cmdNew(arg1);
break;
case "list":
cmdList();
break;
case "show":
cmdShow(arg1);
break;
case "edit":
await cmdEdit(arg1);
break;
case "help":
default:
printUsage();
break;
}
}
main().catch((err) => {
console.error(err?.stack || String(err));
process.exit(1);
});