claudmax 2.0.0 → 2.0.1
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/claudmax-1.0.16.tgz +0 -0
- package/{packages/cli/index.js → index.js} +2 -0
- package/package.json +27 -55
- package/.claude/settings.local.json +0 -7
- package/.env.example +0 -24
- package/.github/workflows/publish.yml +0 -31
- package/README.md +0 -178
- package/claudmax-mcp-1.0.2.tgz +0 -0
- package/help +0 -0
- package/help-wal +0 -0
- package/next-env.d.ts +0 -6
- package/next.config.mjs +0 -43
- package/packages/cli/claudmax-1.0.16.tgz +0 -0
- package/packages/cli/package.json +0 -33
- package/packages/mcp/claudmax-mcp-1.0.0.tgz +0 -0
- package/packages/mcp/claudmax-mcp-1.0.1.tgz +0 -0
- package/packages/mcp/claudmax-mcp-1.0.2.tgz +0 -0
- package/packages/mcp/claudmax-mcp-1.0.3.tgz +0 -0
- package/packages/mcp/index.js +0 -129
- package/packages/mcp/package-lock.json +0 -1146
- package/packages/mcp/package.json +0 -32
- package/postcss.config.mjs +0 -6
- package/prisma/schema.prisma +0 -130
- package/prisma/seed.ts +0 -27
- package/public/favicon.svg +0 -10
- package/public/robots.txt +0 -10
- package/run_build.sh +0 -4
- package/scripts/migrate-plans.js +0 -98
- package/scripts/seed-blog.ts +0 -1014
- package/src/app/admin/dashboard/AdminDashboardClient.tsx +0 -1546
- package/src/app/admin/dashboard/page.tsx +0 -13
- package/src/app/admin/page.tsx +0 -132
- package/src/app/api/admin/auth/me/route.ts +0 -34
- package/src/app/api/admin/health/route.ts +0 -110
- package/src/app/api/admin/keys/[id]/route.ts +0 -116
- package/src/app/api/admin/keys/route.ts +0 -192
- package/src/app/api/admin/keys-list/route.ts +0 -81
- package/src/app/api/admin/login/route.ts +0 -72
- package/src/app/api/admin/logout/route.ts +0 -8
- package/src/app/api/admin/migrate/route.ts +0 -133
- package/src/app/api/admin/plans/[id]/route.ts +0 -65
- package/src/app/api/admin/plans/route.ts +0 -66
- package/src/app/api/admin/posts/[id]/route.ts +0 -81
- package/src/app/api/admin/posts/route.ts +0 -83
- package/src/app/api/admin/seed/route.ts +0 -145
- package/src/app/api/admin/settings/route.ts +0 -44
- package/src/app/api/admin/stats/route.ts +0 -74
- package/src/app/api/admin/users/[id]/route.ts +0 -166
- package/src/app/api/admin/users/plans/route.ts +0 -45
- package/src/app/api/admin/users/route.ts +0 -202
- package/src/app/api/blog/[slug]/route.ts +0 -22
- package/src/app/api/blog/route.ts +0 -40
- package/src/app/api/cron/daily-status/route.ts +0 -208
- package/src/app/api/support/chat/route.ts +0 -55
- package/src/app/api/support/chat/session/route.ts +0 -62
- package/src/app/api/support/chat/stream/route.ts +0 -44
- package/src/app/api/support/email/route.ts +0 -63
- package/src/app/api/tools/understand_image/route.ts +0 -113
- package/src/app/api/tools/upload/route.ts +0 -179
- package/src/app/api/tools/web_search/route.ts +0 -99
- package/src/app/api/v1/audio/route.ts +0 -67
- package/src/app/api/v1/audio/speech/route.ts +0 -73
- package/src/app/api/v1/chat/completions/route.ts +0 -3
- package/src/app/api/v1/chat/route.ts +0 -1079
- package/src/app/api/v1/images/generations/route.ts +0 -93
- package/src/app/api/v1/info/route.ts +0 -30
- package/src/app/api/v1/key-status/route.ts +0 -109
- package/src/app/api/v1/key-status/stream/route.ts +0 -135
- package/src/app/api/v1/messages/count_tokens/route.ts +0 -22
- package/src/app/api/v1/messages/route.ts +0 -807
- package/src/app/api/v1/models/route.ts +0 -14
- package/src/app/api/v1/route.ts +0 -18
- package/src/app/blog/BlogClient.tsx +0 -193
- package/src/app/blog/[slug]/page.tsx +0 -117
- package/src/app/blog/page.tsx +0 -20
- package/src/app/check-usage/CheckUsageClient.tsx +0 -186
- package/src/app/check-usage/layout.tsx +0 -11
- package/src/app/check-usage/page.tsx +0 -15
- package/src/app/docs/layout.tsx +0 -16
- package/src/app/docs/page.tsx +0 -1055
- package/src/app/faq/FAQClient.tsx +0 -227
- package/src/app/faq/page.tsx +0 -21
- package/src/app/globals.css +0 -75
- package/src/app/layout.tsx +0 -80
- package/src/app/page.tsx +0 -256
- package/src/app/reseller/ResellerClient.tsx +0 -435
- package/src/app/reseller/page.tsx +0 -15
- package/src/app/setup.ps1/route.ts +0 -79
- package/src/app/setup.sh/route.ts +0 -113
- package/src/app/sitemap.ts +0 -50
- package/src/app/status/StatusClient.tsx +0 -103
- package/src/app/status/layout.tsx +0 -11
- package/src/app/status/page.tsx +0 -15
- package/src/app/support/SupportClient.tsx +0 -411
- package/src/app/support/page.tsx +0 -25
- package/src/app/v1/chat/completions/route.ts +0 -3
- package/src/app/v1/chat/route.ts +0 -4
- package/src/app/v1/messages/route.ts +0 -3
- package/src/components/Footer.tsx +0 -120
- package/src/components/Header.tsx +0 -131
- package/src/components/landing/features.tsx +0 -99
- package/src/components/ui/badge.tsx +0 -32
- package/src/components/ui/button.tsx +0 -46
- package/src/components/ui/card.tsx +0 -50
- package/src/components/ui/dialog.tsx +0 -97
- package/src/components/ui/dropdown-menu.tsx +0 -156
- package/src/components/ui/input.tsx +0 -21
- package/src/components/ui/label.tsx +0 -15
- package/src/components/ui/separator.tsx +0 -22
- package/src/components/ui/switch.tsx +0 -27
- package/src/components/ui/tabs.tsx +0 -51
- package/src/components/ui/toast.tsx +0 -103
- package/src/lib/auth.ts +0 -45
- package/src/lib/prisma.ts +0 -20
- package/src/lib/providers.ts +0 -158
- package/src/lib/security.ts +0 -165
- package/src/lib/utils.ts +0 -14
- package/src/middleware.ts +0 -30
- package/tailwind.config.ts +0 -53
- package/tsconfig.json +0 -41
- package/tsconfig.tsbuildinfo +0 -1
- package/vercel.json +0 -8
- /package/{packages/cli/bin → bin}/claudmax.js +0 -0
- /package/{packages/cli/claudmax-1.0.17.tgz → claudmax-1.0.17.tgz} +0 -0
|
@@ -1,435 +0,0 @@
|
|
|
1
|
-
'use client';
|
|
2
|
-
|
|
3
|
-
import { useState, useEffect, useCallback } from 'react';
|
|
4
|
-
import Header from '@/components/Header';
|
|
5
|
-
import Footer from '@/components/Footer';
|
|
6
|
-
|
|
7
|
-
interface ApiKey {
|
|
8
|
-
id: string;
|
|
9
|
-
key: string;
|
|
10
|
-
name: string;
|
|
11
|
-
prefix: string;
|
|
12
|
-
tier: string;
|
|
13
|
-
isActive: boolean;
|
|
14
|
-
blockedUntil: string | null;
|
|
15
|
-
tokenLimitOverride: number | null;
|
|
16
|
-
displayMultiplier: number;
|
|
17
|
-
windowTokensUsed: number;
|
|
18
|
-
windowRequestsUsed: number;
|
|
19
|
-
windowStartAt: string | null;
|
|
20
|
-
createdAt: string;
|
|
21
|
-
lastUsedAt: string | null;
|
|
22
|
-
expiresAt: string | null;
|
|
23
|
-
planId: string | null;
|
|
24
|
-
plan: { id: string; name: string; tier: string; durationDays: number } | null;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
interface MeData {
|
|
28
|
-
id: string;
|
|
29
|
-
username: string;
|
|
30
|
-
name: string;
|
|
31
|
-
role: string;
|
|
32
|
-
keysGenLimit: number;
|
|
33
|
-
keysGenToday: number;
|
|
34
|
-
keysGenResetAt: string | null;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
function fmtTokens(n: number) {
|
|
38
|
-
if (n >= 1_000_000_000) return `${(n / 1_000_000_000).toFixed(1)}B`;
|
|
39
|
-
if (n >= 1_000_000) return `${(n / 1_000_000).toFixed(1)}M`;
|
|
40
|
-
if (n >= 1_000) return `${(n / 1_000).toFixed(0)}K`;
|
|
41
|
-
return String(n);
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
function StatCard({ label, value, color }: { label: string; value: string | number; color?: string }) {
|
|
45
|
-
return (
|
|
46
|
-
<div className="bg-white rounded-2xl border border-gray-200 p-5 card-shadow flex flex-col items-center justify-center">
|
|
47
|
-
<p className="text-3xl font-black text-[#111827]">{value}</p>
|
|
48
|
-
<p className={`text-xs font-bold uppercase tracking-wider mt-1 ${color ?? 'text-[#9CA3AF]'}`}>{label}</p>
|
|
49
|
-
</div>
|
|
50
|
-
);
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
function ExtendDaysModal({ key_, onConfirm, onCancel }: { key_: ApiKey; onConfirm: (days: number) => void; onCancel: () => void }) {
|
|
54
|
-
const durations = [
|
|
55
|
-
{ label: '+7 Days', value: 7 },
|
|
56
|
-
{ label: '+15 Days', value: 15 },
|
|
57
|
-
{ label: '+30 Days', value: 30 },
|
|
58
|
-
{ label: '+90 Days', value: 90 },
|
|
59
|
-
{ label: '−7 Days', value: -7 },
|
|
60
|
-
{ label: '−15 Days', value: -15 },
|
|
61
|
-
];
|
|
62
|
-
return (
|
|
63
|
-
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/40 backdrop-blur-sm p-4">
|
|
64
|
-
<div className="bg-white rounded-2xl border border-gray-200 w-full max-w-xs p-6 card-shadow">
|
|
65
|
-
<h3 className="text-lg font-bold text-[#111827] mb-1">Extend / Reduce Plan</h3>
|
|
66
|
-
<p className="text-[#9CA3AF] text-xs font-mono mb-4">For key: {key_.name}</p>
|
|
67
|
-
<div className="space-y-2">
|
|
68
|
-
{durations.map(d => (
|
|
69
|
-
<button
|
|
70
|
-
key={d.value}
|
|
71
|
-
onClick={() => onConfirm(d.value)}
|
|
72
|
-
className={`w-full h-10 rounded-xl border font-semibold text-sm text-left px-3 transition-colors ${d.value < 0 ? 'hover:bg-red-50 hover:border-red-300 hover:text-red-600 border-gray-200 text-[#374151]' : 'hover:bg-purple-50 hover:border-purple-300 hover:text-purple-700 border-gray-200 text-[#374151]'}`}
|
|
73
|
-
>
|
|
74
|
-
{d.label}
|
|
75
|
-
</button>
|
|
76
|
-
))}
|
|
77
|
-
</div>
|
|
78
|
-
<button onClick={onCancel} className="mt-3 w-full h-10 rounded-xl border border-gray-200 text-[#6B7280] font-semibold text-sm hover:bg-gray-50 transition-colors">Cancel</button>
|
|
79
|
-
</div>
|
|
80
|
-
</div>
|
|
81
|
-
);
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
function ConfirmModal({ title, message, onConfirm, onCancel, danger }: { title: string; message: string; onConfirm: () => void; onCancel: () => void; danger?: boolean }) {
|
|
85
|
-
return (
|
|
86
|
-
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/40 backdrop-blur-sm p-4">
|
|
87
|
-
<div className="bg-white rounded-2xl border border-gray-200 w-full max-w-sm p-6 card-shadow-md">
|
|
88
|
-
<h3 className="text-lg font-bold text-[#111827] mb-2">{title}</h3>
|
|
89
|
-
<p className="text-[#6B7280] text-sm mb-6">{message}</p>
|
|
90
|
-
<div className="flex gap-3">
|
|
91
|
-
<button onClick={onCancel} className="flex-1 h-10 rounded-xl border border-gray-200 text-[#6B7280] font-semibold text-sm hover:bg-gray-50 transition-colors">Cancel</button>
|
|
92
|
-
<button onClick={onConfirm} className={`flex-1 h-10 rounded-xl font-semibold text-sm text-white transition-colors ${danger ? 'bg-red-500 hover:bg-red-600' : 'bg-[#5244F3] hover:bg-[#4338CA]'}`}>Confirm</button>
|
|
93
|
-
</div>
|
|
94
|
-
</div>
|
|
95
|
-
</div>
|
|
96
|
-
);
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
export default function ResellerPage() {
|
|
100
|
-
const [me, setMe] = useState<MeData | null>(null);
|
|
101
|
-
const [keys, setKeys] = useState<ApiKey[]>([]);
|
|
102
|
-
const [total, setTotal] = useState(0);
|
|
103
|
-
const [search, setSearch] = useState('');
|
|
104
|
-
const [tierFilter, setTierFilter] = useState('');
|
|
105
|
-
const [statusFilter, setStatusFilter] = useState('');
|
|
106
|
-
const [loading, setLoading] = useState(true);
|
|
107
|
-
const [copied, setCopied] = useState('');
|
|
108
|
-
const [confirm, setConfirm] = useState<{ title: string; message: string; onConfirm: () => void; danger?: boolean } | null>(null);
|
|
109
|
-
const [extendKey, setExtendKey] = useState<ApiKey | null>(null);
|
|
110
|
-
const [actionLoading, setActionLoading] = useState<string | null>(null);
|
|
111
|
-
const [newlyCreated, setNewlyCreated] = useState('');
|
|
112
|
-
|
|
113
|
-
const fetchKeys = useCallback(async () => {
|
|
114
|
-
setLoading(true);
|
|
115
|
-
try {
|
|
116
|
-
const params = new URLSearchParams();
|
|
117
|
-
if (search) params.set('search', search);
|
|
118
|
-
if (tierFilter) params.set('tier', tierFilter);
|
|
119
|
-
if (statusFilter) params.set('status', statusFilter);
|
|
120
|
-
const r = await fetch(`/api/admin/keys-list?${params}`);
|
|
121
|
-
if (r.ok) {
|
|
122
|
-
const d = await r.json();
|
|
123
|
-
setKeys(d.keys.filter((k: any) => k.resellerId === me?.id));
|
|
124
|
-
setTotal(d.total);
|
|
125
|
-
}
|
|
126
|
-
} finally { setLoading(false); }
|
|
127
|
-
}, [search, tierFilter, statusFilter, me]);
|
|
128
|
-
|
|
129
|
-
useEffect(() => {
|
|
130
|
-
fetch('/api/admin/auth/me').then(r => r.ok ? r.json() : null).then(data => {
|
|
131
|
-
if (!data) { window.location.href = '/admin'; return; }
|
|
132
|
-
if (data.role !== 'reseller') { window.location.href = '/admin/dashboard'; return; }
|
|
133
|
-
setMe(data);
|
|
134
|
-
}).catch(() => window.location.href = '/admin');
|
|
135
|
-
}, []);
|
|
136
|
-
|
|
137
|
-
useEffect(() => { if (me) fetchKeys(); }, [me, fetchKeys]);
|
|
138
|
-
|
|
139
|
-
useEffect(() => {
|
|
140
|
-
const t = setTimeout(() => fetchKeys(), 300);
|
|
141
|
-
return () => clearTimeout(t);
|
|
142
|
-
}, [search, tierFilter, statusFilter, fetchKeys]);
|
|
143
|
-
|
|
144
|
-
// Auto-refresh
|
|
145
|
-
useEffect(() => {
|
|
146
|
-
if (!me) return;
|
|
147
|
-
const interval = setInterval(() => fetchKeys(), 15000);
|
|
148
|
-
return () => clearInterval(interval);
|
|
149
|
-
}, [me, fetchKeys]);
|
|
150
|
-
|
|
151
|
-
const doAction = async (id: string, action: string) => {
|
|
152
|
-
setActionLoading(id + action);
|
|
153
|
-
try {
|
|
154
|
-
const r = await fetch(`/api/admin/keys/${id}`, {
|
|
155
|
-
method: 'PATCH',
|
|
156
|
-
headers: { 'Content-Type': 'application/json' },
|
|
157
|
-
body: JSON.stringify({ action }),
|
|
158
|
-
});
|
|
159
|
-
if (r.ok) { await fetchKeys(); }
|
|
160
|
-
} finally { setActionLoading(null); }
|
|
161
|
-
};
|
|
162
|
-
|
|
163
|
-
const handleExtend = async (days: number) => {
|
|
164
|
-
if (!extendKey || !me) return;
|
|
165
|
-
setExtendKey(null);
|
|
166
|
-
setActionLoading(extendKey.id + 'extend');
|
|
167
|
-
try {
|
|
168
|
-
await fetch(`/api/admin/keys/${extendKey.id}`, {
|
|
169
|
-
method: 'PATCH',
|
|
170
|
-
headers: { 'Content-Type': 'application/json' },
|
|
171
|
-
body: JSON.stringify({ action: 'extendDays', days }),
|
|
172
|
-
});
|
|
173
|
-
await fetchKeys();
|
|
174
|
-
} finally { setActionLoading(null); }
|
|
175
|
-
};
|
|
176
|
-
|
|
177
|
-
const handleCreateKey = async () => {
|
|
178
|
-
if (!me) return;
|
|
179
|
-
if (me.keysGenToday >= me.keysGenLimit) {
|
|
180
|
-
setConfirm({ title: 'Key Limit Reached', message: `You've reached your daily limit of ${me.keysGenLimit} keys. The limit resets at midnight.`, onConfirm: () => setConfirm(null) });
|
|
181
|
-
return;
|
|
182
|
-
}
|
|
183
|
-
setActionLoading('creating');
|
|
184
|
-
try {
|
|
185
|
-
const r = await fetch('/api/admin/keys', {
|
|
186
|
-
method: 'POST',
|
|
187
|
-
headers: { 'Content-Type': 'application/json' },
|
|
188
|
-
body: JSON.stringify({ name: 'New API Key' }),
|
|
189
|
-
});
|
|
190
|
-
if (r.ok) {
|
|
191
|
-
const d = await r.json();
|
|
192
|
-
setNewlyCreated(d.key);
|
|
193
|
-
await fetchKeys();
|
|
194
|
-
// Refresh me to update keysGenToday
|
|
195
|
-
const meRes = await fetch('/api/admin/auth/me');
|
|
196
|
-
if (meRes.ok) setMe(await meRes.json());
|
|
197
|
-
}
|
|
198
|
-
} finally { setActionLoading(null); }
|
|
199
|
-
};
|
|
200
|
-
|
|
201
|
-
if (!me) return (
|
|
202
|
-
<div className="min-h-screen bg-[#F9FAFB] flex items-center justify-center">
|
|
203
|
-
<div className="w-8 h-8 border-2 border-purple-600 border-t-transparent rounded-full animate-spin" />
|
|
204
|
-
</div>
|
|
205
|
-
);
|
|
206
|
-
|
|
207
|
-
const activeKeys = keys.filter(k => k.isActive && !k.blockedUntil).length;
|
|
208
|
-
const disabledKeys = keys.filter(k => !k.isActive && !k.blockedUntil).length;
|
|
209
|
-
const expiredKeys = keys.filter(k => k.blockedUntil && new Date(k.blockedUntil) < new Date()).length;
|
|
210
|
-
const overLimitKeys = keys.filter(k => {
|
|
211
|
-
const limit = getKeyLimit(k);
|
|
212
|
-
return k.windowTokensUsed >= limit;
|
|
213
|
-
}).length;
|
|
214
|
-
|
|
215
|
-
const tierPlans: Record<string, { label: string; limit: number }> = {
|
|
216
|
-
free: { label: 'Free', limit: 500_000 },
|
|
217
|
-
'5x': { label: '5x Max', limit: 5_000_000 },
|
|
218
|
-
'20x': { label: '20x Max', limit: 20_000_000 },
|
|
219
|
-
unlimited: { label: 'Unlimited', limit: 999_999_999 },
|
|
220
|
-
};
|
|
221
|
-
|
|
222
|
-
const TIER_LIMIT_MS = 5 * 60 * 60 * 1000; // 5h in ms
|
|
223
|
-
|
|
224
|
-
function getKeyLimit(k: ApiKey) {
|
|
225
|
-
const PLAN_LIMITS: Record<string, number> = { Free: 500_000, Mini: 2_000_000, Pro: 5_000_000, 'Max 20x': 20_000_000, Unlimited: 999_999_999 };
|
|
226
|
-
if (k.plan) return PLAN_LIMITS[k.plan.name] ?? 500_000;
|
|
227
|
-
return k.tokenLimitOverride ?? tierPlans[k.tier]?.limit ?? 500_000;
|
|
228
|
-
}
|
|
229
|
-
function getKeyLabel(k: ApiKey) {
|
|
230
|
-
if (k.plan) return k.plan.name;
|
|
231
|
-
return tierPlans[k.tier]?.label ?? k.tier;
|
|
232
|
-
}
|
|
233
|
-
function getExpiresAt(k: ApiKey): string | null {
|
|
234
|
-
if (k.expiresAt) return new Date(k.expiresAt).toLocaleDateString();
|
|
235
|
-
return null;
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
return (
|
|
239
|
-
<div className="min-h-screen bg-[#F9FAFB] flex flex-col">
|
|
240
|
-
<Header />
|
|
241
|
-
|
|
242
|
-
<div className="flex-1 max-w-7xl mx-auto px-4 sm:px-6 py-8 w-full">
|
|
243
|
-
|
|
244
|
-
{/* Header */}
|
|
245
|
-
<div className="flex items-start justify-between mb-6">
|
|
246
|
-
<div>
|
|
247
|
-
<h1 className="text-3xl font-black text-[#111827]">My API Keys</h1>
|
|
248
|
-
<p className="text-[#9CA3AF] text-sm mt-1">Manage API keys for your applications or clients.</p>
|
|
249
|
-
</div>
|
|
250
|
-
<div className="flex items-center gap-2">
|
|
251
|
-
<span className="text-xs font-bold px-3 py-1.5 rounded-full bg-purple-50 text-purple-700 border border-purple-200">
|
|
252
|
-
{me.keysGenToday}/{me.keysGenLimit} keys today
|
|
253
|
-
</span>
|
|
254
|
-
<button onClick={fetchKeys} className="h-9 px-4 rounded-xl border border-gray-200 text-[#6B7280] font-semibold text-sm hover:bg-gray-50 transition-colors flex items-center gap-1.5">
|
|
255
|
-
<svg className="w-3.5 h-3.5" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><path d="M1 4v6h6M23 20v-6h-6"/><path d="M20.49 9A9 9 0 0 0 5.64 5.64L1 10M23 14l-4.64 4.36A9 9 0 0 1 3.51 15"/></svg>
|
|
256
|
-
Refresh
|
|
257
|
-
</button>
|
|
258
|
-
<button
|
|
259
|
-
onClick={handleCreateKey}
|
|
260
|
-
disabled={actionLoading === 'creating'}
|
|
261
|
-
className="h-9 px-4 bg-[#5244F3] hover:bg-[#4338CA] disabled:bg-gray-300 text-white rounded-xl text-sm font-semibold transition-colors shadow-sm flex items-center gap-1.5"
|
|
262
|
-
>
|
|
263
|
-
<svg className="w-4 h-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5"><path d="M12 5v14M5 12h14"/></svg>
|
|
264
|
-
Create Key
|
|
265
|
-
</button>
|
|
266
|
-
</div>
|
|
267
|
-
</div>
|
|
268
|
-
|
|
269
|
-
{/* Stat cards */}
|
|
270
|
-
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 mb-6">
|
|
271
|
-
<StatCard label="TOTAL" value={total} />
|
|
272
|
-
<StatCard label="ACTIVE" value={activeKeys} color="text-emerald-600" />
|
|
273
|
-
<StatCard label="DISABLED" value={disabledKeys} />
|
|
274
|
-
<StatCard label="OVER LIMIT" value={overLimitKeys} color={overLimitKeys > 0 ? 'text-red-500' : 'text-[#9CA3AF]'} />
|
|
275
|
-
</div>
|
|
276
|
-
|
|
277
|
-
{/* Filters */}
|
|
278
|
-
<div className="flex items-center gap-2 mb-4">
|
|
279
|
-
<div className="relative flex-1 max-w-xs">
|
|
280
|
-
<svg className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-[#D1D5DB]" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><circle cx="11" cy="11" r="8"/><path d="m21 21-4.35-4.35"/></svg>
|
|
281
|
-
<input value={search} onChange={e => setSearch(e.target.value)} placeholder="Search by prefix or name..." className="h-9 pl-9 pr-4 bg-white border border-gray-200 rounded-xl text-sm text-[#111827] placeholder:text-[#D1D5DB] focus:outline-none focus:border-purple-400 w-full transition-all" />
|
|
282
|
-
</div>
|
|
283
|
-
<select value={tierFilter} onChange={e => setTierFilter(e.target.value)} className="h-9 px-3 bg-white border border-gray-200 rounded-xl text-sm text-[#6B7280] focus:outline-none transition-all">
|
|
284
|
-
<option value="">All Plans</option>
|
|
285
|
-
<option value="free">Free</option>
|
|
286
|
-
<option value="5x">5x Max</option>
|
|
287
|
-
<option value="20x">20x Max</option>
|
|
288
|
-
<option value="unlimited">Unlimited</option>
|
|
289
|
-
</select>
|
|
290
|
-
<select value={statusFilter} onChange={e => setStatusFilter(e.target.value)} className="h-9 px-3 bg-white border border-gray-200 rounded-xl text-sm text-[#6B7280] focus:outline-none transition-all">
|
|
291
|
-
<option value="">All Status</option>
|
|
292
|
-
<option value="active">Active</option>
|
|
293
|
-
<option value="inactive">Disabled</option>
|
|
294
|
-
</select>
|
|
295
|
-
</div>
|
|
296
|
-
|
|
297
|
-
{/* Table */}
|
|
298
|
-
<div className="bg-white rounded-2xl border border-gray-200 overflow-hidden card-shadow">
|
|
299
|
-
<div className="overflow-x-auto">
|
|
300
|
-
<table className="w-full text-sm">
|
|
301
|
-
<thead>
|
|
302
|
-
<tr className="border-b border-gray-100 bg-gray-50">
|
|
303
|
-
{['Key', 'Plan', 'Status', 'Usage', 'Actions'].map(h => (
|
|
304
|
-
<th key={h} className="text-left text-[#9CA3AF] font-bold px-4 py-3 text-xs uppercase tracking-wider whitespace-nowrap">{h}</th>
|
|
305
|
-
))}
|
|
306
|
-
</tr>
|
|
307
|
-
</thead>
|
|
308
|
-
<tbody>
|
|
309
|
-
{loading ? (
|
|
310
|
-
Array.from({ length: 3 }).map((_, i) => (
|
|
311
|
-
<tr key={i} className="border-b border-gray-100 last:border-0">
|
|
312
|
-
{Array.from({ length: 5 }).map((_, j) => (<td key={j} className="px-4 py-4"><div className="h-4 bg-gray-100 rounded animate-pulse w-32" /></td>))}
|
|
313
|
-
</tr>
|
|
314
|
-
))
|
|
315
|
-
) : keys.length === 0 ? (
|
|
316
|
-
<tr><td colSpan={6} className="px-4 py-16 text-center text-[#D1D5DB] text-sm">No API keys found.</td></tr>
|
|
317
|
-
) : (
|
|
318
|
-
keys.map(k => {
|
|
319
|
-
const limit = getKeyLimit(k);
|
|
320
|
-
const label = getKeyLabel(k);
|
|
321
|
-
const used = k.windowTokensUsed ?? 0;
|
|
322
|
-
const pct = Math.min(100, (used / limit) * 100);
|
|
323
|
-
const isOver = used >= limit;
|
|
324
|
-
const isActive = k.isActive && (!k.blockedUntil || new Date(k.blockedUntil) < new Date());
|
|
325
|
-
const expires = getExpiresAt(k);
|
|
326
|
-
|
|
327
|
-
return (
|
|
328
|
-
<tr key={k.id} className="border-b border-gray-100 last:border-0 hover:bg-gray-50/50 transition-colors">
|
|
329
|
-
<td className="px-4 py-4">
|
|
330
|
-
<p className="text-purple-600 font-bold text-sm">{k.name}</p>
|
|
331
|
-
<div className="flex items-center gap-2 mt-0.5">
|
|
332
|
-
<code className="text-[#9CA3AF] text-xs font-mono">{k.prefix}••••</code>
|
|
333
|
-
<button onClick={() => { navigator.clipboard.writeText(k.key); setCopied(k.id); setTimeout(() => setCopied(''), 2000); }} className="text-[#9CA3AF] hover:text-[#6B7280] transition-colors">
|
|
334
|
-
{copied === k.id ? (
|
|
335
|
-
<svg className="w-3.5 h-3.5 text-emerald-500" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><path d="M20 6L9 17l-5-5"/></svg>
|
|
336
|
-
) : (
|
|
337
|
-
<svg className="w-3.5 h-3.5" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><rect x="9" y="9" width="13" height="13" rx="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg>
|
|
338
|
-
)}
|
|
339
|
-
</button>
|
|
340
|
-
</div>
|
|
341
|
-
{expires && <p className="text-[#9CA3AF] text-xs mt-0.5">Expires: {expires}</p>}
|
|
342
|
-
</td>
|
|
343
|
-
<td className="px-4 py-4">
|
|
344
|
-
<span className="text-xs font-bold px-2.5 py-1 rounded-full bg-gray-100 text-gray-600 border border-gray-200">{label}</span>
|
|
345
|
-
</td>
|
|
346
|
-
<td className="px-4 py-4">
|
|
347
|
-
<div className="flex items-center gap-2">
|
|
348
|
-
<span className={`text-xs font-bold px-2.5 py-1 rounded-full flex items-center gap-1 ${isActive ? 'bg-emerald-50 text-emerald-700 border border-emerald-200' : 'bg-red-50 text-red-500 border border-red-100'}`}>
|
|
349
|
-
<span className={`w-1.5 h-1.5 rounded-full ${isActive ? 'bg-emerald-500' : 'bg-red-500'}`} />
|
|
350
|
-
{k.blockedUntil ? 'Blocked' : isActive ? 'Active' : 'Disabled'}
|
|
351
|
-
</span>
|
|
352
|
-
</div>
|
|
353
|
-
{expires && (
|
|
354
|
-
<p className="text-[#9CA3AF] text-xs mt-1 flex items-center gap-1">
|
|
355
|
-
<svg className="w-3 h-3" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><rect x="3" y="4" width="18" height="18" rx="2"/><path d="M16 2v4M8 2v4M3 10h18"/></svg>
|
|
356
|
-
{expires}
|
|
357
|
-
</p>
|
|
358
|
-
)}
|
|
359
|
-
</td>
|
|
360
|
-
<td className="px-4 py-4 min-w-48">
|
|
361
|
-
<p className={`text-sm font-mono font-semibold ${isOver ? 'text-red-500' : 'text-[#374151]'}`}>
|
|
362
|
-
{fmtTokens(used)} / {fmtTokens(limit)}
|
|
363
|
-
</p>
|
|
364
|
-
<div className="h-1.5 bg-gray-100 rounded-full mt-1.5 overflow-hidden">
|
|
365
|
-
<div className={`h-full rounded-full transition-all ${isOver ? 'bg-red-500' : pct >= 80 ? 'bg-amber-500' : 'bg-purple-500'}`} style={{ width: `${pct}%` }} />
|
|
366
|
-
</div>
|
|
367
|
-
</td>
|
|
368
|
-
<td className="px-4 py-4">
|
|
369
|
-
<div className="flex items-center gap-1">
|
|
370
|
-
<button
|
|
371
|
-
onClick={() => setExtendKey(k)}
|
|
372
|
-
className="h-8 px-3 rounded-lg border border-gray-200 text-[#6B7280] text-xs font-semibold hover:bg-purple-50 hover:border-purple-300 hover:text-purple-700 transition-colors"
|
|
373
|
-
>
|
|
374
|
-
Extend Days
|
|
375
|
-
</button>
|
|
376
|
-
{isActive ? (
|
|
377
|
-
<button
|
|
378
|
-
onClick={() => setConfirm({ title: 'Block Key', message: `Block "${k.name}"? It will stop accepting requests.`, onConfirm: () => { setConfirm(null); doAction(k.id, 'block'); }, danger: true })}
|
|
379
|
-
disabled={actionLoading === k.id + 'block'}
|
|
380
|
-
className="w-8 h-8 rounded-lg border border-gray-200 flex items-center justify-center text-[#9CA3AF] hover:text-red-500 hover:border-red-200 transition-colors"
|
|
381
|
-
title="Block"
|
|
382
|
-
>
|
|
383
|
-
<svg className="w-3.5 h-3.5" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><circle cx="12" cy="12" r="10"/><path d="M4.93 4.93l14.14 14.14"/></svg>
|
|
384
|
-
</button>
|
|
385
|
-
) : (
|
|
386
|
-
<button
|
|
387
|
-
onClick={() => doAction(k.id, 'unblock')}
|
|
388
|
-
disabled={actionLoading === k.id + 'unblock'}
|
|
389
|
-
className="w-8 h-8 rounded-lg border border-gray-200 flex items-center justify-center text-[#9CA3AF] hover:text-emerald-500 hover:border-emerald-200 transition-colors"
|
|
390
|
-
title="Unblock"
|
|
391
|
-
>
|
|
392
|
-
<svg className="w-3.5 h-3.5" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><circle cx="12" cy="12" r="10"/><path d="M9 12l2 2 4-4"/></svg>
|
|
393
|
-
</button>
|
|
394
|
-
)}
|
|
395
|
-
</div>
|
|
396
|
-
</td>
|
|
397
|
-
</tr>
|
|
398
|
-
);
|
|
399
|
-
})
|
|
400
|
-
)}
|
|
401
|
-
</tbody>
|
|
402
|
-
</table>
|
|
403
|
-
</div>
|
|
404
|
-
</div>
|
|
405
|
-
</div>
|
|
406
|
-
|
|
407
|
-
<Footer />
|
|
408
|
-
|
|
409
|
-
{/* Modals */}
|
|
410
|
-
{confirm && (
|
|
411
|
-
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/40 backdrop-blur-sm p-4">
|
|
412
|
-
<div className="bg-white rounded-2xl border border-gray-200 w-full max-w-sm p-6 card-shadow-md">
|
|
413
|
-
<h3 className="text-lg font-bold text-[#111827] mb-2">{confirm.title}</h3>
|
|
414
|
-
<p className="text-[#6B7280] text-sm mb-6">{confirm.message}</p>
|
|
415
|
-
<button onClick={confirm.onConfirm} className="w-full h-10 rounded-xl bg-[#5244F3] hover:bg-[#4338CA] text-white font-semibold text-sm transition-colors">OK</button>
|
|
416
|
-
</div>
|
|
417
|
-
</div>
|
|
418
|
-
)}
|
|
419
|
-
{extendKey && <ExtendDaysModal key_={extendKey} onConfirm={handleExtend} onCancel={() => setExtendKey(null)} />}
|
|
420
|
-
|
|
421
|
-
{/* Newly created toast */}
|
|
422
|
-
{newlyCreated && (
|
|
423
|
-
<div className="fixed bottom-6 right-6 z-50 bg-white rounded-2xl border border-amber-200 shadow-xl p-4 max-w-sm w-full">
|
|
424
|
-
<p className="text-amber-600 text-xs font-bold uppercase tracking-wider mb-1">Save your API key</p>
|
|
425
|
-
<p className="text-[#374151] text-sm font-medium mb-2">Copy it now — you won't see it again.</p>
|
|
426
|
-
<code className="text-purple-600 font-mono text-xs break-all block mb-2">{newlyCreated}</code>
|
|
427
|
-
<div className="flex gap-2">
|
|
428
|
-
<button onClick={() => { navigator.clipboard.writeText(newlyCreated); setNewlyCreated(''); }} className="flex-1 h-8 rounded-lg bg-purple-100 hover:bg-purple-200 text-purple-700 text-xs font-semibold transition-colors">Copy & Close</button>
|
|
429
|
-
<button onClick={() => setNewlyCreated('')} className="flex-1 h-8 rounded-lg bg-gray-100 hover:bg-gray-200 text-[#6B7280] text-xs font-semibold transition-colors">Close</button>
|
|
430
|
-
</div>
|
|
431
|
-
</div>
|
|
432
|
-
)}
|
|
433
|
-
</div>
|
|
434
|
-
);
|
|
435
|
-
}
|
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
import type { Metadata } from 'next';
|
|
2
|
-
import ResellerClient from './ResellerClient';
|
|
3
|
-
|
|
4
|
-
export const metadata: Metadata = {
|
|
5
|
-
title: 'My API Keys | ClaudMax Reseller Portal',
|
|
6
|
-
description: 'Manage your ClaudMax API keys as a reseller. Create, extend, block, and monitor usage for all your client keys from one dashboard.',
|
|
7
|
-
keywords: 'ClaudMax reseller, API key management, reseller portal, API key dashboard, manage API keys',
|
|
8
|
-
openGraph: { title: 'My API Keys | ClaudMax', description: 'Reseller portal for managing ClaudMax API keys.', type: 'website', url: 'https://claudmax.pro/reseller', siteName: 'ClaudMax' },
|
|
9
|
-
twitter: { card: 'summary_large_image', title: 'My API Keys | ClaudMax' },
|
|
10
|
-
robots: { index: false, follow: false },
|
|
11
|
-
};
|
|
12
|
-
|
|
13
|
-
export default function ResellerPage() {
|
|
14
|
-
return <ResellerClient />;
|
|
15
|
-
}
|
|
@@ -1,79 +0,0 @@
|
|
|
1
|
-
import { NextResponse } from 'next/server';
|
|
2
|
-
|
|
3
|
-
const SCRIPT = `# ClaudMax CLI Installer for Windows (PowerShell)
|
|
4
|
-
# Usage: irm https://claudmax.pro/setup.ps1 | iex
|
|
5
|
-
|
|
6
|
-
\$ErrorActionPreference = "Stop"
|
|
7
|
-
|
|
8
|
-
\$GREEN = "\x1b[32m"
|
|
9
|
-
\$RED = "\x1b[31m"
|
|
10
|
-
\$YELLOW = "\x1b[33m"
|
|
11
|
-
\$CYAN = "\x1b[36m"
|
|
12
|
-
\$ORANGE = "\x1b[33m"
|
|
13
|
-
\$BOLD = "\x1b[1m"
|
|
14
|
-
\$DIM = "\x1b[2m"
|
|
15
|
-
\$RESET = "\x1b[0m"
|
|
16
|
-
|
|
17
|
-
\$V = "\$GREEN\$V\$RESET"
|
|
18
|
-
\$X = "\$RED\$X\$RESET"
|
|
19
|
-
\$W = "\$YELLOW\$W\$RESET"
|
|
20
|
-
\$A = "\$CYAN>\$RESET"
|
|
21
|
-
|
|
22
|
-
function Write-Banner {
|
|
23
|
-
Write-Host ""
|
|
24
|
-
Write-Host "\$ORANGE\`n╔══════════════════════════════════════════════════════════╗\$RESET"
|
|
25
|
-
Write-Host "\$ORANGE║\$RESET" -NoNewline; Write-Host " \$BOLD ClaudMax Setup Wizard\$RESET" -NoNewline; Write-Host "\$ORANGE║\$RESET"
|
|
26
|
-
Write-Host "\$ORANGE╚══════════════════════════════════════════════════════════╝\$RESET"
|
|
27
|
-
Write-Host ""
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
function Test-Command(\$cmd) {
|
|
31
|
-
try { Get-Command \$cmd -ErrorAction Stop | Out-Null; return \$true } catch { return \$false }
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
Write-Banner
|
|
35
|
-
|
|
36
|
-
Write-Host "\$BOLD Checking dependencies... \$RESET"
|
|
37
|
-
|
|
38
|
-
if (Test-Command node) {
|
|
39
|
-
\$v = (node --version)
|
|
40
|
-
Write-Host " \$V Node.js \$DIM\$v\$RESET"
|
|
41
|
-
} else {
|
|
42
|
-
Write-Host " \$X Node.js \$DIMnot found\$RESET"
|
|
43
|
-
Write-Host "\$A Installing Node.js via winget..."
|
|
44
|
-
winget install OpenJS.NodeJS.LTS --accept-package-agreements --accept-source-agreements 2>\$null
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
if (Test-Command git) {
|
|
48
|
-
\$v = (git --version)
|
|
49
|
-
Write-Host " \$V Git \$DIM\$v\$RESET"
|
|
50
|
-
} else {
|
|
51
|
-
Write-Host " \$X Git \$DIMnot found\$RESET"
|
|
52
|
-
Write-Host "\$A Installing Git via winget..."
|
|
53
|
-
winget install Git.Git --accept-package-agreements --accept-source-agreements 2>\$null
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
Write-Host ""
|
|
57
|
-
|
|
58
|
-
# Refresh PATH so node/git are available
|
|
59
|
-
\$env:Path = [System.Environment]::GetEnvironmentVariable("Path","Machine") + ";" + [System.Environment]::GetEnvironmentVariable("Path","User")
|
|
60
|
-
|
|
61
|
-
Write-Host "\$A Running ClaudMax CLI installer..."
|
|
62
|
-
Write-Host ""
|
|
63
|
-
Write-Host "\$CYANGet Your API Key: https://www.claudmax.pro/support\$RESET"
|
|
64
|
-
Write-Host ""
|
|
65
|
-
|
|
66
|
-
# Run the full interactive installer via npx
|
|
67
|
-
npx claudmax
|
|
68
|
-
`;
|
|
69
|
-
|
|
70
|
-
export async function GET() {
|
|
71
|
-
return new NextResponse(SCRIPT, {
|
|
72
|
-
status: 200,
|
|
73
|
-
headers: {
|
|
74
|
-
'Content-Type': 'text/plain; charset=utf-8',
|
|
75
|
-
'Cache-Control': 'no-cache',
|
|
76
|
-
'Content-Disposition': 'attachment; filename="claudmax-setup.ps1"',
|
|
77
|
-
},
|
|
78
|
-
});
|
|
79
|
-
}
|
|
@@ -1,113 +0,0 @@
|
|
|
1
|
-
import { NextResponse } from 'next/server';
|
|
2
|
-
|
|
3
|
-
const SCRIPT = `#!/bin/bash
|
|
4
|
-
# ClaudMax CLI Installer for macOS & Linux
|
|
5
|
-
# Usage: curl -fsSL https://claudmax.pro/setup.sh | bash
|
|
6
|
-
|
|
7
|
-
set -e
|
|
8
|
-
|
|
9
|
-
BOLD='\\033[1m'
|
|
10
|
-
CYAN='\\033[0;36m'
|
|
11
|
-
GREEN='\\033[0;32m'
|
|
12
|
-
ORANGE='\\033[0;33m'
|
|
13
|
-
RED='\\033[0;31m'
|
|
14
|
-
DIM='\\033[2m'
|
|
15
|
-
MAGENTA='\\033[0;35m'
|
|
16
|
-
RESET='\\033[0m'
|
|
17
|
-
|
|
18
|
-
CHECK="\${GREEN}✓\${RESET}"
|
|
19
|
-
CROSS="\${RED}✗\${RESET}"
|
|
20
|
-
ARROW="\${MAGENTA}▶\${RESET}"
|
|
21
|
-
|
|
22
|
-
print_banner() {
|
|
23
|
-
echo ""
|
|
24
|
-
echo -e "\${CYAN}╔════════════════════════════════════════════════════╗\${RESET}"
|
|
25
|
-
echo -e "\${CYAN}║\${RESET} \${BOLD}✦ ClaudMax Setup\${RESET} \${CYAN}║\${RESET}"
|
|
26
|
-
echo -e "\${CYAN}╚════════════════════════════════════════════════════╝\${RESET}"
|
|
27
|
-
echo ""
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
check_command() {
|
|
31
|
-
if command -v "$1" &> /dev/null; then
|
|
32
|
-
local ver=$("$1" --version 2>/dev/null | head -1)
|
|
33
|
-
echo -e " \${CHECK} \${GREEN}$1\${RESET} \${DIM}\${ver}\${RESET}"
|
|
34
|
-
return 0
|
|
35
|
-
else
|
|
36
|
-
echo -e " \${CROSS} \${RED}$1\${RESET} \${DIM}not found\${RESET}"
|
|
37
|
-
return 1
|
|
38
|
-
fi
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
install_nodejs() {
|
|
42
|
-
echo -e "\${ARROW} \${BOLD}Installing Node.js...\${RESET}"
|
|
43
|
-
if command -v brew &> /dev/null; then
|
|
44
|
-
brew install node
|
|
45
|
-
elif command -v apt-get &> /dev/null; then
|
|
46
|
-
curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash - && sudo apt-get install -y nodejs
|
|
47
|
-
elif command -v yum &> /dev/null; then
|
|
48
|
-
curl -fsSL https://rpm.nodesource.com/setup_20.x | sudo bash - && sudo yum install -y nodejs
|
|
49
|
-
else
|
|
50
|
-
echo -e " \${CROSS} No package manager found. Install manually: https://nodejs.org"
|
|
51
|
-
exit 1
|
|
52
|
-
fi
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
install_git() {
|
|
56
|
-
echo -e "\${ARROW} \${BOLD}Installing Git...\${RESET}"
|
|
57
|
-
if command -v brew &> /dev/null; then
|
|
58
|
-
brew install git
|
|
59
|
-
elif command -v apt-get &> /dev/null; then
|
|
60
|
-
sudo apt-get update && sudo apt-get install -y git
|
|
61
|
-
elif command -v yum &> /dev/null; then
|
|
62
|
-
sudo yum install -y git
|
|
63
|
-
fi
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
install_claude() {
|
|
67
|
-
echo -e "\${ARROW} \${BOLD}Installing Claude Code CLI...\${RESET}"
|
|
68
|
-
npm install -g @anthropic-ai/claude-code 2>/dev/null || echo -e " \${DIM}Could not auto-install — run: npm i -g @anthropic-ai/claude-code\${RESET}"
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
print_banner
|
|
72
|
-
|
|
73
|
-
echo -e "\${BOLD}Checking dependencies...\${RESET}"
|
|
74
|
-
MISSING=""
|
|
75
|
-
|
|
76
|
-
if ! check_command node; then MISSING="\${MISSING} node"; fi
|
|
77
|
-
if ! check_command git; then MISSING="\${MISSING} git"; fi
|
|
78
|
-
if ! check_command claude; then MISSING="\${MISSING} claude"; fi
|
|
79
|
-
|
|
80
|
-
echo ""
|
|
81
|
-
|
|
82
|
-
if [[ "$MISSING" == *"node"* ]]; then
|
|
83
|
-
install_nodejs
|
|
84
|
-
fi
|
|
85
|
-
|
|
86
|
-
if [[ "$MISSING" == *"git"* ]]; then
|
|
87
|
-
install_git
|
|
88
|
-
fi
|
|
89
|
-
|
|
90
|
-
if [[ "$MISSING" == *"claude"* ]]; then
|
|
91
|
-
if command -v npm &> /dev/null; then
|
|
92
|
-
install_claude
|
|
93
|
-
fi
|
|
94
|
-
fi
|
|
95
|
-
|
|
96
|
-
echo ""
|
|
97
|
-
echo -e "\${ARROW} \${BOLD}Running ClaudMax CLI installer...\${RESET}"
|
|
98
|
-
echo ""
|
|
99
|
-
echo -e "\${CYAN}Get Your API Key: https://www.claudmax.pro/support\${RESET}"
|
|
100
|
-
echo ""
|
|
101
|
-
npx claudmax
|
|
102
|
-
`;
|
|
103
|
-
|
|
104
|
-
export async function GET() {
|
|
105
|
-
return new NextResponse(SCRIPT, {
|
|
106
|
-
status: 200,
|
|
107
|
-
headers: {
|
|
108
|
-
'Content-Type': 'text/plain; charset=utf-8',
|
|
109
|
-
'Cache-Control': 'no-cache',
|
|
110
|
-
'Content-Disposition': 'attachment; filename="claudmax-setup.sh"',
|
|
111
|
-
},
|
|
112
|
-
});
|
|
113
|
-
}
|