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