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,442 @@
1
+ import fs from "fs";
2
+ import path from "path";
3
+ import matter from "gray-matter";
4
+ import { query } from "@anthropic-ai/claude-agent-sdk";
5
+ import { getAgentContent, getAgents } from "./agents";
6
+ import { isReadOnlyMcpTool } from "./approval-store";
7
+ import { buildCanUseTool, getAllowedToolsForRole } from "./role-permissions";
8
+ import { supabase } from "./supabase";
9
+ import type { Role } from "./auth-guard";
10
+ import type { AgentMeta } from "./types";
11
+
12
+ const PROJECT_ROOT = process.env.PROJECT_ROOT || "/mnt/c/Users/Admin/Documents/PersonalAIssistant";
13
+
14
+ // Read MCP servers from .mcp.json so any additions are picked up automatically
15
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
16
+ function getMcpServers(): Record<string, any> {
17
+ try {
18
+ const mcpPath = path.join(PROJECT_ROOT, ".mcp.json");
19
+ const raw = JSON.parse(fs.readFileSync(mcpPath, "utf-8"));
20
+ return raw.mcpServers || {};
21
+ } catch {
22
+ return {};
23
+ }
24
+ }
25
+
26
+ // Read CLAUDE.md once for project context
27
+ function getProjectInstructions(): string {
28
+ try {
29
+ const claudeMd = path.join(PROJECT_ROOT, "CLAUDE.md");
30
+ return fs.readFileSync(claudeMd, "utf-8").trim();
31
+ } catch {
32
+ return "";
33
+ }
34
+ }
35
+
36
+ // Build subagent definitions from all agent .md files (for the main assistant)
37
+ // AgentMcpServerSpec = string | Record<string, McpServerConfig> — SDK expects an array of these
38
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
39
+ function getSubagentDefinitions(excludeSlug?: string): Record<string, { description: string; prompt: string; tools: string[]; mcpServers?: Array<Record<string, any>> }> {
40
+ const agents = getAgents();
41
+ const allMcpServers = getMcpServers();
42
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
43
+ const defs: Record<string, { description: string; prompt: string; tools: string[]; mcpServers?: Array<Record<string, any>> }> = {};
44
+
45
+ for (const agent of agents) {
46
+ if (agent.slug === excludeSlug || agent.slug === "main") continue;
47
+ try {
48
+ const raw = fs.readFileSync(agent.filePath, "utf-8");
49
+ const { content } = matter(raw);
50
+
51
+ // Filter MCP servers to only those the agent has been granted access to
52
+ // Each element is { serverName: serverConfig } matching AgentMcpServerSpec format
53
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
54
+ let agentMcpServers: Array<Record<string, any>> | undefined;
55
+ if (agent.mcpServers && agent.mcpServers.length > 0) {
56
+ const matched = agent.mcpServers
57
+ .filter((name) => allMcpServers[name])
58
+ .map((name) => ({ [name]: allMcpServers[name] }));
59
+ if (matched.length > 0) {
60
+ agentMcpServers = matched;
61
+ }
62
+ }
63
+
64
+ defs[agent.slug] = {
65
+ description: agent.description,
66
+ prompt: content.trim(),
67
+ tools: agent.tools.length > 0 ? agent.tools : ["Read", "Write", "Edit", "Bash", "Glob", "Grep"],
68
+ ...(agentMcpServers ? { mcpServers: agentMcpServers } : {}),
69
+ };
70
+ } catch {
71
+ // skip agents we can't read
72
+ }
73
+ }
74
+
75
+ return defs;
76
+ }
77
+
78
+ export interface AgentRunResult {
79
+ output: string;
80
+ costUsd: number;
81
+ durationMs: number;
82
+ sessionId: string;
83
+ aborted?: boolean;
84
+ }
85
+
86
+ export interface StreamCallbacks {
87
+ onToken?: (text: string) => void;
88
+ onToolCall?: (name: string, status: "start" | "end", detail?: { input?: unknown; result?: string }) => void;
89
+ onThinking?: (active: boolean) => void;
90
+ onProgress?: (info: string) => void;
91
+ onResult?: (result: AgentRunResult) => void;
92
+ onError?: (error: string) => void;
93
+ onDebug?: (msg: string) => void;
94
+ onSessionInit?: (sessionId: string) => void;
95
+ onApprovalRequired?: (toolName: string, toolInput: Record<string, unknown>, toolUseId: string) => Promise<boolean>;
96
+ signal?: AbortSignal;
97
+ }
98
+
99
+ export async function executeAgent(
100
+ agent: AgentMeta,
101
+ prompt: string,
102
+ callbacks?: StreamCallbacks,
103
+ resumeSessionId?: string,
104
+ userRole?: Role,
105
+ userEmail?: string
106
+ ): Promise<AgentRunResult> {
107
+ const startTime = Date.now();
108
+ let output = "";
109
+ let sessionId = "";
110
+ let costUsd = 0;
111
+ let aborted = false;
112
+ // Track emitted tool starts to avoid duplicates when both stream_event
113
+ // and assistant message fire for the same tool call
114
+ let lastEmittedToolStart = "";
115
+ // Map tool_use_id → { name, input } so we can attach input/result to end events
116
+ const pendingTools = new Map<string, { name: string; input: unknown }>();
117
+ // Cap tool result text sent through SSE to avoid huge payloads
118
+ const TOOL_RESULT_MAX_CHARS = 8000;
119
+
120
+ const debug = (msg: string) => {
121
+ console.log(`[run-agent] ${msg}`);
122
+ callbacks?.onDebug?.(msg);
123
+ };
124
+
125
+ debug(`Starting agent: ${agent.slug} (model: ${agent.model || "default"})`);
126
+ debug(`CWD: ${PROJECT_ROOT}`);
127
+ debug(`Prompt: ${prompt.slice(0, 100)}...`);
128
+
129
+ // Build system prompt: CLAUDE.md is always the base.
130
+ // For the "main" agent, CLAUDE.md IS the full system prompt and other agents become subagents.
131
+ // For specialist agents, append their markdown body after CLAUDE.md.
132
+ const isMainAgent = agent.slug === "main";
133
+ const projectInstructions = getProjectInstructions();
134
+ const agentBody = isMainAgent ? "" : getAgentContent(agent.slug);
135
+
136
+ // Fetch user preferences if we have an email
137
+ let userPreferences = "";
138
+ if (userEmail) {
139
+ try {
140
+ const { data } = await supabase
141
+ .from("user_preferences")
142
+ .select("preferences")
143
+ .eq("user_email", userEmail)
144
+ .single();
145
+ if (data?.preferences?.trim()) {
146
+ userPreferences = data.preferences.trim();
147
+ }
148
+ } catch {
149
+ // No preferences found — that's fine
150
+ }
151
+ }
152
+
153
+ const systemPrompt = [
154
+ projectInstructions,
155
+ agentBody,
156
+ userPreferences ? `## User Preferences\n\n${userPreferences}` : "",
157
+ ].filter(Boolean).join("\n\n---\n\n");
158
+
159
+ // Load subagent definitions for the main agent
160
+ const subagents = isMainAgent ? getSubagentDefinitions() : undefined;
161
+
162
+ debug(`System prompt length: ${systemPrompt.length} chars`);
163
+
164
+ const stderrLines: string[] = [];
165
+
166
+ // Permission handler: auto-approve safe tools, require approval for write MCP tools
167
+ const hasApprovalHandler = !!callbacks?.onApprovalRequired;
168
+ const effectiveRole: Role = userRole || "admin";
169
+
170
+ // Admin gets full bypass — no permission prompts, no canUseTool restrictions.
171
+ // Operators get canUseTool with MCP write approval + path/command blocking.
172
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
173
+ const baseMcpCanUseTool = (hasApprovalHandler && effectiveRole !== "admin")
174
+ ? async (
175
+ toolName: string,
176
+ input: Record<string, unknown>,
177
+ options: { signal: AbortSignal; toolUseID: string }
178
+ ) => {
179
+ // Auto-approve non-MCP tools (built-in: Bash, Read, Write, Edit, Glob, Grep, etc.)
180
+ if (!toolName.startsWith("mcp__")) {
181
+ return { behavior: "allow" as const };
182
+ }
183
+
184
+ // Auto-approve read-only MCP tools
185
+ if (isReadOnlyMcpTool(toolName)) {
186
+ debug(`Auto-approving read-only MCP tool: ${toolName}`);
187
+ return { behavior: "allow" as const };
188
+ }
189
+
190
+ // Write MCP tool — ask user
191
+ debug(`Requesting approval for write MCP tool: ${toolName}`);
192
+ try {
193
+ const approved = await callbacks!.onApprovalRequired!(toolName, input, options.toolUseID);
194
+ if (approved) {
195
+ debug(`User approved: ${toolName}`);
196
+ return { behavior: "allow" as const };
197
+ } else {
198
+ debug(`User denied: ${toolName}`);
199
+ return { behavior: "deny" as const, message: "User denied this action" };
200
+ }
201
+ } catch {
202
+ debug(`Approval error/cancelled for: ${toolName}`);
203
+ return { behavior: "deny" as const, message: "Approval cancelled" };
204
+ }
205
+ }
206
+ : undefined;
207
+
208
+ // Wrap with role-based restrictions (operators get path/command blocking)
209
+ const canUseTool = buildCanUseTool(effectiveRole, baseMcpCanUseTool);
210
+
211
+ // Determine permission mode:
212
+ // - admin → always bypassPermissions (full trust, no prompts)
213
+ // - operator → "default" + canUseTool (MCP approval + path/command blocking)
214
+ // - viewer → restricted allowedTools, no canUseTool
215
+ const useBypass = effectiveRole === "admin";
216
+ const allowedTools = getAllowedToolsForRole(effectiveRole, agent.tools);
217
+
218
+ debug(`Role: ${effectiveRole}, bypass: ${useBypass}, tools: ${allowedTools.length}`);
219
+
220
+ const q = query({
221
+ prompt,
222
+ options: {
223
+ ...(resumeSessionId ? { resume: resumeSessionId } : {}),
224
+ cwd: PROJECT_ROOT,
225
+ env: {
226
+ ...process.env,
227
+ ANTHROPIC_API_KEY: process.env.ANTHROPIC_API_KEY || "",
228
+ },
229
+ ...(useBypass
230
+ ? { permissionMode: "bypassPermissions" as const, allowDangerouslySkipPermissions: true }
231
+ : { permissionMode: "default" as const, ...(canUseTool ? { canUseTool } : {}) }),
232
+ systemPrompt,
233
+ maxBudgetUsd: isMainAgent ? 5.0 : 2.0,
234
+ model: agent.model || undefined,
235
+ mcpServers: getMcpServers(),
236
+ ...(subagents ? { agents: subagents } : {}),
237
+ allowedTools,
238
+ includePartialMessages: true,
239
+ debug: true,
240
+ stderr: (data: string) => {
241
+ stderrLines.push(data);
242
+ debug(`[stderr] ${data.trim()}`);
243
+ },
244
+ },
245
+ });
246
+
247
+ try {
248
+ for await (const message of q) {
249
+ if (callbacks?.signal?.aborted) {
250
+ debug("Aborted by signal");
251
+ aborted = true;
252
+ q.close();
253
+ break;
254
+ }
255
+
256
+ debug(`Message type: ${message.type}${("subtype" in message) ? ` subtype: ${(message as { subtype?: string }).subtype}` : ""}`);
257
+
258
+ if (message.type === "system" && "subtype" in message && message.subtype === "init") {
259
+ sessionId = message.session_id;
260
+ debug(`Session initialized: ${sessionId}`);
261
+ callbacks?.onSessionInit?.(sessionId);
262
+ }
263
+
264
+ // Stream partial text tokens and real-time tool/thinking detection
265
+ if (message.type === "stream_event" && "event" in message) {
266
+ const event = message.event as {
267
+ type: string;
268
+ index?: number;
269
+ delta?: { type: string; text?: string };
270
+ content_block?: { type: string; name?: string };
271
+ };
272
+
273
+ // Real-time text streaming
274
+ if (event.type === "content_block_delta" && event.delta?.type === "text_delta" && event.delta.text) {
275
+ callbacks?.onToken?.(event.delta.text);
276
+ }
277
+
278
+ // Real-time tool call start (fires as soon as the model starts a tool_use block)
279
+ if (event.type === "content_block_start" && event.content_block?.type === "tool_use") {
280
+ const toolName = event.content_block.name || "unknown";
281
+ lastEmittedToolStart = toolName;
282
+ debug(` [stream] Tool use started: ${toolName}`);
283
+ callbacks?.onToolCall?.(toolName, "start");
284
+ }
285
+
286
+ // Real-time thinking start
287
+ if (event.type === "content_block_start" && event.content_block?.type === "thinking") {
288
+ debug(` [stream] Thinking started`);
289
+ callbacks?.onThinking?.(true);
290
+ }
291
+
292
+ // Content block finished — end thinking/tool state
293
+ if (event.type === "content_block_stop") {
294
+ callbacks?.onThinking?.(false);
295
+ }
296
+ }
297
+
298
+ // Collect full assistant messages and emit tool/thinking events
299
+ if (message.type === "assistant" && "message" in message) {
300
+ const msg = message.message as { content?: Array<{ type: string; text?: string; name?: string; input?: unknown }> ; stop_reason?: string; error?: unknown };
301
+ debug(`Assistant message: stop_reason=${msg.stop_reason}, content_blocks=${msg.content?.length || 0}, error=${JSON.stringify(msg.error || null)}`);
302
+ if (msg.content) {
303
+ for (const block of msg.content) {
304
+ const b = block as { type: string; text?: string; name?: string; input?: unknown; id?: string };
305
+ debug(` Block: type=${b.type}${b.name ? ` name=${b.name}` : ""}${b.text ? ` text=${b.text.slice(0, 200)}` : ""}`);
306
+ if (b.type === "text" && b.text) {
307
+ output += b.text;
308
+ }
309
+ // Emit tool call start from assistant messages as a fallback
310
+ // (stream_events may not fire for subagent tool calls)
311
+ if (b.type === "tool_use" && b.name) {
312
+ // Serialize input and cap if huge (e.g., Write tool with large content)
313
+ const inputStr = JSON.stringify(b.input);
314
+ const cappedInput = inputStr.length > TOOL_RESULT_MAX_CHARS
315
+ ? JSON.parse(JSON.stringify(b.input, (_k, v) =>
316
+ typeof v === "string" && v.length > 2000
317
+ ? v.slice(0, 2000) + `… [${v.length.toLocaleString()} chars]`
318
+ : v
319
+ ))
320
+ : b.input;
321
+
322
+ // Track the tool input for later pairing with result
323
+ if (b.id) {
324
+ pendingTools.set(b.id, { name: b.name, input: cappedInput });
325
+ }
326
+ // Skip if already emitted via stream_event for this exact tool
327
+ if (lastEmittedToolStart === b.name) {
328
+ lastEmittedToolStart = "";
329
+ } else {
330
+ callbacks?.onToolCall?.(b.name, "start", { input: cappedInput });
331
+ }
332
+ }
333
+ }
334
+ }
335
+ }
336
+
337
+ // Tool results (user messages with tool_result) signal tool calls ended
338
+ if (message.type === "user" && "message" in message) {
339
+ const userMsg = message.message as { content?: Array<{ type: string; tool_use_id?: string; content?: unknown }> };
340
+ if (userMsg.content) {
341
+ for (const block of userMsg.content) {
342
+ if (block.type === "tool_result") {
343
+ // Extract text from tool_result content
344
+ let resultText = "";
345
+ const rc = block.content;
346
+ if (typeof rc === "string") {
347
+ resultText = rc;
348
+ } else if (Array.isArray(rc)) {
349
+ resultText = (rc as Array<{ type: string; text?: string }>)
350
+ .filter((c) => c.type === "text" && c.text)
351
+ .map((c) => c.text)
352
+ .join("\n");
353
+ }
354
+
355
+ // Look up the pending tool to get its name and input
356
+ const pending = block.tool_use_id ? pendingTools.get(block.tool_use_id) : undefined;
357
+ const toolName = pending?.name || "";
358
+ const toolInput = pending?.input;
359
+ if (block.tool_use_id) pendingTools.delete(block.tool_use_id);
360
+
361
+ // Truncate large results to keep SSE payloads manageable
362
+ const truncated = resultText.length > TOOL_RESULT_MAX_CHARS;
363
+ const cappedResult = truncated
364
+ ? resultText.slice(0, TOOL_RESULT_MAX_CHARS) + `\n\n… [truncated — ${resultText.length.toLocaleString()} chars total]`
365
+ : resultText;
366
+
367
+ callbacks?.onToolCall?.(toolName, "end", {
368
+ input: toolInput,
369
+ result: cappedResult,
370
+ });
371
+ }
372
+ }
373
+ }
374
+ }
375
+
376
+ // Task progress from subagents
377
+ if (message.type === "system" && "subtype" in message) {
378
+ const sub = (message as { subtype?: string }).subtype;
379
+ if (sub === "task_started") {
380
+ callbacks?.onProgress?.("Subagent started");
381
+ } else if (sub === "task_notification") {
382
+ callbacks?.onProgress?.("Subagent completed");
383
+ }
384
+ }
385
+
386
+ // Final result
387
+ if (message.type === "result") {
388
+ // Log the full result for debugging
389
+ const resultJson = JSON.stringify(message, null, 2);
390
+ debug(`Full result message:\n${resultJson.slice(0, 2000)}`);
391
+
392
+ const result = message as {
393
+ total_cost_usd: number;
394
+ duration_ms: number;
395
+ session_id: string;
396
+ result?: string;
397
+ is_error: boolean;
398
+ subtype: string;
399
+ errors?: string[];
400
+ };
401
+ costUsd = result.total_cost_usd;
402
+ sessionId = result.session_id || sessionId;
403
+
404
+ debug(`Result: subtype=${result.subtype}, cost=$${result.total_cost_usd}, is_error=${result.is_error}`);
405
+ if (result.errors) {
406
+ debug(`Errors: ${JSON.stringify(result.errors)}`);
407
+ }
408
+
409
+ // Check is_error regardless of subtype
410
+ if (result.is_error) {
411
+ const errMsg = result.errors?.join("; ") || result.result || `Agent ended with error (subtype: ${result.subtype})`;
412
+ callbacks?.onError?.(errMsg);
413
+ throw new Error(errMsg);
414
+ }
415
+
416
+ if (result.result) {
417
+ output = result.result;
418
+ }
419
+ }
420
+ }
421
+ } catch (err) {
422
+ const errMsg = err instanceof Error ? err.message : String(err);
423
+ debug(`Error caught: ${errMsg}`);
424
+ if (stderrLines.length > 0) {
425
+ debug(`Stderr output (last 20 lines):\n${stderrLines.slice(-20).join("\n")}`);
426
+ }
427
+ callbacks?.onError?.(errMsg);
428
+ throw err;
429
+ }
430
+
431
+ const runResult: AgentRunResult = {
432
+ output,
433
+ costUsd,
434
+ durationMs: Date.now() - startTime,
435
+ sessionId,
436
+ aborted,
437
+ };
438
+
439
+ debug(`Completed: cost=$${costUsd.toFixed(4)}, duration=${runResult.durationMs}ms, aborted=${aborted}`);
440
+ callbacks?.onResult?.(runResult);
441
+ return runResult;
442
+ }
@@ -0,0 +1,8 @@
1
+ import { createBrowserClient } from "@supabase/ssr";
2
+
3
+ export function createClient() {
4
+ return createBrowserClient(
5
+ process.env.NEXT_PUBLIC_SUPABASE_URL!,
6
+ process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
7
+ );
8
+ }
@@ -0,0 +1,63 @@
1
+ import { createServerClient } from "@supabase/ssr";
2
+ import { NextResponse, type NextRequest } from "next/server";
3
+
4
+ export async function updateSession(request: NextRequest) {
5
+ let supabaseResponse = NextResponse.next({ request });
6
+
7
+ const supabase = createServerClient(
8
+ process.env.NEXT_PUBLIC_SUPABASE_URL!,
9
+ process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
10
+ {
11
+ cookies: {
12
+ getAll() {
13
+ return request.cookies.getAll();
14
+ },
15
+ setAll(cookiesToSet) {
16
+ cookiesToSet.forEach(({ name, value }) =>
17
+ request.cookies.set(name, value)
18
+ );
19
+ supabaseResponse = NextResponse.next({ request });
20
+ cookiesToSet.forEach(({ name, value, options }) =>
21
+ supabaseResponse.cookies.set(name, value, options)
22
+ );
23
+ },
24
+ },
25
+ }
26
+ );
27
+
28
+ // Refresh session — do NOT remove this
29
+ const {
30
+ data: { user },
31
+ } = await supabase.auth.getUser();
32
+
33
+ const isLoginPage = request.nextUrl.pathname.startsWith("/login");
34
+ const isAuthCallback = request.nextUrl.pathname.startsWith("/auth");
35
+ const isApi = request.nextUrl.pathname.startsWith("/api");
36
+ const isNotAuthorized = request.nextUrl.pathname === "/not-authorized";
37
+
38
+ // Redirect unauthenticated users to login (except login page and auth callback)
39
+ if (!user && !isLoginPage && !isAuthCallback && !isApi) {
40
+ const url = request.nextUrl.clone();
41
+ url.pathname = "/login";
42
+ return NextResponse.redirect(url);
43
+ }
44
+
45
+ // Check if authenticated user is in the allowed user_roles table
46
+ if (user && !isLoginPage && !isAuthCallback && !isNotAuthorized) {
47
+ const { data: role } = await supabase.rpc("check_user_role", {
48
+ user_email: user.email,
49
+ });
50
+
51
+ if (!role) {
52
+ // User is authenticated but not authorized — sign them out and show error
53
+ if (isApi) {
54
+ return NextResponse.json({ error: "Not authorized" }, { status: 403 });
55
+ }
56
+ const url = request.nextUrl.clone();
57
+ url.pathname = "/not-authorized";
58
+ return NextResponse.redirect(url);
59
+ }
60
+ }
61
+
62
+ return supabaseResponse;
63
+ }
@@ -0,0 +1,28 @@
1
+ import { createServerClient } from "@supabase/ssr";
2
+ import { cookies } from "next/headers";
3
+
4
+ export async function createClient() {
5
+ const cookieStore = await cookies();
6
+
7
+ return createServerClient(
8
+ process.env.NEXT_PUBLIC_SUPABASE_URL!,
9
+ process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
10
+ {
11
+ cookies: {
12
+ getAll() {
13
+ return cookieStore.getAll();
14
+ },
15
+ setAll(cookiesToSet) {
16
+ try {
17
+ cookiesToSet.forEach(({ name, value, options }) =>
18
+ cookieStore.set(name, value, options)
19
+ );
20
+ } catch {
21
+ // Ignored — called from Server Component where cookies can't be set.
22
+ // Middleware will refresh the session instead.
23
+ }
24
+ },
25
+ },
26
+ }
27
+ );
28
+ }
@@ -0,0 +1,6 @@
1
+ import { createClient } from "@supabase/supabase-js";
2
+
3
+ const supabaseUrl = process.env.SUPABASE_URL || process.env.NEXT_PUBLIC_SUPABASE_URL!;
4
+ const supabaseKey = process.env.SUPABASE_ANON_KEY || process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
5
+
6
+ export const supabase = createClient(supabaseUrl, supabaseKey);
@@ -0,0 +1,29 @@
1
+ /** Human-friendly descriptions for technical tool names, shared across all UI surfaces */
2
+ export const TOOL_DESCRIPTIONS: Record<string, string> = {
3
+ Bash: "Runs a terminal command on the system",
4
+ Glob: "Searches for files by name or pattern",
5
+ Grep: "Searches inside files for specific text",
6
+ Read: "Reads the contents of a file",
7
+ Write: "Creates or overwrites a file",
8
+ Edit: "Makes targeted edits to a file",
9
+ WebSearch: "Searches the web for information",
10
+ WebFetch: "Fetches content from a web page",
11
+ Agent: "Delegates a task to a specialized sub-agent",
12
+ Task: "Manages background tasks",
13
+ NotebookEdit: "Edits a Jupyter notebook cell",
14
+ AskUserQuestion: "Asks you a question before continuing",
15
+ TodoWrite: "Updates the to-do list",
16
+ Mcp: "Calls an external integration tool",
17
+ ListMcpResources: "Lists available integration resources",
18
+ ReadMcpResource: "Reads data from an integration resource",
19
+ };
20
+
21
+ export function getToolDescription(name: string): string | undefined {
22
+ if (TOOL_DESCRIPTIONS[name]) return TOOL_DESCRIPTIONS[name];
23
+ if (name.startsWith("mcp__")) {
24
+ const parts = name.replace(/^mcp__/, "").split("__");
25
+ const service = (parts[0] || "").replace(/^claude_ai_/, "");
26
+ return `Action provided by the ${service} integration`;
27
+ }
28
+ return undefined;
29
+ }
@@ -0,0 +1,73 @@
1
+ export interface AgentMeta {
2
+ slug: string;
3
+ name: string;
4
+ description: string;
5
+ tools: string[];
6
+ mcpServers?: string[];
7
+ color?: string;
8
+ emoji?: string;
9
+ vibe?: string;
10
+ model?: string;
11
+ filePath: string;
12
+ }
13
+
14
+ export interface AgentRun {
15
+ id: string;
16
+ agent_slug: string;
17
+ agent_name: string;
18
+ prompt: string;
19
+ output: string | null;
20
+ status: "queued" | "running" | "completed" | "failed" | "stopped";
21
+ cost_usd: number | null;
22
+ duration_ms: number | null;
23
+ session_id: string | null;
24
+ error: string | null;
25
+ metadata: Record<string, unknown>;
26
+ pending_approval: {
27
+ tool_use_id: string;
28
+ tool_name: string;
29
+ tool_input: Record<string, unknown>;
30
+ } | null;
31
+ schedule_id: string | null;
32
+ event_count: number;
33
+ created_at: string;
34
+ started_at: string | null;
35
+ completed_at: string | null;
36
+ }
37
+
38
+ export interface RunEvent {
39
+ id: number;
40
+ run_id: string;
41
+ seq: number;
42
+ event_type: string;
43
+ payload: Record<string, unknown>;
44
+ created_at: string;
45
+ }
46
+
47
+ export interface AgentSchedule {
48
+ id: string;
49
+ agent_slug: string;
50
+ agent_name: string;
51
+ prompt: string;
52
+ cron: string;
53
+ skill_slug: string | null;
54
+ enabled: boolean;
55
+ last_run_at: string | null;
56
+ next_run_at: string | null;
57
+ metadata: Record<string, unknown>;
58
+ created_at: string;
59
+ updated_at: string;
60
+ }
61
+
62
+ export interface Notification {
63
+ id: string;
64
+ run_id: string | null;
65
+ schedule_id: string | null;
66
+ agent_slug: string;
67
+ session_id: string | null;
68
+ type: "completed" | "failed" | "needs_review" | "approval_needed";
69
+ title: string;
70
+ summary: string | null;
71
+ read: boolean;
72
+ created_at: string;
73
+ }