feat(chat): project-scoped chat — mcpctl chat --project <name>
Some checks failed
CI/CD / lint (pull_request) Successful in 1m2s
CI/CD / typecheck (pull_request) Successful in 2m10s
CI/CD / test (pull_request) Successful in 1m17s
CI/CD / smoke (pull_request) Failing after 1m49s
CI/CD / build (pull_request) Successful in 2m8s
CI/CD / publish (pull_request) Has been skipped

Chat directly with a Project (no Agent needed): its Prompts become the system
context, its MCP-server tools are callable, its llmProvider/llmModel drive the
LLM, and (opt-in) the model can read secret values. History is saved inside the
project, attributed per user, resumable, and deletable (RBAC-permitting) — "use
it like Claude, scoped to the project".

Backend (reuses the agent-chat orchestrator):
- ChatThread is now agent-XOR-project (schema + migration + CHECK constraint);
  new listThreadsByProject / deleteThread on the repo.
- ChatService: prepareProjectContext (project prompt + Prompts by priority,
  llm from llmProvider with llmModel override, project tools), shared
  runChatLoop/runChatStreamLoop, project thread CRUD with owner enforcement
  (404-not-403 on foreign threads), admin-override delete.
- Gated get_secret virtual tool: offered only with --allow-secrets AND the
  caller's view:secrets; resolves via SecretService, never routes to a server.
- routes/project-chat.ts (chat SSE+non-stream, threads create/list/delete);
  RBAC run:projects:<name>.

CLI:
- `mcpctl chat --project <name>` (+ --allow-secrets), one-shot/REPL/resume.
- REPL /threads, /resume <id>, /delete <id>; project-aware header + /tools.
- `mcpctl get threads --project <name>`, `mcpctl delete thread <id> --project`.
- completions regenerated (--project completes project names).

Tests: 8 new project-chat unit tests; full mcpd (945) + CLI (508) green;
schema validated against Postgres. Docs: docs/chat.md "Project chat" section.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Michal
2026-07-18 10:34:21 +01:00
parent ae66ad8430
commit a5cab5a096
15 changed files with 995 additions and 84 deletions

View File

@@ -0,0 +1,17 @@
-- Project-scoped chat threads: a ChatThread is now scoped to an Agent XOR a
-- Project. Existing rows are all agent-scoped, so dropping NOT NULL on agentId
-- is backward-compatible.
-- Agent link becomes optional; add the project link.
ALTER TABLE "ChatThread" ALTER COLUMN "agentId" DROP NOT NULL;
ALTER TABLE "ChatThread" ADD COLUMN "projectId" TEXT;
-- Exactly one of agentId / projectId must be set.
ALTER TABLE "ChatThread" ADD CONSTRAINT "ChatThread_agent_xor_project"
CHECK ((("agentId" IS NOT NULL)::int + ("projectId" IS NOT NULL)::int) = 1);
-- Project FK (cascade delete threads with their project, mirroring the agent FK).
ALTER TABLE "ChatThread" ADD CONSTRAINT "ChatThread_projectId_fkey"
FOREIGN KEY ("projectId") REFERENCES "Project"("id") ON DELETE CASCADE ON UPDATE CASCADE;
CREATE INDEX "ChatThread_projectId_lastTurnAt_idx" ON "ChatThread"("projectId", "lastTurnAt" DESC);

View File

@@ -336,6 +336,7 @@ model Project {
skills Skill[]
mcpTokens McpToken[]
agents Agent[]
chatThreads ChatThread[]
@@index([name])
@@index([ownerId])
@@ -751,18 +752,24 @@ model PersonalityPrompt {
model ChatThread {
id String @id @default(cuid())
agentId String
// A thread is scoped to an Agent XOR a Project (enforced by a CHECK
// constraint in the migration + the service layer). Project-scoped threads
// back `mcpctl chat --project <name>`.
agentId String?
projectId String?
ownerId String
title String @default("")
lastTurnAt DateTime @default(now())
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
agent Agent @relation(fields: [agentId], references: [id], onDelete: Cascade)
agent Agent? @relation(fields: [agentId], references: [id], onDelete: Cascade)
project Project? @relation(fields: [projectId], references: [id], onDelete: Cascade)
owner User @relation(fields: [ownerId], references: [id], onDelete: Cascade)
messages ChatMessage[]
@@index([agentId, lastTurnAt(sort: Desc)])
@@index([projectId, lastTurnAt(sort: Desc)])
@@index([ownerId])
}