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:
parent
b7e1063b3a
commit
b1d80f29ae
12 changed files with 437 additions and 87 deletions
195
src/agent/cli/non-interactive.ts
Normal file
195
src/agent/cli/non-interactive.ts
Normal file
|
|
@ -0,0 +1,195 @@
|
|||
#!/usr/bin/env node
|
||||
import { Agent } from "../runner.js";
|
||||
|
||||
type CliOptions = {
|
||||
profile?: string | undefined;
|
||||
provider?: string | undefined;
|
||||
model?: string | undefined;
|
||||
apiKey?: string | undefined;
|
||||
baseUrl?: string | undefined;
|
||||
system?: string | undefined;
|
||||
thinking?: string | undefined;
|
||||
cwd?: string | undefined;
|
||||
session?: string | undefined;
|
||||
debug?: boolean | undefined;
|
||||
help?: boolean | undefined;
|
||||
// Tools configuration
|
||||
toolsProfile?: string | undefined;
|
||||
toolsAllow?: string[] | undefined;
|
||||
toolsDeny?: string[] | undefined;
|
||||
};
|
||||
|
||||
function printUsage() {
|
||||
console.log("Usage: pnpm agent:cli [options] <prompt>");
|
||||
console.log(" echo \"your prompt\" | pnpm agent:cli");
|
||||
console.log("");
|
||||
console.log("Options:");
|
||||
console.log(" --profile ID Load agent profile (identity, soul, tools, memory)");
|
||||
console.log(" --provider NAME LLM provider (e.g., openai, anthropic, kimi)");
|
||||
console.log(" --model NAME Model name");
|
||||
console.log(" --api-key KEY API key (overrides environment variable)");
|
||||
console.log(" --base-url URL Custom base URL for the provider");
|
||||
console.log(" --system TEXT System prompt (ignored if --profile is set)");
|
||||
console.log(" --thinking LEVEL Thinking level");
|
||||
console.log(" --cwd DIR Working directory for commands");
|
||||
console.log(" --session ID Session ID for conversation persistence");
|
||||
console.log(" --debug Enable debug logging");
|
||||
console.log(" --help, -h Show this help");
|
||||
console.log("");
|
||||
console.log("Tools Configuration:");
|
||||
console.log(" --tools-profile PROFILE Tool profile (minimal, coding, web, full)");
|
||||
console.log(" --tools-allow TOOLS Allow specific tools (comma-separated, supports group:*)");
|
||||
console.log(" --tools-deny TOOLS Deny specific tools (comma-separated)");
|
||||
console.log("");
|
||||
console.log("Examples:");
|
||||
console.log(' pnpm agent:cli --tools-profile coding "list files"');
|
||||
console.log(' pnpm agent:cli --tools-profile minimal --tools-allow exec "run ls"');
|
||||
console.log(' pnpm agent:cli --tools-deny exec,process "read file.txt"');
|
||||
}
|
||||
|
||||
function parseArgs(argv: string[]) {
|
||||
const args = [...argv];
|
||||
const opts: CliOptions = {};
|
||||
const promptParts: string[] = [];
|
||||
|
||||
while (args.length > 0) {
|
||||
const arg = args.shift();
|
||||
if (!arg) break;
|
||||
if (arg === "--help" || arg === "-h") {
|
||||
opts.help = true;
|
||||
break;
|
||||
}
|
||||
if (arg === "--profile") {
|
||||
opts.profile = args.shift();
|
||||
continue;
|
||||
}
|
||||
if (arg === "--provider") {
|
||||
opts.provider = args.shift();
|
||||
continue;
|
||||
}
|
||||
if (arg === "--model") {
|
||||
opts.model = args.shift();
|
||||
continue;
|
||||
}
|
||||
if (arg === "--api-key") {
|
||||
opts.apiKey = args.shift();
|
||||
continue;
|
||||
}
|
||||
if (arg === "--base-url") {
|
||||
opts.baseUrl = args.shift();
|
||||
continue;
|
||||
}
|
||||
if (arg === "--system") {
|
||||
opts.system = args.shift();
|
||||
continue;
|
||||
}
|
||||
if (arg === "--thinking") {
|
||||
opts.thinking = args.shift();
|
||||
continue;
|
||||
}
|
||||
if (arg === "--cwd") {
|
||||
opts.cwd = args.shift();
|
||||
continue;
|
||||
}
|
||||
if (arg === "--session") {
|
||||
opts.session = args.shift();
|
||||
continue;
|
||||
}
|
||||
if (arg === "--debug") {
|
||||
opts.debug = true;
|
||||
continue;
|
||||
}
|
||||
if (arg === "--tools-profile") {
|
||||
opts.toolsProfile = args.shift();
|
||||
continue;
|
||||
}
|
||||
if (arg === "--tools-allow") {
|
||||
const value = args.shift();
|
||||
opts.toolsAllow = value?.split(",").map((s) => s.trim()) ?? [];
|
||||
continue;
|
||||
}
|
||||
if (arg === "--tools-deny") {
|
||||
const value = args.shift();
|
||||
opts.toolsDeny = value?.split(",").map((s) => s.trim()) ?? [];
|
||||
continue;
|
||||
}
|
||||
if (arg === "--") {
|
||||
promptParts.push(...args);
|
||||
break;
|
||||
}
|
||||
promptParts.push(arg);
|
||||
}
|
||||
|
||||
return { opts, prompt: promptParts.join(" ") };
|
||||
}
|
||||
|
||||
async function readStdin() {
|
||||
if (process.stdin.isTTY) return "";
|
||||
return new Promise<string>((resolve, reject) => {
|
||||
let data = "";
|
||||
process.stdin.setEncoding("utf8");
|
||||
process.stdin.on("data", (chunk) => (data += chunk));
|
||||
process.stdin.on("end", () => resolve(data.trim()));
|
||||
process.stdin.on("error", reject);
|
||||
});
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const { opts, prompt } = parseArgs(process.argv.slice(2));
|
||||
if (opts.help) {
|
||||
printUsage();
|
||||
return;
|
||||
}
|
||||
|
||||
const stdinPrompt = await readStdin();
|
||||
const finalPrompt = prompt || stdinPrompt;
|
||||
if (!finalPrompt) {
|
||||
printUsage();
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Build tools config if any tools options are set
|
||||
let toolsConfig: import("../tools/policy.js").ToolsConfig | undefined;
|
||||
if (opts.toolsProfile || opts.toolsAllow || opts.toolsDeny) {
|
||||
toolsConfig = {};
|
||||
if (opts.toolsProfile) {
|
||||
toolsConfig.profile = opts.toolsProfile as any;
|
||||
}
|
||||
if (opts.toolsAllow) {
|
||||
toolsConfig.allow = opts.toolsAllow;
|
||||
}
|
||||
if (opts.toolsDeny) {
|
||||
toolsConfig.deny = opts.toolsDeny;
|
||||
}
|
||||
}
|
||||
|
||||
const agent = new Agent({
|
||||
profileId: opts.profile,
|
||||
provider: opts.provider,
|
||||
model: opts.model,
|
||||
apiKey: opts.apiKey,
|
||||
baseUrl: opts.baseUrl,
|
||||
systemPrompt: opts.system,
|
||||
thinkingLevel: opts.thinking as any,
|
||||
cwd: opts.cwd,
|
||||
sessionId: opts.session,
|
||||
debug: opts.debug,
|
||||
tools: toolsConfig,
|
||||
});
|
||||
|
||||
// If it's a newly created session, notify user of sessionId
|
||||
if (!opts.session) {
|
||||
console.error(`[session: ${agent.sessionId}]`);
|
||||
}
|
||||
|
||||
const result = await agent.run(finalPrompt);
|
||||
if (result.error) {
|
||||
console.error(`Error: ${result.error}`);
|
||||
process.exitCode = 1;
|
||||
}
|
||||
}
|
||||
|
||||
main().catch((err) => {
|
||||
console.error(err?.stack || String(err));
|
||||
process.exit(1);
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue