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,116 @@
1
+ import { NextRequest, NextResponse } from "next/server";
2
+ import { createClient } from "@/lib/supabase-server";
3
+
4
+ export type Role = "admin" | "operator" | "viewer";
5
+
6
+ interface AuthResult {
7
+ authorized: boolean;
8
+ user: { email: string; role: Role } | null;
9
+ response?: NextResponse;
10
+ }
11
+
12
+ /**
13
+ * Check if the current user has one of the required roles.
14
+ * Logs every access attempt (allowed or blocked) with a reason.
15
+ */
16
+ export async function requireRole(
17
+ req: NextRequest,
18
+ allowedRoles: Role[],
19
+ action: string,
20
+ resource?: string
21
+ ): Promise<AuthResult> {
22
+ const supabase = await createClient();
23
+ const ip = req.headers.get("x-forwarded-for") || req.headers.get("x-real-ip") || "unknown";
24
+ const ua = req.headers.get("user-agent") || "unknown";
25
+
26
+ // Check authentication
27
+ const { data: { user } } = await supabase.auth.getUser();
28
+
29
+ if (!user?.email) {
30
+ await supabase.rpc("log_access", {
31
+ p_email: null,
32
+ p_role: null,
33
+ p_action: action,
34
+ p_resource: resource || null,
35
+ p_allowed: false,
36
+ p_reason: "Not authenticated — no valid session",
37
+ p_ip_address: ip,
38
+ p_user_agent: ua,
39
+ });
40
+
41
+ return {
42
+ authorized: false,
43
+ user: null,
44
+ response: NextResponse.json(
45
+ { error: "Not authenticated" },
46
+ { status: 401 }
47
+ ),
48
+ };
49
+ }
50
+
51
+ // Check role
52
+ const { data: role } = await supabase.rpc("check_user_role", {
53
+ user_email: user.email,
54
+ }) as { data: Role | null };
55
+
56
+ if (!role) {
57
+ await supabase.rpc("log_access", {
58
+ p_email: user.email,
59
+ p_role: null,
60
+ p_action: action,
61
+ p_resource: resource || null,
62
+ p_allowed: false,
63
+ p_reason: "User has no role assigned — not in user_roles table",
64
+ p_ip_address: ip,
65
+ p_user_agent: ua,
66
+ });
67
+
68
+ return {
69
+ authorized: false,
70
+ user: null,
71
+ response: NextResponse.json(
72
+ { error: "Not authorized — no role assigned" },
73
+ { status: 403 }
74
+ ),
75
+ };
76
+ }
77
+
78
+ if (!allowedRoles.includes(role)) {
79
+ await supabase.rpc("log_access", {
80
+ p_email: user.email,
81
+ p_role: role,
82
+ p_action: action,
83
+ p_resource: resource || null,
84
+ p_allowed: false,
85
+ p_reason: `Role "${role}" is not allowed — requires one of: ${allowedRoles.join(", ")}`,
86
+ p_ip_address: ip,
87
+ p_user_agent: ua,
88
+ });
89
+
90
+ return {
91
+ authorized: false,
92
+ user: { email: user.email, role },
93
+ response: NextResponse.json(
94
+ { error: `Forbidden — your role "${role}" does not have access to this action` },
95
+ { status: 403 }
96
+ ),
97
+ };
98
+ }
99
+
100
+ // Allowed
101
+ await supabase.rpc("log_access", {
102
+ p_email: user.email,
103
+ p_role: role,
104
+ p_action: action,
105
+ p_resource: resource || null,
106
+ p_allowed: true,
107
+ p_reason: `Allowed — role "${role}" permitted for this action`,
108
+ p_ip_address: ip,
109
+ p_user_agent: ua,
110
+ });
111
+
112
+ return {
113
+ authorized: true,
114
+ user: { email: user.email, role },
115
+ };
116
+ }
@@ -0,0 +1,191 @@
1
+ import fs from "fs";
2
+ import path from "path";
3
+ import matter from "gray-matter";
4
+ import { getAgents } from "./agents";
5
+
6
+ const PROJECT_ROOT = process.env.PROJECT_ROOT || path.resolve(__dirname, "../../../..");
7
+ const SKILLS_DIR = path.join(PROJECT_ROOT, ".claude", "skills");
8
+ const COMMANDS_DIR = path.join(PROJECT_ROOT, ".claude", "commands");
9
+ const MCP_PATH = path.join(PROJECT_ROOT, ".mcp.json");
10
+
11
+ export interface SkillInfo {
12
+ slug: string;
13
+ name: string;
14
+ description: string;
15
+ category: string;
16
+ }
17
+
18
+ export interface CommandInfo {
19
+ slug: string;
20
+ name: string;
21
+ description: string;
22
+ group?: string; // e.g., "consider" for consider/* commands
23
+ }
24
+
25
+ export interface McpServerInfo {
26
+ name: string;
27
+ type: "local" | "remote";
28
+ }
29
+
30
+ export interface SubagentInfo {
31
+ slug: string;
32
+ name: string;
33
+ description: string;
34
+ emoji?: string;
35
+ }
36
+
37
+ export interface AgentCapabilities {
38
+ skills: SkillInfo[];
39
+ commands: CommandInfo[];
40
+ mcpServers: McpServerInfo[];
41
+ subagents: SubagentInfo[];
42
+ tools: string[];
43
+ }
44
+
45
+ function categorizeSkill(slug: string): string {
46
+ if (slug.startsWith("gws-workflow")) return "Workflows";
47
+ if (slug.startsWith("gws-")) return "Google Workspace";
48
+ if (slug.startsWith("recipe-")) return "Recipes";
49
+ if (slug.startsWith("persona-")) return "Personas";
50
+ if (slug.includes("frontend") || slug.includes("design")) return "Development";
51
+ return "Other";
52
+ }
53
+
54
+ export function readSkillContent(slug: string): string | null {
55
+ try {
56
+ const skillFile = path.join(SKILLS_DIR, slug, "SKILL.md");
57
+ const raw = fs.readFileSync(skillFile, "utf-8");
58
+ const { content } = matter(raw);
59
+ return content.trim() || null;
60
+ } catch {
61
+ return null;
62
+ }
63
+ }
64
+
65
+ export function readSkills(): SkillInfo[] {
66
+ try {
67
+ const dirs = fs.readdirSync(SKILLS_DIR, { withFileTypes: true })
68
+ .filter((d) => d.isDirectory());
69
+
70
+ return dirs.map((dir) => {
71
+ const skillFile = path.join(SKILLS_DIR, dir.name, "SKILL.md");
72
+ try {
73
+ const raw = fs.readFileSync(skillFile, "utf-8");
74
+ const { data } = matter(raw);
75
+ return {
76
+ slug: dir.name,
77
+ name: data.name || dir.name,
78
+ description: data.description || "",
79
+ category: categorizeSkill(dir.name),
80
+ };
81
+ } catch {
82
+ return {
83
+ slug: dir.name,
84
+ name: dir.name,
85
+ description: "",
86
+ category: categorizeSkill(dir.name),
87
+ };
88
+ }
89
+ }).sort((a, b) => a.category.localeCompare(b.category) || a.name.localeCompare(b.name));
90
+ } catch {
91
+ return [];
92
+ }
93
+ }
94
+
95
+ export function readCommands(): CommandInfo[] {
96
+ try {
97
+ const commands: CommandInfo[] = [];
98
+ const entries = fs.readdirSync(COMMANDS_DIR, { withFileTypes: true });
99
+
100
+ for (const entry of entries) {
101
+ if (entry.isFile() && entry.name.endsWith(".md")) {
102
+ const raw = fs.readFileSync(path.join(COMMANDS_DIR, entry.name), "utf-8");
103
+ const { data } = matter(raw);
104
+ const slug = entry.name.replace(/\.md$/, "");
105
+ commands.push({
106
+ slug,
107
+ name: data.name || slug,
108
+ description: data.description || "",
109
+ });
110
+ } else if (entry.isDirectory()) {
111
+ // Grouped commands like consider/*
112
+ const subDir = path.join(COMMANDS_DIR, entry.name);
113
+ const subFiles = fs.readdirSync(subDir).filter((f) => f.endsWith(".md"));
114
+ for (const sub of subFiles) {
115
+ const raw = fs.readFileSync(path.join(subDir, sub), "utf-8");
116
+ const { data } = matter(raw);
117
+ const slug = `${entry.name}/${sub.replace(/\.md$/, "")}`;
118
+ commands.push({
119
+ slug,
120
+ name: data.name || sub.replace(/\.md$/, ""),
121
+ description: data.description || "",
122
+ group: entry.name,
123
+ });
124
+ }
125
+ }
126
+ }
127
+
128
+ return commands.sort((a, b) => (a.group || "").localeCompare(b.group || "") || a.slug.localeCompare(b.slug));
129
+ } catch {
130
+ return [];
131
+ }
132
+ }
133
+
134
+ function readMcpServers(): McpServerInfo[] {
135
+ try {
136
+ const raw = JSON.parse(fs.readFileSync(MCP_PATH, "utf-8"));
137
+ const servers = raw.mcpServers || {};
138
+ return Object.keys(servers).map((name) => ({
139
+ name,
140
+ type: servers[name].type === "http" ? "remote" as const : "local" as const,
141
+ }));
142
+ } catch {
143
+ return [];
144
+ }
145
+ }
146
+
147
+ function getSubagents(excludeSlug?: string): SubagentInfo[] {
148
+ return getAgents()
149
+ .filter((a) => a.slug !== "main" && a.slug !== excludeSlug)
150
+ .map((a) => ({
151
+ slug: a.slug,
152
+ name: a.name,
153
+ description: a.description,
154
+ emoji: a.emoji,
155
+ }));
156
+ }
157
+
158
+ // Anthropic-hosted MCP integrations (add your own here)
159
+ const HOSTED_INTEGRATIONS: McpServerInfo[] = [];
160
+
161
+ export function getAgentCapabilities(slug: string): AgentCapabilities {
162
+ const isMain = slug === "main";
163
+ const agents = getAgents();
164
+ const agent = agents.find((a) => a.slug === slug);
165
+ const tools = agent?.tools || [];
166
+
167
+ if (isMain) {
168
+ return {
169
+ skills: readSkills(),
170
+ commands: readCommands(),
171
+ mcpServers: [...readMcpServers(), ...HOSTED_INTEGRATIONS],
172
+ subagents: getSubagents(),
173
+ tools,
174
+ };
175
+ }
176
+
177
+ // Specialist agents: only show MCP servers they've been granted access to
178
+ const allMcp = [...readMcpServers(), ...HOSTED_INTEGRATIONS];
179
+ const agentMcpNames = agent?.mcpServers;
180
+ const filteredMcp = agentMcpNames
181
+ ? allMcp.filter((s) => agentMcpNames.includes(s.name))
182
+ : [];
183
+
184
+ return {
185
+ skills: [],
186
+ commands: [],
187
+ mcpServers: filteredMcp,
188
+ subagents: [],
189
+ tools,
190
+ };
191
+ }
@@ -0,0 +1,96 @@
1
+ /**
2
+ * Lightweight line-level diff using LCS (longest common subsequence).
3
+ * No external dependencies. Designed for CLAUDE.md files (~50-300 lines).
4
+ */
5
+
6
+ export type DiffOp =
7
+ | { type: "keep"; text: string }
8
+ | { type: "delete"; text: string }
9
+ | { type: "insert"; text: string }
10
+ | { type: "replace"; oldText: string; newText: string };
11
+
12
+ export function computeLineDiff(oldText: string, newText: string): DiffOp[] {
13
+ const oldLines = oldText.split("\n");
14
+ const newLines = newText.split("\n");
15
+ const n = oldLines.length;
16
+ const m = newLines.length;
17
+
18
+ // Build LCS table
19
+ const dp: number[][] = Array.from({ length: n + 1 }, () => new Array(m + 1).fill(0));
20
+ for (let i = 1; i <= n; i++) {
21
+ for (let j = 1; j <= m; j++) {
22
+ if (oldLines[i - 1] === newLines[j - 1]) {
23
+ dp[i][j] = dp[i - 1][j - 1] + 1;
24
+ } else {
25
+ dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]);
26
+ }
27
+ }
28
+ }
29
+
30
+ // Backtrack to produce raw ops
31
+ const raw: { type: "keep" | "delete" | "insert"; line: string }[] = [];
32
+ let i = n;
33
+ let j = m;
34
+ while (i > 0 || j > 0) {
35
+ if (i > 0 && j > 0 && oldLines[i - 1] === newLines[j - 1]) {
36
+ raw.push({ type: "keep", line: oldLines[i - 1] });
37
+ i--;
38
+ j--;
39
+ } else if (j > 0 && (i === 0 || dp[i][j - 1] >= dp[i - 1][j])) {
40
+ raw.push({ type: "insert", line: newLines[j - 1] });
41
+ j--;
42
+ } else {
43
+ raw.push({ type: "delete", line: oldLines[i - 1] });
44
+ i--;
45
+ }
46
+ }
47
+ raw.reverse();
48
+
49
+ // Merge consecutive ops of the same type, then collapse adjacent delete+insert into replace
50
+ const merged: DiffOp[] = [];
51
+ let idx = 0;
52
+ while (idx < raw.length) {
53
+ const op = raw[idx];
54
+
55
+ if (op.type === "keep") {
56
+ // Collect consecutive keeps
57
+ const lines: string[] = [op.line];
58
+ idx++;
59
+ while (idx < raw.length && raw[idx].type === "keep") {
60
+ lines.push(raw[idx].line);
61
+ idx++;
62
+ }
63
+ merged.push({ type: "keep", text: lines.join("\n") });
64
+ } else if (op.type === "delete") {
65
+ // Collect consecutive deletes
66
+ const delLines: string[] = [op.line];
67
+ idx++;
68
+ while (idx < raw.length && raw[idx].type === "delete") {
69
+ delLines.push(raw[idx].line);
70
+ idx++;
71
+ }
72
+ // Check if followed by inserts → replace
73
+ if (idx < raw.length && raw[idx].type === "insert") {
74
+ const insLines: string[] = [];
75
+ while (idx < raw.length && raw[idx].type === "insert") {
76
+ insLines.push(raw[idx].line);
77
+ idx++;
78
+ }
79
+ merged.push({ type: "replace", oldText: delLines.join("\n"), newText: insLines.join("\n") });
80
+ } else {
81
+ merged.push({ type: "delete", text: delLines.join("\n") });
82
+ }
83
+ } else {
84
+ // insert (not preceded by delete)
85
+ const lines: string[] = [op.line];
86
+ idx++;
87
+ while (idx < raw.length && raw[idx].type === "insert") {
88
+ lines.push(raw[idx].line);
89
+ idx++;
90
+ }
91
+ merged.push({ type: "insert", text: lines.join("\n") });
92
+ }
93
+ }
94
+
95
+ return merged;
96
+ }
@@ -0,0 +1,22 @@
1
+ import { Queue } from "bullmq";
2
+ import { getRedisConfig } from "./redis";
3
+
4
+ const QUEUE_NAME = "agent-runs";
5
+
6
+ let instance: Queue | null = null;
7
+
8
+ export function getQueue(): Queue {
9
+ if (!instance) {
10
+ instance = new Queue(QUEUE_NAME, {
11
+ connection: getRedisConfig(),
12
+ defaultJobOptions: {
13
+ attempts: 1,
14
+ removeOnComplete: { count: 200, age: 86400 },
15
+ removeOnFail: { count: 500, age: 604800 },
16
+ },
17
+ });
18
+ }
19
+ return instance;
20
+ }
21
+
22
+ export { QUEUE_NAME };
@@ -0,0 +1,12 @@
1
+ // Connection config for BullMQ — uses URL string parsed into host/port
2
+ const REDIS_URL = process.env.REDIS_URL || "redis://localhost:6379";
3
+
4
+ export function getRedisConfig() {
5
+ const url = new URL(REDIS_URL);
6
+ return {
7
+ host: url.hostname,
8
+ port: parseInt(url.port || "6379", 10),
9
+ password: url.password || undefined,
10
+ maxRetriesPerRequest: null as null, // required for BullMQ workers
11
+ };
12
+ }
@@ -0,0 +1,166 @@
1
+ // Role-based file/command permissions for agent execution.
2
+ // Operators get the same tools as admin, but a canUseTool wrapper blocks
3
+ // writes to protected paths and dangerous Bash commands.
4
+
5
+ import path from "path";
6
+ import type { Role } from "./auth-guard";
7
+
8
+ const PROJECT_ROOT = process.env.PROJECT_ROOT || "/mnt/c/Users/Admin/Documents/PersonalAIssistant";
9
+
10
+ // ---------------------------------------------------------------------------
11
+ // Protected paths — operators cannot write/edit these (relative to project root)
12
+ // ---------------------------------------------------------------------------
13
+
14
+ export const PROTECTED_PATHS = [
15
+ "package.json",
16
+ "package-lock.json",
17
+ "tsconfig.json",
18
+ ".mcp.json",
19
+ ".env",
20
+ "CLAUDE.md",
21
+ "MEMORY.md",
22
+ "personal-assistant-app/src/",
23
+ "personal-assistant-app/worker/",
24
+ ".claude/agents/",
25
+ ".claude/skills/",
26
+ ".claude/settings",
27
+ "next.config",
28
+ "middleware.ts",
29
+ ];
30
+
31
+ // ---------------------------------------------------------------------------
32
+ // Blocked Bash command patterns
33
+ // ---------------------------------------------------------------------------
34
+
35
+ export const BLOCKED_COMMANDS: { pattern: RegExp; reason: string }[] = [
36
+ { pattern: /\bnpm\s+(install|ci|uninstall|remove)\b/, reason: "npm install/uninstall is not allowed for operators" },
37
+ { pattern: /\bpip3?\s+install\b/, reason: "pip install is not allowed for operators" },
38
+ { pattern: /\brm\s+-rf\b/, reason: "rm -rf is not allowed for operators" },
39
+ { pattern: /\bgit\s+push\b/, reason: "git push is not allowed for operators" },
40
+ { pattern: /\bgit\s+reset\s+--hard\b/, reason: "git reset --hard is not allowed for operators" },
41
+ { pattern: /\bgit\s+checkout\s+--\b/, reason: "git checkout -- is not allowed for operators" },
42
+ { pattern: /\bgit\s+clean\b/, reason: "git clean is not allowed for operators" },
43
+ ];
44
+
45
+ // ---------------------------------------------------------------------------
46
+ // Path helpers
47
+ // ---------------------------------------------------------------------------
48
+
49
+ /** Resolve an absolute or relative path to project-relative. Returns null if outside project. */
50
+ export function relativeToProject(filePath: string): string | null {
51
+ const abs = path.isAbsolute(filePath) ? filePath : path.resolve(PROJECT_ROOT, filePath);
52
+ const normalized = path.normalize(abs);
53
+ if (!normalized.startsWith(PROJECT_ROOT)) return null;
54
+ return path.relative(PROJECT_ROOT, normalized);
55
+ }
56
+
57
+ /** Check if a project-relative path matches any protected prefix. */
58
+ export function isProtectedPath(relPath: string): boolean {
59
+ return PROTECTED_PATHS.some((p) => {
60
+ if (p.endsWith("/")) {
61
+ return relPath.startsWith(p) || relPath === p.slice(0, -1);
62
+ }
63
+ return relPath === p || relPath.startsWith(p + "/");
64
+ });
65
+ }
66
+
67
+ // ---------------------------------------------------------------------------
68
+ // Bash command checking
69
+ // ---------------------------------------------------------------------------
70
+
71
+ /** Check a Bash command for blocked patterns and redirects to protected paths. Returns denial reason or null. */
72
+ export function checkBashCommand(command: string): string | null {
73
+ // Check blocked command patterns
74
+ for (const { pattern, reason } of BLOCKED_COMMANDS) {
75
+ if (pattern.test(command)) {
76
+ return reason;
77
+ }
78
+ }
79
+
80
+ // Check redirect operators targeting protected paths
81
+ // Matches: > file, >> file, tee file, tee -a file
82
+ const redirectPattern = /(?:>{1,2}|tee(?:\s+-a)?)\s+["']?([^\s;"'|&]+)/g;
83
+ let match;
84
+ while ((match = redirectPattern.exec(command)) !== null) {
85
+ const target = match[1];
86
+ const rel = relativeToProject(target);
87
+ if (rel !== null && isProtectedPath(rel)) {
88
+ return `Redirect to protected path: ${rel}`;
89
+ }
90
+ }
91
+
92
+ return null;
93
+ }
94
+
95
+ // ---------------------------------------------------------------------------
96
+ // canUseTool wrapper factory
97
+ // ---------------------------------------------------------------------------
98
+
99
+ type CanUseToolFn = (
100
+ toolName: string,
101
+ input: Record<string, unknown>,
102
+ options: { signal: AbortSignal; toolUseID: string }
103
+ ) => Promise<{ behavior: "allow" } | { behavior: "deny"; message: string }>;
104
+
105
+ /**
106
+ * Build a canUseTool function that wraps existing MCP approval logic
107
+ * with role-based blocking.
108
+ *
109
+ * - admin: returns base canUseTool unchanged (no extra restrictions)
110
+ * - operator: checks Write/Edit file paths and Bash commands before delegating to base
111
+ * - viewer: returns undefined (viewer uses restricted allowedTools only)
112
+ */
113
+ export function buildCanUseTool(
114
+ role: Role,
115
+ baseCanUseTool?: CanUseToolFn
116
+ ): CanUseToolFn | undefined {
117
+ if (role === "admin") return baseCanUseTool;
118
+ if (role === "viewer") return undefined;
119
+
120
+ // Operator: wrap with path/command checks
121
+ return async (toolName, input, options) => {
122
+ // Check Write/Edit file paths
123
+ if (toolName === "Write" || toolName === "Edit") {
124
+ const filePath = (input.file_path as string) || (input.filePath as string) || "";
125
+ if (filePath) {
126
+ const rel = relativeToProject(filePath);
127
+ if (rel !== null && isProtectedPath(rel)) {
128
+ return {
129
+ behavior: "deny" as const,
130
+ message: `Operators cannot modify protected file: ${rel}`,
131
+ };
132
+ }
133
+ }
134
+ }
135
+
136
+ // Check Bash commands
137
+ if (toolName === "Bash") {
138
+ const command = (input.command as string) || "";
139
+ const denial = checkBashCommand(command);
140
+ if (denial) {
141
+ return { behavior: "deny" as const, message: denial };
142
+ }
143
+ }
144
+
145
+ // Delegate to base canUseTool (MCP approval logic) if present
146
+ if (baseCanUseTool) {
147
+ return baseCanUseTool(toolName, input, options);
148
+ }
149
+
150
+ return { behavior: "allow" as const };
151
+ };
152
+ }
153
+
154
+ // ---------------------------------------------------------------------------
155
+ // Allowed tools per role
156
+ // ---------------------------------------------------------------------------
157
+
158
+ const DEFAULT_TOOLS = ["Read", "Write", "Edit", "Bash", "Glob", "Grep", "WebSearch", "WebFetch", "Agent", "Mcp", "Task", "NotebookEdit", "AskUserQuestion"];
159
+ const VIEWER_TOOLS = ["Read", "Glob", "Grep"];
160
+
161
+ /** Return the tool list for a given role. */
162
+ export function getAllowedToolsForRole(role: Role, agentTools?: string[]): string[] {
163
+ if (role === "viewer") return VIEWER_TOOLS;
164
+ // Operator and admin get the agent's configured tools or the full default set
165
+ return agentTools && agentTools.length > 0 ? agentTools : DEFAULT_TOOLS;
166
+ }