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,201 @@
|
|
|
1
|
+
import { NextRequest } from "next/server";
|
|
2
|
+
import { supabase } from "@/lib/supabase";
|
|
3
|
+
import { getRunContext } from "@/lib/agent-runner";
|
|
4
|
+
|
|
5
|
+
export const maxDuration = 300; // 5 min max for long-running SSE observation
|
|
6
|
+
|
|
7
|
+
export async function GET(
|
|
8
|
+
req: NextRequest,
|
|
9
|
+
{ params }: { params: Promise<{ id: string }> }
|
|
10
|
+
) {
|
|
11
|
+
const { id: runId } = await params;
|
|
12
|
+
|
|
13
|
+
// Fetch the run record
|
|
14
|
+
const { data: run, error } = await supabase
|
|
15
|
+
.from("agent_runs")
|
|
16
|
+
.select("*")
|
|
17
|
+
.eq("id", runId)
|
|
18
|
+
.single();
|
|
19
|
+
|
|
20
|
+
if (error || !run) {
|
|
21
|
+
return new Response(`data: ${JSON.stringify({ type: "error", error: "Run not found" })}\n\n`, {
|
|
22
|
+
status: 404,
|
|
23
|
+
headers: { "Content-Type": "text/event-stream" },
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// If already completed/failed/stopped and no RunContext, return final state
|
|
28
|
+
const ctx = getRunContext(runId);
|
|
29
|
+
if (!ctx && (run.status === "completed" || run.status === "failed" || run.status === "stopped")) {
|
|
30
|
+
// Map DB status to the SSE event types the client expects
|
|
31
|
+
const eventType = run.status === "completed" ? "done"
|
|
32
|
+
: run.status === "stopped" ? "stopped"
|
|
33
|
+
: "error";
|
|
34
|
+
const payload = eventType === "error"
|
|
35
|
+
? { type: "error", error: run.error || "Run failed" }
|
|
36
|
+
: { type: eventType, output: run.output, cost_usd: run.cost_usd, duration_ms: run.duration_ms };
|
|
37
|
+
const encoder = new TextEncoder();
|
|
38
|
+
const body = encoder.encode(`data: ${JSON.stringify(payload)}\n\n`);
|
|
39
|
+
return new Response(body, {
|
|
40
|
+
headers: {
|
|
41
|
+
"Content-Type": "text/event-stream",
|
|
42
|
+
"Cache-Control": "no-cache",
|
|
43
|
+
Connection: "keep-alive",
|
|
44
|
+
},
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const stream = new ReadableStream({
|
|
49
|
+
async start(controller) {
|
|
50
|
+
const encoder = new TextEncoder();
|
|
51
|
+
let closed = false;
|
|
52
|
+
|
|
53
|
+
const send = (data: Record<string, unknown>) => {
|
|
54
|
+
if (closed) return;
|
|
55
|
+
try {
|
|
56
|
+
controller.enqueue(encoder.encode(`data: ${JSON.stringify(data)}\n\n`));
|
|
57
|
+
} catch {
|
|
58
|
+
// stream closed
|
|
59
|
+
closed = true;
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
// --- Mode 1: Live attach (RunContext exists) ---
|
|
64
|
+
if (ctx) {
|
|
65
|
+
ctx.subscriberCount++;
|
|
66
|
+
|
|
67
|
+
// Load historical events written before this connection (for mid-run reconnection)
|
|
68
|
+
const afterSeq = parseInt(req.nextUrl.searchParams.get("after_seq") || "0", 10);
|
|
69
|
+
if (afterSeq > 0) {
|
|
70
|
+
const { data: events } = await supabase
|
|
71
|
+
.from("run_events")
|
|
72
|
+
.select("*")
|
|
73
|
+
.eq("run_id", runId)
|
|
74
|
+
.gt("seq", afterSeq)
|
|
75
|
+
.order("seq", { ascending: true });
|
|
76
|
+
|
|
77
|
+
if (events) {
|
|
78
|
+
for (const evt of events) {
|
|
79
|
+
send({ type: evt.event_type, seq: evt.seq, ...evt.payload });
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Subscribe to live emitter
|
|
85
|
+
const onEvent = (data: Record<string, unknown>) => {
|
|
86
|
+
send(data);
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
ctx.emitter.on("event", onEvent);
|
|
90
|
+
|
|
91
|
+
// Wait for client disconnect or run completion
|
|
92
|
+
const cleanup = () => {
|
|
93
|
+
ctx.emitter.off("event", onEvent);
|
|
94
|
+
ctx.subscriberCount = Math.max(0, ctx.subscriberCount - 1);
|
|
95
|
+
if (!closed) {
|
|
96
|
+
closed = true;
|
|
97
|
+
try { controller.close(); } catch { /* already closed */ }
|
|
98
|
+
}
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
// Watch for client disconnect
|
|
102
|
+
req.signal.addEventListener("abort", cleanup);
|
|
103
|
+
|
|
104
|
+
// Also close when run finishes (after a small delay to flush final events)
|
|
105
|
+
const checkDone = () => {
|
|
106
|
+
if (ctx.status === "done") {
|
|
107
|
+
setTimeout(() => cleanup(), 500);
|
|
108
|
+
} else {
|
|
109
|
+
setTimeout(checkDone, 1000);
|
|
110
|
+
}
|
|
111
|
+
};
|
|
112
|
+
checkDone();
|
|
113
|
+
|
|
114
|
+
return; // keep stream open
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// --- Mode 2: Historical replay (RunContext not found) ---
|
|
118
|
+
// Load all events from DB
|
|
119
|
+
const { data: events } = await supabase
|
|
120
|
+
.from("run_events")
|
|
121
|
+
.select("*")
|
|
122
|
+
.eq("run_id", runId)
|
|
123
|
+
.order("seq", { ascending: true });
|
|
124
|
+
|
|
125
|
+
if (events && events.length > 0) {
|
|
126
|
+
for (const evt of events) {
|
|
127
|
+
send({ type: evt.event_type, seq: evt.seq, ...evt.payload });
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// If run is still "running" but no RunContext (server restarted), poll for new events
|
|
132
|
+
if (run.status === "running" || run.status === "queued") {
|
|
133
|
+
let lastSeq = events?.length ? events[events.length - 1].seq : 0;
|
|
134
|
+
const pollInterval = setInterval(async () => {
|
|
135
|
+
if (closed || req.signal.aborted) {
|
|
136
|
+
clearInterval(pollInterval);
|
|
137
|
+
if (!closed) {
|
|
138
|
+
closed = true;
|
|
139
|
+
try { controller.close(); } catch { /* ok */ }
|
|
140
|
+
}
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Check for new events
|
|
145
|
+
const { data: newEvents } = await supabase
|
|
146
|
+
.from("run_events")
|
|
147
|
+
.select("*")
|
|
148
|
+
.eq("run_id", runId)
|
|
149
|
+
.gt("seq", lastSeq)
|
|
150
|
+
.order("seq", { ascending: true });
|
|
151
|
+
|
|
152
|
+
if (newEvents && newEvents.length > 0) {
|
|
153
|
+
for (const evt of newEvents) {
|
|
154
|
+
send({ type: evt.event_type, seq: evt.seq, ...evt.payload });
|
|
155
|
+
}
|
|
156
|
+
lastSeq = newEvents[newEvents.length - 1].seq;
|
|
157
|
+
|
|
158
|
+
// Check if run is done
|
|
159
|
+
const doneEvt = newEvents.find((e) => e.event_type === "done" || e.event_type === "stopped" || e.event_type === "error");
|
|
160
|
+
if (doneEvt) {
|
|
161
|
+
clearInterval(pollInterval);
|
|
162
|
+
closed = true;
|
|
163
|
+
try { controller.close(); } catch { /* ok */ }
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Also check if the run status changed (stale cleanup may have updated it)
|
|
168
|
+
const { data: freshRun } = await supabase
|
|
169
|
+
.from("agent_runs")
|
|
170
|
+
.select("status")
|
|
171
|
+
.eq("id", runId)
|
|
172
|
+
.single();
|
|
173
|
+
|
|
174
|
+
if (freshRun && (freshRun.status === "completed" || freshRun.status === "failed" || freshRun.status === "stopped")) {
|
|
175
|
+
clearInterval(pollInterval);
|
|
176
|
+
// Send final event if no done event was sent
|
|
177
|
+
if (!newEvents?.some((e) => e.event_type === "done" || e.event_type === "stopped")) {
|
|
178
|
+
const eventType = freshRun.status === "completed" ? "done" : freshRun.status === "stopped" ? "stopped" : "error";
|
|
179
|
+
send({ type: eventType });
|
|
180
|
+
}
|
|
181
|
+
closed = true;
|
|
182
|
+
try { controller.close(); } catch { /* ok */ }
|
|
183
|
+
}
|
|
184
|
+
}, 2000);
|
|
185
|
+
|
|
186
|
+
return; // keep stream open for polling
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Run is terminal and no events — just close
|
|
190
|
+
controller.close();
|
|
191
|
+
},
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
return new Response(stream, {
|
|
195
|
+
headers: {
|
|
196
|
+
"Content-Type": "text/event-stream",
|
|
197
|
+
"Cache-Control": "no-cache",
|
|
198
|
+
Connection: "keep-alive",
|
|
199
|
+
},
|
|
200
|
+
});
|
|
201
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { NextRequest, NextResponse } from "next/server";
|
|
2
|
+
import { supabase } from "@/lib/supabase";
|
|
3
|
+
import { requireRole } from "@/lib/auth-guard";
|
|
4
|
+
|
|
5
|
+
export async function GET(req: NextRequest) {
|
|
6
|
+
const { searchParams } = req.nextUrl;
|
|
7
|
+
const agentSlug = searchParams.get("agent_slug");
|
|
8
|
+
const limit = parseInt(searchParams.get("limit") || "50", 10);
|
|
9
|
+
const offset = parseInt(searchParams.get("offset") || "0", 10);
|
|
10
|
+
|
|
11
|
+
let query = supabase
|
|
12
|
+
.from("agent_runs")
|
|
13
|
+
.select("*, run_events(count)")
|
|
14
|
+
.order("created_at", { ascending: false })
|
|
15
|
+
.range(offset, offset + limit - 1);
|
|
16
|
+
|
|
17
|
+
if (agentSlug) {
|
|
18
|
+
query = query.eq("agent_slug", agentSlug);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const { data, error } = await query;
|
|
22
|
+
|
|
23
|
+
if (error) {
|
|
24
|
+
return NextResponse.json({ error: error.message }, { status: 500 });
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Flatten the nested run_events count into a top-level event_count field
|
|
28
|
+
const normalized = (data || []).map((run: Record<string, unknown>) => {
|
|
29
|
+
const events = run.run_events as { count: number }[] | undefined;
|
|
30
|
+
const { run_events: _, ...rest } = run;
|
|
31
|
+
return { ...rest, event_count: events?.[0]?.count ?? 0 };
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
return NextResponse.json(normalized);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export async function DELETE(req: NextRequest) {
|
|
38
|
+
const { searchParams } = req.nextUrl;
|
|
39
|
+
const sessionId = searchParams.get("session_id");
|
|
40
|
+
|
|
41
|
+
if (!sessionId) {
|
|
42
|
+
return NextResponse.json(
|
|
43
|
+
{ error: "session_id is required" },
|
|
44
|
+
{ status: 400 }
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const { error } = await supabase
|
|
49
|
+
.from("agent_runs")
|
|
50
|
+
.delete()
|
|
51
|
+
.eq("session_id", sessionId);
|
|
52
|
+
|
|
53
|
+
if (error) {
|
|
54
|
+
return NextResponse.json({ error: error.message }, { status: 500 });
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return NextResponse.json({ deleted: true });
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export async function POST(req: NextRequest) {
|
|
61
|
+
const auth = await requireRole(req, ["admin", "operator"], "create_run", "agent_runs");
|
|
62
|
+
if (!auth.authorized) return auth.response;
|
|
63
|
+
|
|
64
|
+
const body = await req.json();
|
|
65
|
+
const { agent_slug, agent_name, prompt, session_id } = body;
|
|
66
|
+
|
|
67
|
+
if (!agent_slug || !agent_name || !prompt) {
|
|
68
|
+
return NextResponse.json(
|
|
69
|
+
{ error: "agent_slug, agent_name, and prompt are required" },
|
|
70
|
+
{ status: 400 }
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const { data, error } = await supabase
|
|
75
|
+
.from("agent_runs")
|
|
76
|
+
.insert({
|
|
77
|
+
agent_slug,
|
|
78
|
+
agent_name,
|
|
79
|
+
prompt,
|
|
80
|
+
status: "queued",
|
|
81
|
+
...(session_id ? { session_id } : {}),
|
|
82
|
+
metadata: {
|
|
83
|
+
created_by: auth.user!.email,
|
|
84
|
+
created_by_role: auth.user!.role,
|
|
85
|
+
},
|
|
86
|
+
})
|
|
87
|
+
.select()
|
|
88
|
+
.single();
|
|
89
|
+
|
|
90
|
+
if (error) {
|
|
91
|
+
return NextResponse.json({ error: error.message }, { status: 500 });
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return NextResponse.json(data, { status: 201 });
|
|
95
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { NextRequest, NextResponse } from "next/server";
|
|
2
|
+
import { supabase } from "@/lib/supabase";
|
|
3
|
+
import { getQueue } from "@/lib/queue";
|
|
4
|
+
|
|
5
|
+
export async function PATCH(
|
|
6
|
+
req: NextRequest,
|
|
7
|
+
{ params }: { params: Promise<{ id: string }> }
|
|
8
|
+
) {
|
|
9
|
+
const { id } = await params;
|
|
10
|
+
const body = await req.json();
|
|
11
|
+
|
|
12
|
+
const updates: Record<string, unknown> = { updated_at: new Date().toISOString() };
|
|
13
|
+
if (body.prompt !== undefined) updates.prompt = body.prompt;
|
|
14
|
+
if (body.cron !== undefined) updates.cron = body.cron;
|
|
15
|
+
if (body.enabled !== undefined) updates.enabled = body.enabled;
|
|
16
|
+
if (body.skill_slug !== undefined) updates.skill_slug = body.skill_slug || null;
|
|
17
|
+
|
|
18
|
+
const { data, error } = await supabase
|
|
19
|
+
.from("agent_schedules")
|
|
20
|
+
.update(updates)
|
|
21
|
+
.eq("id", id)
|
|
22
|
+
.select()
|
|
23
|
+
.single();
|
|
24
|
+
|
|
25
|
+
if (error) {
|
|
26
|
+
return NextResponse.json({ error: error.message }, { status: 500 });
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Sync with BullMQ
|
|
30
|
+
try {
|
|
31
|
+
const queue = getQueue();
|
|
32
|
+
if (data.enabled) {
|
|
33
|
+
await queue.upsertJobScheduler(
|
|
34
|
+
`schedule-${id}`,
|
|
35
|
+
{ pattern: data.cron },
|
|
36
|
+
{
|
|
37
|
+
name: "scheduled-agent-run",
|
|
38
|
+
data: {
|
|
39
|
+
scheduleId: data.id,
|
|
40
|
+
agentSlug: data.agent_slug,
|
|
41
|
+
agentName: data.agent_name,
|
|
42
|
+
prompt: data.prompt,
|
|
43
|
+
skillSlug: data.skill_slug || undefined,
|
|
44
|
+
},
|
|
45
|
+
}
|
|
46
|
+
);
|
|
47
|
+
} else {
|
|
48
|
+
await queue.removeJobScheduler(`schedule-${id}`);
|
|
49
|
+
}
|
|
50
|
+
} catch (err) {
|
|
51
|
+
console.error("Failed to sync BullMQ scheduler:", err);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return NextResponse.json(data);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export async function DELETE(
|
|
58
|
+
_req: NextRequest,
|
|
59
|
+
{ params }: { params: Promise<{ id: string }> }
|
|
60
|
+
) {
|
|
61
|
+
const { id } = await params;
|
|
62
|
+
|
|
63
|
+
// Remove from BullMQ first
|
|
64
|
+
try {
|
|
65
|
+
const queue = getQueue();
|
|
66
|
+
await queue.removeJobScheduler(`schedule-${id}`);
|
|
67
|
+
} catch (err) {
|
|
68
|
+
console.error("Failed to remove BullMQ scheduler:", err);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const { error } = await supabase
|
|
72
|
+
.from("agent_schedules")
|
|
73
|
+
.delete()
|
|
74
|
+
.eq("id", id);
|
|
75
|
+
|
|
76
|
+
if (error) {
|
|
77
|
+
return NextResponse.json({ error: error.message }, { status: 500 });
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return NextResponse.json({ ok: true });
|
|
81
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { NextRequest, NextResponse } from "next/server";
|
|
2
|
+
import { supabase } from "@/lib/supabase";
|
|
3
|
+
import { getQueue } from "@/lib/queue";
|
|
4
|
+
|
|
5
|
+
export async function GET() {
|
|
6
|
+
const { data, error } = await supabase
|
|
7
|
+
.from("agent_schedules")
|
|
8
|
+
.select("*")
|
|
9
|
+
.order("created_at", { ascending: false });
|
|
10
|
+
|
|
11
|
+
if (error) {
|
|
12
|
+
return NextResponse.json({ error: error.message }, { status: 500 });
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
return NextResponse.json(data);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export async function POST(req: NextRequest) {
|
|
19
|
+
const body = await req.json();
|
|
20
|
+
const { agent_slug, agent_name, prompt, cron, skill_slug } = body;
|
|
21
|
+
|
|
22
|
+
if (!agent_slug || !agent_name || !cron) {
|
|
23
|
+
return NextResponse.json(
|
|
24
|
+
{ error: "agent_slug, agent_name, and cron are required" },
|
|
25
|
+
{ status: 400 }
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (!prompt && !skill_slug) {
|
|
30
|
+
return NextResponse.json(
|
|
31
|
+
{ error: "Either prompt or skill_slug is required" },
|
|
32
|
+
{ status: 400 }
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const { data, error } = await supabase
|
|
37
|
+
.from("agent_schedules")
|
|
38
|
+
.insert({
|
|
39
|
+
agent_slug,
|
|
40
|
+
agent_name,
|
|
41
|
+
prompt: prompt || "",
|
|
42
|
+
cron,
|
|
43
|
+
skill_slug: skill_slug || null,
|
|
44
|
+
enabled: true,
|
|
45
|
+
})
|
|
46
|
+
.select()
|
|
47
|
+
.single();
|
|
48
|
+
|
|
49
|
+
if (error) {
|
|
50
|
+
return NextResponse.json({ error: error.message }, { status: 500 });
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Register with BullMQ
|
|
54
|
+
try {
|
|
55
|
+
const queue = getQueue();
|
|
56
|
+
await queue.upsertJobScheduler(
|
|
57
|
+
`schedule-${data.id}`,
|
|
58
|
+
{ pattern: cron },
|
|
59
|
+
{
|
|
60
|
+
name: "scheduled-agent-run",
|
|
61
|
+
data: {
|
|
62
|
+
scheduleId: data.id,
|
|
63
|
+
agentSlug: data.agent_slug,
|
|
64
|
+
agentName: data.agent_name,
|
|
65
|
+
prompt: data.prompt,
|
|
66
|
+
skillSlug: data.skill_slug || undefined,
|
|
67
|
+
},
|
|
68
|
+
}
|
|
69
|
+
);
|
|
70
|
+
} catch (err) {
|
|
71
|
+
console.error("Failed to register BullMQ scheduler:", err);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return NextResponse.json(data, { status: 201 });
|
|
75
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
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_access_logs", "settings/access-logs");
|
|
7
|
+
if (!auth.authorized) return auth.response;
|
|
8
|
+
|
|
9
|
+
const { searchParams } = req.nextUrl;
|
|
10
|
+
const limit = parseInt(searchParams.get("limit") || "100", 10);
|
|
11
|
+
const filter = searchParams.get("filter"); // "blocked" | "allowed" | null
|
|
12
|
+
|
|
13
|
+
const supabase = await createClient();
|
|
14
|
+
let query = supabase
|
|
15
|
+
.from("access_logs")
|
|
16
|
+
.select("*")
|
|
17
|
+
.order("created_at", { ascending: false })
|
|
18
|
+
.limit(limit);
|
|
19
|
+
|
|
20
|
+
if (filter === "blocked") {
|
|
21
|
+
query = query.eq("allowed", false);
|
|
22
|
+
} else if (filter === "allowed") {
|
|
23
|
+
query = query.eq("allowed", true);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const { data, error } = await query;
|
|
27
|
+
|
|
28
|
+
if (error) {
|
|
29
|
+
return NextResponse.json({ error: error.message }, { status: 500 });
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return NextResponse.json(data);
|
|
33
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { NextRequest, NextResponse } from "next/server";
|
|
2
|
+
import { requireRole } from "@/lib/auth-guard";
|
|
3
|
+
import { readFile, writeFile } from "fs/promises";
|
|
4
|
+
import { join } from "path";
|
|
5
|
+
|
|
6
|
+
const PROJECT_ROOT = process.env.PROJECT_ROOT || "/mnt/c/Users/Admin/Documents/PersonalAIssistant";
|
|
7
|
+
const CLAUDE_MD_PATH = join(PROJECT_ROOT, "CLAUDE.md");
|
|
8
|
+
|
|
9
|
+
export async function GET(req: NextRequest) {
|
|
10
|
+
const auth = await requireRole(req, ["admin"], "view_claude_md", "settings/claude-md");
|
|
11
|
+
if (!auth.authorized) return auth.response;
|
|
12
|
+
|
|
13
|
+
try {
|
|
14
|
+
const content = await readFile(CLAUDE_MD_PATH, "utf-8");
|
|
15
|
+
return NextResponse.json({ content, path: CLAUDE_MD_PATH });
|
|
16
|
+
} catch (err: unknown) {
|
|
17
|
+
if (err && typeof err === "object" && "code" in err && err.code === "ENOENT") {
|
|
18
|
+
return NextResponse.json({ content: "", path: CLAUDE_MD_PATH });
|
|
19
|
+
}
|
|
20
|
+
return NextResponse.json(
|
|
21
|
+
{ error: "Failed to read CLAUDE.md" },
|
|
22
|
+
{ status: 500 }
|
|
23
|
+
);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export async function PUT(req: NextRequest) {
|
|
28
|
+
const auth = await requireRole(req, ["admin"], "edit_claude_md", "settings/claude-md");
|
|
29
|
+
if (!auth.authorized) return auth.response;
|
|
30
|
+
|
|
31
|
+
try {
|
|
32
|
+
const { content } = await req.json();
|
|
33
|
+
if (typeof content !== "string") {
|
|
34
|
+
return NextResponse.json({ error: "content must be a string" }, { status: 400 });
|
|
35
|
+
}
|
|
36
|
+
await writeFile(CLAUDE_MD_PATH, content, "utf-8");
|
|
37
|
+
return NextResponse.json({ success: true });
|
|
38
|
+
} catch {
|
|
39
|
+
return NextResponse.json(
|
|
40
|
+
{ error: "Failed to write CLAUDE.md" },
|
|
41
|
+
{ status: 500 }
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
}
|