feat(agents): mcpctl chat REPL + agent CRUD + completions (Stage 5)

This is the moment the user can actually talk to an agent end-to-end:

  mcpctl create llm qwen3-thinking --type openai --model qwen3-thinking \
    --url http://litellm.nvidia-nim.svc.cluster.local:4000/v1 \
    --api-key-ref litellm-key/API_KEY
  mcpctl create agent reviewer --llm qwen3-thinking --project mcpctl-dev \
    --description "I review security design — ask me after each major change."
  mcpctl chat reviewer

Pieces:

* src/cli/src/commands/chat.ts (new) — REPL + one-shot. Streams the SSE
  endpoint and prints text deltas to stdout as they arrive; tool_call /
  tool_result events go to stderr in dim-style brackets so the chat
  output stays clean. LiteLLM-style flags (--temperature / --top-p /
  --top-k / --max-tokens / --seed / --stop / --allow-tool / --extra)
  layer over agent.defaultParams. In-REPL slash-commands: /set KEY VAL,
  /system <text>, /tools (list project's MCP servers), /clear (new
  thread), /save (PATCH agent.defaultParams = current overrides),
  /quit.

* src/cli/src/commands/create.ts — `create agent` mirroring the llm
  pattern. Every yaml-applyable field has a corresponding flag (memory
  rule); --default-temperature / --default-top-p / --default-top-k /
  --default-max-tokens / --default-seed / --default-stop /
  --default-extra / --default-params-file all populate agent.defaultParams.

* src/cli/src/commands/apply.ts — AgentSpecSchema accepts both `llm:
  qwen3-thinking` shorthand and `llm: { name: ... }` long form; runs
  after llms in the apply order so apiKey/llm references resolve. Round-
  trips with `get agent foo -o yaml | apply -f -` (memory rule).

* src/cli/src/commands/get.ts — agentColumns (NAME, LLM, PROJECT,
  DESCRIPTION, ID); RESOURCE_KIND mapping for yaml export.

* src/cli/src/commands/shared.ts — `agent`/`agents`/`thread`/`threads`
  added to RESOURCE_ALIASES.

* src/cli/src/index.ts — wires createChatCommand into the program; passes
  the resolved baseUrl + token so chat can stream SSE without going
  through ApiClient (which only does buffered request/response).

* completions/mcpctl.{fish,bash} regenerated. scripts/generate-completions.ts
  knows about agents (canonical + aliases) and emits a special-case
  `chat)` block that completes the first arg with `mcpctl get agents`
  names. tests/completions.test.ts: +9 new assertions covering agents in
  the resource list, chat in the commands list, --llm flag for create
  agent, agent-name completion for chat, etc.

CLI suite: 430/430 (was 421). Completions --check is clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Michal
2026-04-25 17:02:38 +01:00
parent 285be11dd5
commit 727e7d628c
10 changed files with 701 additions and 13 deletions

View File

@@ -183,3 +183,51 @@ describe('bash completions', () => {
expect(fnMatch, '_mcpctl_resource_names must not use grep on name').not.toMatch(/grep.*"name"/);
});
});
describe('agent + chat completions', () => {
it('fish lists agents as a resource type', () => {
expect(fishFile).toMatch(/set -l resources [^\n]*\bagents\b/);
});
it('fish accepts both `agent` and `agents` aliases', () => {
const aliasLine = fishFile.split('\n').find((l) => l.startsWith(' set -l resource_aliases'));
expect(aliasLine).toMatch(/\bagent\b/);
expect(aliasLine).toMatch(/\bagents\b/);
});
it('fish offers `chat` as a top-level command', () => {
expect(fishFile).toMatch(/set -l commands [^\n]*\bchat\b/);
});
it('fish offers `agent` under `mcpctl create`', () => {
expect(fishFile).toMatch(/-a agent\b[^\n]*Create an Agent/);
});
it('fish wires --llm flag for create agent', () => {
expect(fishFile).toMatch(/__mcpctl_subcmd_active create agent[^\n]*-l llm\b/);
});
it('bash lists agents in resources and resource_aliases', () => {
expect(bashFile).toMatch(/local resources="[^"]*\bagents\b[^"]*"/);
expect(bashFile).toMatch(/local resource_aliases="[^"]*\bagent\b[^"]*"/);
});
it('bash includes `chat` in the commands list', () => {
expect(bashFile).toMatch(/local commands="[^"]*\bchat\b[^"]*"/);
});
it('bash dispatches a `chat)` case that completes with agent names + LiteLLM-style flags', () => {
const chatBlock = bashFile.match(/chat\)[\s\S]*?return ;;/)?.[0] ?? '';
expect(chatBlock, 'chat must call _mcpctl_resource_names with "agents"').toContain('"agents"');
expect(chatBlock, 'chat must offer --temperature').toContain('--temperature');
expect(chatBlock, 'chat must offer --thread').toContain('--thread');
expect(chatBlock, 'chat must offer --no-stream').toContain('--no-stream');
});
it('bash dispatches `create agent` with the correct flags', () => {
const createBlock = bashFile.match(/agent\)[\s\S]*?;;/)?.[0] ?? '';
expect(createBlock).toContain('--llm');
expect(createBlock).toContain('--system-prompt');
expect(createBlock).toContain('--default-temperature');
});
});