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,271 @@
|
|
|
1
|
+
import { NextRequest, NextResponse } from "next/server";
|
|
2
|
+
import { requireRole } from "@/lib/auth-guard";
|
|
3
|
+
import fs from "fs";
|
|
4
|
+
import path from "path";
|
|
5
|
+
|
|
6
|
+
// ---------------------------------------------------------------------------
|
|
7
|
+
// Key manifest — the keys this app cares about
|
|
8
|
+
// ---------------------------------------------------------------------------
|
|
9
|
+
|
|
10
|
+
interface KeyDef {
|
|
11
|
+
name: string;
|
|
12
|
+
category: "core" | "auth";
|
|
13
|
+
optional: boolean;
|
|
14
|
+
/** Which .env file this key lives in (relative to PROJECT_ROOT) */
|
|
15
|
+
envFile: string;
|
|
16
|
+
/** Validation description shown in UI */
|
|
17
|
+
validationHint: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const KEY_MANIFEST: KeyDef[] = [
|
|
21
|
+
{
|
|
22
|
+
name: "ANTHROPIC_API_KEY",
|
|
23
|
+
category: "core",
|
|
24
|
+
optional: false,
|
|
25
|
+
envFile: "personal-assistant-app/.env.local",
|
|
26
|
+
validationHint: "Tested against Anthropic API",
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
name: "GEMINI_API_KEY",
|
|
30
|
+
category: "core",
|
|
31
|
+
optional: false,
|
|
32
|
+
envFile: "personal-assistant-app/.env.local",
|
|
33
|
+
validationHint: "Tested against Gemini API",
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
name: "SUPABASE_URL",
|
|
37
|
+
category: "core",
|
|
38
|
+
optional: false,
|
|
39
|
+
envFile: "personal-assistant-app/.env.local",
|
|
40
|
+
validationHint: "Must be a valid URL",
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
name: "SUPABASE_ANON_KEY",
|
|
44
|
+
category: "core",
|
|
45
|
+
optional: false,
|
|
46
|
+
envFile: "personal-assistant-app/.env.local",
|
|
47
|
+
validationHint: "JWT format (eyJ...)",
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
name: "GOOGLE_CLIENT_ID",
|
|
51
|
+
category: "auth",
|
|
52
|
+
optional: true,
|
|
53
|
+
envFile: "personal-assistant-app/.env.local",
|
|
54
|
+
validationHint: "OAuth client ID",
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
name: "GOOGLE_CLIENT_SECRET",
|
|
58
|
+
category: "auth",
|
|
59
|
+
optional: true,
|
|
60
|
+
envFile: "personal-assistant-app/.env.local",
|
|
61
|
+
validationHint: "OAuth client secret",
|
|
62
|
+
},
|
|
63
|
+
];
|
|
64
|
+
|
|
65
|
+
const PROJECT_ROOT = process.env.PROJECT_ROOT || "/mnt/c/Users/Admin/Documents/PersonalAIssistant";
|
|
66
|
+
|
|
67
|
+
// ---------------------------------------------------------------------------
|
|
68
|
+
// Helpers
|
|
69
|
+
// ---------------------------------------------------------------------------
|
|
70
|
+
|
|
71
|
+
function maskValue(value: string): string {
|
|
72
|
+
if (value.length <= 8) return "••••••••";
|
|
73
|
+
return "••••••••" + value.slice(-4);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
type Source = "env-file" | "process" | "missing";
|
|
77
|
+
|
|
78
|
+
function detectSource(name: string, envFile: string): { source: Source; value: string } {
|
|
79
|
+
// Check .env file first
|
|
80
|
+
const envPath = path.join(PROJECT_ROOT, envFile);
|
|
81
|
+
try {
|
|
82
|
+
const content = fs.readFileSync(envPath, "utf-8");
|
|
83
|
+
const regex = new RegExp(`^${name}=(.*)$`, "m");
|
|
84
|
+
const match = content.match(regex);
|
|
85
|
+
if (match && match[1].trim()) {
|
|
86
|
+
return { source: "env-file", value: match[1].trim() };
|
|
87
|
+
}
|
|
88
|
+
} catch { /* file doesn't exist */ }
|
|
89
|
+
|
|
90
|
+
// Check process.env
|
|
91
|
+
if (process.env[name]) {
|
|
92
|
+
return { source: "process", value: process.env[name]! };
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return { source: "missing", value: "" };
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
async function validateKey(name: string, value: string): Promise<{ valid: boolean; error?: string }> {
|
|
99
|
+
if (!value) return { valid: false, error: "Empty value" };
|
|
100
|
+
|
|
101
|
+
switch (name) {
|
|
102
|
+
case "ANTHROPIC_API_KEY": {
|
|
103
|
+
try {
|
|
104
|
+
// Make a real API call — /v1/models doesn't check credit balance, so a "stuck" key looks valid there
|
|
105
|
+
const res = await fetch("https://api.anthropic.com/v1/messages", {
|
|
106
|
+
method: "POST",
|
|
107
|
+
headers: {
|
|
108
|
+
"Content-Type": "application/json",
|
|
109
|
+
"x-api-key": value,
|
|
110
|
+
"anthropic-version": "2023-06-01",
|
|
111
|
+
},
|
|
112
|
+
body: JSON.stringify({
|
|
113
|
+
model: "claude-haiku-4-5-20251001",
|
|
114
|
+
max_tokens: 1,
|
|
115
|
+
messages: [{ role: "user", content: "." }],
|
|
116
|
+
}),
|
|
117
|
+
});
|
|
118
|
+
if (res.ok) return { valid: true };
|
|
119
|
+
if (res.status === 401) return { valid: false, error: "Invalid API key" };
|
|
120
|
+
const body = await res.text();
|
|
121
|
+
if (body.includes("credit balance")) {
|
|
122
|
+
return { valid: false, error: "Key stuck — Anthropic cached a zero balance on this key. Generate a new one at console.anthropic.com/settings/keys" };
|
|
123
|
+
}
|
|
124
|
+
return { valid: false, error: `API returned ${res.status}` };
|
|
125
|
+
} catch (e) {
|
|
126
|
+
return { valid: false, error: `Connection failed: ${(e as Error).message}` };
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
case "GEMINI_API_KEY": {
|
|
131
|
+
try {
|
|
132
|
+
const res = await fetch(
|
|
133
|
+
`https://generativelanguage.googleapis.com/v1beta/models?key=${value}`
|
|
134
|
+
);
|
|
135
|
+
if (res.ok) return { valid: true };
|
|
136
|
+
if (res.status === 400 || res.status === 403) return { valid: false, error: "Invalid API key" };
|
|
137
|
+
return { valid: false, error: `API returned ${res.status}` };
|
|
138
|
+
} catch (e) {
|
|
139
|
+
return { valid: false, error: `Connection failed: ${(e as Error).message}` };
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
case "SUPABASE_URL": {
|
|
144
|
+
try {
|
|
145
|
+
new URL(value);
|
|
146
|
+
return { valid: true };
|
|
147
|
+
} catch {
|
|
148
|
+
return { valid: false, error: "Not a valid URL" };
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
case "SUPABASE_ANON_KEY": {
|
|
153
|
+
if (value.startsWith("eyJ") && value.includes(".")) return { valid: true };
|
|
154
|
+
return { valid: false, error: "Doesn't look like a JWT (should start with eyJ)" };
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
case "GOOGLE_CLIENT_ID":
|
|
158
|
+
case "GOOGLE_CLIENT_SECRET":
|
|
159
|
+
return { valid: true }; // Just non-empty, already checked above
|
|
160
|
+
|
|
161
|
+
default:
|
|
162
|
+
return { valid: true };
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// ---------------------------------------------------------------------------
|
|
167
|
+
// GET — return manifest with masked values and health status
|
|
168
|
+
// ---------------------------------------------------------------------------
|
|
169
|
+
|
|
170
|
+
export async function GET(req: NextRequest) {
|
|
171
|
+
const auth = await requireRole(req, ["admin"], "read", "env-keys");
|
|
172
|
+
if (!auth.authorized) {
|
|
173
|
+
return NextResponse.json({ error: "Forbidden" }, { status: 403 });
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const keys = await Promise.all(
|
|
177
|
+
KEY_MANIFEST.map(async (def) => {
|
|
178
|
+
const { source, value } = detectSource(def.name, def.envFile);
|
|
179
|
+
let health: "valid" | "invalid" | "unchecked" | "missing" = "unchecked";
|
|
180
|
+
let healthError: string | undefined;
|
|
181
|
+
|
|
182
|
+
if (source === "missing") {
|
|
183
|
+
health = def.optional ? "unchecked" : "missing";
|
|
184
|
+
} else {
|
|
185
|
+
const result = await validateKey(def.name, value);
|
|
186
|
+
health = result.valid ? "valid" : "invalid";
|
|
187
|
+
healthError = result.error;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
return {
|
|
191
|
+
name: def.name,
|
|
192
|
+
category: def.category,
|
|
193
|
+
optional: def.optional,
|
|
194
|
+
masked: source !== "missing" ? maskValue(value) : null,
|
|
195
|
+
source,
|
|
196
|
+
health,
|
|
197
|
+
healthError,
|
|
198
|
+
validationHint: def.validationHint,
|
|
199
|
+
};
|
|
200
|
+
})
|
|
201
|
+
);
|
|
202
|
+
|
|
203
|
+
return NextResponse.json(keys);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// ---------------------------------------------------------------------------
|
|
207
|
+
// PATCH — update a single key
|
|
208
|
+
// ---------------------------------------------------------------------------
|
|
209
|
+
|
|
210
|
+
export async function PATCH(req: NextRequest) {
|
|
211
|
+
const auth = await requireRole(req, ["admin"], "write", "env-keys");
|
|
212
|
+
if (!auth.authorized) {
|
|
213
|
+
return NextResponse.json({ error: "Forbidden" }, { status: 403 });
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const { name, value } = await req.json();
|
|
217
|
+
|
|
218
|
+
if (typeof name !== "string" || typeof value !== "string") {
|
|
219
|
+
return NextResponse.json({ error: "name and value must be strings" }, { status: 400 });
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
const def = KEY_MANIFEST.find((k) => k.name === name);
|
|
223
|
+
if (!def) {
|
|
224
|
+
return NextResponse.json({ error: `Unknown key: ${name}` }, { status: 400 });
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// Validate before writing
|
|
228
|
+
const validation = await validateKey(name, value);
|
|
229
|
+
if (!validation.valid) {
|
|
230
|
+
return NextResponse.json(
|
|
231
|
+
{ error: `Validation failed: ${validation.error}`, validated: false },
|
|
232
|
+
{ status: 422 }
|
|
233
|
+
);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// Write to .env file
|
|
237
|
+
const envPath = path.join(PROJECT_ROOT, def.envFile);
|
|
238
|
+
try {
|
|
239
|
+
let content = "";
|
|
240
|
+
try {
|
|
241
|
+
content = fs.readFileSync(envPath, "utf-8");
|
|
242
|
+
} catch {
|
|
243
|
+
// File doesn't exist, will create
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
const regex = new RegExp(`^${name}=.*$`, "m");
|
|
247
|
+
const newLine = `${name}=${value}`;
|
|
248
|
+
|
|
249
|
+
if (regex.test(content)) {
|
|
250
|
+
content = content.replace(regex, newLine);
|
|
251
|
+
} else {
|
|
252
|
+
content = content.trimEnd() + "\n" + newLine + "\n";
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
fs.writeFileSync(envPath, content, "utf-8");
|
|
256
|
+
|
|
257
|
+
// Update process.env in-place (no restart needed)
|
|
258
|
+
process.env[name] = value;
|
|
259
|
+
|
|
260
|
+
return NextResponse.json({
|
|
261
|
+
success: true,
|
|
262
|
+
masked: maskValue(value),
|
|
263
|
+
health: "valid",
|
|
264
|
+
});
|
|
265
|
+
} catch (e) {
|
|
266
|
+
return NextResponse.json(
|
|
267
|
+
{ error: `Failed to write: ${(e as Error).message}` },
|
|
268
|
+
{ status: 500 }
|
|
269
|
+
);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { NextRequest, NextResponse } from "next/server";
|
|
2
|
+
import { createClient } from "@/lib/supabase-server";
|
|
3
|
+
import { requireRole } from "@/lib/auth-guard";
|
|
4
|
+
|
|
5
|
+
export async function GET(req: NextRequest) {
|
|
6
|
+
const auth = await requireRole(req, ["admin"], "view_users", "settings/users");
|
|
7
|
+
if (!auth.authorized) return auth.response;
|
|
8
|
+
|
|
9
|
+
const supabase = await createClient();
|
|
10
|
+
const { data, error } = await supabase
|
|
11
|
+
.from("user_roles")
|
|
12
|
+
.select("*")
|
|
13
|
+
.order("created_at", { ascending: true });
|
|
14
|
+
|
|
15
|
+
if (error) {
|
|
16
|
+
return NextResponse.json({ error: error.message }, { status: 500 });
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
return NextResponse.json(data);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export async function POST(req: NextRequest) {
|
|
23
|
+
const auth = await requireRole(req, ["admin"], "add_user", "settings/users");
|
|
24
|
+
if (!auth.authorized) return auth.response;
|
|
25
|
+
|
|
26
|
+
const supabase = await createClient();
|
|
27
|
+
const { email, role } = await req.json();
|
|
28
|
+
|
|
29
|
+
if (!email || !role) {
|
|
30
|
+
return NextResponse.json(
|
|
31
|
+
{ error: "email and role are required" },
|
|
32
|
+
{ status: 400 }
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (!["admin", "operator", "viewer"].includes(role)) {
|
|
37
|
+
return NextResponse.json(
|
|
38
|
+
{ error: "role must be admin, operator, or viewer" },
|
|
39
|
+
{ status: 400 }
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const { data, error } = await supabase
|
|
44
|
+
.from("user_roles")
|
|
45
|
+
.insert({ email: email.toLowerCase().trim(), role, added_by: auth.user!.email })
|
|
46
|
+
.select()
|
|
47
|
+
.single();
|
|
48
|
+
|
|
49
|
+
if (error) {
|
|
50
|
+
if (error.code === "23505") {
|
|
51
|
+
return NextResponse.json(
|
|
52
|
+
{ error: "This email is already added" },
|
|
53
|
+
{ status: 409 }
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
return NextResponse.json({ error: error.message }, { status: 500 });
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return NextResponse.json(data, { status: 201 });
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export async function PATCH(req: NextRequest) {
|
|
63
|
+
const auth = await requireRole(req, ["admin"], "update_user_role", "settings/users");
|
|
64
|
+
if (!auth.authorized) return auth.response;
|
|
65
|
+
|
|
66
|
+
const supabase = await createClient();
|
|
67
|
+
const { id, role } = await req.json();
|
|
68
|
+
|
|
69
|
+
if (!id || !role || !["admin", "operator", "viewer"].includes(role)) {
|
|
70
|
+
return NextResponse.json({ error: "Invalid request" }, { status: 400 });
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const { data, error } = await supabase
|
|
74
|
+
.from("user_roles")
|
|
75
|
+
.update({ role })
|
|
76
|
+
.eq("id", id)
|
|
77
|
+
.select()
|
|
78
|
+
.single();
|
|
79
|
+
|
|
80
|
+
if (error) {
|
|
81
|
+
return NextResponse.json({ error: error.message }, { status: 500 });
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return NextResponse.json(data);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export async function DELETE(req: NextRequest) {
|
|
88
|
+
const auth = await requireRole(req, ["admin"], "remove_user", "settings/users");
|
|
89
|
+
if (!auth.authorized) return auth.response;
|
|
90
|
+
|
|
91
|
+
const supabase = await createClient();
|
|
92
|
+
const { id, email } = await req.json();
|
|
93
|
+
|
|
94
|
+
if (email === auth.user!.email) {
|
|
95
|
+
return NextResponse.json(
|
|
96
|
+
{ error: "You cannot remove yourself" },
|
|
97
|
+
{ status: 400 }
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const { error } = await supabase.from("user_roles").delete().eq("id", id);
|
|
102
|
+
|
|
103
|
+
if (error) {
|
|
104
|
+
return NextResponse.json({ error: error.message }, { status: 500 });
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return NextResponse.json({ deleted: true });
|
|
108
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
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 SKILLS_DIR = path.join(PROJECT_ROOT, ".claude", "skills");
|
|
7
|
+
|
|
8
|
+
export async function GET(
|
|
9
|
+
_req: NextRequest,
|
|
10
|
+
{ params }: { params: Promise<{ slug: string }> }
|
|
11
|
+
) {
|
|
12
|
+
const { slug } = await params;
|
|
13
|
+
const skillFile = path.join(SKILLS_DIR, slug, "SKILL.md");
|
|
14
|
+
|
|
15
|
+
try {
|
|
16
|
+
const content = fs.readFileSync(skillFile, "utf-8");
|
|
17
|
+
return NextResponse.json({ slug, content });
|
|
18
|
+
} catch {
|
|
19
|
+
return NextResponse.json({ error: "Skill not found" }, { status: 404 });
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export async function PATCH(
|
|
24
|
+
req: NextRequest,
|
|
25
|
+
{ params }: { params: Promise<{ slug: string }> }
|
|
26
|
+
) {
|
|
27
|
+
const { slug } = await params;
|
|
28
|
+
const skillFile = path.join(SKILLS_DIR, slug, "SKILL.md");
|
|
29
|
+
|
|
30
|
+
// Verify skill directory exists
|
|
31
|
+
const skillDir = path.join(SKILLS_DIR, slug);
|
|
32
|
+
if (!fs.existsSync(skillDir)) {
|
|
33
|
+
return NextResponse.json({ error: "Skill not found" }, { status: 404 });
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const { content } = await req.json();
|
|
37
|
+
if (typeof content !== "string") {
|
|
38
|
+
return NextResponse.json({ error: "content must be a string" }, { status: 400 });
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
fs.writeFileSync(skillFile, content, "utf-8");
|
|
42
|
+
return NextResponse.json({ slug, saved: true });
|
|
43
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { NextResponse } from "next/server";
|
|
2
|
+
import { readSkills } from "@/lib/capabilities";
|
|
3
|
+
import fs from "fs";
|
|
4
|
+
import path from "path";
|
|
5
|
+
|
|
6
|
+
const PROJECT_ROOT = process.env.PROJECT_ROOT || path.resolve(__dirname, "../../../../..");
|
|
7
|
+
const MCP_PATH = path.join(PROJECT_ROOT, ".mcp.json");
|
|
8
|
+
|
|
9
|
+
interface ToolOption {
|
|
10
|
+
name: string;
|
|
11
|
+
category: "builtin" | "mcp" | "skill";
|
|
12
|
+
description?: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const BUILTIN_TOOLS: ToolOption[] = [
|
|
16
|
+
{ name: "Read", category: "builtin", description: "Read files from the filesystem" },
|
|
17
|
+
{ name: "Write", category: "builtin", description: "Write/create files" },
|
|
18
|
+
{ name: "Edit", category: "builtin", description: "Edit existing files" },
|
|
19
|
+
{ name: "Bash", category: "builtin", description: "Execute shell commands" },
|
|
20
|
+
{ name: "Glob", category: "builtin", description: "Find files by pattern" },
|
|
21
|
+
{ name: "Grep", category: "builtin", description: "Search file contents" },
|
|
22
|
+
{ name: "WebSearch", category: "builtin", description: "Search the web" },
|
|
23
|
+
{ name: "WebFetch", category: "builtin", description: "Fetch web page content" },
|
|
24
|
+
{ name: "Agent", category: "builtin", description: "Delegate to subagents" },
|
|
25
|
+
];
|
|
26
|
+
|
|
27
|
+
export async function GET() {
|
|
28
|
+
const tools: ToolOption[] = [...BUILTIN_TOOLS];
|
|
29
|
+
|
|
30
|
+
// MCP servers
|
|
31
|
+
try {
|
|
32
|
+
const raw = JSON.parse(fs.readFileSync(MCP_PATH, "utf-8"));
|
|
33
|
+
const servers = raw.mcpServers || {};
|
|
34
|
+
for (const name of Object.keys(servers)) {
|
|
35
|
+
tools.push({
|
|
36
|
+
name: `mcp:${name}`,
|
|
37
|
+
category: "mcp",
|
|
38
|
+
description: `MCP server: ${name}`,
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
} catch {
|
|
42
|
+
// no MCP config
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Anthropic-hosted integrations
|
|
46
|
+
for (const name of ["Gmail", "Linear", "Canva", "ClickUp"]) {
|
|
47
|
+
tools.push({
|
|
48
|
+
name: `hosted:${name}`,
|
|
49
|
+
category: "mcp",
|
|
50
|
+
description: `Anthropic-hosted integration: ${name}`,
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Skills
|
|
55
|
+
const skills = readSkills();
|
|
56
|
+
for (const skill of skills) {
|
|
57
|
+
tools.push({
|
|
58
|
+
name: `skill:${skill.slug}`,
|
|
59
|
+
category: "skill",
|
|
60
|
+
description: skill.description,
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return NextResponse.json(tools);
|
|
65
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { NextResponse } from "next/server";
|
|
2
|
+
import fs from "fs";
|
|
3
|
+
import path from "path";
|
|
4
|
+
|
|
5
|
+
const UPLOADS_ROOT = path.join(process.cwd(), "uploads");
|
|
6
|
+
const MAX_AGE_MS = 3 * 24 * 60 * 60 * 1000; // 3 days
|
|
7
|
+
|
|
8
|
+
export async function POST() {
|
|
9
|
+
if (!fs.existsSync(UPLOADS_ROOT)) {
|
|
10
|
+
return NextResponse.json({ deleted: 0 });
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const now = Date.now();
|
|
14
|
+
let deleted = 0;
|
|
15
|
+
|
|
16
|
+
const entries = fs.readdirSync(UPLOADS_ROOT, { withFileTypes: true });
|
|
17
|
+
for (const entry of entries) {
|
|
18
|
+
if (!entry.isDirectory()) continue;
|
|
19
|
+
const dirPath = path.join(UPLOADS_ROOT, entry.name);
|
|
20
|
+
const stat = fs.statSync(dirPath);
|
|
21
|
+
|
|
22
|
+
if (now - stat.mtimeMs > MAX_AGE_MS) {
|
|
23
|
+
fs.rmSync(dirPath, { recursive: true, force: true });
|
|
24
|
+
deleted++;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return NextResponse.json({ deleted });
|
|
29
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { NextRequest, NextResponse } from "next/server";
|
|
2
|
+
import fs from "fs";
|
|
3
|
+
import path from "path";
|
|
4
|
+
import { requireRole } from "@/lib/auth-guard";
|
|
5
|
+
|
|
6
|
+
const UPLOADS_ROOT = path.join(process.cwd(), "uploads");
|
|
7
|
+
|
|
8
|
+
const ALLOWED_TYPES = new Set([
|
|
9
|
+
"image/png",
|
|
10
|
+
"image/jpeg",
|
|
11
|
+
"image/gif",
|
|
12
|
+
"image/webp",
|
|
13
|
+
"text/markdown",
|
|
14
|
+
]);
|
|
15
|
+
|
|
16
|
+
const ALLOWED_EXTENSIONS = new Set([".png", ".jpg", ".jpeg", ".gif", ".webp", ".md"]);
|
|
17
|
+
|
|
18
|
+
function isAllowed(file: File): boolean {
|
|
19
|
+
if (ALLOWED_TYPES.has(file.type)) return true;
|
|
20
|
+
const ext = path.extname(file.name).toLowerCase();
|
|
21
|
+
return ALLOWED_EXTENSIONS.has(ext);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export async function POST(req: NextRequest) {
|
|
25
|
+
const auth = await requireRole(req, ["admin", "operator"], "upload_file", "uploads");
|
|
26
|
+
if (!auth.authorized) return auth.response;
|
|
27
|
+
|
|
28
|
+
const formData = await req.formData();
|
|
29
|
+
const runId = formData.get("run_id") as string | null;
|
|
30
|
+
|
|
31
|
+
if (!runId) {
|
|
32
|
+
return NextResponse.json({ error: "run_id is required" }, { status: 400 });
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Sanitize runId to prevent path traversal
|
|
36
|
+
const safeRunId = runId.replace(/[^a-zA-Z0-9_-]/g, "");
|
|
37
|
+
if (!safeRunId) {
|
|
38
|
+
return NextResponse.json({ error: "Invalid run_id" }, { status: 400 });
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const files = formData.getAll("files") as File[];
|
|
42
|
+
if (files.length === 0) {
|
|
43
|
+
return NextResponse.json({ error: "No files provided" }, { status: 400 });
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Validate file types
|
|
47
|
+
const rejected = files.filter((f) => !isAllowed(f));
|
|
48
|
+
if (rejected.length > 0) {
|
|
49
|
+
return NextResponse.json(
|
|
50
|
+
{ error: `Only images and markdown files are allowed. Rejected: ${rejected.map((f) => f.name).join(", ")}` },
|
|
51
|
+
{ status: 400 }
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const runDir = path.join(UPLOADS_ROOT, safeRunId);
|
|
56
|
+
fs.mkdirSync(runDir, { recursive: true });
|
|
57
|
+
|
|
58
|
+
const saved: { name: string; path: string; size: number; type: string }[] = [];
|
|
59
|
+
|
|
60
|
+
for (const file of files) {
|
|
61
|
+
// Sanitize filename — keep extension, strip dangerous chars
|
|
62
|
+
const safeName = file.name.replace(/[^a-zA-Z0-9._-]/g, "_");
|
|
63
|
+
const filePath = path.join(runDir, safeName);
|
|
64
|
+
|
|
65
|
+
const buffer = Buffer.from(await file.arrayBuffer());
|
|
66
|
+
fs.writeFileSync(filePath, buffer);
|
|
67
|
+
|
|
68
|
+
saved.push({
|
|
69
|
+
name: file.name,
|
|
70
|
+
path: filePath,
|
|
71
|
+
size: buffer.length,
|
|
72
|
+
type: file.type,
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return NextResponse.json({ files: saved }, { status: 201 });
|
|
77
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { NextResponse } from "next/server";
|
|
2
|
+
import { createClient } from "@/lib/supabase-server";
|
|
3
|
+
|
|
4
|
+
export async function GET(request: Request) {
|
|
5
|
+
const { searchParams, origin } = new URL(request.url);
|
|
6
|
+
const code = searchParams.get("code");
|
|
7
|
+
const next = searchParams.get("next") ?? "/";
|
|
8
|
+
|
|
9
|
+
if (code) {
|
|
10
|
+
const supabase = await createClient();
|
|
11
|
+
const { error } = await supabase.auth.exchangeCodeForSession(code);
|
|
12
|
+
if (!error) {
|
|
13
|
+
return NextResponse.redirect(`${origin}${next}`);
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// Auth error — redirect to login with error
|
|
18
|
+
return NextResponse.redirect(`${origin}/login?error=auth`);
|
|
19
|
+
}
|