feat(cli): command architecture — agents, missions, gateway-aware prdy (#158)
Some checks failed
ci/woodpecker/push/ci Pipeline failed

Co-authored-by: Jason Woltje <jason@diversecanvas.com>
Co-committed-by: Jason Woltje <jason@diversecanvas.com>
This commit was merged in pull request #158.
This commit is contained in:
2026-03-15 23:10:23 +00:00
committed by jason.woltje
parent 82c10a7b33
commit 4da255bf04
28 changed files with 1747 additions and 394 deletions

View File

@@ -0,0 +1,241 @@
import type { Command } from 'commander';
import { withAuth } from './with-auth.js';
import { selectItem } from './select-dialog.js';
import {
fetchAgentConfigs,
createAgentConfig,
updateAgentConfig,
deleteAgentConfig,
fetchProjects,
fetchProviders,
} from '../tui/gateway-api.js';
import type { AgentConfigInfo } from '../tui/gateway-api.js';
function formatAgent(a: AgentConfigInfo): string {
const sys = a.isSystem ? ' [system]' : '';
return `${a.name}${sys}${a.provider}/${a.model} (${a.status})`;
}
function showAgentDetail(a: AgentConfigInfo) {
console.log(` ID: ${a.id}`);
console.log(` Name: ${a.name}`);
console.log(` Provider: ${a.provider}`);
console.log(` Model: ${a.model}`);
console.log(` Status: ${a.status}`);
console.log(` System: ${a.isSystem ? 'yes' : 'no'}`);
console.log(` Project: ${a.projectId ?? '—'}`);
console.log(` System Prompt: ${a.systemPrompt ? `${a.systemPrompt.slice(0, 80)}...` : '—'}`);
console.log(` Tools: ${a.allowedTools ? a.allowedTools.join(', ') : 'all'}`);
console.log(` Skills: ${a.skills ? a.skills.join(', ') : '—'}`);
console.log(` Created: ${new Date(a.createdAt).toLocaleString()}`);
}
export function registerAgentCommand(program: Command) {
const cmd = program
.command('agent')
.description('Manage agent configurations')
.option('-g, --gateway <url>', 'Gateway URL', 'http://localhost:4000')
.option('--list', 'List all agents')
.option('--new', 'Create a new agent')
.option('--show <idOrName>', 'Show agent details')
.option('--update <idOrName>', 'Update an agent')
.option('--delete <idOrName>', 'Delete an agent')
.action(
async (opts: {
gateway: string;
list?: boolean;
new?: boolean;
show?: string;
update?: string;
delete?: string;
}) => {
const auth = await withAuth(opts.gateway);
if (opts.list) {
return listAgents(auth.gateway, auth.cookie);
}
if (opts.new) {
return createAgentWizard(auth.gateway, auth.cookie);
}
if (opts.show) {
return showAgent(auth.gateway, auth.cookie, opts.show);
}
if (opts.update) {
return updateAgentWizard(auth.gateway, auth.cookie, opts.update);
}
if (opts.delete) {
return deleteAgent(auth.gateway, auth.cookie, opts.delete);
}
// Default: interactive select
return interactiveSelect(auth.gateway, auth.cookie);
},
);
return cmd;
}
async function resolveAgent(
gateway: string,
cookie: string,
idOrName: string,
): Promise<AgentConfigInfo | undefined> {
const agents = await fetchAgentConfigs(gateway, cookie);
return agents.find((a) => a.id === idOrName || a.name === idOrName);
}
async function listAgents(gateway: string, cookie: string) {
const agents = await fetchAgentConfigs(gateway, cookie);
if (agents.length === 0) {
console.log('No agents found.');
return;
}
console.log(`Agents (${agents.length}):\n`);
for (const a of agents) {
const sys = a.isSystem ? ' [system]' : '';
const project = a.projectId ? ` project=${a.projectId.slice(0, 8)}` : '';
console.log(` ${a.name}${sys} ${a.provider}/${a.model} ${a.status}${project}`);
}
}
async function showAgent(gateway: string, cookie: string, idOrName: string) {
const agent = await resolveAgent(gateway, cookie, idOrName);
if (!agent) {
console.error(`Agent "${idOrName}" not found.`);
process.exit(1);
}
showAgentDetail(agent);
}
async function interactiveSelect(gateway: string, cookie: string) {
const agents = await fetchAgentConfigs(gateway, cookie);
const selected = await selectItem(agents, {
message: 'Select an agent:',
render: formatAgent,
emptyMessage: 'No agents found. Create one with `mosaic agent --new`.',
});
if (selected) {
showAgentDetail(selected);
}
}
async function createAgentWizard(gateway: string, cookie: string) {
const readline = await import('node:readline');
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
const ask = (q: string): Promise<string> => new Promise((resolve) => rl.question(q, resolve));
try {
const name = await ask('Agent name: ');
if (!name.trim()) {
console.error('Name is required.');
return;
}
// Project selection
const projects = await fetchProjects(gateway, cookie);
let projectId: string | undefined;
if (projects.length > 0) {
const selected = await selectItem(projects, {
message: 'Assign to project (optional):',
render: (p) => `${p.name} (${p.status})`,
});
if (selected) projectId = selected.id;
}
// Provider / model selection
const providers = await fetchProviders(gateway, cookie);
let provider = 'default';
let model = 'default';
if (providers.length > 0) {
const allModels = providers.flatMap((p) =>
p.models.map((m) => ({ provider: p.name, model: m.id, label: `${p.name}/${m.id}` })),
);
if (allModels.length > 0) {
const selected = await selectItem(allModels, {
message: 'Select model:',
render: (m) => m.label,
});
if (selected) {
provider = selected.provider;
model = selected.model;
}
}
}
const systemPrompt = await ask('System prompt (optional, press Enter to skip): ');
const agent = await createAgentConfig(gateway, cookie, {
name: name.trim(),
provider,
model,
projectId,
systemPrompt: systemPrompt.trim() || undefined,
});
console.log(`\nAgent "${agent.name}" created (${agent.id}).`);
} finally {
rl.close();
}
}
async function updateAgentWizard(gateway: string, cookie: string, idOrName: string) {
const agent = await resolveAgent(gateway, cookie, idOrName);
if (!agent) {
console.error(`Agent "${idOrName}" not found.`);
process.exit(1);
}
const readline = await import('node:readline');
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
const ask = (q: string): Promise<string> => new Promise((resolve) => rl.question(q, resolve));
try {
console.log(`Updating agent: ${agent.name}\n`);
const name = await ask(`Name [${agent.name}]: `);
const systemPrompt = await ask(`System prompt [${agent.systemPrompt ? 'set' : 'none'}]: `);
const updates: Record<string, unknown> = {};
if (name.trim()) updates['name'] = name.trim();
if (systemPrompt.trim()) updates['systemPrompt'] = systemPrompt.trim();
if (Object.keys(updates).length === 0) {
console.log('No changes.');
return;
}
const updated = await updateAgentConfig(gateway, cookie, agent.id, updates);
console.log(`\nAgent "${updated.name}" updated.`);
} finally {
rl.close();
}
}
async function deleteAgent(gateway: string, cookie: string, idOrName: string) {
const agent = await resolveAgent(gateway, cookie, idOrName);
if (!agent) {
console.error(`Agent "${idOrName}" not found.`);
process.exit(1);
}
if (agent.isSystem) {
console.error('Cannot delete system agents.');
process.exit(1);
}
const readline = await import('node:readline');
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
const answer = await new Promise<string>((resolve) =>
rl.question(`Delete agent "${agent.name}"? (y/N): `, resolve),
);
rl.close();
if (answer.toLowerCase() !== 'y') {
console.log('Cancelled.');
return;
}
await deleteAgentConfig(gateway, cookie, agent.id);
console.log(`Agent "${agent.name}" deleted.`);
}