create-claude-code-visualizer 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/index.js +393 -0
- package/package.json +31 -0
- package/templates/CLAUDE.md +108 -0
- package/templates/app/.env.local.example +14 -0
- package/templates/app/ecosystem.config.js +29 -0
- package/templates/app/next-env.d.ts +6 -0
- package/templates/app/next.config.ts +16 -0
- package/templates/app/package-lock.json +4581 -0
- package/templates/app/package.json +38 -0
- package/templates/app/postcss.config.js +5 -0
- package/templates/app/src/app/agents/[slug]/chat/loading.tsx +26 -0
- package/templates/app/src/app/agents/[slug]/chat/page.tsx +579 -0
- package/templates/app/src/app/agents/[slug]/loading.tsx +19 -0
- package/templates/app/src/app/agents/page.tsx +8 -0
- package/templates/app/src/app/api/agents/[slug]/capabilities/route.ts +11 -0
- package/templates/app/src/app/api/agents/[slug]/route.ts +57 -0
- package/templates/app/src/app/api/agents/route.ts +28 -0
- package/templates/app/src/app/api/ai/generate-agent/route.ts +87 -0
- package/templates/app/src/app/api/ai/improve-claude-md/route.ts +78 -0
- package/templates/app/src/app/api/ai/suggestions/route.ts +64 -0
- package/templates/app/src/app/api/ai/title/route.ts +88 -0
- package/templates/app/src/app/api/auth/role/route.ts +17 -0
- package/templates/app/src/app/api/commands/[slug]/route.ts +61 -0
- package/templates/app/src/app/api/commands/route.ts +6 -0
- package/templates/app/src/app/api/governance/costs/route.ts +117 -0
- package/templates/app/src/app/api/governance/sessions/route.ts +335 -0
- package/templates/app/src/app/api/notifications/route.ts +62 -0
- package/templates/app/src/app/api/preferences/route.ts +44 -0
- package/templates/app/src/app/api/runs/[id]/approve/route.ts +38 -0
- package/templates/app/src/app/api/runs/[id]/events/route.ts +28 -0
- package/templates/app/src/app/api/runs/[id]/metadata/route.ts +30 -0
- package/templates/app/src/app/api/runs/[id]/route.ts +21 -0
- package/templates/app/src/app/api/runs/[id]/start/route.ts +61 -0
- package/templates/app/src/app/api/runs/[id]/stop/route.ts +16 -0
- package/templates/app/src/app/api/runs/[id]/stream/route.ts +201 -0
- package/templates/app/src/app/api/runs/route.ts +95 -0
- package/templates/app/src/app/api/schedules/[id]/route.ts +81 -0
- package/templates/app/src/app/api/schedules/route.ts +75 -0
- package/templates/app/src/app/api/settings/access-logs/route.ts +33 -0
- package/templates/app/src/app/api/settings/claude-md/route.ts +44 -0
- package/templates/app/src/app/api/settings/env-keys/route.ts +271 -0
- package/templates/app/src/app/api/settings/users/route.ts +108 -0
- package/templates/app/src/app/api/skills/[slug]/route.ts +43 -0
- package/templates/app/src/app/api/skills/route.ts +6 -0
- package/templates/app/src/app/api/tools/route.ts +65 -0
- package/templates/app/src/app/api/uploads/cleanup/route.ts +29 -0
- package/templates/app/src/app/api/uploads/route.ts +77 -0
- package/templates/app/src/app/auth/callback/route.ts +19 -0
- package/templates/app/src/app/globals.css +115 -0
- package/templates/app/src/app/layout.tsx +24 -0
- package/templates/app/src/app/loading.tsx +16 -0
- package/templates/app/src/app/login/page.tsx +64 -0
- package/templates/app/src/app/not-authorized/page.tsx +33 -0
- package/templates/app/src/app/runs/page.tsx +55 -0
- package/templates/app/src/app/schedules/page.tsx +110 -0
- package/templates/app/src/app/settings/page.tsx +1294 -0
- package/templates/app/src/app/skills/page.tsx +7 -0
- package/templates/app/src/components/agent-card.tsx +58 -0
- package/templates/app/src/components/agent-grid.tsx +90 -0
- package/templates/app/src/components/auth/auth-context.tsx +79 -0
- package/templates/app/src/components/chat-thread.tsx +50 -0
- package/templates/app/src/components/chat-view.tsx +670 -0
- package/templates/app/src/components/commands-browser.tsx +349 -0
- package/templates/app/src/components/create-agent-modal.tsx +388 -0
- package/templates/app/src/components/governance-dashboard.tsx +397 -0
- package/templates/app/src/components/icons.tsx +401 -0
- package/templates/app/src/components/layout/agent-sidebar.tsx +504 -0
- package/templates/app/src/components/layout/app-shell.tsx +29 -0
- package/templates/app/src/components/layout/nav.tsx +87 -0
- package/templates/app/src/components/layout/overview-inner.tsx +14 -0
- package/templates/app/src/components/layout/profile-menu.tsx +95 -0
- package/templates/app/src/components/layout/sidebar.tsx +30 -0
- package/templates/app/src/components/markdown.tsx +57 -0
- package/templates/app/src/components/message-bar.tsx +161 -0
- package/templates/app/src/components/notifications/notification-bell.tsx +104 -0
- package/templates/app/src/components/notifications/notification-panel.tsx +116 -0
- package/templates/app/src/components/overview/overview-content.tsx +287 -0
- package/templates/app/src/components/overview/overview-context.tsx +88 -0
- package/templates/app/src/components/preferences-modal.tsx +112 -0
- package/templates/app/src/components/run-form.tsx +73 -0
- package/templates/app/src/components/run-history-table.tsx +226 -0
- package/templates/app/src/components/run-output.tsx +187 -0
- package/templates/app/src/components/schedule-form.tsx +148 -0
- package/templates/app/src/components/skills-browser.tsx +338 -0
- package/templates/app/src/components/tool-tooltip.tsx +82 -0
- package/templates/app/src/hooks/use-sse.ts +115 -0
- package/templates/app/src/instrumentation.ts +9 -0
- package/templates/app/src/lib/agent-cache.ts +19 -0
- package/templates/app/src/lib/agent-runner.ts +411 -0
- package/templates/app/src/lib/agents.ts +168 -0
- package/templates/app/src/lib/ai.ts +40 -0
- package/templates/app/src/lib/approval-store.ts +70 -0
- package/templates/app/src/lib/auth-guard.ts +116 -0
- package/templates/app/src/lib/capabilities.ts +191 -0
- package/templates/app/src/lib/line-diff.ts +96 -0
- package/templates/app/src/lib/queue.ts +22 -0
- package/templates/app/src/lib/redis.ts +12 -0
- package/templates/app/src/lib/role-permissions.ts +166 -0
- package/templates/app/src/lib/run-agent.ts +442 -0
- package/templates/app/src/lib/supabase-browser.ts +8 -0
- package/templates/app/src/lib/supabase-middleware.ts +63 -0
- package/templates/app/src/lib/supabase-server.ts +28 -0
- package/templates/app/src/lib/supabase.ts +6 -0
- package/templates/app/src/lib/tool-descriptions.ts +29 -0
- package/templates/app/src/lib/types.ts +73 -0
- package/templates/app/src/lib/typewriter-animation.ts +159 -0
- package/templates/app/src/middleware.ts +13 -0
- package/templates/app/tsconfig.json +21 -0
- package/templates/app/uploads/.gitkeep +0 -0
- package/templates/app/worker/index.ts +342 -0
- package/templates/claude/agents/ai-trends-scout.md +66 -0
- package/templates/claude/commands/add-to-todos.md +56 -0
- package/templates/claude/commands/check-todos.md +56 -0
- package/templates/claude/hooks/auto-approve-safe.sh +34 -0
- package/templates/claude/hooks/auto-format.sh +25 -0
- package/templates/claude/hooks/block-destructive.sh +32 -0
- package/templates/claude/hooks/compaction-preserver.sh +16 -0
- package/templates/claude/hooks/notify.sh +26 -0
- package/templates/claude/settings.local.json +66 -0
- package/templates/claude/skills/frontend-design/SKILL.md +127 -0
- package/templates/claude/skills/frontend-design/reference/color-and-contrast.md +132 -0
- package/templates/claude/skills/frontend-design/reference/interaction-design.md +123 -0
- package/templates/claude/skills/frontend-design/reference/motion-design.md +99 -0
- package/templates/claude/skills/frontend-design/reference/responsive-design.md +114 -0
- package/templates/claude/skills/frontend-design/reference/spatial-design.md +100 -0
- package/templates/claude/skills/frontend-design/reference/typography.md +131 -0
- package/templates/claude/skills/frontend-design/reference/ux-writing.md +107 -0
- package/templates/claude/skills/gws-admin-reports/SKILL.md +57 -0
- package/templates/claude/skills/gws-calendar/SKILL.md +108 -0
- package/templates/claude/skills/gws-calendar-agenda/SKILL.md +52 -0
- package/templates/claude/skills/gws-calendar-insert/SKILL.md +55 -0
- package/templates/claude/skills/gws-chat/SKILL.md +73 -0
- package/templates/claude/skills/gws-chat-send/SKILL.md +49 -0
- package/templates/claude/skills/gws-classroom/SKILL.md +75 -0
- package/templates/claude/skills/gws-docs/SKILL.md +48 -0
- package/templates/claude/skills/gws-docs-write/SKILL.md +49 -0
- package/templates/claude/skills/gws-drive/SKILL.md +137 -0
- package/templates/claude/skills/gws-drive-upload/SKILL.md +52 -0
- package/templates/claude/skills/gws-events/SKILL.md +67 -0
- package/templates/claude/skills/gws-events-renew/SKILL.md +48 -0
- package/templates/claude/skills/gws-events-subscribe/SKILL.md +59 -0
- package/templates/claude/skills/gws-forms/SKILL.md +45 -0
- package/templates/claude/skills/gws-gmail/SKILL.md +59 -0
- package/templates/claude/skills/gws-gmail-forward/SKILL.md +53 -0
- package/templates/claude/skills/gws-gmail-reply/SKILL.md +56 -0
- package/templates/claude/skills/gws-gmail-reply-all/SKILL.md +60 -0
- package/templates/claude/skills/gws-gmail-send/SKILL.md +55 -0
- package/templates/claude/skills/gws-gmail-triage/SKILL.md +50 -0
- package/templates/claude/skills/gws-gmail-watch/SKILL.md +58 -0
- package/templates/claude/skills/gws-keep/SKILL.md +48 -0
- package/templates/claude/skills/gws-meet/SKILL.md +51 -0
- package/templates/claude/skills/gws-modelarmor/SKILL.md +42 -0
- package/templates/claude/skills/gws-modelarmor-create-template/SKILL.md +53 -0
- package/templates/claude/skills/gws-modelarmor-sanitize-prompt/SKILL.md +48 -0
- package/templates/claude/skills/gws-modelarmor-sanitize-response/SKILL.md +48 -0
- package/templates/claude/skills/gws-people/SKILL.md +67 -0
- package/templates/claude/skills/gws-shared/SKILL.md +66 -0
- package/templates/claude/skills/gws-sheets/SKILL.md +53 -0
- package/templates/claude/skills/gws-sheets-append/SKILL.md +51 -0
- package/templates/claude/skills/gws-sheets-read/SKILL.md +47 -0
- package/templates/claude/skills/gws-slides/SKILL.md +43 -0
- package/templates/claude/skills/gws-tasks/SKILL.md +56 -0
- package/templates/claude/skills/gws-workflow/SKILL.md +44 -0
- package/templates/claude/skills/gws-workflow-email-to-task/SKILL.md +47 -0
- package/templates/claude/skills/gws-workflow-file-announce/SKILL.md +50 -0
- package/templates/claude/skills/gws-workflow-meeting-prep/SKILL.md +47 -0
- package/templates/claude/skills/gws-workflow-standup-report/SKILL.md +46 -0
- package/templates/claude/skills/gws-workflow-weekly-digest/SKILL.md +46 -0
- package/templates/claude/skills/persona-content-creator/SKILL.md +33 -0
- package/templates/claude/skills/persona-customer-support/SKILL.md +34 -0
- package/templates/claude/skills/persona-event-coordinator/SKILL.md +35 -0
- package/templates/claude/skills/persona-exec-assistant/SKILL.md +35 -0
- package/templates/claude/skills/persona-hr-coordinator/SKILL.md +33 -0
- package/templates/claude/skills/persona-it-admin/SKILL.md +30 -0
- package/templates/claude/skills/persona-project-manager/SKILL.md +35 -0
- package/templates/claude/skills/persona-researcher/SKILL.md +33 -0
- package/templates/claude/skills/persona-sales-ops/SKILL.md +35 -0
- package/templates/claude/skills/persona-team-lead/SKILL.md +36 -0
- package/templates/claude/skills/recipe-backup-sheet-as-csv/SKILL.md +25 -0
- package/templates/claude/skills/recipe-batch-invite-to-event/SKILL.md +25 -0
- package/templates/claude/skills/recipe-block-focus-time/SKILL.md +24 -0
- package/templates/claude/skills/recipe-bulk-download-folder/SKILL.md +25 -0
- package/templates/claude/skills/recipe-collect-form-responses/SKILL.md +25 -0
- package/templates/claude/skills/recipe-compare-sheet-tabs/SKILL.md +25 -0
- package/templates/claude/skills/recipe-copy-sheet-for-new-month/SKILL.md +25 -0
- package/templates/claude/skills/recipe-create-classroom-course/SKILL.md +25 -0
- package/templates/claude/skills/recipe-create-doc-from-template/SKILL.md +29 -0
- package/templates/claude/skills/recipe-create-events-from-sheet/SKILL.md +24 -0
- package/templates/claude/skills/recipe-create-expense-tracker/SKILL.md +26 -0
- package/templates/claude/skills/recipe-create-feedback-form/SKILL.md +25 -0
- package/templates/claude/skills/recipe-create-gmail-filter/SKILL.md +26 -0
- package/templates/claude/skills/recipe-create-meet-space/SKILL.md +25 -0
- package/templates/claude/skills/recipe-create-presentation/SKILL.md +25 -0
- package/templates/claude/skills/recipe-create-shared-drive/SKILL.md +25 -0
- package/templates/claude/skills/recipe-create-task-list/SKILL.md +26 -0
- package/templates/claude/skills/recipe-create-vacation-responder/SKILL.md +25 -0
- package/templates/claude/skills/recipe-draft-email-from-doc/SKILL.md +25 -0
- package/templates/claude/skills/recipe-email-drive-link/SKILL.md +25 -0
- package/templates/claude/skills/recipe-find-free-time/SKILL.md +25 -0
- package/templates/claude/skills/recipe-find-large-files/SKILL.md +24 -0
- package/templates/claude/skills/recipe-forward-labeled-emails/SKILL.md +27 -0
- package/templates/claude/skills/recipe-generate-report-from-sheet/SKILL.md +34 -0
- package/templates/claude/skills/recipe-label-and-archive-emails/SKILL.md +25 -0
- package/templates/claude/skills/recipe-log-deal-update/SKILL.md +25 -0
- package/templates/claude/skills/recipe-organize-drive-folder/SKILL.md +26 -0
- package/templates/claude/skills/recipe-plan-weekly-schedule/SKILL.md +26 -0
- package/templates/claude/skills/recipe-post-mortem-setup/SKILL.md +25 -0
- package/templates/claude/skills/recipe-reschedule-meeting/SKILL.md +25 -0
- package/templates/claude/skills/recipe-review-meet-participants/SKILL.md +25 -0
- package/templates/claude/skills/recipe-review-overdue-tasks/SKILL.md +25 -0
- package/templates/claude/skills/recipe-save-email-attachments/SKILL.md +26 -0
- package/templates/claude/skills/recipe-save-email-to-doc/SKILL.md +29 -0
- package/templates/claude/skills/recipe-schedule-recurring-event/SKILL.md +24 -0
- package/templates/claude/skills/recipe-send-team-announcement/SKILL.md +24 -0
- package/templates/claude/skills/recipe-share-doc-and-notify/SKILL.md +25 -0
- package/templates/claude/skills/recipe-share-event-materials/SKILL.md +25 -0
- package/templates/claude/skills/recipe-share-folder-with-team/SKILL.md +26 -0
- package/templates/claude/skills/recipe-sync-contacts-to-sheet/SKILL.md +25 -0
- package/templates/claude/skills/recipe-watch-drive-changes/SKILL.md +25 -0
- package/templates/mcp.json +12 -0
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { NextRequest, NextResponse } from "next/server";
|
|
2
|
+
import { getAgent, updateAgentMeta, deleteAgent } from "@/lib/agents";
|
|
3
|
+
import { getAgentCapabilities } from "@/lib/capabilities";
|
|
4
|
+
|
|
5
|
+
export async function GET(
|
|
6
|
+
_req: Request,
|
|
7
|
+
{ params }: { params: Promise<{ slug: string }> }
|
|
8
|
+
) {
|
|
9
|
+
const { slug } = await params;
|
|
10
|
+
const agent = getAgent(slug);
|
|
11
|
+
if (!agent) {
|
|
12
|
+
return NextResponse.json({ error: "Agent not found" }, { status: 404 });
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const capabilities = getAgentCapabilities(slug);
|
|
16
|
+
|
|
17
|
+
return NextResponse.json({ ...agent, capabilities });
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export async function PATCH(
|
|
21
|
+
req: NextRequest,
|
|
22
|
+
{ params }: { params: Promise<{ slug: string }> }
|
|
23
|
+
) {
|
|
24
|
+
const { slug } = await params;
|
|
25
|
+
|
|
26
|
+
if (slug === "main") {
|
|
27
|
+
return NextResponse.json({ error: "Main agent cannot be edited" }, { status: 400 });
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const body = await req.json();
|
|
31
|
+
const { name, description, emoji, vibe, tools, mcpServers } = body;
|
|
32
|
+
|
|
33
|
+
const updated = updateAgentMeta(slug, { name, description, emoji, vibe, tools, mcpServers });
|
|
34
|
+
if (!updated) {
|
|
35
|
+
return NextResponse.json({ error: "Agent not found" }, { status: 404 });
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return NextResponse.json(updated);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export async function DELETE(
|
|
42
|
+
_req: NextRequest,
|
|
43
|
+
{ params }: { params: Promise<{ slug: string }> }
|
|
44
|
+
) {
|
|
45
|
+
const { slug } = await params;
|
|
46
|
+
|
|
47
|
+
if (slug === "main") {
|
|
48
|
+
return NextResponse.json({ error: "Main agent cannot be deleted" }, { status: 400 });
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const deleted = deleteAgent(slug);
|
|
52
|
+
if (!deleted) {
|
|
53
|
+
return NextResponse.json({ error: "Agent not found" }, { status: 404 });
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return NextResponse.json({ ok: true });
|
|
57
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { NextRequest, NextResponse } from "next/server";
|
|
2
|
+
import { getAgents, createAgent } from "@/lib/agents";
|
|
3
|
+
|
|
4
|
+
export async function GET() {
|
|
5
|
+
const agents = getAgents();
|
|
6
|
+
return NextResponse.json(agents);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export async function POST(req: NextRequest) {
|
|
10
|
+
try {
|
|
11
|
+
const body = await req.json();
|
|
12
|
+
const { name, description, tools, mcpServers, color, emoji, vibe, model, content } = body;
|
|
13
|
+
|
|
14
|
+
if (!name || !description || !content) {
|
|
15
|
+
return NextResponse.json(
|
|
16
|
+
{ error: "name, description, and content are required" },
|
|
17
|
+
{ status: 400 }
|
|
18
|
+
);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const agent = createAgent({ name, description, tools, mcpServers, color, emoji, vibe, model, content });
|
|
22
|
+
return NextResponse.json(agent, { status: 201 });
|
|
23
|
+
} catch (err) {
|
|
24
|
+
const message = err instanceof Error ? err.message : "Failed to create agent";
|
|
25
|
+
const status = message.includes("already exists") ? 409 : 500;
|
|
26
|
+
return NextResponse.json({ error: message }, { status });
|
|
27
|
+
}
|
|
28
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { NextRequest, NextResponse } from "next/server";
|
|
2
|
+
|
|
3
|
+
const ANTHROPIC_API_URL = "https://api.anthropic.com/v1/messages";
|
|
4
|
+
const MODEL = "claude-haiku-4-5-20251001";
|
|
5
|
+
|
|
6
|
+
const SYSTEM_PROMPT = `You are an agent architect. Given a user's description of what they want an agent to do, generate a complete agent definition.
|
|
7
|
+
|
|
8
|
+
Return a JSON object with these fields:
|
|
9
|
+
- name: A short, descriptive name (2-4 words)
|
|
10
|
+
- description: One sentence explaining what the agent does
|
|
11
|
+
- tools: Comma-separated list of tools the agent needs. Available tools: WebFetch, WebSearch, Read, Write, Edit, Bash, Glob, Grep
|
|
12
|
+
- color: A hex color or named color that fits the agent's theme (e.g., "#10B981", "teal", "purple", "#F59E0B")
|
|
13
|
+
- emoji: A single emoji that represents the agent
|
|
14
|
+
- vibe: A short, witty tagline (under 80 chars) describing the agent's personality
|
|
15
|
+
- content: The full system prompt for the agent in markdown. This should be detailed and include:
|
|
16
|
+
- An identity section explaining who the agent is and what it does
|
|
17
|
+
- Specific instructions for how it should operate
|
|
18
|
+
- Any relevant constraints or guidelines
|
|
19
|
+
- Output format preferences if applicable
|
|
20
|
+
|
|
21
|
+
The content should be well-structured markdown with headers. Make the agent focused and practical.
|
|
22
|
+
|
|
23
|
+
Return ONLY valid JSON, no markdown fences or extra text.`;
|
|
24
|
+
|
|
25
|
+
export async function POST(req: NextRequest) {
|
|
26
|
+
const apiKey = process.env.ANTHROPIC_API_KEY;
|
|
27
|
+
if (!apiKey) {
|
|
28
|
+
return NextResponse.json({ error: "ANTHROPIC_API_KEY not set" }, { status: 500 });
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const { prompt } = await req.json();
|
|
32
|
+
if (!prompt || typeof prompt !== "string") {
|
|
33
|
+
return NextResponse.json({ error: "prompt is required" }, { status: 400 });
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
try {
|
|
37
|
+
const res = await fetch(ANTHROPIC_API_URL, {
|
|
38
|
+
method: "POST",
|
|
39
|
+
headers: {
|
|
40
|
+
"Content-Type": "application/json",
|
|
41
|
+
"x-api-key": apiKey,
|
|
42
|
+
"anthropic-version": "2023-06-01",
|
|
43
|
+
},
|
|
44
|
+
body: JSON.stringify({
|
|
45
|
+
model: MODEL,
|
|
46
|
+
max_tokens: 4096,
|
|
47
|
+
system: SYSTEM_PROMPT,
|
|
48
|
+
messages: [
|
|
49
|
+
{
|
|
50
|
+
role: "user",
|
|
51
|
+
content: `Create an agent for the following purpose:\n\n${prompt}`,
|
|
52
|
+
},
|
|
53
|
+
],
|
|
54
|
+
}),
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
if (!res.ok) {
|
|
58
|
+
const body = await res.text();
|
|
59
|
+
return NextResponse.json(
|
|
60
|
+
{ error: `Anthropic API error ${res.status}: ${body}` },
|
|
61
|
+
{ status: 502 }
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const data = await res.json();
|
|
66
|
+
const text = data.content?.[0]?.text || "";
|
|
67
|
+
|
|
68
|
+
// Strip markdown fences if present
|
|
69
|
+
const cleaned = text.trim().replace(/^```(?:json)?\s*\n?/i, "").replace(/\n?```\s*$/i, "").trim();
|
|
70
|
+
const agent = JSON.parse(cleaned);
|
|
71
|
+
|
|
72
|
+
// Validate required fields
|
|
73
|
+
if (!agent.name || !agent.description || !agent.content) {
|
|
74
|
+
return NextResponse.json(
|
|
75
|
+
{ error: "AI generated incomplete agent definition" },
|
|
76
|
+
{ status: 500 }
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return NextResponse.json(agent);
|
|
81
|
+
} catch (err) {
|
|
82
|
+
return NextResponse.json(
|
|
83
|
+
{ error: err instanceof Error ? err.message : "Failed to generate agent" },
|
|
84
|
+
{ status: 500 }
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { NextRequest, NextResponse } from "next/server";
|
|
2
|
+
import { requireRole } from "@/lib/auth-guard";
|
|
3
|
+
|
|
4
|
+
const GEMINI_URL =
|
|
5
|
+
"https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent";
|
|
6
|
+
|
|
7
|
+
const SYSTEM_PROMPT = `You are a precise editor for CLAUDE.md files (project-level instructions for Claude Code).
|
|
8
|
+
|
|
9
|
+
Rules:
|
|
10
|
+
- Apply ONLY the changes the user requests. Do not rewrite, reformat, or "improve" anything else.
|
|
11
|
+
- Preserve the exact structure, formatting, whitespace, and ordering of sections you don't touch.
|
|
12
|
+
- Return the COMPLETE file contents with your changes applied — nothing else.
|
|
13
|
+
- Do NOT wrap output in markdown code fences or add any explanation.
|
|
14
|
+
- Do NOT add or remove blank lines unless the user specifically asks.
|
|
15
|
+
- If the instruction is unclear, make the minimal reasonable interpretation.`;
|
|
16
|
+
|
|
17
|
+
export async function POST(req: NextRequest) {
|
|
18
|
+
const auth = await requireRole(req, ["admin"], "improve_claude_md", "settings/claude-md");
|
|
19
|
+
if (!auth.authorized) return auth.response;
|
|
20
|
+
|
|
21
|
+
const { content, instruction } = await req.json();
|
|
22
|
+
|
|
23
|
+
if (typeof content !== "string" || typeof instruction !== "string" || !instruction.trim()) {
|
|
24
|
+
return NextResponse.json(
|
|
25
|
+
{ error: "content and instruction are required" },
|
|
26
|
+
{ status: 400 }
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const apiKey = process.env.GEMINI_API_KEY;
|
|
31
|
+
if (!apiKey) {
|
|
32
|
+
return NextResponse.json({ error: "GEMINI_API_KEY not set" }, { status: 500 });
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
try {
|
|
36
|
+
const res = await fetch(`${GEMINI_URL}?key=${apiKey}`, {
|
|
37
|
+
method: "POST",
|
|
38
|
+
headers: { "Content-Type": "application/json" },
|
|
39
|
+
body: JSON.stringify({
|
|
40
|
+
system_instruction: { parts: [{ text: SYSTEM_PROMPT }] },
|
|
41
|
+
contents: [
|
|
42
|
+
{
|
|
43
|
+
parts: [
|
|
44
|
+
{
|
|
45
|
+
text: `Here is the current CLAUDE.md file:\n\n${content}\n\n---\n\nUser instruction: ${instruction}`,
|
|
46
|
+
},
|
|
47
|
+
],
|
|
48
|
+
},
|
|
49
|
+
],
|
|
50
|
+
generationConfig: {
|
|
51
|
+
maxOutputTokens: 16384,
|
|
52
|
+
temperature: 0.1,
|
|
53
|
+
},
|
|
54
|
+
}),
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
if (!res.ok) {
|
|
58
|
+
const body = await res.text();
|
|
59
|
+
throw new Error(`Gemini API error ${res.status}: ${body}`);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const data = await res.json();
|
|
63
|
+
let improved =
|
|
64
|
+
data.candidates?.[0]?.content?.parts?.[0]?.text || "";
|
|
65
|
+
|
|
66
|
+
// Strip any accidental code fences the model might add
|
|
67
|
+
improved = improved
|
|
68
|
+
.replace(/^```(?:markdown|md)?\s*\n/i, "")
|
|
69
|
+
.replace(/\n```\s*$/, "");
|
|
70
|
+
|
|
71
|
+
return NextResponse.json({ improved });
|
|
72
|
+
} catch (err) {
|
|
73
|
+
return NextResponse.json(
|
|
74
|
+
{ error: err instanceof Error ? err.message : "Failed to improve content" },
|
|
75
|
+
{ status: 500 }
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { NextRequest, NextResponse } from "next/server";
|
|
2
|
+
import { callHaiku } from "@/lib/ai";
|
|
3
|
+
|
|
4
|
+
// In-memory cache: key = "slug:hash" → suggestions[]
|
|
5
|
+
const cache = new Map<string, { suggestions: string[]; hash: string }>();
|
|
6
|
+
|
|
7
|
+
function hashString(s: string): string {
|
|
8
|
+
let h = 0;
|
|
9
|
+
for (let i = 0; i < s.length; i++) {
|
|
10
|
+
h = ((h << 5) - h + s.charCodeAt(i)) | 0;
|
|
11
|
+
}
|
|
12
|
+
return h.toString(36);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export async function GET(req: NextRequest) {
|
|
16
|
+
const slug = req.nextUrl.searchParams.get("slug");
|
|
17
|
+
const name = req.nextUrl.searchParams.get("name");
|
|
18
|
+
const description = req.nextUrl.searchParams.get("description");
|
|
19
|
+
|
|
20
|
+
if (!slug || !name || !description) {
|
|
21
|
+
return NextResponse.json(
|
|
22
|
+
{ error: "slug, name, and description are required" },
|
|
23
|
+
{ status: 400 }
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const hash = hashString(`${name}:${description}`);
|
|
28
|
+
const cached = cache.get(slug);
|
|
29
|
+
if (cached && cached.hash === hash) {
|
|
30
|
+
return NextResponse.json({ suggestions: cached.suggestions });
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
try {
|
|
34
|
+
const raw = await callHaiku(
|
|
35
|
+
"You generate short prompt suggestions for an AI assistant chat interface. Return exactly 3 suggestions as a JSON array of strings. Each suggestion should be a natural, conversational request that a user might ask this specific agent. Keep them short (under 60 chars), varied in topic, and practical. Return ONLY the JSON array, no other text.",
|
|
36
|
+
[
|
|
37
|
+
{
|
|
38
|
+
role: "user",
|
|
39
|
+
content: `Agent name: ${name}\nAgent description: ${description}\n\nGenerate 4 prompt suggestions:`,
|
|
40
|
+
},
|
|
41
|
+
],
|
|
42
|
+
200
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
// Strip markdown code fences if present
|
|
46
|
+
const cleaned = raw.trim().replace(/^```(?:json)?\s*\n?/i, "").replace(/\n?```\s*$/i, "").trim();
|
|
47
|
+
const suggestions = JSON.parse(cleaned);
|
|
48
|
+
if (Array.isArray(suggestions) && suggestions.length >= 3) {
|
|
49
|
+
const result = suggestions.slice(0, 3).map(String);
|
|
50
|
+
cache.set(slug, { suggestions: result, hash });
|
|
51
|
+
return NextResponse.json({ suggestions: result });
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return NextResponse.json(
|
|
55
|
+
{ error: "Invalid response from model" },
|
|
56
|
+
{ status: 500 }
|
|
57
|
+
);
|
|
58
|
+
} catch (err) {
|
|
59
|
+
return NextResponse.json(
|
|
60
|
+
{ error: err instanceof Error ? err.message : "Failed to generate suggestions" },
|
|
61
|
+
{ status: 500 }
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { NextRequest, NextResponse } from "next/server";
|
|
2
|
+
import { supabase } from "@/lib/supabase";
|
|
3
|
+
|
|
4
|
+
const GEMINI_URL = "https://generativelanguage.googleapis.com/v1beta/models/gemini-3.1-flash-lite-preview:generateContent";
|
|
5
|
+
|
|
6
|
+
const SYSTEM_PROMPT =
|
|
7
|
+
"You name chat conversations. Given the user's first message, generate a very short chat title (2-5 words max). Start with a relevant emoji. Return ONLY the title, nothing else. Examples:\n🐛 Fix login bug\n📊 Sales dashboard layout\n🚀 Deploy to production\n✍️ Write blog post\n📧 Email template help";
|
|
8
|
+
|
|
9
|
+
async function generateTitle(message: string): Promise<string> {
|
|
10
|
+
const apiKey = process.env.GEMINI_API_KEY;
|
|
11
|
+
if (!apiKey) throw new Error("GEMINI_API_KEY not set");
|
|
12
|
+
|
|
13
|
+
const res = await fetch(`${GEMINI_URL}?key=${apiKey}`, {
|
|
14
|
+
method: "POST",
|
|
15
|
+
headers: { "Content-Type": "application/json" },
|
|
16
|
+
body: JSON.stringify({
|
|
17
|
+
system_instruction: { parts: [{ text: SYSTEM_PROMPT }] },
|
|
18
|
+
contents: [{ parts: [{ text: message }] }],
|
|
19
|
+
generationConfig: { maxOutputTokens: 30, temperature: 0.7 },
|
|
20
|
+
}),
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
if (!res.ok) {
|
|
24
|
+
const body = await res.text();
|
|
25
|
+
throw new Error(`Gemini API error ${res.status}: ${body}`);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const data = await res.json();
|
|
29
|
+
const text = data.candidates?.[0]?.content?.parts?.[0]?.text || "";
|
|
30
|
+
return text.trim().replace(/^["']|["']$/g, "");
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export async function POST(req: NextRequest) {
|
|
34
|
+
const { session_id, message } = await req.json();
|
|
35
|
+
|
|
36
|
+
if (!session_id || !message) {
|
|
37
|
+
return NextResponse.json(
|
|
38
|
+
{ error: "session_id and message are required" },
|
|
39
|
+
{ status: 400 }
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Check if title already exists
|
|
44
|
+
const { data: existing } = await supabase
|
|
45
|
+
.from("session_titles")
|
|
46
|
+
.select("title")
|
|
47
|
+
.eq("session_id", session_id)
|
|
48
|
+
.single();
|
|
49
|
+
|
|
50
|
+
if (existing?.title) {
|
|
51
|
+
return NextResponse.json({ title: existing.title });
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
try {
|
|
55
|
+
const title = await generateTitle(message);
|
|
56
|
+
|
|
57
|
+
await supabase
|
|
58
|
+
.from("session_titles")
|
|
59
|
+
.upsert({ session_id, title });
|
|
60
|
+
|
|
61
|
+
return NextResponse.json({ title });
|
|
62
|
+
} catch (err) {
|
|
63
|
+
return NextResponse.json(
|
|
64
|
+
{ error: err instanceof Error ? err.message : "Failed to generate title" },
|
|
65
|
+
{ status: 500 }
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export async function GET(req: NextRequest) {
|
|
71
|
+
const sessionIds = req.nextUrl.searchParams.get("session_ids");
|
|
72
|
+
if (!sessionIds) {
|
|
73
|
+
return NextResponse.json({ error: "session_ids required" }, { status: 400 });
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const ids = sessionIds.split(",").filter(Boolean);
|
|
77
|
+
const { data } = await supabase
|
|
78
|
+
.from("session_titles")
|
|
79
|
+
.select("session_id, title")
|
|
80
|
+
.in("session_id", ids);
|
|
81
|
+
|
|
82
|
+
const map: Record<string, string> = {};
|
|
83
|
+
for (const row of data || []) {
|
|
84
|
+
map[row.session_id] = row.title;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return NextResponse.json({ titles: map });
|
|
88
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { NextResponse } from "next/server";
|
|
2
|
+
import { createClient } from "@/lib/supabase-server";
|
|
3
|
+
|
|
4
|
+
export async function GET() {
|
|
5
|
+
const supabase = await createClient();
|
|
6
|
+
const { data: { user } } = await supabase.auth.getUser();
|
|
7
|
+
|
|
8
|
+
if (!user?.email) {
|
|
9
|
+
return NextResponse.json({ role: null });
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const { data: role } = await supabase.rpc("check_user_role", {
|
|
13
|
+
user_email: user.email,
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
return NextResponse.json({ role: role || null, email: user.email });
|
|
17
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { NextRequest, NextResponse } from "next/server";
|
|
2
|
+
import fs from "fs";
|
|
3
|
+
import path from "path";
|
|
4
|
+
|
|
5
|
+
const PROJECT_ROOT = process.env.PROJECT_ROOT || path.resolve(__dirname, "../../../../..");
|
|
6
|
+
const COMMANDS_DIR = path.join(PROJECT_ROOT, ".claude", "commands");
|
|
7
|
+
|
|
8
|
+
function resolveCommandPath(slug: string): string | null {
|
|
9
|
+
// slug can be "foo" (top-level) or "consider/pareto" (grouped)
|
|
10
|
+
const parts = slug.split("/");
|
|
11
|
+
if (parts.length === 1) {
|
|
12
|
+
const p = path.join(COMMANDS_DIR, `${slug}.md`);
|
|
13
|
+
return fs.existsSync(p) ? p : null;
|
|
14
|
+
}
|
|
15
|
+
if (parts.length === 2) {
|
|
16
|
+
const p = path.join(COMMANDS_DIR, parts[0], `${parts[1]}.md`);
|
|
17
|
+
return fs.existsSync(p) ? p : null;
|
|
18
|
+
}
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export async function GET(
|
|
23
|
+
_req: NextRequest,
|
|
24
|
+
{ params }: { params: Promise<{ slug: string }> }
|
|
25
|
+
) {
|
|
26
|
+
const { slug } = await params;
|
|
27
|
+
const decoded = decodeURIComponent(slug);
|
|
28
|
+
const filePath = resolveCommandPath(decoded);
|
|
29
|
+
|
|
30
|
+
if (!filePath) {
|
|
31
|
+
return NextResponse.json({ error: "Command not found" }, { status: 404 });
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
try {
|
|
35
|
+
const content = fs.readFileSync(filePath, "utf-8");
|
|
36
|
+
return NextResponse.json({ slug: decoded, content });
|
|
37
|
+
} catch {
|
|
38
|
+
return NextResponse.json({ error: "Command not found" }, { status: 404 });
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export async function PATCH(
|
|
43
|
+
req: NextRequest,
|
|
44
|
+
{ params }: { params: Promise<{ slug: string }> }
|
|
45
|
+
) {
|
|
46
|
+
const { slug } = await params;
|
|
47
|
+
const decoded = decodeURIComponent(slug);
|
|
48
|
+
const filePath = resolveCommandPath(decoded);
|
|
49
|
+
|
|
50
|
+
if (!filePath) {
|
|
51
|
+
return NextResponse.json({ error: "Command not found" }, { status: 404 });
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const { content } = await req.json();
|
|
55
|
+
if (typeof content !== "string") {
|
|
56
|
+
return NextResponse.json({ error: "content must be a string" }, { status: 400 });
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
fs.writeFileSync(filePath, content, "utf-8");
|
|
60
|
+
return NextResponse.json({ slug: decoded, saved: true });
|
|
61
|
+
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { NextRequest, NextResponse } from "next/server";
|
|
2
|
+
import { supabase } from "@/lib/supabase";
|
|
3
|
+
import { requireRole } from "@/lib/auth-guard";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* GET /api/governance/costs
|
|
7
|
+
*
|
|
8
|
+
* Returns cost aggregations from agent_runs for today, this week, and this month.
|
|
9
|
+
* Also returns per-agent breakdowns and top runs by cost.
|
|
10
|
+
*/
|
|
11
|
+
export async function GET(req: NextRequest) {
|
|
12
|
+
const auth = await requireRole(req, ["admin"], "governance:costs", "/api/governance/costs");
|
|
13
|
+
if (!auth.authorized) return auth.response!;
|
|
14
|
+
|
|
15
|
+
const now = new Date();
|
|
16
|
+
|
|
17
|
+
// Start of today (UTC)
|
|
18
|
+
const todayStart = new Date(now);
|
|
19
|
+
todayStart.setUTCHours(0, 0, 0, 0);
|
|
20
|
+
|
|
21
|
+
// Start of this week (Monday, UTC)
|
|
22
|
+
const weekStart = new Date(now);
|
|
23
|
+
const dayOfWeek = weekStart.getUTCDay();
|
|
24
|
+
const daysToMonday = dayOfWeek === 0 ? 6 : dayOfWeek - 1;
|
|
25
|
+
weekStart.setUTCDate(weekStart.getUTCDate() - daysToMonday);
|
|
26
|
+
weekStart.setUTCHours(0, 0, 0, 0);
|
|
27
|
+
|
|
28
|
+
// Start of this month (UTC)
|
|
29
|
+
const monthStart = new Date(now.getUTCFullYear(), now.getUTCMonth(), 1);
|
|
30
|
+
|
|
31
|
+
// Fetch all runs with cost data from this month (superset of week and day)
|
|
32
|
+
const { data: runs, error } = await supabase
|
|
33
|
+
.from("agent_runs")
|
|
34
|
+
.select("cost_usd, agent_slug, agent_name, created_at, duration_ms, prompt")
|
|
35
|
+
.gte("created_at", monthStart.toISOString())
|
|
36
|
+
.not("cost_usd", "is", null)
|
|
37
|
+
.order("created_at", { ascending: false });
|
|
38
|
+
|
|
39
|
+
if (error) {
|
|
40
|
+
return NextResponse.json({ error: error.message }, { status: 500 });
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const safeRuns = runs || [];
|
|
44
|
+
|
|
45
|
+
// Aggregate by time period
|
|
46
|
+
let todayTotal = 0;
|
|
47
|
+
let todayCount = 0;
|
|
48
|
+
let weekTotal = 0;
|
|
49
|
+
let weekCount = 0;
|
|
50
|
+
let monthTotal = 0;
|
|
51
|
+
let monthCount = 0;
|
|
52
|
+
|
|
53
|
+
// Per-agent breakdown (for the month)
|
|
54
|
+
const agentCosts: Record<string, { name: string; total: number; count: number }> = {};
|
|
55
|
+
|
|
56
|
+
for (const run of safeRuns) {
|
|
57
|
+
const cost = run.cost_usd as number;
|
|
58
|
+
const createdAt = new Date(run.created_at);
|
|
59
|
+
const slug = run.agent_slug as string;
|
|
60
|
+
const name = run.agent_name as string;
|
|
61
|
+
|
|
62
|
+
// Month (all runs in result set)
|
|
63
|
+
monthTotal += cost;
|
|
64
|
+
monthCount++;
|
|
65
|
+
|
|
66
|
+
// Week
|
|
67
|
+
if (createdAt >= weekStart) {
|
|
68
|
+
weekTotal += cost;
|
|
69
|
+
weekCount++;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Today
|
|
73
|
+
if (createdAt >= todayStart) {
|
|
74
|
+
todayTotal += cost;
|
|
75
|
+
todayCount++;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Per-agent
|
|
79
|
+
if (!agentCosts[slug]) {
|
|
80
|
+
agentCosts[slug] = { name, total: 0, count: 0 };
|
|
81
|
+
}
|
|
82
|
+
agentCosts[slug].total += cost;
|
|
83
|
+
agentCosts[slug].count++;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Sort agents by total cost descending
|
|
87
|
+
const agentBreakdown = Object.entries(agentCosts)
|
|
88
|
+
.map(([slug, data]) => ({
|
|
89
|
+
slug,
|
|
90
|
+
name: data.name,
|
|
91
|
+
total: data.total,
|
|
92
|
+
count: data.count,
|
|
93
|
+
avgPerRun: data.count > 0 ? data.total / data.count : 0,
|
|
94
|
+
}))
|
|
95
|
+
.sort((a, b) => b.total - a.total);
|
|
96
|
+
|
|
97
|
+
// Top 5 most expensive individual runs this month
|
|
98
|
+
const topRuns = safeRuns
|
|
99
|
+
.sort((a, b) => (b.cost_usd as number) - (a.cost_usd as number))
|
|
100
|
+
.slice(0, 5)
|
|
101
|
+
.map((r) => ({
|
|
102
|
+
agent_name: r.agent_name,
|
|
103
|
+
cost_usd: r.cost_usd,
|
|
104
|
+
prompt: (r.prompt as string)?.slice(0, 80) || "—",
|
|
105
|
+
created_at: r.created_at,
|
|
106
|
+
}));
|
|
107
|
+
|
|
108
|
+
return NextResponse.json({
|
|
109
|
+
periods: {
|
|
110
|
+
today: { total: todayTotal, count: todayCount },
|
|
111
|
+
week: { total: weekTotal, count: weekCount },
|
|
112
|
+
month: { total: monthTotal, count: monthCount },
|
|
113
|
+
},
|
|
114
|
+
agentBreakdown,
|
|
115
|
+
topRuns,
|
|
116
|
+
});
|
|
117
|
+
}
|