diff --git a/README.md b/README.md index a3368ce..660b453 100644 --- a/README.md +++ b/README.md @@ -252,22 +252,36 @@ What the dashboard can now do (all state-changing actions require `ADMIN_TOKEN`) - **Activity** — every enqueued command with its status (queued → running → done/failed) — the audit log of what the dashboard triggered. The UI polls `GET /commands/{id}` for live status. -- **Claude Login** — log Claude Code in on the host from the browser (see below), so agents - spawn against a real authenticated `claude` with no shell access to the container. +- **Claude** — the management page for the Claude Code install agents run on. The account + login lives here (see below), plus web-managed **skills**, **MCP connectors**, + **plugins**, and **permission overrides**. These are plain DB rows the control container + applies at every launch: skills sync to each worker's user-level `~/.claude/skills` + (marker-file managed, so hand-installed skills survive), enabled connectors become the + run's `--mcp-config` file (nothing lands in the repo tree), and plugins/permissions fold + into the generated per-agent `settings.json` — so a change in the UI reaches the next + launch of every agent, no redeploy. Skills can also be **installed from a marketplace + prompt** (SkillsMP and friends): paste the page's install prompt and a `skill_install` + command runs it through a one-off headless claude in a staging dir on the worker, then + imports whatever `/SKILL.md` (+ auxiliary files) landed as managed rows. + Headless means nobody can answer questions mid-install, so the wrapped prompt makes the + choices a human would be asked — always user scope, the instructions' defaults — and + reports them in the command result for after-the-fact review. The command queue is exposed over HTTP as `POST …/agents/spawn`, `POST …/agents/{n}/kill`, `POST …/approvals`, `POST …/forge-init`, `POST …/poll-ci`, `POST …/sync`, `POST /login/start`, `POST /login/submit`, and `GET /commands[/{id}]`; hosts as `/hosts`; schedules as `/schedules` + `/projects/{id}/schedules`; project mutation as -`PATCH`/`DELETE /projects/{id}`. Run the worker with `handler worker` (the control image's -default command). +`PATCH`/`DELETE /projects/{id}`; Claude management as `/claude/skills`, +`/claude/connectors`, `/claude/plugins` (CRUD), and `GET`/`PUT /claude/permissions` +(reads with the normal token, writes admin-gated). Run the worker with `handler worker` +(the control image's default command). ### Claude login from the web UI Agents *are* `claude` processes, so the control container needs a logged-in Claude Code. -Because that container has no interactive shell in normal operation, the **Claude Login** -pane logs it in from the browser — the same command-queue handoff every other control -action uses: +Because that container has no interactive shell in normal operation, the **Claude** page's +Account tab logs it in from the browser — the same command-queue handoff every other +control action uses: 1. **Log in to Claude** enqueues a `login_start` command. The worker opens `claude` in a dedicated (wide) tmux session in the control container, navigates whatever onboarding a diff --git a/frontend/app/claude/page.tsx b/frontend/app/claude/page.tsx new file mode 100644 index 0000000..bf45af7 --- /dev/null +++ b/frontend/app/claude/page.tsx @@ -0,0 +1,13 @@ +/* Claude management page. The shell (sidebar, banners, store) comes from the root + * layout; this route contributes only its section, in the shared scroll frame. */ +"use client"; + +import { ClaudeSection } from "@/components/sections/ClaudeSection"; + +export default function ClaudePage() { + return ( +
+ +
+ ); +} diff --git a/frontend/app/login/page.tsx b/frontend/app/login/page.tsx index a8bf3dd..e4797c5 100644 --- a/frontend/app/login/page.tsx +++ b/frontend/app/login/page.tsx @@ -1,13 +1,14 @@ -/* Claude Login page. The shell (sidebar, banners, store) comes from the root layout; this - * route contributes only its section, in the shared scroll frame. */ +/* The old Claude Login route. The login flow now lives on the Claude management page + * (/claude, Account tab); this stub only redirects old bookmarks there. */ "use client"; -import { LoginSection } from "@/components/sections/LoginSection"; +import { useEffect } from "react"; +import { useRouter } from "next/navigation"; -export default function LoginPage() { - return ( -
- -
- ); +export default function LoginRedirect() { + const router = useRouter(); + useEffect(() => { + router.replace("/claude"); + }, [router]); + return null; } diff --git a/frontend/components/Shell.tsx b/frontend/components/Shell.tsx index 680a1d2..d0399d3 100644 --- a/frontend/components/Shell.tsx +++ b/frontend/components/Shell.tsx @@ -1,5 +1,5 @@ /* The Control Center shell: a left nav (Runs / Repositories / Agents / Approvals / Git - * Servers / Activity / Shared / Claude Login) and the active route's page on the right, + * Servers / Activity / Shared / Claude) and the active route's page on the right, * matching the design's hub layout. Each nav item is a real route, so pages are modular * and independently loadable; this shell lives in the root layout and persists across * navigation, keeping the store, polling loop, and auth alive between pages. Command @@ -29,8 +29,9 @@ const BADGES: Partial number; accent?: (s servers: { count: (s) => s.hosts.length }, activity: { count: (s) => s.commands.length }, shared: { count: (s) => s.shared.context.length }, - login: { - count: () => 0, + claude: { + // Everything managed on the page: skills + connectors + plugins. + count: (s) => s.claudeSkills.length + s.claudeConnectors.length + s.claudePlugins.length, // Draw the eye to it until Claude is logged in on the host this session. accent: (s) => s.claudeLogin.status !== "done", }, diff --git a/frontend/components/sections/ClaudeSection.tsx b/frontend/components/sections/ClaudeSection.tsx new file mode 100644 index 0000000..c5d60e2 --- /dev/null +++ b/frontend/components/sections/ClaudeSection.tsx @@ -0,0 +1,628 @@ +/* Claude — the management page for the Claude Code install agents run on: the account + * login (moved here from the old Claude Login page), plus the operator-managed skills, + * MCP connectors, plugins, and permission overrides. Everything except the login is a + * plain DB write that the control container applies at the NEXT launch of every agent — + * skills sync to the workers' user-level ~/.claude/skills, connectors become each run's + * --mcp-config file, and plugins/permissions fold into the generated settings.json. */ +"use client"; + +import { useState } from "react"; +import { useDashboard } from "@/components/store"; +import type { ConnectorBody, PluginBody, SkillBody } from "@/components/store"; +import { Badge, Button, Card, Input, Select, Tabs, Textarea, Toggle } from "@/components/ui"; +import type { ClaudeConnector, ClaudePlugin, ClaudeSkill } from "@/lib/api"; +import { ClaudeLoginPanel } from "@/components/sections/LoginSection"; + +/* KEY=VALUE-per-line <-> map helpers for connector env/headers. */ +function parseKeyValues(text: string): Record { + const out: Record = {}; + for (const line of text.split("\n")) { + const trimmed = line.trim(); + if (!trimmed) continue; + const eq = trimmed.indexOf("="); + if (eq <= 0) continue; + out[trimmed.slice(0, eq).trim()] = trimmed.slice(eq + 1).trim(); + } + return out; +} + +function formatKeyValues(map: Record | null | undefined): string { + return Object.entries(map ?? {}) + .map(([k, v]) => `${k}=${v}`) + .join("\n"); +} + +function parseLines(text: string): string[] { + return text + .split("\n") + .map((l) => l.trim()) + .filter(Boolean); +} + +/* ---- Skills ------------------------------------------------------------------------ */ + +const emptySkill = { name: "", description: "", content: "", enabled: true }; + +/* Install-from-prompt: paste the "install prompt" a marketplace page (SkillsMP etc.) + * shows, and the worker runs it through a one-off headless claude, importing whatever + * it fetches as managed skills. Headless = nobody to answer questions, so the run makes + * the choices a human would be asked (scope, options) itself — always user scope, + * sensible defaults — and reports them; the import lands below for review/editing. */ +function InstallCard() { + const s = useDashboard(); + const [prompt, setPrompt] = useState(""); + + const run = async () => { + const ok = await s.installClaudeSkill(prompt.trim()); + if (ok) setPrompt(""); + }; + + return ( + +
+ + Install from a marketplace prompt + +
+