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 CHANGED
@@ -1,13 +1,13 @@
1
1
  {
2
2
  "name": "kentutai",
3
- "version": "1.7.6",
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
- <div className="flex flex-col gap-0.5">
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
- ? "on"
384
- : "off"
378
+ : key.cavemanEnabled === false
379
+ ? "disabled"
380
+ : key.cavemanLevel || "full"
385
381
  }
386
382
  onChange={(e) => {
387
- const val =
388
- e.target.value === ""
389
- ? null
390
- : e.target.value === "on";
391
- handlePatchKey(key.id, { cavemanEnabled: val });
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 ? "On" : "Off"})`
394
+ ? `Global (${globalSettings.cavemanEnabled ? globalSettings.cavemanLevel || "full" : "Off"})`
396
395
  : "Global"
397
396
  }
398
397
  options={[
399
- {
400
- value: "",
401
- label: `Global${globalSettings ? ` (${globalSettings.cavemanEnabled ? "On" : "Off"})` : ""}`,
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
- setRtkEnabledState(data.rtkEnabled !== false);
232
- setCavemanEnabled(!!data.cavemanEnabled);
233
- setCavemanLevel(data.cavemanLevel || "full");
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 res = await fetch("/api/settings", {
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
- await fetch("/api/settings", {
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
- setCavemanEnabled(value);
311
- patchSetting({ cavemanEnabled: value });
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
- setCavemanLevel(level);
316
- patchSetting({ cavemanLevel: level });
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
- {/* Endpoint rows */}
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
- {/* Tailscale */}
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 size="sm" variant="outline" icon="content_copy" onClick={() => handleCopy(step.id)}>Copy</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
- {/* View API Keys Link */}
499
- <Link href="/dashboard/access?tab=keys" className="no-underline">
500
- <Card
501
- title={`View API Keys`}
502
- icon="vpn_key"
503
- className="cursor-pointer hover:bg-surface-2 transition-colors"
504
- >
505
- <p className="text-sm text-text-muted">Click to view and manage all API keys in the Access Hub.</p>
506
- </Card>
507
- </Link>
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&apos;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 apiKey = await createApiKey(name, targetUserId, machineId, comment ?? "");
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,
@@ -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
- await db.transaction(async () => {
75
- const row = await db.get(`SELECT * FROM apiKeys WHERE id = ?`, [id]);
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
- await db.run(
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
+ }
@@ -36,6 +36,9 @@ const DEFAULT_SETTINGS = {
36
36
  rtkEnabled: true,
37
37
  cavemanEnabled: false,
38
38
  cavemanLevel: "full",
39
+ userRtkEnabled: false,
40
+ userCavemanEnabled: false,
41
+ userCavemanLevel: null,
39
42
  };
40
43
 
41
44
  async function readRaw() {
@@ -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 min-h-full">
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}
@@ -78,7 +78,7 @@ export default function DashboardLayout({ children }) {
78
78
  )}
79
79
 
80
80
  {/* Sidebar - Desktop */}
81
- <div className="hidden lg:flex">
81
+ <div className="hidden lg:flex h-full">
82
82
  <Sidebar />
83
83
  </div>
84
84
 
@@ -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/kentutai/kentutai/refs/heads/master/CHANGELOG.md",
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
 
@@ -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 || bypassResponse;
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);