Files
mcpctl/src/cli/src/commands/delete.ts
Michal 9050918a83 feat(cli): personality flag + create/get/edit/delete personalities (Stage 4)
End-to-end CLI surface for the personality overlay:

  mcpctl create personality grumpy --agent reviewer --description "be terse"
  mcpctl create prompt tone --agent reviewer --content "Be very terse."
  mcpctl get personalities
  mcpctl get personalities --agent reviewer
  mcpctl edit personality <id>
  mcpctl delete personality grumpy --agent reviewer
  mcpctl chat reviewer --personality grumpy

Chat banner gains a "Personality:" line that shows either the active
flag value or the agent's `defaultPersonality` (when no flag given),
so the user knows which overlay is in effect before sending a message.

`--personality` is stripped from `/save` (it's a per-turn override,
not a `defaultParams` field — the agent's defaultPersonality lives on
its own column and is set via PUT /agents).

Backend (small additions to land Stage 4 cleanly):
- `GET /api/v1/personalities[?agent=name]` so `mcpctl get
  personalities` doesn't require an agent filter.
- PersonalityService.listAll() aggregates across agents.

Completions: regenerated fish + bash. `personalities` added as a
canonical resource with `personality` alias; edit-resource list
extended; the per-resource argument completers pick up the new
type automatically.

CLI suite: 430/430. mcpd: 801/801. Typecheck clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 19:32:48 +01:00

90 lines
3.7 KiB
TypeScript

import { Command } from 'commander';
import type { ApiClient } from '../api-client.js';
import { resolveResource, resolveNameOrId } from './shared.js';
export interface DeleteCommandDeps {
client: ApiClient;
log: (...args: unknown[]) => void;
}
export function createDeleteCommand(deps: DeleteCommandDeps): Command {
const { client, log } = deps;
return new Command('delete')
.description('Delete a resource (server, instance, secret, project, user, group, rbac, personality)')
.argument('<resource>', 'resource type')
.argument('<id>', 'resource ID or name')
.option('-p, --project <name>', 'Project name (for serverattachment)')
.option('--agent <name>', 'Agent name (for personality delete-by-name)')
.action(async (resourceArg: string, idOrName: string, opts: { project?: string; agent?: string }) => {
const resource = resolveResource(resourceArg);
// Serverattachments: delete serverattachment <server> --project <project>
if (resource === 'serverattachments') {
if (!opts.project) {
throw new Error('--project is required. Usage: mcpctl delete serverattachment <server> --project <name>');
}
const projectId = await resolveNameOrId(client, 'projects', opts.project);
await client.delete(`/api/v1/projects/${projectId}/servers/${idOrName}`);
log(`server '${idOrName}' detached from project '${opts.project}'`);
return;
}
// Personalities: names are unique per-agent, so by-name delete requires --agent
// (or pass a CUID directly).
if (resource === 'personalities') {
let personalityId: string;
if (/^c[a-z0-9]{24}/.test(idOrName)) {
personalityId = idOrName;
} else {
if (!opts.agent) {
throw new Error('--agent is required to delete a personality by name (or pass the id).');
}
const items = await client.get<Array<{ id: string; name: string }>>(
`/api/v1/agents/${encodeURIComponent(opts.agent)}/personalities`,
);
const match = items.find((i) => i.name === idOrName);
if (!match) throw new Error(`personality '${idOrName}' not found on agent '${opts.agent}'`);
personalityId = match.id;
}
await client.delete(`/api/v1/personalities/${personalityId}`);
log(`personality '${idOrName}' deleted.`);
return;
}
// Mcptokens: names are scoped to a project, so require --project unless the caller passes a CUID
if (resource === 'mcptokens') {
let tokenId: string;
if (/^c[a-z0-9]{24}/.test(idOrName)) {
tokenId = idOrName;
} else {
if (!opts.project) {
throw new Error('--project is required to delete an mcptoken by name (or pass the id).');
}
const items = await client.get<Array<{ id: string; name: string }>>(
`/api/v1/mcptokens?projectName=${encodeURIComponent(opts.project)}`,
);
const match = items.find((i) => i.name === idOrName);
if (!match) throw new Error(`mcptoken '${idOrName}' not found in project '${opts.project}'`);
tokenId = match.id;
}
await client.delete(`/api/v1/mcptokens/${tokenId}`);
log(`mcptoken '${idOrName}' deleted.`);
return;
}
// Resolve name → ID for any resource type
let id: string;
try {
id = await resolveNameOrId(client, resource, idOrName);
} catch {
id = idOrName; // Fall through with original
}
await client.delete(`/api/v1/${resource}/${id}`);
const singular = resource.replace(/s$/, '');
log(`${singular} '${idOrName}' deleted.`);
});
}