Skip to content

Commit 1a92f5f

Browse files
authored
feat(cli): add some (hidden) agent handling commands (#187)
Also fix a few other small things: * preserve agent version info where needed * better error messages for bad server calls
1 parent c82d528 commit 1a92f5f

7 files changed

Lines changed: 492 additions & 12 deletions

File tree

src/commands/agent/list.ts

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
/**
2+
* List agents command
3+
*/
4+
5+
import chalk from "chalk";
6+
import { listAgents, type Agent } from "../../services/agentService.js";
7+
import { output, outputError } from "../../utils/output.js";
8+
import { formatTimeAgo } from "../../utils/time.js";
9+
10+
interface ListOptions {
11+
full?: boolean;
12+
name?: string;
13+
search?: string;
14+
public?: boolean;
15+
private?: boolean;
16+
output?: string;
17+
}
18+
19+
// Column widths (NAME is dynamic, takes remaining space)
20+
const COL_VERSION = 14;
21+
const COL_VISIBILITY = 10;
22+
const COL_ID = 30;
23+
const COL_CREATED = 10;
24+
const FIXED_WIDTH = COL_VERSION + COL_VISIBILITY + COL_ID + COL_CREATED + 4; // 4 for spacing
25+
26+
function truncate(str: string, maxLen: number): string {
27+
if (str.length <= maxLen) return str;
28+
return str.slice(0, maxLen - 1) + "…";
29+
}
30+
31+
function printTable(agents: Agent[]): void {
32+
if (agents.length === 0) {
33+
console.log(chalk.dim("No agents found"));
34+
return;
35+
}
36+
37+
const termWidth = process.stdout.columns || 120;
38+
const nameWidth = Math.max(10, termWidth - FIXED_WIDTH);
39+
40+
// Header
41+
const header =
42+
"NAME".padEnd(nameWidth) +
43+
" " +
44+
"VERSION".padEnd(COL_VERSION) +
45+
" " +
46+
"VISIBILITY".padEnd(COL_VISIBILITY) +
47+
" " +
48+
"ID".padEnd(COL_ID) +
49+
" " +
50+
"CREATED".padEnd(COL_CREATED);
51+
console.log(chalk.bold(header));
52+
console.log(chalk.dim("─".repeat(Math.min(header.length, termWidth))));
53+
54+
for (const agent of agents) {
55+
const name = truncate(agent.name, nameWidth).padEnd(nameWidth);
56+
const version = truncate(agent.version, COL_VERSION).padEnd(COL_VERSION);
57+
const visibility = (agent.is_public ? "public" : "private").padEnd(
58+
COL_VISIBILITY,
59+
);
60+
const visibilityColored = agent.is_public
61+
? chalk.green(visibility)
62+
: chalk.dim(visibility);
63+
const id = truncate(agent.id, COL_ID).padEnd(COL_ID);
64+
const created = formatTimeAgo(agent.create_time_ms).padEnd(COL_CREATED);
65+
66+
console.log(
67+
`${name} ${version} ${visibilityColored} ${chalk.dim(id)} ${chalk.dim(created)}`,
68+
);
69+
}
70+
71+
console.log();
72+
console.log(
73+
chalk.dim(`${agents.length} agent${agents.length !== 1 ? "s" : ""}`),
74+
);
75+
}
76+
77+
/**
78+
* Keep only the most recently created agent for each name.
79+
*/
80+
function keepLatestPerName(agents: Agent[]): Agent[] {
81+
const latestByName = new Map<string, Agent>();
82+
for (const agent of agents) {
83+
const existing = latestByName.get(agent.name);
84+
if (!existing || agent.create_time_ms > existing.create_time_ms) {
85+
latestByName.set(agent.name, agent);
86+
}
87+
}
88+
return Array.from(latestByName.values());
89+
}
90+
91+
export async function listAgentsCommand(options: ListOptions): Promise<void> {
92+
try {
93+
const result = await listAgents({
94+
publicOnly: options.public,
95+
privateOnly: options.private,
96+
name: options.name,
97+
search: options.search,
98+
});
99+
100+
const agents = options.full
101+
? result.agents
102+
: keepLatestPerName(result.agents);
103+
104+
const format = options.output || "text";
105+
if (format !== "text") {
106+
output(agents, { format, defaultFormat: "json" });
107+
} else {
108+
printTable(agents);
109+
}
110+
} catch (error) {
111+
outputError("Failed to list agents", error);
112+
}
113+
}

src/commands/agent/show.ts

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
/**
2+
* Show agent details command
3+
*/
4+
5+
import {
6+
getAgent,
7+
listAgents,
8+
type Agent,
9+
} from "../../services/agentService.js";
10+
import { output, outputError } from "../../utils/output.js";
11+
12+
interface ShowOptions {
13+
output?: string;
14+
}
15+
16+
/**
17+
* Determine whether the input looks like an agent ID (starts with "agt_")
18+
* vs. a name, then retrieve the corresponding agent.
19+
*/
20+
async function resolveAgent(idOrName: string): Promise<Agent> {
21+
if (idOrName.startsWith("agt_")) {
22+
return getAgent(idOrName);
23+
}
24+
25+
// Look up by name — fetch all versions with this name and pick the latest.
26+
const result = await listAgents({ name: idOrName });
27+
const matches = result.agents.filter((a) => a.name === idOrName);
28+
if (matches.length === 0) {
29+
throw new Error(`No agent found with name: ${idOrName}`);
30+
}
31+
32+
// Pick the most recently created version
33+
matches.sort((a, b) => b.create_time_ms - a.create_time_ms);
34+
return matches[0];
35+
}
36+
37+
export async function showAgentCommand(
38+
idOrName: string,
39+
options: ShowOptions,
40+
): Promise<void> {
41+
try {
42+
const agent = await resolveAgent(idOrName);
43+
output(agent, { format: options.output, defaultFormat: "text" });
44+
} catch (error) {
45+
outputError("Failed to get agent", error);
46+
}
47+
}

src/services/agentService.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ export interface ListAgentsOptions {
1414
privateOnly?: boolean;
1515
name?: string;
1616
search?: string;
17+
version?: string;
1718
}
1819

1920
export interface ListAgentsResult {
@@ -37,6 +38,7 @@ export async function listAgents(
3738
is_public?: boolean;
3839
name?: string;
3940
search?: string;
41+
version?: string;
4042
} = {
4143
limit: options.limit || 50,
4244
};
@@ -59,6 +61,10 @@ export async function listAgents(
5961
queryParams.search = options.search;
6062
}
6163

64+
if (options.version) {
65+
queryParams.version = options.version;
66+
}
67+
6268
const page = await client.agents.list(queryParams);
6369
const agents: Agent[] = [];
6470

src/utils/commands.ts

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1127,6 +1127,41 @@ export function createProgram(): Command {
11271127
await listBenchmarkJobsCommand(options);
11281128
});
11291129

1130+
// Agent commands
1131+
const agent = program
1132+
.command("agent", { hidden: true })
1133+
.description("Manage agents")
1134+
.alias("agt");
1135+
1136+
agent
1137+
.command("list")
1138+
.description("List agents")
1139+
.option("--full", "Show all versions for all agents")
1140+
.option("--name <name>", "Filter by name (partial match)")
1141+
.option("--search <query>", "Search by agent ID or name")
1142+
.option("--public", "Show only public agents")
1143+
.option("--private", "Show only private agents")
1144+
.option(
1145+
"-o, --output [format]",
1146+
"Output format: text|json|yaml (default: text)",
1147+
)
1148+
.action(async (options) => {
1149+
const { listAgentsCommand } = await import("../commands/agent/list.js");
1150+
await listAgentsCommand(options);
1151+
});
1152+
1153+
agent
1154+
.command("show <id-or-name>")
1155+
.description("Show agent details")
1156+
.option(
1157+
"-o, --output [format]",
1158+
"Output format: text|json|yaml (default: text)",
1159+
)
1160+
.action(async (idOrName, options) => {
1161+
const { showAgentCommand } = await import("../commands/agent/show.js");
1162+
await showAgentCommand(idOrName, options);
1163+
});
1164+
11301165
// Hidden command: 'rli mcp' without subcommand starts the server (for Claude Desktop config compatibility)
11311166
program
11321167
.command("mcp-server", { hidden: true })

src/utils/output.ts

Lines changed: 35 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -167,20 +167,43 @@ export function output(data: unknown, options: SimpleOutputOptions = {}): void {
167167
* outputError('Failed to get devbox', error);
168168
*/
169169
export function outputError(message: string, error?: Error | unknown): never {
170-
const errorMessage =
171-
error instanceof Error ? error.message : String(error || message);
172170
console.error(`Error: ${message}`);
173-
// Only print the error message if it adds new information
174-
// Skip if: same as message, message contains it, or it contains the message
175-
const messageLower = message.toLowerCase();
176-
const errorLower = errorMessage.toLowerCase();
177-
const isRedundant =
178-
errorMessage === message ||
179-
messageLower.includes(errorLower) ||
180-
errorLower.includes(messageLower);
181-
if (error && !isRedundant) {
182-
console.error(` ${errorMessage}`);
171+
172+
if (error && typeof error === "object") {
173+
// Extract API error details (status code, response body)
174+
const apiError = error as {
175+
status?: number;
176+
error?: unknown;
177+
message?: string;
178+
};
179+
180+
if (apiError.status) {
181+
console.error(` HTTP ${apiError.status}`);
182+
}
183+
184+
// Show the error body if it has useful detail beyond the message
185+
if (
186+
apiError.error &&
187+
typeof apiError.error === "object" &&
188+
Object.keys(apiError.error).length > 0
189+
) {
190+
const body = JSON.stringify(apiError.error);
191+
console.error(` ${body}`);
192+
}
193+
194+
// Show the error message if it adds info beyond what we already printed
195+
const errorMessage = error instanceof Error ? error.message : String(error);
196+
const messageLower = message.toLowerCase();
197+
const errorLower = errorMessage.toLowerCase();
198+
const isRedundant =
199+
errorMessage === message ||
200+
messageLower.includes(errorLower) ||
201+
errorLower.includes(messageLower);
202+
if (!isRedundant && !apiError.status) {
203+
console.error(` ${errorMessage}`);
204+
}
183205
}
206+
184207
processUtils.exit(1);
185208
}
186209

0 commit comments

Comments
 (0)