Skip to content

Commit 512d41f

Browse files
authored
feat: update agents support in rli command line (#200)
This adds support for agent creation and deletion via the RLI command-line interface. This also improves the 'list' display in a few ways and adds some augmented unit tests. Originally from Jason's jason/agents_tab branch, with additional updates from Rob.
1 parent d4aa73f commit 512d41f

10 files changed

Lines changed: 1051 additions & 26 deletions

File tree

README.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -203,6 +203,15 @@ rli benchmark-job logs <id> # Download devbox logs for all scenario
203203
rli benchmark-job list # List benchmark jobs
204204
```
205205

206+
### Agent Commands (alias: `agt`)
207+
208+
```bash
209+
rli agent list # List agents
210+
rli agent create # Create a new agent
211+
rli agent delete <id-or-name> # Delete an agent
212+
rli agent show <id-or-name> # Show agent details
213+
```
214+
206215

207216
## MCP Server (AI Integration)
208217

src/commands/agent/create.ts

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
/**
2+
* Create agent command
3+
*/
4+
import chalk from "chalk";
5+
import { createAgent } from "../../services/agentService.js";
6+
import { output, outputError } from "../../utils/output.js";
7+
8+
interface CreateOptions {
9+
name: string;
10+
agentVersion: string;
11+
source: string;
12+
package?: string;
13+
registryUrl?: string;
14+
repository?: string;
15+
ref?: string;
16+
objectId?: string;
17+
setupCommands?: string[];
18+
output?: string;
19+
}
20+
21+
// Maps each source type to the options it accepts (beyond --setup-commands, which all types accept)
22+
const validOptionsBySource: Record<string, (keyof CreateOptions)[]> = {
23+
npm: ["package", "registryUrl"],
24+
pip: ["package", "registryUrl"],
25+
git: ["repository", "ref"],
26+
object: ["objectId"],
27+
};
28+
29+
// All source-specific option flags (for error messages)
30+
const allSourceOptions: { key: keyof CreateOptions; flag: string }[] = [
31+
{ key: "package", flag: "--package" },
32+
{ key: "registryUrl", flag: "--registry-url" },
33+
{ key: "repository", flag: "--repository" },
34+
{ key: "ref", flag: "--ref" },
35+
{ key: "objectId", flag: "--object-id" },
36+
];
37+
38+
function rejectInvalidOptions(
39+
sourceType: string,
40+
options: CreateOptions,
41+
): void {
42+
const allowed = validOptionsBySource[sourceType] || [];
43+
const invalid = allSourceOptions
44+
.filter(
45+
(opt) => options[opt.key] !== undefined && !allowed.includes(opt.key),
46+
)
47+
.map((opt) => opt.flag);
48+
49+
if (invalid.length > 0) {
50+
throw new Error(
51+
`${invalid.join(", ")} cannot be used with ${sourceType} source type`,
52+
);
53+
}
54+
}
55+
56+
function buildSourceOptions(
57+
sourceType: string,
58+
options: CreateOptions,
59+
): Record<string, unknown> {
60+
rejectInvalidOptions(sourceType, options);
61+
62+
switch (sourceType) {
63+
case "npm":
64+
case "pip":
65+
if (!options.package) {
66+
throw new Error(`--package is required for ${sourceType} source type`);
67+
}
68+
return {
69+
package_name: options.package,
70+
registry_url: options.registryUrl || undefined,
71+
agent_setup: options.setupCommands || undefined,
72+
};
73+
case "git":
74+
if (!options.repository) {
75+
throw new Error("--repository is required for git source type");
76+
}
77+
return {
78+
repository: options.repository,
79+
ref: options.ref || undefined,
80+
agent_setup: options.setupCommands || undefined,
81+
};
82+
case "object":
83+
if (!options.objectId) {
84+
throw new Error("--object-id is required for object source type");
85+
}
86+
return {
87+
object_id: options.objectId,
88+
agent_setup: options.setupCommands || undefined,
89+
};
90+
default:
91+
throw new Error(
92+
`Unknown source type: ${sourceType}. Use npm, pip, git, or object.`,
93+
);
94+
}
95+
}
96+
97+
export async function createAgentCommand(
98+
options: CreateOptions,
99+
): Promise<void> {
100+
try {
101+
const sourceType = options.source;
102+
const sourceOptions = buildSourceOptions(sourceType, options);
103+
104+
const agent = await createAgent({
105+
name: options.name,
106+
version: options.agentVersion,
107+
source: { type: sourceType, [sourceType]: sourceOptions },
108+
});
109+
110+
const format = options.output || "text";
111+
if (format !== "text") {
112+
output(agent, { format, defaultFormat: "json" });
113+
} else {
114+
console.log(chalk.green("✓") + " Agent created successfully");
115+
console.log();
116+
console.log(` ${chalk.bold("Name:")} ${agent.name}`);
117+
console.log(` ${chalk.bold("ID:")} ${chalk.dim(agent.id)}`);
118+
console.log(` ${chalk.bold("Version:")} ${agent.version}`);
119+
console.log(` ${chalk.bold("Source:")} ${sourceType}`);
120+
}
121+
} catch (error) {
122+
outputError("Failed to create agent", error);
123+
}
124+
}

src/commands/agent/delete.ts

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
/**
2+
* Delete agent command
3+
*/
4+
import chalk from "chalk";
5+
import readline from "readline";
6+
import {
7+
getAgent,
8+
listAgents,
9+
deleteAgent,
10+
} from "../../services/agentService.js";
11+
import { printAgentTable } from "./list.js";
12+
import { output, outputError } from "../../utils/output.js";
13+
14+
interface DeleteOptions {
15+
yes?: boolean;
16+
output?: string;
17+
}
18+
19+
async function confirm(message: string): Promise<boolean> {
20+
const rl = readline.createInterface({
21+
input: process.stdin,
22+
output: process.stdout,
23+
});
24+
25+
return new Promise((resolve) => {
26+
rl.question(message, (answer) => {
27+
rl.close();
28+
resolve(answer.toLowerCase() === "y" || answer.toLowerCase() === "yes");
29+
});
30+
});
31+
}
32+
33+
export async function deleteAgentCommand(
34+
idOrName: string,
35+
options: DeleteOptions,
36+
): Promise<void> {
37+
try {
38+
// Direct ID lookup
39+
if (idOrName.startsWith("agt_")) {
40+
const agent = await getAgent(idOrName);
41+
await confirmAndDelete(agent, options);
42+
return;
43+
}
44+
45+
// Name lookup — require exactly one match
46+
const result = await listAgents({ name: idOrName });
47+
const matches = result.agents.filter((a) => a.name === idOrName);
48+
49+
if (matches.length === 0) {
50+
throw new Error(`No agent found with name: ${idOrName}`);
51+
}
52+
53+
if (matches.length > 1) {
54+
console.log(
55+
`Multiple agents found with name "${idOrName}". Delete by ID instead:`,
56+
);
57+
console.log();
58+
printAgentTable(matches);
59+
return;
60+
}
61+
62+
await confirmAndDelete(matches[0], options);
63+
} catch (error) {
64+
outputError("Failed to delete agent", error);
65+
}
66+
}
67+
68+
async function confirmAndDelete(
69+
agent: { id: string; name: string },
70+
options: DeleteOptions,
71+
): Promise<void> {
72+
if (!options.yes) {
73+
const confirmed = await confirm(
74+
`Delete agent "${agent.name}" (${agent.id})? [y/N] `,
75+
);
76+
if (!confirmed) {
77+
console.log(chalk.dim("Cancelled"));
78+
return;
79+
}
80+
}
81+
82+
await deleteAgent(agent.id);
83+
84+
const format = options.output || "text";
85+
if (format !== "text") {
86+
output(
87+
{ deleted: true, id: agent.id, name: agent.name },
88+
{ format, defaultFormat: "json" },
89+
);
90+
} else {
91+
console.log(
92+
chalk.green("✓") + ` Agent "${agent.name}" (${agent.id}) deleted`,
93+
);
94+
}
95+
}

src/commands/agent/list.ts

Lines changed: 19 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -96,18 +96,10 @@ function padStyled(raw: string, styled: string, width: number): string {
9696
return styled + " ".repeat(Math.max(0, width - raw.length));
9797
}
9898

99-
function printTable(agents: Agent[], isPublic: boolean): void {
100-
if (isPublic) {
101-
console.log(
102-
chalk.dim("Showing PUBLIC agents. Use --private to see private agents"),
103-
);
104-
} else {
105-
console.log(
106-
chalk.dim("Showing PRIVATE agents. Use --public to see public agents"),
107-
);
108-
}
109-
console.log();
110-
99+
/**
100+
* Render a table of agents to stdout. Reusable by other commands.
101+
*/
102+
export function printAgentTable(agents: Agent[]): void {
111103
if (agents.length === 0) {
112104
console.log(chalk.dim("No agents found"));
113105
return;
@@ -135,6 +127,21 @@ function printTable(agents: Agent[], isPublic: boolean): void {
135127
);
136128
}
137129

130+
function printTable(agents: Agent[], isPublic: boolean): void {
131+
if (isPublic) {
132+
console.log(
133+
chalk.dim("Showing PUBLIC agents. Use --private to see private agents"),
134+
);
135+
} else {
136+
console.log(
137+
chalk.dim("Showing PRIVATE agents. Use --public to see public agents"),
138+
);
139+
}
140+
console.log();
141+
142+
printAgentTable(agents);
143+
}
144+
138145
/**
139146
* Keep only the most recently created agent for each name.
140147
*/

src/commands/devbox/create.ts

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,11 @@
44

55
import { getClient } from "../../utils/client.js";
66
import { output, outputError } from "../../utils/output.js";
7+
import {
8+
listAgents,
9+
getAgent,
10+
type Agent,
11+
} from "../../services/agentService.js";
712

813
interface CreateOptions {
914
name?: string;
@@ -26,6 +31,8 @@ interface CreateOptions {
2631
tunnel?: string;
2732
gateways?: string[];
2833
mcp?: string[];
34+
agent?: string[];
35+
agentPath?: string;
2936
output?: string;
3037
}
3138

@@ -147,6 +154,19 @@ function parseMcpSpecs(
147154
return result;
148155
}
149156

157+
async function resolveAgent(idOrName: string): Promise<Agent> {
158+
if (idOrName.startsWith("agt_")) {
159+
return getAgent(idOrName);
160+
}
161+
const result = await listAgents({ name: idOrName });
162+
const matches = result.agents.filter((a) => a.name === idOrName);
163+
if (matches.length === 0) {
164+
throw new Error(`No agent found with name: ${idOrName}`);
165+
}
166+
matches.sort((a, b) => b.create_time_ms - a.create_time_ms);
167+
return matches[0];
168+
}
169+
150170
export async function createDevbox(options: CreateOptions = {}) {
151171
try {
152172
const client = getClient();
@@ -273,6 +293,30 @@ export async function createDevbox(options: CreateOptions = {}) {
273293
createRequest.mcp = parseMcpSpecs(options.mcp);
274294
}
275295

296+
// Handle agent mount
297+
if (options.agent && options.agent.length > 0) {
298+
if (options.agent.length > 1) {
299+
throw new Error(
300+
"Mounting multiple agents via rli is not supported yet",
301+
);
302+
}
303+
const agent = await resolveAgent(options.agent[0]);
304+
const mount: Record<string, unknown> = {
305+
type: "agent_mount",
306+
agent_id: agent.id,
307+
agent_name: null,
308+
};
309+
// agent_path only makes sense for git and object agents. Since
310+
// we don't know at this stage what type of agent it is,
311+
// however, we'll let the server error inform the user if they
312+
// add this option in a case where it doesn't make sense.
313+
if (options.agentPath) {
314+
mount.agent_path = options.agentPath;
315+
}
316+
if (!createRequest.mounts) createRequest.mounts = [];
317+
(createRequest.mounts as unknown[]).push(mount);
318+
}
319+
276320
if (Object.keys(launchParameters).length > 0) {
277321
createRequest.launch_parameters = launchParameters;
278322
}

0 commit comments

Comments
 (0)