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.
Files changed (220) hide show
  1. package/index.js +393 -0
  2. package/package.json +31 -0
  3. package/templates/CLAUDE.md +108 -0
  4. package/templates/app/.env.local.example +14 -0
  5. package/templates/app/ecosystem.config.js +29 -0
  6. package/templates/app/next-env.d.ts +6 -0
  7. package/templates/app/next.config.ts +16 -0
  8. package/templates/app/package-lock.json +4581 -0
  9. package/templates/app/package.json +38 -0
  10. package/templates/app/postcss.config.js +5 -0
  11. package/templates/app/src/app/agents/[slug]/chat/loading.tsx +26 -0
  12. package/templates/app/src/app/agents/[slug]/chat/page.tsx +579 -0
  13. package/templates/app/src/app/agents/[slug]/loading.tsx +19 -0
  14. package/templates/app/src/app/agents/page.tsx +8 -0
  15. package/templates/app/src/app/api/agents/[slug]/capabilities/route.ts +11 -0
  16. package/templates/app/src/app/api/agents/[slug]/route.ts +57 -0
  17. package/templates/app/src/app/api/agents/route.ts +28 -0
  18. package/templates/app/src/app/api/ai/generate-agent/route.ts +87 -0
  19. package/templates/app/src/app/api/ai/improve-claude-md/route.ts +78 -0
  20. package/templates/app/src/app/api/ai/suggestions/route.ts +64 -0
  21. package/templates/app/src/app/api/ai/title/route.ts +88 -0
  22. package/templates/app/src/app/api/auth/role/route.ts +17 -0
  23. package/templates/app/src/app/api/commands/[slug]/route.ts +61 -0
  24. package/templates/app/src/app/api/commands/route.ts +6 -0
  25. package/templates/app/src/app/api/governance/costs/route.ts +117 -0
  26. package/templates/app/src/app/api/governance/sessions/route.ts +335 -0
  27. package/templates/app/src/app/api/notifications/route.ts +62 -0
  28. package/templates/app/src/app/api/preferences/route.ts +44 -0
  29. package/templates/app/src/app/api/runs/[id]/approve/route.ts +38 -0
  30. package/templates/app/src/app/api/runs/[id]/events/route.ts +28 -0
  31. package/templates/app/src/app/api/runs/[id]/metadata/route.ts +30 -0
  32. package/templates/app/src/app/api/runs/[id]/route.ts +21 -0
  33. package/templates/app/src/app/api/runs/[id]/start/route.ts +61 -0
  34. package/templates/app/src/app/api/runs/[id]/stop/route.ts +16 -0
  35. package/templates/app/src/app/api/runs/[id]/stream/route.ts +201 -0
  36. package/templates/app/src/app/api/runs/route.ts +95 -0
  37. package/templates/app/src/app/api/schedules/[id]/route.ts +81 -0
  38. package/templates/app/src/app/api/schedules/route.ts +75 -0
  39. package/templates/app/src/app/api/settings/access-logs/route.ts +33 -0
  40. package/templates/app/src/app/api/settings/claude-md/route.ts +44 -0
  41. package/templates/app/src/app/api/settings/env-keys/route.ts +271 -0
  42. package/templates/app/src/app/api/settings/users/route.ts +108 -0
  43. package/templates/app/src/app/api/skills/[slug]/route.ts +43 -0
  44. package/templates/app/src/app/api/skills/route.ts +6 -0
  45. package/templates/app/src/app/api/tools/route.ts +65 -0
  46. package/templates/app/src/app/api/uploads/cleanup/route.ts +29 -0
  47. package/templates/app/src/app/api/uploads/route.ts +77 -0
  48. package/templates/app/src/app/auth/callback/route.ts +19 -0
  49. package/templates/app/src/app/globals.css +115 -0
  50. package/templates/app/src/app/layout.tsx +24 -0
  51. package/templates/app/src/app/loading.tsx +16 -0
  52. package/templates/app/src/app/login/page.tsx +64 -0
  53. package/templates/app/src/app/not-authorized/page.tsx +33 -0
  54. package/templates/app/src/app/runs/page.tsx +55 -0
  55. package/templates/app/src/app/schedules/page.tsx +110 -0
  56. package/templates/app/src/app/settings/page.tsx +1294 -0
  57. package/templates/app/src/app/skills/page.tsx +7 -0
  58. package/templates/app/src/components/agent-card.tsx +58 -0
  59. package/templates/app/src/components/agent-grid.tsx +90 -0
  60. package/templates/app/src/components/auth/auth-context.tsx +79 -0
  61. package/templates/app/src/components/chat-thread.tsx +50 -0
  62. package/templates/app/src/components/chat-view.tsx +670 -0
  63. package/templates/app/src/components/commands-browser.tsx +349 -0
  64. package/templates/app/src/components/create-agent-modal.tsx +388 -0
  65. package/templates/app/src/components/governance-dashboard.tsx +397 -0
  66. package/templates/app/src/components/icons.tsx +401 -0
  67. package/templates/app/src/components/layout/agent-sidebar.tsx +504 -0
  68. package/templates/app/src/components/layout/app-shell.tsx +29 -0
  69. package/templates/app/src/components/layout/nav.tsx +87 -0
  70. package/templates/app/src/components/layout/overview-inner.tsx +14 -0
  71. package/templates/app/src/components/layout/profile-menu.tsx +95 -0
  72. package/templates/app/src/components/layout/sidebar.tsx +30 -0
  73. package/templates/app/src/components/markdown.tsx +57 -0
  74. package/templates/app/src/components/message-bar.tsx +161 -0
  75. package/templates/app/src/components/notifications/notification-bell.tsx +104 -0
  76. package/templates/app/src/components/notifications/notification-panel.tsx +116 -0
  77. package/templates/app/src/components/overview/overview-content.tsx +287 -0
  78. package/templates/app/src/components/overview/overview-context.tsx +88 -0
  79. package/templates/app/src/components/preferences-modal.tsx +112 -0
  80. package/templates/app/src/components/run-form.tsx +73 -0
  81. package/templates/app/src/components/run-history-table.tsx +226 -0
  82. package/templates/app/src/components/run-output.tsx +187 -0
  83. package/templates/app/src/components/schedule-form.tsx +148 -0
  84. package/templates/app/src/components/skills-browser.tsx +338 -0
  85. package/templates/app/src/components/tool-tooltip.tsx +82 -0
  86. package/templates/app/src/hooks/use-sse.ts +115 -0
  87. package/templates/app/src/instrumentation.ts +9 -0
  88. package/templates/app/src/lib/agent-cache.ts +19 -0
  89. package/templates/app/src/lib/agent-runner.ts +411 -0
  90. package/templates/app/src/lib/agents.ts +168 -0
  91. package/templates/app/src/lib/ai.ts +40 -0
  92. package/templates/app/src/lib/approval-store.ts +70 -0
  93. package/templates/app/src/lib/auth-guard.ts +116 -0
  94. package/templates/app/src/lib/capabilities.ts +191 -0
  95. package/templates/app/src/lib/line-diff.ts +96 -0
  96. package/templates/app/src/lib/queue.ts +22 -0
  97. package/templates/app/src/lib/redis.ts +12 -0
  98. package/templates/app/src/lib/role-permissions.ts +166 -0
  99. package/templates/app/src/lib/run-agent.ts +442 -0
  100. package/templates/app/src/lib/supabase-browser.ts +8 -0
  101. package/templates/app/src/lib/supabase-middleware.ts +63 -0
  102. package/templates/app/src/lib/supabase-server.ts +28 -0
  103. package/templates/app/src/lib/supabase.ts +6 -0
  104. package/templates/app/src/lib/tool-descriptions.ts +29 -0
  105. package/templates/app/src/lib/types.ts +73 -0
  106. package/templates/app/src/lib/typewriter-animation.ts +159 -0
  107. package/templates/app/src/middleware.ts +13 -0
  108. package/templates/app/tsconfig.json +21 -0
  109. package/templates/app/uploads/.gitkeep +0 -0
  110. package/templates/app/worker/index.ts +342 -0
  111. package/templates/claude/agents/ai-trends-scout.md +66 -0
  112. package/templates/claude/commands/add-to-todos.md +56 -0
  113. package/templates/claude/commands/check-todos.md +56 -0
  114. package/templates/claude/hooks/auto-approve-safe.sh +34 -0
  115. package/templates/claude/hooks/auto-format.sh +25 -0
  116. package/templates/claude/hooks/block-destructive.sh +32 -0
  117. package/templates/claude/hooks/compaction-preserver.sh +16 -0
  118. package/templates/claude/hooks/notify.sh +26 -0
  119. package/templates/claude/settings.local.json +66 -0
  120. package/templates/claude/skills/frontend-design/SKILL.md +127 -0
  121. package/templates/claude/skills/frontend-design/reference/color-and-contrast.md +132 -0
  122. package/templates/claude/skills/frontend-design/reference/interaction-design.md +123 -0
  123. package/templates/claude/skills/frontend-design/reference/motion-design.md +99 -0
  124. package/templates/claude/skills/frontend-design/reference/responsive-design.md +114 -0
  125. package/templates/claude/skills/frontend-design/reference/spatial-design.md +100 -0
  126. package/templates/claude/skills/frontend-design/reference/typography.md +131 -0
  127. package/templates/claude/skills/frontend-design/reference/ux-writing.md +107 -0
  128. package/templates/claude/skills/gws-admin-reports/SKILL.md +57 -0
  129. package/templates/claude/skills/gws-calendar/SKILL.md +108 -0
  130. package/templates/claude/skills/gws-calendar-agenda/SKILL.md +52 -0
  131. package/templates/claude/skills/gws-calendar-insert/SKILL.md +55 -0
  132. package/templates/claude/skills/gws-chat/SKILL.md +73 -0
  133. package/templates/claude/skills/gws-chat-send/SKILL.md +49 -0
  134. package/templates/claude/skills/gws-classroom/SKILL.md +75 -0
  135. package/templates/claude/skills/gws-docs/SKILL.md +48 -0
  136. package/templates/claude/skills/gws-docs-write/SKILL.md +49 -0
  137. package/templates/claude/skills/gws-drive/SKILL.md +137 -0
  138. package/templates/claude/skills/gws-drive-upload/SKILL.md +52 -0
  139. package/templates/claude/skills/gws-events/SKILL.md +67 -0
  140. package/templates/claude/skills/gws-events-renew/SKILL.md +48 -0
  141. package/templates/claude/skills/gws-events-subscribe/SKILL.md +59 -0
  142. package/templates/claude/skills/gws-forms/SKILL.md +45 -0
  143. package/templates/claude/skills/gws-gmail/SKILL.md +59 -0
  144. package/templates/claude/skills/gws-gmail-forward/SKILL.md +53 -0
  145. package/templates/claude/skills/gws-gmail-reply/SKILL.md +56 -0
  146. package/templates/claude/skills/gws-gmail-reply-all/SKILL.md +60 -0
  147. package/templates/claude/skills/gws-gmail-send/SKILL.md +55 -0
  148. package/templates/claude/skills/gws-gmail-triage/SKILL.md +50 -0
  149. package/templates/claude/skills/gws-gmail-watch/SKILL.md +58 -0
  150. package/templates/claude/skills/gws-keep/SKILL.md +48 -0
  151. package/templates/claude/skills/gws-meet/SKILL.md +51 -0
  152. package/templates/claude/skills/gws-modelarmor/SKILL.md +42 -0
  153. package/templates/claude/skills/gws-modelarmor-create-template/SKILL.md +53 -0
  154. package/templates/claude/skills/gws-modelarmor-sanitize-prompt/SKILL.md +48 -0
  155. package/templates/claude/skills/gws-modelarmor-sanitize-response/SKILL.md +48 -0
  156. package/templates/claude/skills/gws-people/SKILL.md +67 -0
  157. package/templates/claude/skills/gws-shared/SKILL.md +66 -0
  158. package/templates/claude/skills/gws-sheets/SKILL.md +53 -0
  159. package/templates/claude/skills/gws-sheets-append/SKILL.md +51 -0
  160. package/templates/claude/skills/gws-sheets-read/SKILL.md +47 -0
  161. package/templates/claude/skills/gws-slides/SKILL.md +43 -0
  162. package/templates/claude/skills/gws-tasks/SKILL.md +56 -0
  163. package/templates/claude/skills/gws-workflow/SKILL.md +44 -0
  164. package/templates/claude/skills/gws-workflow-email-to-task/SKILL.md +47 -0
  165. package/templates/claude/skills/gws-workflow-file-announce/SKILL.md +50 -0
  166. package/templates/claude/skills/gws-workflow-meeting-prep/SKILL.md +47 -0
  167. package/templates/claude/skills/gws-workflow-standup-report/SKILL.md +46 -0
  168. package/templates/claude/skills/gws-workflow-weekly-digest/SKILL.md +46 -0
  169. package/templates/claude/skills/persona-content-creator/SKILL.md +33 -0
  170. package/templates/claude/skills/persona-customer-support/SKILL.md +34 -0
  171. package/templates/claude/skills/persona-event-coordinator/SKILL.md +35 -0
  172. package/templates/claude/skills/persona-exec-assistant/SKILL.md +35 -0
  173. package/templates/claude/skills/persona-hr-coordinator/SKILL.md +33 -0
  174. package/templates/claude/skills/persona-it-admin/SKILL.md +30 -0
  175. package/templates/claude/skills/persona-project-manager/SKILL.md +35 -0
  176. package/templates/claude/skills/persona-researcher/SKILL.md +33 -0
  177. package/templates/claude/skills/persona-sales-ops/SKILL.md +35 -0
  178. package/templates/claude/skills/persona-team-lead/SKILL.md +36 -0
  179. package/templates/claude/skills/recipe-backup-sheet-as-csv/SKILL.md +25 -0
  180. package/templates/claude/skills/recipe-batch-invite-to-event/SKILL.md +25 -0
  181. package/templates/claude/skills/recipe-block-focus-time/SKILL.md +24 -0
  182. package/templates/claude/skills/recipe-bulk-download-folder/SKILL.md +25 -0
  183. package/templates/claude/skills/recipe-collect-form-responses/SKILL.md +25 -0
  184. package/templates/claude/skills/recipe-compare-sheet-tabs/SKILL.md +25 -0
  185. package/templates/claude/skills/recipe-copy-sheet-for-new-month/SKILL.md +25 -0
  186. package/templates/claude/skills/recipe-create-classroom-course/SKILL.md +25 -0
  187. package/templates/claude/skills/recipe-create-doc-from-template/SKILL.md +29 -0
  188. package/templates/claude/skills/recipe-create-events-from-sheet/SKILL.md +24 -0
  189. package/templates/claude/skills/recipe-create-expense-tracker/SKILL.md +26 -0
  190. package/templates/claude/skills/recipe-create-feedback-form/SKILL.md +25 -0
  191. package/templates/claude/skills/recipe-create-gmail-filter/SKILL.md +26 -0
  192. package/templates/claude/skills/recipe-create-meet-space/SKILL.md +25 -0
  193. package/templates/claude/skills/recipe-create-presentation/SKILL.md +25 -0
  194. package/templates/claude/skills/recipe-create-shared-drive/SKILL.md +25 -0
  195. package/templates/claude/skills/recipe-create-task-list/SKILL.md +26 -0
  196. package/templates/claude/skills/recipe-create-vacation-responder/SKILL.md +25 -0
  197. package/templates/claude/skills/recipe-draft-email-from-doc/SKILL.md +25 -0
  198. package/templates/claude/skills/recipe-email-drive-link/SKILL.md +25 -0
  199. package/templates/claude/skills/recipe-find-free-time/SKILL.md +25 -0
  200. package/templates/claude/skills/recipe-find-large-files/SKILL.md +24 -0
  201. package/templates/claude/skills/recipe-forward-labeled-emails/SKILL.md +27 -0
  202. package/templates/claude/skills/recipe-generate-report-from-sheet/SKILL.md +34 -0
  203. package/templates/claude/skills/recipe-label-and-archive-emails/SKILL.md +25 -0
  204. package/templates/claude/skills/recipe-log-deal-update/SKILL.md +25 -0
  205. package/templates/claude/skills/recipe-organize-drive-folder/SKILL.md +26 -0
  206. package/templates/claude/skills/recipe-plan-weekly-schedule/SKILL.md +26 -0
  207. package/templates/claude/skills/recipe-post-mortem-setup/SKILL.md +25 -0
  208. package/templates/claude/skills/recipe-reschedule-meeting/SKILL.md +25 -0
  209. package/templates/claude/skills/recipe-review-meet-participants/SKILL.md +25 -0
  210. package/templates/claude/skills/recipe-review-overdue-tasks/SKILL.md +25 -0
  211. package/templates/claude/skills/recipe-save-email-attachments/SKILL.md +26 -0
  212. package/templates/claude/skills/recipe-save-email-to-doc/SKILL.md +29 -0
  213. package/templates/claude/skills/recipe-schedule-recurring-event/SKILL.md +24 -0
  214. package/templates/claude/skills/recipe-send-team-announcement/SKILL.md +24 -0
  215. package/templates/claude/skills/recipe-share-doc-and-notify/SKILL.md +25 -0
  216. package/templates/claude/skills/recipe-share-event-materials/SKILL.md +25 -0
  217. package/templates/claude/skills/recipe-share-folder-with-team/SKILL.md +26 -0
  218. package/templates/claude/skills/recipe-sync-contacts-to-sheet/SKILL.md +25 -0
  219. package/templates/claude/skills/recipe-watch-drive-changes/SKILL.md +25 -0
  220. 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,6 @@
1
+ import { NextResponse } from "next/server";
2
+ import { readSkills } from "@/lib/capabilities";
3
+
4
+ export async function GET() {
5
+ return NextResponse.json(readSkills());
6
+ }
@@ -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
+ }