feat: web prompt editor + agent personalities #58

Merged
michal merged 6 commits from feat/web-prompt-editor-personalities into main 2026-04-26 20:21:54 +00:00
Owner

Summary

Six-stage feature landing in one branch. Adds agent personalities
(VLAN-on-ethernet overlays of prompts on top of an existing agent) and a
browser-based prompt editor served by mcpd at /ui.

  • Stage 1 — schema (f60f00f): Prompt.agentId, Personality, PersonalityPrompt, Agent.defaultPersonalityId. Migration + 8 new prisma-level assertions.
  • Stage 2 — services (6b5bd78): PersonalityRepository + PersonalityService (with scope enforcement so you can't smuggle a foreign-project prompt into a personality), PromptService.findByAgent + agentId on create/upsert.
  • Stage 3 — routes + chat overlay (faef1e7): /api/v1/personalities/*, /api/v1/agents/:name/prompts, personality: <name> field on the chat body. chat.service.ts now assembles agent.systemPrompt + agent-direct + project + personality + systemAppend (additive, backwards-compatible by construction).
  • Stage 4 — CLI (9050918): mcpctl chat --personality, mcpctl create personality, mcpctl get/edit/delete personalities, regenerated fish + bash completions, chat banner shows the active personality.
  • Stage 5 — web UI (0010cc1): new @mcpctl/web workspace package. Vite + React 19 + Monaco. Pages for projects/prompts, agents/direct-prompts, agents/personalities, personality detail with bind/unbind picker. PAT/session login, dark terminal theme.
  • Stage 6 — packaging + smoke + docs (4cbf58d): mcpd serves /ui via @fastify/static. Dockerfile.mcpd builds the SPA in-place and copies it to /usr/share/mcpd/web. personality.smoke.test.ts exercises the live HTTP surface. New docs/personalities.md; cross-links from agents.md, chat.md, README.

How it composes at chat time

agent.systemPrompt
+ agent-direct prompts          (Prompt.agentId == agent.id, priority desc)
+ project prompts               (existing — Prompt.projectId == agent.projectId)
+ personality-bound prompts     (PersonalityPrompt[chosen],   priority desc)
+ systemAppend

personality selected by --personality <name> flag, falling back to agent.defaultPersonalityId. Without either, the resulting block is byte-identical to today's layout (regression test included).

Test plan

  • Unit: pnpm --filter @mcpctl/mcpd exec vitest run801/801 (was 777, +24 new)
  • Unit: pnpm --filter @mcpctl/cli exec vitest run430/430
  • Unit: pnpm --filter @mcpctl/web exec vitest run7/7 (new)
  • Schema: pnpm --filter db exec vitest run tests/agent-schema.test.ts16/16 (was 8, +8 new)
  • Typecheck clean across mcpd / cli / web / db
  • Web build green: 269 kB JS / 84 kB gzipped
  • Manual: bash fulldeploy.sh rebuilds the mcpd Docker image with the SPA baked in, then run the new personality.smoke.test.ts against the deployed instance.
  • Manual: open https://mcpctl.ad.itaz.eu/ui/, paste a token, walk Projects → Agents → Personalities, create + bind a prompt, run mcpctl chat reviewer --personality grumpy and see the banner + overlay take effect.

🤖 Generated with Claude Code

## Summary Six-stage feature landing in one branch. Adds **agent personalities** (VLAN-on-ethernet overlays of prompts on top of an existing agent) and a **browser-based prompt editor** served by mcpd at `/ui`. - **Stage 1 — schema** (`f60f00f`): `Prompt.agentId`, `Personality`, `PersonalityPrompt`, `Agent.defaultPersonalityId`. Migration + 8 new prisma-level assertions. - **Stage 2 — services** (`6b5bd78`): `PersonalityRepository` + `PersonalityService` (with scope enforcement so you can't smuggle a foreign-project prompt into a personality), `PromptService.findByAgent` + agentId on create/upsert. - **Stage 3 — routes + chat overlay** (`faef1e7`): `/api/v1/personalities/*`, `/api/v1/agents/:name/prompts`, `personality: <name>` field on the chat body. `chat.service.ts` now assembles `agent.systemPrompt + agent-direct + project + personality + systemAppend` (additive, backwards-compatible by construction). - **Stage 4 — CLI** (`9050918`): `mcpctl chat --personality`, `mcpctl create personality`, `mcpctl get/edit/delete personalities`, regenerated fish + bash completions, chat banner shows the active personality. - **Stage 5 — web UI** (`0010cc1`): new `@mcpctl/web` workspace package. Vite + React 19 + Monaco. Pages for projects/prompts, agents/direct-prompts, agents/personalities, personality detail with bind/unbind picker. PAT/session login, dark terminal theme. - **Stage 6 — packaging + smoke + docs** (`4cbf58d`): mcpd serves `/ui` via `@fastify/static`. `Dockerfile.mcpd` builds the SPA in-place and copies it to `/usr/share/mcpd/web`. `personality.smoke.test.ts` exercises the live HTTP surface. New `docs/personalities.md`; cross-links from `agents.md`, `chat.md`, README. ## How it composes at chat time ``` agent.systemPrompt + agent-direct prompts (Prompt.agentId == agent.id, priority desc) + project prompts (existing — Prompt.projectId == agent.projectId) + personality-bound prompts (PersonalityPrompt[chosen], priority desc) + systemAppend ``` `personality` selected by `--personality <name>` flag, falling back to `agent.defaultPersonalityId`. Without either, the resulting block is byte-identical to today's layout (regression test included). ## Test plan - [x] Unit: `pnpm --filter @mcpctl/mcpd exec vitest run` → **801/801** (was 777, +24 new) - [x] Unit: `pnpm --filter @mcpctl/cli exec vitest run` → **430/430** - [x] Unit: `pnpm --filter @mcpctl/web exec vitest run` → **7/7** (new) - [x] Schema: `pnpm --filter db exec vitest run tests/agent-schema.test.ts` → **16/16** (was 8, +8 new) - [x] Typecheck clean across mcpd / cli / web / db - [x] Web build green: 269 kB JS / 84 kB gzipped - [ ] Manual: `bash fulldeploy.sh` rebuilds the mcpd Docker image with the SPA baked in, then run the new `personality.smoke.test.ts` against the deployed instance. - [ ] Manual: open `https://mcpctl.ad.itaz.eu/ui/`, paste a token, walk Projects → Agents → Personalities, create + bind a prompt, run `mcpctl chat reviewer --personality grumpy` and see the banner + overlay take effect. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
michal added 6 commits 2026-04-26 18:49:15 +00:00
A Personality is a named overlay on top of an Agent — same agent,
same LLM, but a different bundle of prompts injected into the system
block at chat time. VLAN-on-ethernet semantics: ethernet still works
without VLAN; with a VLAN tag, frames are segmented but still ethernet.

Schema additions:
- Prompt.agentId (nullable FK + index, cascade on delete) so prompts
  can attach directly to an agent without going through a project.
- Personality { id, name, description, agentId, priority } with
  unique (name, agentId).
- PersonalityPrompt join table with per-binding priority override.
- Agent.defaultPersonalityId (SetNull on delete) so an agent can pick
  one personality as the default when no --personality flag is passed.

Backwards-compatible by construction: every new column is nullable;
existing rows are valid as-is; the chat.service systemBlock changes
land in Stage 3.

8 new prisma-level assertions in agent-schema.test.ts cover unique
constraints, cascade behavior, the SetNull on defaultPersonalityId,
and shared-prompt-across-personalities. All 16 db tests pass; mcpd
typecheck + 777 mcpd unit tests still green.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Wires the schema landed in Stage 1 into the service layer. No HTTP
routes yet — Stage 3 will register `/api/v1/...` endpoints and update
chat.service to read agent-direct + personality prompts when building
the system block.

Repositories:
- PersonalityRepository: CRUD + listPrompts/attach/detach bindings.
- PromptRepository: findByAgent + findByNameAndAgent; create/update
  accept the new agentId column. findGlobal now also filters
  agentId=null so agent-direct prompts don't leak into global lists.
- AgentRepository: defaultPersonalityId on create + connect/disconnect
  in update.

Services:
- PersonalityService: CRUD scoped per agent, plus attach/detach with
  scope enforcement — a prompt may bind only if it's agent-direct on
  the same agent, in the agent's project, or global. Foreign-project
  / foreign-agent attachments are rejected with 400.
- PromptService: createPrompt / upsertByName accept agentId and
  resolve `agent: <name>`, with XOR-with-project guard. Adds
  listPromptsForAgent.
- AgentService: defaultPersonality (by name on the agent's own
  personality set) round-trips through update + AgentView.

Validation:
- prompt.schema.ts: refine() rejects projectId+agentId together.
- personality.schema.ts: new Create/Update/AttachPrompt schemas.
- agent.schema.ts: defaultPersonality { name } | null on update.

Tests: 12 PersonalityService + 7 PromptService agent-scope tests
covering happy paths, XOR/scope enforcement, double-attach guard,
detach-not-bound. mcpd suite: 796/796 (was 777). Typecheck clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
End-to-end backend wiring for the agents-feature evolution. After
this stage you can curl all the endpoints; CLI + Web UI follow.

Routes (new):
  GET    /api/v1/agents/:agentName/personalities
  POST   /api/v1/agents/:agentName/personalities
  GET    /api/v1/personalities/:id
  PUT    /api/v1/personalities/:id
  DELETE /api/v1/personalities/:id
  GET    /api/v1/personalities/:id/prompts
  POST   /api/v1/personalities/:id/prompts
  DELETE /api/v1/personalities/:id/prompts/:promptId
  GET    /api/v1/agents/:agentName/prompts            (agent-direct)

Routes (extended):
  POST /api/v1/prompts now resolves `agent: <name>` like `project: <name>`
  POST /api/v1/agents/:name/chat accepts `personality: <name>`

RBAC: `personalities` segment maps to the `agents` resource so
view/edit/create/delete on the parent agent governs personality access.
No new RBAC roles — piggybacking keeps the surface flat.

System block (chat.service.ts):
  agent.systemPrompt
  + agent-direct prompts (Prompt.agentId === agent.id, priority desc)
  + project prompts        (existing behavior, priority desc)
  + personality prompts    (PersonalityPrompt[chosen], priority desc)
  + systemAppend

Personality is selected by request body `personality: <name>`, falling
back to `agent.defaultPersonalityId` if unset. A typo'd flag throws
404 rather than silently dropping back to no overlay — failing loudly
on misconfiguration is the only way users learn it didn't apply.

Backwards-compatible by construction: when no agent-direct prompts
exist and no personality is selected, the resulting block is byte-
identical to the old layout (verified by a regression test).

Tests: 5 new chat-service.test cases cover ordering, default-
personality fallback, missing-personality 404, and the regression
guard. mcpd suite: 801/801 (was 796). Typecheck clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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>
New workspace package @mcpctl/web — a Vite + React 19 SPA that talks
to mcpd's existing HTTP API. Bundles to a static dist/ which Stage 6
will bake into the RPM and serve from mcpd at /ui via @fastify/static.

Pages:
  /ui/projects                       list projects
  /ui/projects/:name/prompts         CRUD project prompts (Monaco editor)
  /ui/agents                         list agents
  /ui/agents/:name                   tabs: Direct prompts | Personalities
  /ui/personalities/:id              bind/unbind prompts to a personality

Auth: paste a session token (mcpctl auth login) or PAT (mcpctl_pat_*)
once on a login screen, kept in localStorage; logout clears it.

API client: 60-line fetch wrapper, attaches the bearer header from
storage, throws an ApiError with status + parsed body on non-2xx.
A 200-line useFetch hook provides loading/error/data without a
state-management library — we are not building Notion.

UX:
  - Dark terminal-adjacent theme so the page feels like the CLI.
  - Monaco @monaco-editor/react for prompt content (markdown mode,
    word-wrap, search, multi-cursor).
  - Personality detail's "attach prompt" picker filters in-scope
    candidates: agent-direct + same-project + globals.

Dev loop:  pnpm --filter @mcpctl/web dev   (vite at :5173, proxies
  /api to https://mcpctl.ad.itaz.eu — override with MCPCTL_API_URL).
Build:     pnpm --filter @mcpctl/web build → src/web/dist/.

Tests: 7 vitest cases covering the bearer header / 4xx body / 204
no-content path on the api wrapper, and the login storage round-trip
+ help toggle. Production build green: 269 KB JS / 84 KB gzipped.
Typecheck clean (TS strict + exactOptionalPropertyTypes carried over).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
feat(mcpd+deploy): serve web UI at /ui + smoke tests + docs (Stage 6)
Some checks failed
CI/CD / lint (pull_request) Successful in 54s
CI/CD / test (pull_request) Failing after 1m8s
CI/CD / typecheck (pull_request) Successful in 2m35s
CI/CD / smoke (pull_request) Has been skipped
CI/CD / build (pull_request) Has been skipped
CI/CD / publish (pull_request) Has been skipped
4cbf58d212
The closing stage. mcpd now hosts the Stage 5 SPA, the Docker image
bundles the build artifact, a smoke test exercises the personality
HTTP surface end-to-end, and the user-facing docs spell out the
mental model.

mcpd:
- Add @fastify/static dep.
- New routes/web-ui.ts: registers /ui/* against a static bundle. Looks
  for the bundle at $MCPD_WEB_ROOT, then /usr/share/mcpd/web (the
  Docker image path), then a dev-tree fallback. Logs and skips
  cleanly if missing — API-only deploys keep working.
- SPA fallback: any /ui/<path> that doesn't match a file falls through
  to index.html so direct hits to react-router URLs work.
- /ui/* falls through to `kind: skip` in mapUrlToPermission, so the
  static assets are served unauthenticated. Each API call from the
  SPA still carries the bearer token.

Deploy:
- Dockerfile.mcpd builds the @mcpctl/web bundle in the same builder
  stage and copies dist/ to /usr/share/mcpd/web in the runtime image.

Smoke (personality.smoke.test.ts):
- Live mcpd flow: create secret/llm/agent/personality, attach an
  agent-direct prompt, verify the binding listing, reject double-
  attach (409) + foreign-agent prompt (400), set defaultPersonality
  by name, detach + delete cleanup.

Docs:
- New docs/personalities.md: VLAN-on-ethernet model, system-block
  ordering table, three prompt scopes, CLI walkthrough, web UI
  walkthrough, full API surface, RBAC notes.
- agents.md and chat.md cross-link.
- README's Agents section gains a Personalities subsection.

Test count after Stage 6:
  mcpd:   801/801      cli:  430/430
  web:    7/7          db:   58/62 (4 pre-existing)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
michal merged commit c0ba0a9040 into main 2026-04-26 20:21:54 +00:00
Sign in to join this conversation.
No Reviewers
No Label
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: michal/mcpctl#58