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,335 @@
1
+ import { NextRequest, NextResponse } from "next/server";
2
+ import fs from "fs";
3
+ import path from "path";
4
+ import readline from "readline";
5
+
6
+ // ---------------------------------------------------------------------------
7
+ // Risk classification
8
+ // ---------------------------------------------------------------------------
9
+
10
+ type RiskLevel = "critical" | "warning" | "info";
11
+
12
+ interface RiskRule {
13
+ level: RiskLevel;
14
+ label: string;
15
+ test: (toolName: string, input: Record<string, unknown>) => boolean;
16
+ }
17
+
18
+ const RISK_RULES: RiskRule[] = [
19
+ // Critical — destructive or dangerous
20
+ {
21
+ level: "critical",
22
+ label: "Destructive delete",
23
+ test: (tool, input) =>
24
+ tool === "Bash" && /\brm\s+-rf\b/.test(String(input.command || "")),
25
+ },
26
+ {
27
+ level: "critical",
28
+ label: "Force push",
29
+ test: (tool, input) =>
30
+ tool === "Bash" &&
31
+ /\bgit\s+push\s+.*--force\b/.test(String(input.command || "")),
32
+ },
33
+ {
34
+ level: "critical",
35
+ label: "Hard reset",
36
+ test: (tool, input) =>
37
+ tool === "Bash" &&
38
+ /\bgit\s+reset\s+--hard\b/.test(String(input.command || "")),
39
+ },
40
+ {
41
+ level: "critical",
42
+ label: "Drop table",
43
+ test: (tool, input) =>
44
+ tool === "Bash" &&
45
+ /\bDROP\s+TABLE\b/i.test(String(input.command || "")),
46
+ },
47
+ {
48
+ level: "critical",
49
+ label: "Env/credentials write",
50
+ test: (tool, input) => {
51
+ if (tool !== "Write" && tool !== "Edit") return false;
52
+ const fp = String(input.file_path || input.filePath || "");
53
+ return /\.(env|pem|key|credentials|secret)/.test(fp);
54
+ },
55
+ },
56
+ {
57
+ level: "critical",
58
+ label: "Sudo command",
59
+ test: (tool, input) =>
60
+ tool === "Bash" && /\bsudo\b/.test(String(input.command || "")),
61
+ },
62
+ // Warning — potentially risky
63
+ {
64
+ level: "warning",
65
+ label: "Package install",
66
+ test: (tool, input) =>
67
+ tool === "Bash" &&
68
+ /\b(npm\s+(install|ci|uninstall)|pip3?\s+install)\b/.test(
69
+ String(input.command || "")
70
+ ),
71
+ },
72
+ {
73
+ level: "warning",
74
+ label: "Git push",
75
+ test: (tool, input) =>
76
+ tool === "Bash" && /\bgit\s+push\b/.test(String(input.command || "")),
77
+ },
78
+ {
79
+ level: "warning",
80
+ label: "File write",
81
+ test: (tool) => tool === "Write" || tool === "Edit",
82
+ },
83
+ {
84
+ level: "warning",
85
+ label: "Config file modified",
86
+ test: (tool, input) => {
87
+ if (tool !== "Write" && tool !== "Edit") return false;
88
+ const fp = String(input.file_path || input.filePath || "");
89
+ return /\b(package\.json|tsconfig|\.mcp\.json|next\.config|CLAUDE\.md)\b/.test(fp);
90
+ },
91
+ },
92
+ {
93
+ level: "warning",
94
+ label: "External network",
95
+ test: (tool) => tool === "WebFetch" || tool === "WebSearch",
96
+ },
97
+ ];
98
+
99
+ function classifyRisk(
100
+ toolName: string,
101
+ input: Record<string, unknown>
102
+ ): { level: RiskLevel; label: string } {
103
+ for (const rule of RISK_RULES) {
104
+ if (rule.test(toolName, input)) {
105
+ return { level: rule.level, label: rule.label };
106
+ }
107
+ }
108
+ return { level: "info", label: "Normal" };
109
+ }
110
+
111
+ // ---------------------------------------------------------------------------
112
+ // JSONL session parser
113
+ // ---------------------------------------------------------------------------
114
+
115
+ interface ToolCall {
116
+ toolName: string;
117
+ input: Record<string, unknown>;
118
+ inputSummary: string;
119
+ risk: { level: RiskLevel; label: string };
120
+ timestamp: string;
121
+ }
122
+
123
+ interface SessionSummary {
124
+ sessionId: string;
125
+ project: string;
126
+ startedAt: string;
127
+ lastActivity: string;
128
+ version: string;
129
+ gitBranch: string;
130
+ messageCount: number;
131
+ toolCalls: ToolCall[];
132
+ riskCounts: { critical: number; warning: number; info: number };
133
+ highestRisk: RiskLevel;
134
+ }
135
+
136
+ const CLAUDE_DIR = path.join(
137
+ process.env.HOME || "/home/collin",
138
+ ".claude",
139
+ "projects"
140
+ );
141
+
142
+ function summarizeInput(toolName: string, input: Record<string, unknown>): string {
143
+ if (toolName === "Bash") return String(input.command || "").slice(0, 200);
144
+ if (toolName === "Read") return String(input.file_path || "").slice(0, 200);
145
+ if (toolName === "Write" || toolName === "Edit")
146
+ return String(input.file_path || input.filePath || "").slice(0, 200);
147
+ if (toolName === "Glob") return String(input.pattern || "").slice(0, 200);
148
+ if (toolName === "Grep") return String(input.pattern || "").slice(0, 200);
149
+ if (toolName === "Agent")
150
+ return String(input.description || "").slice(0, 200);
151
+ if (toolName === "WebFetch" || toolName === "WebSearch")
152
+ return String(input.url || input.query || "").slice(0, 200);
153
+ // MCP tools
154
+ if (toolName.startsWith("mcp__"))
155
+ return JSON.stringify(input).slice(0, 200);
156
+ return JSON.stringify(input).slice(0, 150);
157
+ }
158
+
159
+ async function parseSession(
160
+ filePath: string,
161
+ projectName: string
162
+ ): Promise<SessionSummary | null> {
163
+ const sessionId = path.basename(filePath, ".jsonl");
164
+ const toolCalls: ToolCall[] = [];
165
+ let startedAt = "";
166
+ let lastActivity = "";
167
+ let version = "";
168
+ let gitBranch = "";
169
+ let messageCount = 0;
170
+
171
+ const stream = fs.createReadStream(filePath, { encoding: "utf-8" });
172
+ const rl = readline.createInterface({ input: stream, crlfDelay: Infinity });
173
+
174
+ for await (const line of rl) {
175
+ if (!line.trim()) continue;
176
+ let entry: Record<string, unknown>;
177
+ try {
178
+ entry = JSON.parse(line);
179
+ } catch {
180
+ continue;
181
+ }
182
+
183
+ const ts = String(entry.timestamp || "");
184
+ if (ts) {
185
+ if (!startedAt || ts < startedAt) startedAt = ts;
186
+ if (!lastActivity || ts > lastActivity) lastActivity = ts;
187
+ }
188
+
189
+ if (entry.version) version = String(entry.version);
190
+ if (entry.gitBranch) gitBranch = String(entry.gitBranch);
191
+
192
+ // Count user/assistant messages
193
+ if (entry.type === "user" || entry.type === "assistant") {
194
+ messageCount++;
195
+ }
196
+
197
+ // Extract tool_use blocks from assistant messages
198
+ if (entry.type === "assistant") {
199
+ const msg = entry.message as { content?: unknown[] } | undefined;
200
+ if (msg?.content && Array.isArray(msg.content)) {
201
+ for (const block of msg.content) {
202
+ const b = block as Record<string, unknown>;
203
+ if (b.type === "tool_use" && b.name) {
204
+ const toolName = String(b.name);
205
+ const input = (b.input as Record<string, unknown>) || {};
206
+ const risk = classifyRisk(toolName, input);
207
+ toolCalls.push({
208
+ toolName,
209
+ input,
210
+ inputSummary: summarizeInput(toolName, input),
211
+ risk,
212
+ timestamp: ts,
213
+ });
214
+ }
215
+ }
216
+ }
217
+ }
218
+ }
219
+
220
+ if (messageCount === 0) return null;
221
+
222
+ const riskCounts = { critical: 0, warning: 0, info: 0 };
223
+ for (const tc of toolCalls) {
224
+ riskCounts[tc.risk.level]++;
225
+ }
226
+ const highestRisk: RiskLevel = riskCounts.critical > 0
227
+ ? "critical"
228
+ : riskCounts.warning > 0
229
+ ? "warning"
230
+ : "info";
231
+
232
+ return {
233
+ sessionId,
234
+ project: projectName,
235
+ startedAt,
236
+ lastActivity,
237
+ version,
238
+ gitBranch,
239
+ messageCount,
240
+ toolCalls,
241
+ riskCounts,
242
+ highestRisk,
243
+ };
244
+ }
245
+
246
+ // ---------------------------------------------------------------------------
247
+ // GET /api/governance/sessions
248
+ // ---------------------------------------------------------------------------
249
+
250
+ export async function GET(req: NextRequest) {
251
+ const { searchParams } = req.nextUrl;
252
+ const projectFilter = searchParams.get("project"); // optional filter
253
+ const limit = Math.min(parseInt(searchParams.get("limit") || "50", 10), 200);
254
+ const riskFilter = searchParams.get("risk") as RiskLevel | null; // optional
255
+
256
+ try {
257
+ if (!fs.existsSync(CLAUDE_DIR)) {
258
+ return NextResponse.json({ sessions: [], projects: [] });
259
+ }
260
+
261
+ const projectDirs = fs.readdirSync(CLAUDE_DIR).filter((d) => {
262
+ const full = path.join(CLAUDE_DIR, d);
263
+ return fs.statSync(full).isDirectory();
264
+ });
265
+
266
+ const projects = projectDirs.map((d) =>
267
+ d.replace(/-/g, "/").replace(/^\//, "")
268
+ );
269
+
270
+ // Collect all JSONL files across projects (or filtered project)
271
+ const filesToParse: { file: string; project: string }[] = [];
272
+
273
+ for (const dir of projectDirs) {
274
+ const projectName = dir.replace(/-/g, "/").replace(/^\//, "");
275
+ if (projectFilter && projectName !== projectFilter && dir !== projectFilter) {
276
+ continue;
277
+ }
278
+ const fullDir = path.join(CLAUDE_DIR, dir);
279
+ const jsonlFiles = fs.readdirSync(fullDir).filter((f) => f.endsWith(".jsonl"));
280
+
281
+ // Sort by mtime descending to get most recent first
282
+ const withStats = jsonlFiles.map((f) => {
283
+ const fp = path.join(fullDir, f);
284
+ return { file: fp, mtime: fs.statSync(fp).mtimeMs, project: projectName };
285
+ });
286
+ withStats.sort((a, b) => b.mtime - a.mtime);
287
+
288
+ for (const w of withStats) {
289
+ filesToParse.push({ file: w.file, project: w.project });
290
+ }
291
+ }
292
+
293
+ // Sort all files by mtime descending
294
+ filesToParse.sort((a, b) => {
295
+ const sa = fs.statSync(a.file).mtimeMs;
296
+ const sb = fs.statSync(b.file).mtimeMs;
297
+ return sb - sa;
298
+ });
299
+
300
+ // Parse sessions (limit to avoid huge responses)
301
+ const sessions: SessionSummary[] = [];
302
+ const parseLimit = limit * 2; // parse extra to allow risk filtering
303
+ for (const { file, project } of filesToParse.slice(0, parseLimit)) {
304
+ const session = await parseSession(file, project);
305
+ if (!session) continue;
306
+ if (riskFilter && session.highestRisk !== riskFilter) continue;
307
+ sessions.push(session);
308
+ if (sessions.length >= limit) break;
309
+ }
310
+
311
+ // Strip full tool inputs from response to keep payload small
312
+ // (keep inputSummary for display)
313
+ const lightweight = sessions.map((s) => ({
314
+ ...s,
315
+ toolCalls: s.toolCalls.map(({ input, ...rest }) => rest),
316
+ }));
317
+
318
+ // Aggregate stats
319
+ const stats = {
320
+ totalSessions: sessions.length,
321
+ totalToolCalls: sessions.reduce((n, s) => n + s.toolCalls.length, 0),
322
+ criticalCount: sessions.reduce((n, s) => n + s.riskCounts.critical, 0),
323
+ warningCount: sessions.reduce((n, s) => n + s.riskCounts.warning, 0),
324
+ infoCount: sessions.reduce((n, s) => n + s.riskCounts.info, 0),
325
+ };
326
+
327
+ return NextResponse.json({ sessions: lightweight, projects, stats });
328
+ } catch (err) {
329
+ console.error("Governance API error:", err);
330
+ return NextResponse.json(
331
+ { error: "Failed to parse session data" },
332
+ { status: 500 }
333
+ );
334
+ }
335
+ }
@@ -0,0 +1,62 @@
1
+ import { NextRequest, NextResponse } from "next/server";
2
+ import { supabase } from "@/lib/supabase";
3
+
4
+ export async function GET(req: NextRequest) {
5
+ const unreadOnly = req.nextUrl.searchParams.get("unread") === "true";
6
+
7
+ let query = supabase
8
+ .from("notifications")
9
+ .select("*")
10
+ .order("created_at", { ascending: false })
11
+ .limit(50);
12
+
13
+ if (unreadOnly) {
14
+ query = query.eq("read", false);
15
+ }
16
+
17
+ const { data, error } = await query;
18
+
19
+ if (error) {
20
+ return NextResponse.json({ error: error.message }, { status: 500 });
21
+ }
22
+
23
+ return NextResponse.json(data);
24
+ }
25
+
26
+ export async function PATCH(req: NextRequest) {
27
+ const body = await req.json();
28
+
29
+ if (body.all) {
30
+ const { error } = await supabase
31
+ .from("notifications")
32
+ .update({ read: true })
33
+ .eq("read", false);
34
+
35
+ if (error) {
36
+ return NextResponse.json({ error: error.message }, { status: 500 });
37
+ }
38
+ } else if (body.ids && Array.isArray(body.ids)) {
39
+ const { error } = await supabase
40
+ .from("notifications")
41
+ .update({ read: true })
42
+ .in("id", body.ids);
43
+
44
+ if (error) {
45
+ return NextResponse.json({ error: error.message }, { status: 500 });
46
+ }
47
+ } else if (body.session_id) {
48
+ const { error } = await supabase
49
+ .from("notifications")
50
+ .update({ read: true })
51
+ .eq("session_id", body.session_id)
52
+ .eq("read", false);
53
+
54
+ if (error) {
55
+ return NextResponse.json({ error: error.message }, { status: 500 });
56
+ }
57
+ } else {
58
+ return NextResponse.json({ error: "Provide { ids: [...] }, { session_id: '...' }, or { all: true }" }, { status: 400 });
59
+ }
60
+
61
+ return NextResponse.json({ ok: true });
62
+ }
@@ -0,0 +1,44 @@
1
+ import { NextRequest, 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({ error: "Not authenticated" }, { status: 401 });
10
+ }
11
+
12
+ const { data } = await supabase
13
+ .from("user_preferences")
14
+ .select("preferences")
15
+ .eq("user_email", user.email)
16
+ .single();
17
+
18
+ return NextResponse.json({ preferences: data?.preferences || "" });
19
+ }
20
+
21
+ export async function PUT(req: NextRequest) {
22
+ const supabase = await createClient();
23
+ const { data: { user } } = await supabase.auth.getUser();
24
+
25
+ if (!user?.email) {
26
+ return NextResponse.json({ error: "Not authenticated" }, { status: 401 });
27
+ }
28
+
29
+ const body = await req.json();
30
+ const preferences = typeof body.preferences === "string" ? body.preferences : "";
31
+
32
+ const { error } = await supabase
33
+ .from("user_preferences")
34
+ .upsert(
35
+ { user_email: user.email, preferences, updated_at: new Date().toISOString() },
36
+ { onConflict: "user_email" }
37
+ );
38
+
39
+ if (error) {
40
+ return NextResponse.json({ error: error.message }, { status: 500 });
41
+ }
42
+
43
+ return NextResponse.json({ saved: true });
44
+ }
@@ -0,0 +1,38 @@
1
+ import { NextRequest, NextResponse } from "next/server";
2
+ import { submitApproval } from "@/lib/approval-store";
3
+
4
+ // Retry with short delay — in dev mode, the approve POST can arrive
5
+ // before or after the approval promise is registered due to route compilation timing.
6
+ async function trySubmit(runId: string, toolUseId: string, approved: boolean, retries = 5): Promise<boolean> {
7
+ for (let i = 0; i < retries; i++) {
8
+ if (submitApproval(runId, toolUseId, approved)) return true;
9
+ if (i < retries - 1) await new Promise((r) => setTimeout(r, 500));
10
+ }
11
+ return false;
12
+ }
13
+
14
+ export async function POST(
15
+ req: NextRequest,
16
+ { params }: { params: Promise<{ id: string }> }
17
+ ) {
18
+ const { id: runId } = await params;
19
+ const body = await req.json();
20
+ const { tool_use_id, approved } = body;
21
+
22
+ if (!tool_use_id || typeof approved !== "boolean") {
23
+ return NextResponse.json(
24
+ { error: "tool_use_id and approved (boolean) are required" },
25
+ { status: 400 }
26
+ );
27
+ }
28
+
29
+ const found = await trySubmit(runId, tool_use_id, approved);
30
+ if (!found) {
31
+ return NextResponse.json(
32
+ { error: "No pending approval found — it may have timed out" },
33
+ { status: 404 }
34
+ );
35
+ }
36
+
37
+ return NextResponse.json({ ok: true });
38
+ }
@@ -0,0 +1,28 @@
1
+ import { NextRequest, NextResponse } from "next/server";
2
+ import { supabase } from "@/lib/supabase";
3
+
4
+ export async function GET(
5
+ req: NextRequest,
6
+ { params }: { params: Promise<{ id: string }> }
7
+ ) {
8
+ const { id: runId } = await params;
9
+ const afterSeq = parseInt(req.nextUrl.searchParams.get("after_seq") || "0", 10);
10
+
11
+ let query = supabase
12
+ .from("run_events")
13
+ .select("*")
14
+ .eq("run_id", runId)
15
+ .order("seq", { ascending: true });
16
+
17
+ if (afterSeq > 0) {
18
+ query = query.gt("seq", afterSeq);
19
+ }
20
+
21
+ const { data, error } = await query;
22
+
23
+ if (error) {
24
+ return NextResponse.json({ error: error.message }, { status: 500 });
25
+ }
26
+
27
+ return NextResponse.json(data || []);
28
+ }
@@ -0,0 +1,30 @@
1
+ import { NextRequest, NextResponse } from "next/server";
2
+ import { supabase } from "@/lib/supabase";
3
+
4
+ export async function PATCH(
5
+ req: NextRequest,
6
+ { params }: { params: Promise<{ id: string }> }
7
+ ) {
8
+ const { id } = await params;
9
+ const body = await req.json();
10
+
11
+ // Merge new metadata with existing
12
+ const { data: existing } = await supabase
13
+ .from("agent_runs")
14
+ .select("metadata")
15
+ .eq("id", id)
16
+ .single();
17
+
18
+ const merged = { ...(existing?.metadata || {}), ...body };
19
+
20
+ const { error } = await supabase
21
+ .from("agent_runs")
22
+ .update({ metadata: merged })
23
+ .eq("id", id);
24
+
25
+ if (error) {
26
+ return NextResponse.json({ error: error.message }, { status: 500 });
27
+ }
28
+
29
+ return NextResponse.json({ ok: true });
30
+ }
@@ -0,0 +1,21 @@
1
+ import { NextRequest, NextResponse } from "next/server";
2
+ import { supabase } from "@/lib/supabase";
3
+
4
+ export async function GET(
5
+ _req: NextRequest,
6
+ { params }: { params: Promise<{ id: string }> }
7
+ ) {
8
+ const { id } = await params;
9
+
10
+ const { data, error } = await supabase
11
+ .from("agent_runs")
12
+ .select("*")
13
+ .eq("id", id)
14
+ .single();
15
+
16
+ if (error) {
17
+ return NextResponse.json({ error: error.message }, { status: 404 });
18
+ }
19
+
20
+ return NextResponse.json(data);
21
+ }
@@ -0,0 +1,61 @@
1
+ import { NextRequest, NextResponse } from "next/server";
2
+ import { supabase } from "@/lib/supabase";
3
+ import { getAgent } from "@/lib/agents";
4
+ import { executeDetached } from "@/lib/agent-runner";
5
+ import { requireRole } from "@/lib/auth-guard";
6
+
7
+ export async function POST(
8
+ req: NextRequest,
9
+ { params }: { params: Promise<{ id: string }> }
10
+ ) {
11
+ const auth = await requireRole(req, ["admin", "operator"], "start_run", "agent_runs");
12
+ if (!auth.authorized) return auth.response!;
13
+
14
+ const { id: runId } = await params;
15
+
16
+ const { data: run, error } = await supabase
17
+ .from("agent_runs")
18
+ .select("*")
19
+ .eq("id", runId)
20
+ .single();
21
+
22
+ if (error || !run) {
23
+ return NextResponse.json({ error: "Run not found" }, { status: 404 });
24
+ }
25
+
26
+ if (run.status !== "queued") {
27
+ return NextResponse.json({ error: `Run is already ${run.status}` }, { status: 409 });
28
+ }
29
+
30
+ const agent = getAgent(run.agent_slug);
31
+ if (!agent) {
32
+ return NextResponse.json({ error: `Agent ${run.agent_slug} not found` }, { status: 404 });
33
+ }
34
+
35
+ // Parse optional files from request body
36
+ const body = await req.json().catch(() => ({}));
37
+ const files = body.files as string[] | undefined;
38
+
39
+ // For follow-up messages in a session, find the SDK session ID from a previous run
40
+ let resumeSessionId: string | undefined;
41
+ if (run.session_id) {
42
+ const { data: prevRuns } = await supabase
43
+ .from("agent_runs")
44
+ .select("metadata")
45
+ .eq("session_id", run.session_id)
46
+ .neq("id", runId)
47
+ .order("created_at", { ascending: false })
48
+ .limit(1);
49
+
50
+ if (prevRuns?.[0]) {
51
+ const meta = prevRuns[0].metadata as Record<string, unknown> | null;
52
+ if (meta?.sdk_session_id) {
53
+ resumeSessionId = meta.sdk_session_id as string;
54
+ }
55
+ }
56
+ }
57
+
58
+ executeDetached(runId, agent, run.prompt, resumeSessionId, files, auth.user!.role, auth.user!.email);
59
+
60
+ return NextResponse.json({ started: true });
61
+ }
@@ -0,0 +1,16 @@
1
+ import { NextRequest, NextResponse } from "next/server";
2
+ import { stopRun } from "@/lib/agent-runner";
3
+
4
+ export async function POST(
5
+ _req: NextRequest,
6
+ { params }: { params: Promise<{ id: string }> }
7
+ ) {
8
+ const { id: runId } = await params;
9
+ const stopped = await stopRun(runId);
10
+
11
+ if (!stopped) {
12
+ return NextResponse.json({ error: "Run not found or already stopped" }, { status: 404 });
13
+ }
14
+
15
+ return NextResponse.json({ stopped: true });
16
+ }