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,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
|
+
}
|