kentutai 1.7.6 → 1.7.7
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/package.json +2 -2
- package/src/app/(dashboard)/dashboard/access/components/UserExpandableRow.jsx +8 -0
- package/src/app/(dashboard)/dashboard/access/tabs/KeysTab.jsx +22 -55
- package/src/app/(dashboard)/dashboard/endpoint/EndpointPageClient.js +44 -17
- package/src/app/(dashboard)/dashboard/translator/page.js +10 -2
- package/src/app/(dashboard)/dashboard/users/[id]/page.js +228 -12
- package/src/app/api/keys/route.js +46 -1
- package/src/lib/db/index.js +1 -1
- package/src/lib/db/repos/apiKeysRepo.js +11 -3
- package/src/lib/db/repos/settingsRepo.js +3 -0
- package/src/lib/localDb.js +1 -1
- package/src/shared/components/Sidebar.js +1 -1
- package/src/shared/components/layouts/DashboardLayout.js +1 -1
- package/src/shared/constants/config.js +1 -1
- package/src/sse/handlers/chat.js +1 -1
package/package.json
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "kentutai",
|
|
3
|
-
"version": "1.7.
|
|
3
|
+
"version": "1.7.7",
|
|
4
4
|
"description": "KentutAI - AI Router & Token Saver CLI",
|
|
5
5
|
"private": false,
|
|
6
6
|
"bin": {
|
|
7
7
|
"kentutai": "./cli/cli.js"
|
|
8
8
|
},
|
|
9
9
|
"scripts": {
|
|
10
|
-
"postinstall": "node cli/hooks/postinstall.js",
|
|
10
|
+
"postinstall": "node cli/hooks/postinstall.js",
|
|
11
11
|
"dev": "next dev --webpack --port 20123",
|
|
12
12
|
"build": "next build",
|
|
13
13
|
"build:cli": "powershell -ExecutionPolicy Bypass -File scripts/build-cli.ps1",
|
|
@@ -171,9 +171,17 @@ export default function UserExpandableRow({ user, keyCount, onApprove, onDelete
|
|
|
171
171
|
{key.isActive ? "Active" : "Disabled"}
|
|
172
172
|
</Badge>
|
|
173
173
|
</div>
|
|
174
|
+
{key.rtkEnabled === false && (
|
|
175
|
+
<Badge variant="error" size="sm" className="mt-1">RTK: Disabled</Badge>
|
|
176
|
+
)}
|
|
174
177
|
{key.rtkEnabled === true && (
|
|
175
178
|
<Badge variant="info" size="sm" className="mt-1">RTK</Badge>
|
|
176
179
|
)}
|
|
180
|
+
{key.cavemanEnabled === false && (
|
|
181
|
+
<Badge variant="error" size="sm" className="mt-1 ml-1">
|
|
182
|
+
Caveman: Disabled
|
|
183
|
+
</Badge>
|
|
184
|
+
)}
|
|
177
185
|
{key.cavemanEnabled === true && (
|
|
178
186
|
<Badge variant="warning" size="sm" className="mt-1 ml-1">
|
|
179
187
|
Caveman{key.cavemanLevel ? `: ${key.cavemanLevel}` : ""}
|
|
@@ -361,10 +361,6 @@ export default function KeysTab() {
|
|
|
361
361
|
: "Global"
|
|
362
362
|
}
|
|
363
363
|
options={[
|
|
364
|
-
{
|
|
365
|
-
value: "",
|
|
366
|
-
label: `Global${globalSettings ? ` (${globalSettings.rtkEnabled !== false ? "On" : "Off"})` : ""}`,
|
|
367
|
-
},
|
|
368
364
|
{ value: "on", label: "Enabled" },
|
|
369
365
|
{ value: "off", label: "Disabled" },
|
|
370
366
|
]}
|
|
@@ -373,73 +369,42 @@ export default function KeysTab() {
|
|
|
373
369
|
title="RTK: Reduce input tokens for git/grep/ls/tree/logs"
|
|
374
370
|
/>
|
|
375
371
|
</div>
|
|
376
|
-
|
|
372
|
+
<div className="flex flex-col gap-0.5">
|
|
377
373
|
<span className="text-[10px] text-text-muted uppercase">Caveman</span>
|
|
378
374
|
<Select
|
|
379
375
|
value={
|
|
380
376
|
key.cavemanEnabled === null
|
|
381
377
|
? ""
|
|
382
|
-
: key.cavemanEnabled
|
|
383
|
-
? "
|
|
384
|
-
: "
|
|
378
|
+
: key.cavemanEnabled === false
|
|
379
|
+
? "disabled"
|
|
380
|
+
: key.cavemanLevel || "full"
|
|
385
381
|
}
|
|
386
382
|
onChange={(e) => {
|
|
387
|
-
const val =
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
383
|
+
const val = e.target.value;
|
|
384
|
+
if (val === "") {
|
|
385
|
+
handlePatchKey(key.id, { cavemanEnabled: null, cavemanLevel: null });
|
|
386
|
+
} else if (val === "disabled") {
|
|
387
|
+
handlePatchKey(key.id, { cavemanEnabled: false, cavemanLevel: null });
|
|
388
|
+
} else {
|
|
389
|
+
handlePatchKey(key.id, { cavemanEnabled: true, cavemanLevel: val });
|
|
390
|
+
}
|
|
392
391
|
}}
|
|
393
392
|
placeholder={
|
|
394
393
|
globalSettings
|
|
395
|
-
? `Global (${globalSettings.cavemanEnabled ? "
|
|
394
|
+
? `Global (${globalSettings.cavemanEnabled ? globalSettings.cavemanLevel || "full" : "Off"})`
|
|
396
395
|
: "Global"
|
|
397
396
|
}
|
|
398
397
|
options={[
|
|
399
|
-
{
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
},
|
|
403
|
-
{ value: "on", label: "Enabled" },
|
|
404
|
-
{ value: "off", label: "Disabled" },
|
|
398
|
+
{ value: "disabled", label: "Disabled" },
|
|
399
|
+
{ value: "lite", label: "Lite" },
|
|
400
|
+
{ value: "full", label: "Full" },
|
|
401
|
+
{ value: "ultra", label: "Ultra" },
|
|
405
402
|
]}
|
|
406
403
|
className="min-w-[110px]"
|
|
407
404
|
selectClassName="!py-1.5 !text-xs"
|
|
408
|
-
title="Caveman: Compress LLM output tokens"
|
|
405
|
+
title="Caveman: Compress LLM output tokens (select level to enable)"
|
|
409
406
|
/>
|
|
410
407
|
</div>
|
|
411
|
-
{(key.cavemanEnabled === true ||
|
|
412
|
-
(key.cavemanEnabled === null &&
|
|
413
|
-
globalSettings?.cavemanEnabled)) && (
|
|
414
|
-
<div className="flex flex-col gap-0.5">
|
|
415
|
-
<span className="text-[10px] text-text-muted uppercase">Level</span>
|
|
416
|
-
<Select
|
|
417
|
-
value={key.cavemanLevel ?? ""}
|
|
418
|
-
onChange={(e) => {
|
|
419
|
-
handlePatchKey(key.id, {
|
|
420
|
-
cavemanLevel: e.target.value || null,
|
|
421
|
-
});
|
|
422
|
-
}}
|
|
423
|
-
placeholder={
|
|
424
|
-
globalSettings?.cavemanLevel
|
|
425
|
-
? `Global (${globalSettings.cavemanLevel})`
|
|
426
|
-
: "Global (full)"
|
|
427
|
-
}
|
|
428
|
-
options={[
|
|
429
|
-
{
|
|
430
|
-
value: "",
|
|
431
|
-
label: `Global${globalSettings?.cavemanLevel ? ` (${globalSettings.cavemanLevel})` : " (full)"}`,
|
|
432
|
-
},
|
|
433
|
-
{ value: "lite", label: "Lite" },
|
|
434
|
-
{ value: "full", label: "Full" },
|
|
435
|
-
{ value: "ultra", label: "Ultra" },
|
|
436
|
-
]}
|
|
437
|
-
className="min-w-[100px]"
|
|
438
|
-
selectClassName="!py-1.5 !text-xs"
|
|
439
|
-
title="Caveman compression level"
|
|
440
|
-
/>
|
|
441
|
-
</div>
|
|
442
|
-
)}
|
|
443
408
|
</div>
|
|
444
409
|
|
|
445
410
|
{/* Actions */}
|
|
@@ -452,9 +417,10 @@ export default function KeysTab() {
|
|
|
452
417
|
<Button
|
|
453
418
|
size="sm"
|
|
454
419
|
variant="ghost"
|
|
455
|
-
icon="content_copy"
|
|
420
|
+
icon={copied === key.key ? "check" : "content_copy"}
|
|
456
421
|
title="Copy key"
|
|
457
422
|
onClick={() => copy(key.key)}
|
|
423
|
+
className={copied === key.key ? "text-green-500" : ""}
|
|
458
424
|
>
|
|
459
425
|
{copied === key.key ? "Copied" : "Copy"}
|
|
460
426
|
</Button>
|
|
@@ -521,9 +487,10 @@ export default function KeysTab() {
|
|
|
521
487
|
</Card.Section>
|
|
522
488
|
<Button
|
|
523
489
|
variant="outline"
|
|
524
|
-
icon="content_copy"
|
|
490
|
+
icon={copied === newKeyPreview.key ? "check" : "content_copy"}
|
|
525
491
|
fullWidth
|
|
526
492
|
onClick={() => copy(newKeyPreview.key)}
|
|
493
|
+
className={copied === newKeyPreview.key ? "text-green-500" : ""}
|
|
527
494
|
>
|
|
528
495
|
{copied === newKeyPreview.key ? "Copied" : "Copy Key"}
|
|
529
496
|
</Button>
|
|
@@ -228,9 +228,28 @@ export default function APIPageClient({ machineId, isAdmin = false }) {
|
|
|
228
228
|
setRequireLogin(data.requireLogin !== false);
|
|
229
229
|
setHasPassword(data.hasPassword || false);
|
|
230
230
|
setTunnelDashboardAccess(data.tunnelDashboardAccess || false);
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
231
|
+
|
|
232
|
+
if (isAdmin) {
|
|
233
|
+
setRtkEnabledState(data.rtkEnabled !== false);
|
|
234
|
+
setCavemanEnabled(!!data.cavemanEnabled);
|
|
235
|
+
setCavemanLevel(data.cavemanLevel || "full");
|
|
236
|
+
} else {
|
|
237
|
+
const keysRes = await fetch("/api/keys");
|
|
238
|
+
if (keysRes.ok) {
|
|
239
|
+
const keysData = await keysRes.json();
|
|
240
|
+
const keys = keysData.keys || [];
|
|
241
|
+
if (keys.length > 0) {
|
|
242
|
+
const firstKey = keys[0];
|
|
243
|
+
setRtkEnabledState(firstKey.rtkEnabled !== null ? firstKey.rtkEnabled : (data.rtkEnabled !== false));
|
|
244
|
+
setCavemanEnabled(firstKey.cavemanEnabled !== null ? firstKey.cavemanEnabled : !!data.cavemanEnabled);
|
|
245
|
+
setCavemanLevel(firstKey.cavemanLevel || data.cavemanLevel || "full");
|
|
246
|
+
} else {
|
|
247
|
+
setRtkEnabledState(data.userRtkEnabled !== undefined ? data.userRtkEnabled : (data.rtkEnabled !== false));
|
|
248
|
+
setCavemanEnabled(data.userCavemanEnabled !== undefined ? data.userCavemanEnabled : !!data.cavemanEnabled);
|
|
249
|
+
setCavemanLevel(data.userCavemanLevel || data.cavemanLevel || "full");
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
}
|
|
234
253
|
}
|
|
235
254
|
if (statusRes.ok) {
|
|
236
255
|
const data = await statusRes.json();
|
|
@@ -283,7 +302,8 @@ export default function APIPageClient({ machineId, isAdmin = false }) {
|
|
|
283
302
|
|
|
284
303
|
const handleRtkEnabled = async (value) => {
|
|
285
304
|
try {
|
|
286
|
-
const
|
|
305
|
+
const endpoint = isAdmin ? "/api/settings" : "/api/keys";
|
|
306
|
+
const res = await fetch(endpoint, {
|
|
287
307
|
method: "PATCH",
|
|
288
308
|
headers: { "Content-Type": "application/json" },
|
|
289
309
|
body: JSON.stringify({ rtkEnabled: value }),
|
|
@@ -296,24 +316,27 @@ export default function APIPageClient({ machineId, isAdmin = false }) {
|
|
|
296
316
|
|
|
297
317
|
const patchSetting = async (patch) => {
|
|
298
318
|
try {
|
|
299
|
-
|
|
319
|
+
const endpoint = isAdmin ? "/api/settings" : "/api/keys";
|
|
320
|
+
const res = await fetch(endpoint, {
|
|
300
321
|
method: "PATCH",
|
|
301
322
|
headers: { "Content-Type": "application/json" },
|
|
302
323
|
body: JSON.stringify(patch),
|
|
303
324
|
});
|
|
325
|
+
return res.ok;
|
|
304
326
|
} catch (error) {
|
|
305
327
|
console.log("Error updating setting:", error);
|
|
328
|
+
return false;
|
|
306
329
|
}
|
|
307
330
|
};
|
|
308
331
|
|
|
309
|
-
const handleCavemanEnabled = (value) => {
|
|
310
|
-
|
|
311
|
-
|
|
332
|
+
const handleCavemanEnabled = async (value) => {
|
|
333
|
+
const ok = await patchSetting({ cavemanEnabled: value });
|
|
334
|
+
if (ok) setCavemanEnabled(value);
|
|
312
335
|
};
|
|
313
336
|
|
|
314
|
-
const handleCavemanLevel = (level) => {
|
|
315
|
-
|
|
316
|
-
|
|
337
|
+
const handleCavemanLevel = async (level) => {
|
|
338
|
+
const ok = await patchSetting({ cavemanLevel: level });
|
|
339
|
+
if (ok) setCavemanLevel(level);
|
|
317
340
|
};
|
|
318
341
|
|
|
319
342
|
// u2500u2500u2500 Cloudflare Tunnel handlers
|
|
@@ -691,7 +714,7 @@ export default function APIPageClient({ machineId, isAdmin = false }) {
|
|
|
691
714
|
API Endpoint
|
|
692
715
|
</h2>
|
|
693
716
|
|
|
694
|
-
|
|
717
|
+
{/* Endpoint rows */}
|
|
695
718
|
<div className="flex flex-col gap-2">
|
|
696
719
|
{/* Local */}
|
|
697
720
|
<EndpointRow
|
|
@@ -701,11 +724,12 @@ export default function APIPageClient({ machineId, isAdmin = false }) {
|
|
|
701
724
|
copied={copied}
|
|
702
725
|
onCopy={copy}
|
|
703
726
|
/>
|
|
704
|
-
{/* Cloudflare Tunnel */}
|
|
727
|
+
{/* Cloudflare Tunnel - Admin only */}
|
|
728
|
+
{isAdmin && (
|
|
705
729
|
<div className="flex items-center gap-2">
|
|
706
730
|
<span className={`text-xs font-mono px-1.5 py-0.5 rounded shrink-0 min-w-[88px] text-center ${
|
|
707
731
|
tunnelEnabled ? "bg-primary/10 text-primary" : "bg-surface-2 text-text-muted"
|
|
708
|
-
}`}>Tunnel</span>
|
|
732
|
+
}`}>Tunnel</span>
|
|
709
733
|
{tunnelEnabled && !tunnelLoading && tunnelReachable ? (
|
|
710
734
|
<>
|
|
711
735
|
<Input value={`${tunnelPublicUrl || tunnelUrl}/v1`} readOnly className="flex-1 font-mono text-sm" />
|
|
@@ -793,7 +817,9 @@ export default function APIPageClient({ machineId, isAdmin = false }) {
|
|
|
793
817
|
</Button>
|
|
794
818
|
)}
|
|
795
819
|
</div>
|
|
796
|
-
|
|
820
|
+
)}
|
|
821
|
+
{/* Tailscale - Admin only */}
|
|
822
|
+
{isAdmin && (
|
|
797
823
|
<div className="flex items-center gap-2">
|
|
798
824
|
<span className={`text-xs font-mono px-1.5 py-0.5 rounded shrink-0 min-w-[88px] text-center ${
|
|
799
825
|
tsEnabled ? "bg-primary/10 text-primary" : "bg-surface-2 text-text-muted"
|
|
@@ -877,6 +903,7 @@ export default function APIPageClient({ machineId, isAdmin = false }) {
|
|
|
877
903
|
</Button>
|
|
878
904
|
)}
|
|
879
905
|
</div>
|
|
906
|
+
)}
|
|
880
907
|
</div>
|
|
881
908
|
|
|
882
909
|
{/* Pre-enable security gate banner */}
|
|
@@ -914,8 +941,8 @@ export default function APIPageClient({ machineId, isAdmin = false }) {
|
|
|
914
941
|
</div>
|
|
915
942
|
)}
|
|
916
943
|
|
|
917
|
-
{/* Tunnel dashboard access option */}
|
|
918
|
-
{(tunnelEnabled || tsEnabled) && (
|
|
944
|
+
{/* Tunnel dashboard access option - Admin only */}
|
|
945
|
+
{isAdmin && (tunnelEnabled || tsEnabled) && (
|
|
919
946
|
<div className="mt-4 pt-4 border-t border-border flex items-center gap-3">
|
|
920
947
|
<Toggle
|
|
921
948
|
checked={tunnelDashboardAccess}
|
|
@@ -188,7 +188,7 @@ export default function TranslatorPage() {
|
|
|
188
188
|
}
|
|
189
189
|
};
|
|
190
190
|
|
|
191
|
-
const { copy } = useCopyToClipboard();
|
|
191
|
+
const { copied, copy } = useCopyToClipboard();
|
|
192
192
|
|
|
193
193
|
const handleCopy = async (id) => {
|
|
194
194
|
if (!contents[id]) return;
|
|
@@ -275,7 +275,15 @@ export default function TranslatorPage() {
|
|
|
275
275
|
<div className="flex gap-2 flex-wrap">
|
|
276
276
|
<Button size="sm" variant="outline" icon="folder_open" loading={loading[`load-${step.id}`]} onClick={() => handleLoad(step.id)}>Load</Button>
|
|
277
277
|
<Button size="sm" variant="outline" icon="data_object" onClick={() => handleFormat(step.id)}>Format</Button>
|
|
278
|
-
<Button
|
|
278
|
+
<Button
|
|
279
|
+
size="sm"
|
|
280
|
+
variant="outline"
|
|
281
|
+
icon={copied === `translator-step-${step.id}` ? "check" : "content_copy"}
|
|
282
|
+
onClick={() => handleCopy(step.id)}
|
|
283
|
+
className={copied === `translator-step-${step.id}` ? "text-green-500" : ""}
|
|
284
|
+
>
|
|
285
|
+
Copy
|
|
286
|
+
</Button>
|
|
279
287
|
{action}
|
|
280
288
|
</div>
|
|
281
289
|
</>
|
|
@@ -5,6 +5,7 @@ import { Card, Button, Badge, Toggle, Input, CardSkeleton, Modal } from "@/share
|
|
|
5
5
|
import { useParams } from "next/navigation";
|
|
6
6
|
import Link from "next/link";
|
|
7
7
|
import { format } from "date-fns";
|
|
8
|
+
import { useCopyToClipboard } from "@/shared/hooks/useCopyToClipboard";
|
|
8
9
|
|
|
9
10
|
export default function UserDetailPage() {
|
|
10
11
|
const params = useParams();
|
|
@@ -24,6 +25,15 @@ export default function UserDetailPage() {
|
|
|
24
25
|
const [packageSaving, setPackageSaving] = useState(false);
|
|
25
26
|
const [packageMessage, setPackageMessage] = useState("");
|
|
26
27
|
|
|
28
|
+
const [userKeys, setUserKeys] = useState([]);
|
|
29
|
+
const [keysLoading, setKeysLoading] = useState(true);
|
|
30
|
+
const [showKeyModal, setShowKeyModal] = useState(false);
|
|
31
|
+
const [newKeyName, setNewKeyName] = useState("");
|
|
32
|
+
const [newKeyComment, setNewKeyComment] = useState("");
|
|
33
|
+
const [creatingKey, setCreatingKey] = useState(false);
|
|
34
|
+
const [newKeyPreview, setNewKeyPreview] = useState(null);
|
|
35
|
+
const { copied, copy } = useCopyToClipboard(2000);
|
|
36
|
+
|
|
27
37
|
const fetchData = useCallback(async () => {
|
|
28
38
|
try {
|
|
29
39
|
setError("");
|
|
@@ -47,8 +57,6 @@ export default function UserDetailPage() {
|
|
|
47
57
|
setLimits(limitsData.limits);
|
|
48
58
|
}
|
|
49
59
|
|
|
50
|
-
|
|
51
|
-
|
|
52
60
|
if (pkgRes.ok) {
|
|
53
61
|
const pkgData = await pkgRes.json();
|
|
54
62
|
setUserPackage(pkgData.userPackage);
|
|
@@ -61,6 +69,70 @@ export default function UserDetailPage() {
|
|
|
61
69
|
}
|
|
62
70
|
}, [id]);
|
|
63
71
|
|
|
72
|
+
const fetchKeys = useCallback(async () => {
|
|
73
|
+
try {
|
|
74
|
+
setKeysLoading(true);
|
|
75
|
+
const res = await fetch(`/api/keys?userId=${encodeURIComponent(id)}`);
|
|
76
|
+
if (res.ok) {
|
|
77
|
+
const data = await res.json();
|
|
78
|
+
setUserKeys(data.keys || []);
|
|
79
|
+
}
|
|
80
|
+
} catch {
|
|
81
|
+
// keys are optional display
|
|
82
|
+
} finally {
|
|
83
|
+
setKeysLoading(false);
|
|
84
|
+
}
|
|
85
|
+
}, [id]);
|
|
86
|
+
|
|
87
|
+
useEffect(() => {
|
|
88
|
+
fetchKeys();
|
|
89
|
+
}, [fetchKeys]);
|
|
90
|
+
|
|
91
|
+
const handleCreateKey = async () => {
|
|
92
|
+
if (!newKeyName.trim()) return;
|
|
93
|
+
setCreatingKey(true);
|
|
94
|
+
try {
|
|
95
|
+
const body = { name: newKeyName.trim(), userId: id };
|
|
96
|
+
if (newKeyComment.trim()) body.comment = newKeyComment.trim();
|
|
97
|
+
const res = await fetch("/api/keys", {
|
|
98
|
+
method: "POST",
|
|
99
|
+
headers: { "Content-Type": "application/json" },
|
|
100
|
+
body: JSON.stringify(body),
|
|
101
|
+
});
|
|
102
|
+
if (!res.ok) {
|
|
103
|
+
const data = await res.json();
|
|
104
|
+
throw new Error(data.error || "Failed to create key");
|
|
105
|
+
}
|
|
106
|
+
const data = await res.json();
|
|
107
|
+
setNewKeyPreview(data);
|
|
108
|
+
setNewKeyName("");
|
|
109
|
+
setNewKeyComment("");
|
|
110
|
+
await fetchKeys();
|
|
111
|
+
} catch (err) {
|
|
112
|
+
setError(err.message);
|
|
113
|
+
} finally {
|
|
114
|
+
setCreatingKey(false);
|
|
115
|
+
}
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
const handleDeleteKey = async (keyId) => {
|
|
119
|
+
try {
|
|
120
|
+
const res = await fetch(`/api/keys/${keyId}`, { method: "DELETE" });
|
|
121
|
+
if (res.ok) {
|
|
122
|
+
await fetchKeys();
|
|
123
|
+
}
|
|
124
|
+
} catch {
|
|
125
|
+
// error handled silently
|
|
126
|
+
}
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
const closeKeyModal = () => {
|
|
130
|
+
setShowKeyModal(false);
|
|
131
|
+
setNewKeyPreview(null);
|
|
132
|
+
setNewKeyName("");
|
|
133
|
+
setNewKeyComment("");
|
|
134
|
+
};
|
|
135
|
+
|
|
64
136
|
useEffect(() => {
|
|
65
137
|
fetchData();
|
|
66
138
|
}, [fetchData]);
|
|
@@ -495,16 +567,86 @@ export default function UserDetailPage() {
|
|
|
495
567
|
</div>
|
|
496
568
|
</Card>
|
|
497
569
|
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
570
|
+
<Card
|
|
571
|
+
title="API Keys"
|
|
572
|
+
icon="vpn_key"
|
|
573
|
+
className="cursor-pointer hover:bg-surface-2 transition-colors"
|
|
574
|
+
>
|
|
575
|
+
<div className="flex items-center justify-between mb-4">
|
|
576
|
+
<p className="text-sm text-text-muted">
|
|
577
|
+
{keysLoading
|
|
578
|
+
? "Loading keys..."
|
|
579
|
+
: userKeys.length === 0
|
|
580
|
+
? "No API keys yet"
|
|
581
|
+
: `${userKeys.length} key${userKeys.length !== 1 ? "s" : ""}`}
|
|
582
|
+
</p>
|
|
583
|
+
<Button
|
|
584
|
+
size="sm"
|
|
585
|
+
variant="outline"
|
|
586
|
+
icon="add"
|
|
587
|
+
onClick={() => {
|
|
588
|
+
setNewKeyPreview(null);
|
|
589
|
+
setShowKeyModal(true);
|
|
590
|
+
}}
|
|
591
|
+
>
|
|
592
|
+
Add Key
|
|
593
|
+
</Button>
|
|
594
|
+
</div>
|
|
595
|
+
|
|
596
|
+
{!keysLoading && userKeys.length > 0 && (
|
|
597
|
+
<div className="flex flex-col gap-2">
|
|
598
|
+
{userKeys.slice(0, 3).map((key) => (
|
|
599
|
+
<div
|
|
600
|
+
key={key.id}
|
|
601
|
+
className="flex items-center justify-between p-3 rounded-lg bg-bg/50"
|
|
602
|
+
>
|
|
603
|
+
<div className="flex flex-col gap-1 min-w-0 flex-1">
|
|
604
|
+
<div className="flex items-center gap-2">
|
|
605
|
+
<span className="text-sm text-text-main font-medium">
|
|
606
|
+
{key.name}
|
|
607
|
+
</span>
|
|
608
|
+
<Badge
|
|
609
|
+
variant={key.isActive ? "success" : "error"}
|
|
610
|
+
size="sm"
|
|
611
|
+
dot
|
|
612
|
+
>
|
|
613
|
+
{key.isActive ? "Active" : "Off"}
|
|
614
|
+
</Badge>
|
|
615
|
+
</div>
|
|
616
|
+
<div className="flex items-center gap-2">
|
|
617
|
+
<code className="text-xs font-mono text-text-muted truncate">
|
|
618
|
+
{key.key}
|
|
619
|
+
</code>
|
|
620
|
+
</div>
|
|
621
|
+
</div>
|
|
622
|
+
<div className="flex items-center gap-1 shrink-0">
|
|
623
|
+
<Button
|
|
624
|
+
size="sm"
|
|
625
|
+
variant="ghost"
|
|
626
|
+
icon={copied === key.key ? "check" : "content_copy"}
|
|
627
|
+
onClick={() => copy(key.key)}
|
|
628
|
+
className={copied === key.key ? "text-green-500" : ""}
|
|
629
|
+
/>
|
|
630
|
+
<Button
|
|
631
|
+
size="sm"
|
|
632
|
+
variant="ghost"
|
|
633
|
+
icon="delete"
|
|
634
|
+
onClick={() => handleDeleteKey(key.id)}
|
|
635
|
+
/>
|
|
636
|
+
</div>
|
|
637
|
+
</div>
|
|
638
|
+
))}
|
|
639
|
+
{userKeys.length > 3 && (
|
|
640
|
+
<Link
|
|
641
|
+
href="/dashboard/access?tab=keys"
|
|
642
|
+
className="text-xs text-primary hover:underline text-center"
|
|
643
|
+
>
|
|
644
|
+
View all {userKeys.length} keys
|
|
645
|
+
</Link>
|
|
646
|
+
)}
|
|
647
|
+
</div>
|
|
648
|
+
)}
|
|
649
|
+
</Card>
|
|
508
650
|
|
|
509
651
|
<PackageSelectorModal
|
|
510
652
|
isOpen={showPackageModal}
|
|
@@ -517,6 +659,80 @@ export default function UserDetailPage() {
|
|
|
517
659
|
onSave={handleSavePackage}
|
|
518
660
|
saving={packageSaving}
|
|
519
661
|
/>
|
|
662
|
+
|
|
663
|
+
<Modal
|
|
664
|
+
isOpen={showKeyModal}
|
|
665
|
+
onClose={closeKeyModal}
|
|
666
|
+
title={
|
|
667
|
+
newKeyPreview && newKeyPreview.key
|
|
668
|
+
? "Key Created"
|
|
669
|
+
: "Create API Key"
|
|
670
|
+
}
|
|
671
|
+
size="sm"
|
|
672
|
+
footer={
|
|
673
|
+
newKeyPreview && newKeyPreview.key ? (
|
|
674
|
+
<Button variant="primary" onClick={closeKeyModal}>
|
|
675
|
+
Done
|
|
676
|
+
</Button>
|
|
677
|
+
) : (
|
|
678
|
+
<>
|
|
679
|
+
<Button variant="ghost" onClick={closeKeyModal} disabled={creatingKey}>
|
|
680
|
+
Cancel
|
|
681
|
+
</Button>
|
|
682
|
+
<Button
|
|
683
|
+
variant="primary"
|
|
684
|
+
onClick={handleCreateKey}
|
|
685
|
+
loading={creatingKey}
|
|
686
|
+
disabled={!newKeyName.trim()}
|
|
687
|
+
>
|
|
688
|
+
Create
|
|
689
|
+
</Button>
|
|
690
|
+
</>
|
|
691
|
+
)
|
|
692
|
+
}
|
|
693
|
+
>
|
|
694
|
+
{newKeyPreview && newKeyPreview.key ? (
|
|
695
|
+
<div className="flex flex-col gap-4">
|
|
696
|
+
<p className="text-sm text-text-muted">
|
|
697
|
+
Your API key has been created. Copy it now — you won't be
|
|
698
|
+
able to see it again.
|
|
699
|
+
</p>
|
|
700
|
+
<div className="p-3 rounded-lg bg-bg border border-border-subtle">
|
|
701
|
+
<code className="text-sm font-mono break-all text-text-main">
|
|
702
|
+
{newKeyPreview.key}
|
|
703
|
+
</code>
|
|
704
|
+
</div>
|
|
705
|
+
<Button
|
|
706
|
+
variant="outline"
|
|
707
|
+
icon={copied === newKeyPreview.key ? "check" : "content_copy"}
|
|
708
|
+
fullWidth
|
|
709
|
+
onClick={() => copy(newKeyPreview.key)}
|
|
710
|
+
className={copied === newKeyPreview.key ? "text-green-500" : ""}
|
|
711
|
+
>
|
|
712
|
+
Copy Key
|
|
713
|
+
</Button>
|
|
714
|
+
</div>
|
|
715
|
+
) : (
|
|
716
|
+
<div className="flex flex-col gap-4">
|
|
717
|
+
<Input
|
|
718
|
+
label="Key Name"
|
|
719
|
+
placeholder="e.g. Claude Code, Cursor"
|
|
720
|
+
value={newKeyName}
|
|
721
|
+
onChange={(e) => setNewKeyName(e.target.value)}
|
|
722
|
+
required
|
|
723
|
+
hint="A label to help you identify this key"
|
|
724
|
+
autoFocus
|
|
725
|
+
/>
|
|
726
|
+
<Input
|
|
727
|
+
label="Comment (optional)"
|
|
728
|
+
placeholder="e.g. Production server, Testing"
|
|
729
|
+
value={newKeyComment}
|
|
730
|
+
onChange={(e) => setNewKeyComment(e.target.value)}
|
|
731
|
+
hint="Notes to help you remember this key's purpose"
|
|
732
|
+
/>
|
|
733
|
+
</div>
|
|
734
|
+
)}
|
|
735
|
+
</Modal>
|
|
520
736
|
</div>
|
|
521
737
|
);
|
|
522
738
|
}
|
|
@@ -64,6 +64,45 @@ export async function GET(request) {
|
|
|
64
64
|
}
|
|
65
65
|
}
|
|
66
66
|
|
|
67
|
+
// PATCH /api/keys — Update all user's keys with RTK/Caveman settings
|
|
68
|
+
export async function PATCH(request) {
|
|
69
|
+
try {
|
|
70
|
+
const userId = await getUserIdFromJwt();
|
|
71
|
+
if (!userId) {
|
|
72
|
+
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const body = await request.json();
|
|
76
|
+
const { rtkEnabled, cavemanEnabled, cavemanLevel } = body;
|
|
77
|
+
|
|
78
|
+
const { updateApiKeysByUserId } = await import("@/lib/localDb");
|
|
79
|
+
const updates = {};
|
|
80
|
+
if (rtkEnabled !== undefined) updates.rtkEnabled = rtkEnabled;
|
|
81
|
+
if (cavemanEnabled !== undefined) updates.cavemanEnabled = cavemanEnabled;
|
|
82
|
+
if (cavemanLevel !== undefined) updates.cavemanLevel = cavemanLevel;
|
|
83
|
+
|
|
84
|
+
if (Object.keys(updates).length === 0) {
|
|
85
|
+
return NextResponse.json({ error: "No updates provided" }, { status: 400 });
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
await updateApiKeysByUserId(userId, updates);
|
|
89
|
+
|
|
90
|
+
const { updateSettings, getSettings } = await import("@/lib/localDb");
|
|
91
|
+
const userDefaults = {};
|
|
92
|
+
if (rtkEnabled !== undefined) userDefaults.userRtkEnabled = rtkEnabled;
|
|
93
|
+
if (cavemanEnabled !== undefined) userDefaults.userCavemanEnabled = cavemanEnabled;
|
|
94
|
+
if (cavemanLevel !== undefined) userDefaults.userCavemanLevel = cavemanLevel;
|
|
95
|
+
if (Object.keys(userDefaults).length > 0) {
|
|
96
|
+
await updateSettings(userDefaults);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return NextResponse.json({ success: true });
|
|
100
|
+
} catch (error) {
|
|
101
|
+
console.error("Error updating keys:", error);
|
|
102
|
+
return NextResponse.json({ error: "Failed to update keys" }, { status: 500 });
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
67
106
|
// POST /api/keys — Create API key (admin can specify userId + comment)
|
|
68
107
|
export async function POST(request) {
|
|
69
108
|
try {
|
|
@@ -94,7 +133,13 @@ export async function POST(request) {
|
|
|
94
133
|
}
|
|
95
134
|
|
|
96
135
|
const machineId = await getConsistentMachineId();
|
|
97
|
-
const
|
|
136
|
+
const { getSettings } = await import("@/lib/localDb");
|
|
137
|
+
const settings = await getSettings();
|
|
138
|
+
const apiKey = await createApiKey(name, targetUserId, machineId, comment ?? "", {
|
|
139
|
+
rtkEnabled: settings.userRtkEnabled ?? false,
|
|
140
|
+
cavemanEnabled: settings.userCavemanEnabled ?? false,
|
|
141
|
+
cavemanLevel: settings.userCavemanLevel ?? null,
|
|
142
|
+
});
|
|
98
143
|
|
|
99
144
|
return NextResponse.json({
|
|
100
145
|
key: apiKey.key,
|
package/src/lib/db/index.js
CHANGED
|
@@ -30,7 +30,7 @@ export {
|
|
|
30
30
|
// API keys
|
|
31
31
|
export {
|
|
32
32
|
getApiKeys, getApiKeyById, createApiKey, updateApiKey, deleteApiKey, validateApiKey,
|
|
33
|
-
findByUserId, findByIdAndUserId, deleteApiKeyForUser, getApiKeyWithSettings,
|
|
33
|
+
findByUserId, findByIdAndUserId, deleteApiKeyForUser, getApiKeyWithSettings, updateApiKeysByUserId,
|
|
34
34
|
} from "./repos/apiKeysRepo.js";
|
|
35
35
|
|
|
36
36
|
// Combos
|
|
@@ -71,11 +71,11 @@ export async function createApiKey(name, userId, machineId, comment = "", option
|
|
|
71
71
|
export async function updateApiKey(id, data) {
|
|
72
72
|
const db = await getAdapter();
|
|
73
73
|
let result = null;
|
|
74
|
-
|
|
75
|
-
const row =
|
|
74
|
+
db.transaction(() => {
|
|
75
|
+
const row = db.get(`SELECT * FROM apiKeys WHERE id = ?`, [id]);
|
|
76
76
|
if (!row) return;
|
|
77
77
|
const merged = { ...rowToKey(row), ...data };
|
|
78
|
-
|
|
78
|
+
db.run(
|
|
79
79
|
`UPDATE apiKeys SET key = ?, name = ?, machineId = ?, userId = ?, comment = ?, isActive = ?, rtkEnabled = ?, cavemanEnabled = ?, cavemanLevel = ? WHERE id = ?`,
|
|
80
80
|
[merged.key, merged.name, merged.machineId, merged.userId, merged.comment ?? "", merged.isActive ? 1 : 0, merged.rtkEnabled === null || merged.rtkEnabled === undefined ? null : (merged.rtkEnabled ? 1 : 0), merged.cavemanEnabled === null || merged.cavemanEnabled === undefined ? null : (merged.cavemanEnabled ? 1 : 0), merged.cavemanLevel ?? null, id]
|
|
81
81
|
);
|
|
@@ -108,3 +108,11 @@ export async function getApiKeyWithSettings(key) {
|
|
|
108
108
|
const row = await db.get(`SELECT * FROM apiKeys WHERE key = ?`, [key]);
|
|
109
109
|
return rowToKey(row);
|
|
110
110
|
}
|
|
111
|
+
|
|
112
|
+
export async function updateApiKeysByUserId(userId, updates) {
|
|
113
|
+
const db = await getAdapter();
|
|
114
|
+
const keys = await findByUserId(userId);
|
|
115
|
+
for (const key of keys) {
|
|
116
|
+
await updateApiKey(key.id, { ...key, ...updates });
|
|
117
|
+
}
|
|
118
|
+
}
|
package/src/lib/localDb.js
CHANGED
|
@@ -12,7 +12,7 @@ export {
|
|
|
12
12
|
createProxyPool, updateProxyPool, deleteProxyPool,
|
|
13
13
|
getApiKeys, getApiKeyById, createApiKey, updateApiKey, deleteApiKey, validateApiKey,
|
|
14
14
|
findByUserId, findByIdAndUserId, deleteApiKeyForUser,
|
|
15
|
-
getApiKeyWithSettings,
|
|
15
|
+
getApiKeyWithSettings, updateApiKeysByUserId,
|
|
16
16
|
getCombos, getComboById, getComboByName,
|
|
17
17
|
createCombo, updateCombo, deleteCombo,
|
|
18
18
|
getModelAliases, setModelAlias, deleteModelAlias,
|
|
@@ -403,7 +403,7 @@ export default function Sidebar({ onClose }) {
|
|
|
403
403
|
|
|
404
404
|
return (
|
|
405
405
|
<>
|
|
406
|
-
<aside className="flex w-72 flex-col border-r border-border-subtle bg-vibrancy backdrop-blur-xl transition-colors duration-300
|
|
406
|
+
<aside className="flex w-72 flex-col border-r border-border-subtle bg-vibrancy backdrop-blur-xl transition-colors duration-300 h-full">
|
|
407
407
|
<SidebarHeader
|
|
408
408
|
updateInfo={updateInfo}
|
|
409
409
|
copied={copied}
|
|
@@ -9,7 +9,7 @@ export const APP_CONFIG = {
|
|
|
9
9
|
|
|
10
10
|
// GitHub configuration
|
|
11
11
|
export const GITHUB_CONFIG = {
|
|
12
|
-
changelogUrl: "https://raw.githubusercontent.com/
|
|
12
|
+
changelogUrl: "https://raw.githubusercontent.com/decolua/kentutai/refs/heads/master/CHANGELOG.md",
|
|
13
13
|
donateUrl: "https://kentutai.com/api/donate",
|
|
14
14
|
};
|
|
15
15
|
|
package/src/sse/handlers/chat.js
CHANGED
|
@@ -88,7 +88,7 @@ export async function handleChat(request, clientRawRequest = null) {
|
|
|
88
88
|
// Bypass naming/warmup requests before combo rotation to avoid wasting rotation slots
|
|
89
89
|
const userAgent = request?.headers?.get("user-agent") || "";
|
|
90
90
|
const bypassResponse = handleBypassRequest(body, modelStr, userAgent, !!settings.ccFilterNaming);
|
|
91
|
-
if (bypassResponse) return bypassResponse.response
|
|
91
|
+
if (bypassResponse) return bypassResponse.response;
|
|
92
92
|
|
|
93
93
|
// Check if model is a combo (has multiple models with fallback)
|
|
94
94
|
const comboModels = await getComboModels(modelStr);
|