@simonsbs/keylore 1.0.0-rc5 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +24 -13
- package/data/catalog.json +4 -0
- package/dist/config.js +1 -1
- package/dist/domain/types.js +60 -46
- package/dist/http/admin-ui.js +1068 -371
- package/dist/http/server.js +173 -0
- package/dist/mcp/create-server.js +4 -4
- package/dist/repositories/credential-repository.js +37 -7
- package/dist/repositories/pg-credential-repository.js +50 -11
- package/dist/services/backup-service.js +10 -4
- package/dist/services/core-mode-service.js +17 -1
- package/migrations/009_v1_context_split.sql +30 -0
- package/package.json +1 -1
package/dist/http/admin-ui.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
3
4
|
const adminStyles = String.raw `
|
|
4
5
|
:root {
|
|
5
6
|
--canvas: #f4efe3;
|
|
@@ -22,6 +23,10 @@ const adminStyles = String.raw `
|
|
|
22
23
|
box-sizing: border-box;
|
|
23
24
|
}
|
|
24
25
|
|
|
26
|
+
[hidden] {
|
|
27
|
+
display: none !important;
|
|
28
|
+
}
|
|
29
|
+
|
|
25
30
|
html {
|
|
26
31
|
scroll-behavior: smooth;
|
|
27
32
|
}
|
|
@@ -148,6 +153,30 @@ textarea {
|
|
|
148
153
|
font-weight: 600;
|
|
149
154
|
}
|
|
150
155
|
|
|
156
|
+
.tab-row {
|
|
157
|
+
display: flex;
|
|
158
|
+
flex-wrap: wrap;
|
|
159
|
+
gap: 10px;
|
|
160
|
+
margin-bottom: 18px;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
.tab-button {
|
|
164
|
+
appearance: none;
|
|
165
|
+
border: 1px solid var(--line);
|
|
166
|
+
border-radius: 999px;
|
|
167
|
+
padding: 10px 14px;
|
|
168
|
+
background: #f7f2e8;
|
|
169
|
+
color: var(--ink);
|
|
170
|
+
cursor: pointer;
|
|
171
|
+
font-weight: 600;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
.tab-button.is-active {
|
|
175
|
+
background: var(--accent-soft);
|
|
176
|
+
color: var(--accent);
|
|
177
|
+
border-color: rgba(19, 93, 74, 0.24);
|
|
178
|
+
}
|
|
179
|
+
|
|
151
180
|
.button,
|
|
152
181
|
.button-secondary,
|
|
153
182
|
.button-danger {
|
|
@@ -264,29 +293,71 @@ textarea {
|
|
|
264
293
|
|
|
265
294
|
.notice {
|
|
266
295
|
display: none;
|
|
267
|
-
margin: 20px 0 0;
|
|
268
|
-
padding: 14px 16px;
|
|
269
|
-
border-radius: 16px;
|
|
270
|
-
border: 1px solid var(--line);
|
|
271
296
|
}
|
|
272
297
|
|
|
273
298
|
.notice.is-visible {
|
|
274
|
-
display:
|
|
299
|
+
display: none;
|
|
275
300
|
}
|
|
276
301
|
|
|
277
|
-
.
|
|
278
|
-
|
|
279
|
-
|
|
302
|
+
.toast-console {
|
|
303
|
+
position: fixed;
|
|
304
|
+
right: 20px;
|
|
305
|
+
bottom: 20px;
|
|
306
|
+
z-index: 80;
|
|
307
|
+
display: grid;
|
|
308
|
+
gap: 10px;
|
|
309
|
+
width: min(420px, calc(100vw - 32px));
|
|
280
310
|
}
|
|
281
311
|
|
|
282
|
-
.
|
|
283
|
-
|
|
284
|
-
|
|
312
|
+
.toast {
|
|
313
|
+
border: 1px solid var(--line);
|
|
314
|
+
background: rgba(255, 250, 241, 0.96);
|
|
315
|
+
border-radius: 16px;
|
|
316
|
+
box-shadow: 0 20px 40px rgba(23, 33, 31, 0.18);
|
|
317
|
+
padding: 14px 16px;
|
|
285
318
|
}
|
|
286
319
|
|
|
287
|
-
.
|
|
288
|
-
|
|
289
|
-
|
|
320
|
+
.toast.is-info {
|
|
321
|
+
border-color: rgba(19, 93, 74, 0.16);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
.toast.is-error {
|
|
325
|
+
border-color: rgba(143, 45, 35, 0.2);
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
.toast.is-warning {
|
|
329
|
+
border-color: rgba(157, 75, 20, 0.18);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
.toast-head {
|
|
333
|
+
display: flex;
|
|
334
|
+
align-items: center;
|
|
335
|
+
justify-content: space-between;
|
|
336
|
+
gap: 12px;
|
|
337
|
+
margin-bottom: 6px;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
.toast-title {
|
|
341
|
+
font-size: 0.82rem;
|
|
342
|
+
font-weight: 700;
|
|
343
|
+
letter-spacing: 0.08em;
|
|
344
|
+
text-transform: uppercase;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
.toast-close {
|
|
348
|
+
appearance: none;
|
|
349
|
+
border: 0;
|
|
350
|
+
background: transparent;
|
|
351
|
+
color: var(--muted);
|
|
352
|
+
font-size: 1rem;
|
|
353
|
+
cursor: pointer;
|
|
354
|
+
padding: 0;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
.toast-copy {
|
|
358
|
+
margin: 0;
|
|
359
|
+
color: var(--ink);
|
|
360
|
+
line-height: 1.5;
|
|
290
361
|
}
|
|
291
362
|
|
|
292
363
|
.dashboard {
|
|
@@ -439,6 +510,16 @@ textarea {
|
|
|
439
510
|
grid-column: 1 / -1;
|
|
440
511
|
}
|
|
441
512
|
|
|
513
|
+
.field.has-error input,
|
|
514
|
+
.field.has-error textarea,
|
|
515
|
+
.field.has-error select,
|
|
516
|
+
.field-wide.has-error input,
|
|
517
|
+
.field-wide.has-error textarea,
|
|
518
|
+
.field-wide.has-error select {
|
|
519
|
+
border-color: rgba(143, 45, 35, 0.45);
|
|
520
|
+
box-shadow: 0 0 0 3px rgba(143, 45, 35, 0.08);
|
|
521
|
+
}
|
|
522
|
+
|
|
442
523
|
.field label,
|
|
443
524
|
.field-wide label {
|
|
444
525
|
font-size: 0.84rem;
|
|
@@ -447,6 +528,58 @@ textarea {
|
|
|
447
528
|
letter-spacing: 0.08em;
|
|
448
529
|
}
|
|
449
530
|
|
|
531
|
+
.field-label {
|
|
532
|
+
display: inline-flex;
|
|
533
|
+
align-items: center;
|
|
534
|
+
gap: 8px;
|
|
535
|
+
flex-wrap: wrap;
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
.info-glyph {
|
|
539
|
+
position: relative;
|
|
540
|
+
display: inline-flex;
|
|
541
|
+
align-items: center;
|
|
542
|
+
justify-content: center;
|
|
543
|
+
width: 18px;
|
|
544
|
+
height: 18px;
|
|
545
|
+
border-radius: 999px;
|
|
546
|
+
border: 1px solid rgba(23, 33, 31, 0.18);
|
|
547
|
+
background: rgba(255, 255, 255, 0.8);
|
|
548
|
+
color: var(--muted);
|
|
549
|
+
font-size: 0.72rem;
|
|
550
|
+
font-weight: 700;
|
|
551
|
+
line-height: 1;
|
|
552
|
+
cursor: help;
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
.info-glyph::after {
|
|
556
|
+
content: attr(data-tooltip);
|
|
557
|
+
position: absolute;
|
|
558
|
+
left: 50%;
|
|
559
|
+
bottom: calc(100% + 10px);
|
|
560
|
+
transform: translateX(-50%);
|
|
561
|
+
width: min(280px, 70vw);
|
|
562
|
+
padding: 10px 12px;
|
|
563
|
+
border-radius: 12px;
|
|
564
|
+
background: rgba(23, 33, 31, 0.96);
|
|
565
|
+
color: #fff8ef;
|
|
566
|
+
font-size: 0.8rem;
|
|
567
|
+
font-weight: 500;
|
|
568
|
+
line-height: 1.45;
|
|
569
|
+
text-transform: none;
|
|
570
|
+
letter-spacing: normal;
|
|
571
|
+
box-shadow: 0 14px 30px rgba(23, 33, 31, 0.22);
|
|
572
|
+
opacity: 0;
|
|
573
|
+
pointer-events: none;
|
|
574
|
+
transition: opacity 120ms ease;
|
|
575
|
+
z-index: 5;
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
.info-glyph:hover::after,
|
|
579
|
+
.info-glyph:focus-visible::after {
|
|
580
|
+
opacity: 1;
|
|
581
|
+
}
|
|
582
|
+
|
|
450
583
|
.field input,
|
|
451
584
|
.field textarea,
|
|
452
585
|
.field select,
|
|
@@ -469,6 +602,27 @@ textarea {
|
|
|
469
602
|
line-height: 1.5;
|
|
470
603
|
}
|
|
471
604
|
|
|
605
|
+
.field-error {
|
|
606
|
+
display: none;
|
|
607
|
+
margin-top: 6px;
|
|
608
|
+
color: var(--danger);
|
|
609
|
+
font-size: 0.86rem;
|
|
610
|
+
line-height: 1.4;
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
.field-error.is-visible {
|
|
614
|
+
display: block;
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
pre {
|
|
618
|
+
max-width: 100%;
|
|
619
|
+
margin: 0;
|
|
620
|
+
overflow: auto;
|
|
621
|
+
white-space: pre-wrap;
|
|
622
|
+
overflow-wrap: anywhere;
|
|
623
|
+
word-break: break-word;
|
|
624
|
+
}
|
|
625
|
+
|
|
472
626
|
.form-actions,
|
|
473
627
|
.panel-actions,
|
|
474
628
|
.toolbar {
|
|
@@ -493,6 +647,90 @@ textarea {
|
|
|
493
647
|
align-items: center;
|
|
494
648
|
}
|
|
495
649
|
|
|
650
|
+
.utility-shell {
|
|
651
|
+
display: none;
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
.token-toolbar {
|
|
655
|
+
display: flex;
|
|
656
|
+
flex-wrap: wrap;
|
|
657
|
+
justify-content: space-between;
|
|
658
|
+
gap: 12px;
|
|
659
|
+
align-items: center;
|
|
660
|
+
margin-bottom: 16px;
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
.token-list {
|
|
664
|
+
display: grid;
|
|
665
|
+
gap: 12px;
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
.token-row {
|
|
669
|
+
padding: 16px;
|
|
670
|
+
border-radius: 16px;
|
|
671
|
+
background: rgba(255, 255, 255, 0.72);
|
|
672
|
+
border: 1px solid rgba(23, 33, 31, 0.08);
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
.token-row h3 {
|
|
676
|
+
margin: 0 0 6px;
|
|
677
|
+
font-size: 1rem;
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
.token-row p {
|
|
681
|
+
margin: 0;
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
dialog.modal {
|
|
685
|
+
width: min(920px, calc(100vw - 32px));
|
|
686
|
+
max-height: calc(100vh - 32px);
|
|
687
|
+
padding: 0;
|
|
688
|
+
border: 0;
|
|
689
|
+
border-radius: 24px;
|
|
690
|
+
background: transparent;
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
dialog.modal::backdrop {
|
|
694
|
+
background: rgba(23, 33, 31, 0.38);
|
|
695
|
+
backdrop-filter: blur(6px);
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
.modal-card {
|
|
699
|
+
border: 1px solid var(--line);
|
|
700
|
+
background: rgba(255, 250, 241, 0.98);
|
|
701
|
+
border-radius: 24px;
|
|
702
|
+
box-shadow: var(--shadow);
|
|
703
|
+
overflow: auto;
|
|
704
|
+
max-height: calc(100vh - 32px);
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
.modal-header {
|
|
708
|
+
display: flex;
|
|
709
|
+
justify-content: space-between;
|
|
710
|
+
gap: 16px;
|
|
711
|
+
align-items: flex-start;
|
|
712
|
+
padding: 22px 24px 0;
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
.modal-body {
|
|
716
|
+
padding: 18px 24px 24px;
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
.modal-feedback {
|
|
720
|
+
display: none;
|
|
721
|
+
margin-bottom: 16px;
|
|
722
|
+
padding: 14px 16px;
|
|
723
|
+
border-radius: 16px;
|
|
724
|
+
border: 1px solid rgba(143, 45, 35, 0.16);
|
|
725
|
+
background: rgba(143, 45, 35, 0.08);
|
|
726
|
+
color: var(--danger);
|
|
727
|
+
line-height: 1.5;
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
.modal-feedback.is-visible {
|
|
731
|
+
display: block;
|
|
732
|
+
}
|
|
733
|
+
|
|
496
734
|
.pill {
|
|
497
735
|
display: inline-flex;
|
|
498
736
|
align-items: center;
|
|
@@ -550,6 +788,40 @@ textarea {
|
|
|
550
788
|
gap: 12px;
|
|
551
789
|
}
|
|
552
790
|
|
|
791
|
+
.snippet-stack {
|
|
792
|
+
display: grid;
|
|
793
|
+
gap: 14px;
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
.snippet-box {
|
|
797
|
+
position: relative;
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
.snippet-box textarea {
|
|
801
|
+
padding-right: 54px;
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
.copy-glyph {
|
|
805
|
+
position: absolute;
|
|
806
|
+
top: 10px;
|
|
807
|
+
right: 10px;
|
|
808
|
+
width: 34px;
|
|
809
|
+
height: 34px;
|
|
810
|
+
border-radius: 10px;
|
|
811
|
+
border: 1px solid var(--line);
|
|
812
|
+
background: rgba(255, 250, 241, 0.92);
|
|
813
|
+
color: var(--ink);
|
|
814
|
+
cursor: pointer;
|
|
815
|
+
font-size: 1rem;
|
|
816
|
+
font-weight: 700;
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
.copy-glyph:hover,
|
|
820
|
+
.copy-glyph:focus-visible {
|
|
821
|
+
background: var(--accent-soft);
|
|
822
|
+
color: var(--accent);
|
|
823
|
+
}
|
|
824
|
+
|
|
553
825
|
.list-card {
|
|
554
826
|
padding: 16px;
|
|
555
827
|
border-radius: 16px;
|
|
@@ -716,6 +988,15 @@ pre {
|
|
|
716
988
|
}
|
|
717
989
|
}
|
|
718
990
|
`;
|
|
991
|
+
function resolveLocalStdioEntryPath() {
|
|
992
|
+
const moduleDir = path.dirname(fileURLToPath(import.meta.url));
|
|
993
|
+
const packageRoot = path.resolve(moduleDir, "..", "..");
|
|
994
|
+
const builtEntry = path.join(packageRoot, "dist", "index.js");
|
|
995
|
+
if (fs.existsSync(builtEntry)) {
|
|
996
|
+
return builtEntry;
|
|
997
|
+
}
|
|
998
|
+
return path.join(packageRoot, "src", "index.ts");
|
|
999
|
+
}
|
|
719
1000
|
const adminApp = String.raw `
|
|
720
1001
|
const config = window.__KEYLORE_ADMIN_CONFIG__;
|
|
721
1002
|
const state = {
|
|
@@ -739,8 +1020,11 @@ const state = {
|
|
|
739
1020
|
currentCredentialContext: null,
|
|
740
1021
|
lastMcpConnection: null,
|
|
741
1022
|
mcpToken: '',
|
|
1023
|
+
connectTab: 'codex',
|
|
742
1024
|
advancedVisible: false,
|
|
743
|
-
credentialIdManuallyEdited: false
|
|
1025
|
+
credentialIdManuallyEdited: false,
|
|
1026
|
+
credentialModalMode: 'create',
|
|
1027
|
+
toastCounter: 0
|
|
744
1028
|
};
|
|
745
1029
|
|
|
746
1030
|
const storageKey = 'keylore-admin-session';
|
|
@@ -758,6 +1042,26 @@ function byId(id) {
|
|
|
758
1042
|
return document.getElementById(id);
|
|
759
1043
|
}
|
|
760
1044
|
|
|
1045
|
+
function showDialog(id) {
|
|
1046
|
+
const dialog = byId(id);
|
|
1047
|
+
if (!(dialog instanceof HTMLDialogElement)) {
|
|
1048
|
+
return;
|
|
1049
|
+
}
|
|
1050
|
+
if (!dialog.open) {
|
|
1051
|
+
dialog.showModal();
|
|
1052
|
+
}
|
|
1053
|
+
}
|
|
1054
|
+
|
|
1055
|
+
function closeDialog(id) {
|
|
1056
|
+
const dialog = byId(id);
|
|
1057
|
+
if (!(dialog instanceof HTMLDialogElement)) {
|
|
1058
|
+
return;
|
|
1059
|
+
}
|
|
1060
|
+
if (dialog.open) {
|
|
1061
|
+
dialog.close();
|
|
1062
|
+
}
|
|
1063
|
+
}
|
|
1064
|
+
|
|
761
1065
|
function splitList(value) {
|
|
762
1066
|
return String(value ?? '')
|
|
763
1067
|
.split(/[\n,]/)
|
|
@@ -811,17 +1115,17 @@ function slugifyTokenKey(value) {
|
|
|
811
1115
|
return normalized || 'token';
|
|
812
1116
|
}
|
|
813
1117
|
|
|
814
|
-
function
|
|
1118
|
+
function firstPrompt() {
|
|
815
1119
|
const credential = state.currentCredentialContext || selectedCredentialSummary() || visibleCredentials()[0];
|
|
816
1120
|
if (!credential) {
|
|
817
|
-
return '
|
|
1121
|
+
return 'Search KeyLore for the best credential for the target service, explain why you chose it, and use it through the broker without exposing the raw token.';
|
|
818
1122
|
}
|
|
819
1123
|
|
|
820
1124
|
const domain = credential.allowedDomains && credential.allowedDomains.length > 0
|
|
821
1125
|
? credential.allowedDomains[0]
|
|
822
1126
|
: 'target-service.example.com';
|
|
823
1127
|
const targetUrl = defaultTestUrlForCredential(credential) || ('https://' + domain);
|
|
824
|
-
return '
|
|
1128
|
+
return 'Search KeyLore for the best credential for ' + credential.service + ' on ' + domain + '. Explain why you chose it, then use it through KeyLore to fetch ' + targetUrl + ' without exposing the raw token.';
|
|
825
1129
|
}
|
|
826
1130
|
|
|
827
1131
|
function mcpHttpTokenValue() {
|
|
@@ -868,6 +1172,21 @@ function geminiHttpSnippet() {
|
|
|
868
1172
|
});
|
|
869
1173
|
}
|
|
870
1174
|
|
|
1175
|
+
function claudeStdioSnippet() {
|
|
1176
|
+
return [
|
|
1177
|
+
'claude mcp add keylore_stdio -- node ' + config.stdioEntryPath + ' --transport stdio',
|
|
1178
|
+
'claude mcp list'
|
|
1179
|
+
].join('\n');
|
|
1180
|
+
}
|
|
1181
|
+
|
|
1182
|
+
function claudeHttpSnippet() {
|
|
1183
|
+
return [
|
|
1184
|
+
'export KEYLORE_MCP_ACCESS_TOKEN=' + mcpHttpTokenValue(),
|
|
1185
|
+
'claude mcp add --transport http --header "Authorization: Bearer $KEYLORE_MCP_ACCESS_TOKEN" keylore_http ' + config.baseUrl.replace(/\/$/, '') + '/mcp',
|
|
1186
|
+
'claude mcp list'
|
|
1187
|
+
].join('\n');
|
|
1188
|
+
}
|
|
1189
|
+
|
|
871
1190
|
function genericHttpSnippet() {
|
|
872
1191
|
return [
|
|
873
1192
|
'MCP endpoint: ' + config.baseUrl.replace(/\/$/, '') + '/mcp',
|
|
@@ -875,16 +1194,107 @@ function genericHttpSnippet() {
|
|
|
875
1194
|
].join('\n');
|
|
876
1195
|
}
|
|
877
1196
|
|
|
1197
|
+
function humanizeErrorMessage(message) {
|
|
1198
|
+
if (message.includes('Missing secret material in local secret store for')) {
|
|
1199
|
+
return 'This token record exists, but its stored secret is missing. Open Edit token, paste the token again, and save changes.';
|
|
1200
|
+
}
|
|
1201
|
+
return message;
|
|
1202
|
+
}
|
|
1203
|
+
|
|
1204
|
+
function pushToast(kind, message) {
|
|
1205
|
+
const node = byId('toast-console');
|
|
1206
|
+
if (!node) {
|
|
1207
|
+
return;
|
|
1208
|
+
}
|
|
1209
|
+
state.toastCounter += 1;
|
|
1210
|
+
const toast = document.createElement('div');
|
|
1211
|
+
toast.className = 'toast is-' + kind;
|
|
1212
|
+
toast.dataset.toastId = String(state.toastCounter);
|
|
1213
|
+
toast.innerHTML = [
|
|
1214
|
+
'<div class="toast-head">',
|
|
1215
|
+
'<span class="toast-title">' + escapeHtml(kind === 'error' ? 'Error' : kind === 'warning' ? 'Warning' : 'Info') + '</span>',
|
|
1216
|
+
'<button class="toast-close" type="button" data-toast-close="' + escapeHtml(String(state.toastCounter)) + '" aria-label="Close notification">×</button>',
|
|
1217
|
+
'</div>',
|
|
1218
|
+
'<p class="toast-copy">' + escapeHtml(message) + '</p>'
|
|
1219
|
+
].join('');
|
|
1220
|
+
node.prepend(toast);
|
|
1221
|
+
while (node.childElementCount > 6) {
|
|
1222
|
+
node.lastElementChild?.remove();
|
|
1223
|
+
}
|
|
1224
|
+
if (kind !== 'error') {
|
|
1225
|
+
const dismissAfterMs = kind === 'warning' ? 7000 : 4000;
|
|
1226
|
+
window.setTimeout(function() {
|
|
1227
|
+
if (toast.isConnected) {
|
|
1228
|
+
toast.remove();
|
|
1229
|
+
}
|
|
1230
|
+
}, dismissAfterMs);
|
|
1231
|
+
}
|
|
1232
|
+
}
|
|
1233
|
+
|
|
878
1234
|
function setNotice(kind, message) {
|
|
879
|
-
|
|
880
|
-
node.className = 'notice is-visible is-' + kind;
|
|
881
|
-
node.textContent = message;
|
|
1235
|
+
pushToast(kind, humanizeErrorMessage(String(message ?? '')));
|
|
882
1236
|
}
|
|
883
1237
|
|
|
884
1238
|
function clearNotice() {
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
1239
|
+
// Intentionally keep previous toasts visible until the user dismisses them.
|
|
1240
|
+
}
|
|
1241
|
+
|
|
1242
|
+
function setCredentialModalFeedback(kind, message) {
|
|
1243
|
+
const node = byId('credential-modal-feedback');
|
|
1244
|
+
if (!node) {
|
|
1245
|
+
return;
|
|
1246
|
+
}
|
|
1247
|
+
const normalized = humanizeErrorMessage(String(message ?? ''));
|
|
1248
|
+
if (!normalized) {
|
|
1249
|
+
node.textContent = '';
|
|
1250
|
+
node.classList.remove('is-visible');
|
|
1251
|
+
return;
|
|
1252
|
+
}
|
|
1253
|
+
node.textContent = normalized;
|
|
1254
|
+
node.classList.add('is-visible');
|
|
1255
|
+
node.dataset.kind = kind;
|
|
1256
|
+
}
|
|
1257
|
+
|
|
1258
|
+
function clearCredentialModalFeedback() {
|
|
1259
|
+
setCredentialModalFeedback('error', '');
|
|
1260
|
+
}
|
|
1261
|
+
|
|
1262
|
+
function clearCredentialFieldErrors() {
|
|
1263
|
+
[
|
|
1264
|
+
'credential-name',
|
|
1265
|
+
'credential-id',
|
|
1266
|
+
'credential-service',
|
|
1267
|
+
'credential-domains',
|
|
1268
|
+
'credential-secret',
|
|
1269
|
+
'credential-user-context',
|
|
1270
|
+
'credential-llm-context',
|
|
1271
|
+
].forEach(function(fieldId) {
|
|
1272
|
+
const input = byId(fieldId);
|
|
1273
|
+
const container = input?.closest('.field, .field-wide');
|
|
1274
|
+
const errorNode = byId(fieldId + '-error');
|
|
1275
|
+
container?.classList.remove('has-error');
|
|
1276
|
+
if (errorNode) {
|
|
1277
|
+
errorNode.textContent = '';
|
|
1278
|
+
errorNode.classList.remove('is-visible');
|
|
1279
|
+
}
|
|
1280
|
+
});
|
|
1281
|
+
}
|
|
1282
|
+
|
|
1283
|
+
function setCredentialFieldErrors(fieldErrors) {
|
|
1284
|
+
clearCredentialFieldErrors();
|
|
1285
|
+
Object.entries(fieldErrors || {}).forEach(function([fieldId, message]) {
|
|
1286
|
+
if (!message) {
|
|
1287
|
+
return;
|
|
1288
|
+
}
|
|
1289
|
+
const input = byId(fieldId);
|
|
1290
|
+
const container = input?.closest('.field, .field-wide');
|
|
1291
|
+
const errorNode = byId(fieldId + '-error');
|
|
1292
|
+
container?.classList.add('has-error');
|
|
1293
|
+
if (errorNode) {
|
|
1294
|
+
errorNode.textContent = humanizeErrorMessage(String(message));
|
|
1295
|
+
errorNode.classList.add('is-visible');
|
|
1296
|
+
}
|
|
1297
|
+
});
|
|
888
1298
|
}
|
|
889
1299
|
|
|
890
1300
|
function setBusy(value) {
|
|
@@ -907,6 +1317,7 @@ function persistSession() {
|
|
|
907
1317
|
sessionClientId: state.sessionClientId,
|
|
908
1318
|
sessionScopes: state.sessionScopes,
|
|
909
1319
|
sessionTenantId: state.sessionTenantId,
|
|
1320
|
+
connectTab: state.connectTab,
|
|
910
1321
|
advancedVisible: state.advancedVisible
|
|
911
1322
|
};
|
|
912
1323
|
localStorage.setItem(storageKey, JSON.stringify(payload));
|
|
@@ -926,6 +1337,7 @@ function loadPersistedSession() {
|
|
|
926
1337
|
state.sessionClientId = parsed.sessionClientId || '';
|
|
927
1338
|
state.sessionScopes = parsed.sessionScopes || '';
|
|
928
1339
|
state.sessionTenantId = parsed.sessionTenantId || '';
|
|
1340
|
+
state.connectTab = parsed.connectTab || 'codex';
|
|
929
1341
|
state.advancedVisible = parsed.advancedVisible === true;
|
|
930
1342
|
} catch (_error) {
|
|
931
1343
|
localStorage.removeItem(storageKey);
|
|
@@ -948,6 +1360,7 @@ function clearSession() {
|
|
|
948
1360
|
state.currentCredentialContext = null;
|
|
949
1361
|
state.lastMcpConnection = null;
|
|
950
1362
|
state.mcpToken = '';
|
|
1363
|
+
state.connectTab = 'codex';
|
|
951
1364
|
state.advancedVisible = false;
|
|
952
1365
|
state.credentialIdManuallyEdited = false;
|
|
953
1366
|
localStorage.removeItem(storageKey);
|
|
@@ -955,6 +1368,15 @@ function clearSession() {
|
|
|
955
1368
|
renderAll();
|
|
956
1369
|
}
|
|
957
1370
|
|
|
1371
|
+
function handleExpiredSession() {
|
|
1372
|
+
clearSession();
|
|
1373
|
+
if (state.localQuickstartEnabled) {
|
|
1374
|
+
setNotice('warning', 'Your saved session expired. Start working locally to open a fresh session, then try again.');
|
|
1375
|
+
return;
|
|
1376
|
+
}
|
|
1377
|
+
setNotice('warning', 'Your saved session expired. Open a new operator session, then try again.');
|
|
1378
|
+
}
|
|
1379
|
+
|
|
958
1380
|
function syncSessionFields() {
|
|
959
1381
|
byId('base-url').value = state.baseUrl;
|
|
960
1382
|
byId('resource').value = state.resource;
|
|
@@ -1049,6 +1471,10 @@ async function fetchJson(path, options) {
|
|
|
1049
1471
|
return { error: 'Unable to parse server response.' };
|
|
1050
1472
|
});
|
|
1051
1473
|
if (!response.ok) {
|
|
1474
|
+
if (response.status === 401 && state.token) {
|
|
1475
|
+
handleExpiredSession();
|
|
1476
|
+
throw new Error('Session expired. Open a fresh session and try again.');
|
|
1477
|
+
}
|
|
1052
1478
|
throw new Error(payload.error || ('Request failed with status ' + response.status));
|
|
1053
1479
|
}
|
|
1054
1480
|
return payload;
|
|
@@ -1124,25 +1550,40 @@ function renderCoreJourney() {
|
|
|
1124
1550
|
: !hasTest
|
|
1125
1551
|
? 'Run Test Credential to verify the broker path.'
|
|
1126
1552
|
: !hasConnection
|
|
1127
|
-
? 'Open Connect your AI tool, copy
|
|
1553
|
+
? 'Open Connect your AI tool, copy the setup for Codex, Gemini, or Claude, and try the first prompt.'
|
|
1128
1554
|
: 'Restart your MCP client and try the suggested first prompt.';
|
|
1129
1555
|
|
|
1556
|
+
const steps = !state.token
|
|
1557
|
+
? [
|
|
1558
|
+
'<article class="step-card"><span class="state-warning">Step 1</span><h3>Open a session</h3><p>Use local quickstart or manual sign-in so KeyLore can save and test tokens for you.</p><div class="panel-actions"><button class="button-secondary" type="button" data-nav-target="login-panel">Go there</button></div></article>',
|
|
1559
|
+
'<article class="step-card"><span class="' + (hasCredential ? 'state-active' : 'state-warning') + '">Step 2</span><h3>Add a token</h3><p>Pick a template, paste the token, and explain when the AI should use it.</p><div class="panel-actions"><button class="button-secondary" type="button" data-nav-target="credentials-section">Open tokens</button></div></article>',
|
|
1560
|
+
'<article class="step-card"><span class="' + (hasTest ? 'state-active' : 'state-warning') + '">Step 3</span><h3>Test it safely</h3><p>Run a brokered check to confirm the token works without exposing the secret.</p><div class="panel-actions"><button class="button-secondary" type="button" data-nav-target="credentials-section">Open test</button></div></article>',
|
|
1561
|
+
'<article class="step-card"><span class="' + (hasConnection ? 'state-active' : 'state-warning') + '">Step 4</span><h3>Connect your AI tool</h3><p>Choose Codex, Gemini, or Claude, follow the setup steps, then try the suggested prompt.</p><div class="panel-actions"><button class="button-secondary" type="button" data-nav-target="connect-section">Open connect</button></div></article>',
|
|
1562
|
+
]
|
|
1563
|
+
: [
|
|
1564
|
+
'<article class="step-card"><span class="' + (hasCredential ? 'state-active' : 'state-warning') + '">Step 1</span><h3>Add a token</h3><p>Pick a template, paste the token, and explain when the AI should use it.</p><div class="panel-actions"><button class="button-secondary" type="button" id="journey-open-token-modal">Add token</button></div></article>',
|
|
1565
|
+
'<article class="step-card"><span class="' + (hasTest ? 'state-active' : 'state-warning') + '">Step 2</span><h3>Test it safely</h3><p>Run a brokered check to confirm the token works without exposing the secret.</p><div class="panel-actions"><button class="button-secondary" type="button" data-nav-target="credentials-section">Open test</button></div></article>',
|
|
1566
|
+
'<article class="step-card"><span class="' + (hasConnection ? 'state-active' : 'state-warning') + '">Step 3</span><h3>Connect your AI tool</h3><p>Choose Codex, Gemini, or Claude, follow the setup steps, then try the suggested prompt.</p><div class="panel-actions"><button class="button-secondary" type="button" data-nav-target="connect-section">Open connect</button></div></article>',
|
|
1567
|
+
];
|
|
1568
|
+
|
|
1130
1569
|
node.innerHTML = [
|
|
1131
|
-
'<div class="section-heading"><div><h2 style="font-size:1.4rem;">
|
|
1570
|
+
'<div class="section-heading"><div><h2 style="font-size:1.4rem;">Step by step</h2><p>Follow the short path. Everything else can wait until later.</p></div></div>',
|
|
1132
1571
|
'<div class="step-grid">',
|
|
1133
|
-
|
|
1134
|
-
'<article class="step-card"><span class="' + (hasCredential ? 'state-active' : 'state-warning') + '">Step 2</span><h3>Save a token</h3><p>Pick a template, paste the token, and explain when the AI should use it.</p><div class="panel-actions"><button class="button-secondary" type="button" data-nav-target="credentials-section">Open tokens</button></div></article>',
|
|
1135
|
-
'<article class="step-card"><span class="' + (hasTest ? 'state-active' : 'state-warning') + '">Step 3</span><h3>Test it safely</h3><p>Run a brokered check to confirm the token works without exposing the secret.</p><div class="panel-actions"><button class="button-secondary" type="button" data-nav-target="credentials-section">Open test</button></div></article>',
|
|
1136
|
-
'<article class="step-card"><span class="' + (hasConnection ? 'state-active' : 'state-warning') + '">Step 4</span><h3>Connect your AI tool</h3><p>Copy the Codex or Gemini snippet, restart the tool, and try the suggested prompt.</p><div class="panel-actions"><button class="button-secondary" type="button" data-nav-target="connect-section">Open connect</button></div></article>',
|
|
1572
|
+
steps.join(''),
|
|
1137
1573
|
'</div>',
|
|
1138
1574
|
'<p class="panel-footnote" style="margin-top:12px;"><strong>Next:</strong> ' + escapeHtml(nextAction) + '</p>'
|
|
1139
1575
|
].join('');
|
|
1576
|
+
|
|
1577
|
+
const journeyOpenTokenModal = byId('journey-open-token-modal');
|
|
1578
|
+
if (journeyOpenTokenModal) {
|
|
1579
|
+
journeyOpenTokenModal.addEventListener('click', openCreateCredentialModal);
|
|
1580
|
+
}
|
|
1140
1581
|
}
|
|
1141
1582
|
|
|
1142
1583
|
function renderCredentials() {
|
|
1143
1584
|
byId('credential-list').innerHTML = renderResultState(state.data.credentials, function(payload) {
|
|
1144
1585
|
if (!payload.credentials.length) {
|
|
1145
|
-
return '<div class="empty-state">No
|
|
1586
|
+
return '<div class="empty-state">No tokens are saved yet. Use Add token to create the first one.</div>';
|
|
1146
1587
|
}
|
|
1147
1588
|
|
|
1148
1589
|
function renderCredentialCard(credential) {
|
|
@@ -1150,25 +1591,25 @@ function renderCredentials() {
|
|
|
1150
1591
|
const statusActionLabel = credential.status === 'active' ? 'Archive' : 'Restore';
|
|
1151
1592
|
const isNew = credential.id === state.lastCreatedCredentialId;
|
|
1152
1593
|
const isSelected = credential.id === state.selectedCredentialId;
|
|
1594
|
+
const ownershipLabel = credential.owner === 'local' ? 'saved token' : 'example';
|
|
1153
1595
|
return [
|
|
1154
|
-
'<article class="
|
|
1596
|
+
'<article class="token-row">',
|
|
1155
1597
|
'<div class="toolbar"><div><h3>' + escapeHtml(credential.displayName) + '</h3><p class="mono"><strong>Token key:</strong> ' + escapeHtml(credential.id) + '</p></div><div class="panel-actions">' + (isNew ? '<span class="state-active">Just added</span>' : '') + (isSelected ? '<span class="state-ok">Selected</span>' : '') + '<span class="' + (credential.status === 'active' ? 'state-active' : 'state-disabled') + '">' + escapeHtml(credential.status) + '</span></div></div>',
|
|
1156
1598
|
'<div class="list-meta">',
|
|
1599
|
+
'<span class="pill"><strong>Kind</strong> ' + escapeHtml(ownershipLabel) + '</span>',
|
|
1157
1600
|
'<span class="pill"><strong>Service</strong> ' + escapeHtml(credential.service) + '</span>',
|
|
1158
1601
|
'<span class="pill"><strong>Scope</strong> ' + escapeHtml(credential.scopeTier) + '</span>',
|
|
1159
1602
|
'<span class="pill"><strong>Sensitivity</strong> ' + escapeHtml(credential.sensitivity) + '</span>',
|
|
1160
1603
|
'</div>',
|
|
1161
|
-
'<p class="panel-footnote">' + escapeHtml(credential.selectionNotes) + '</p>',
|
|
1604
|
+
'<p class="panel-footnote"><strong>LLM context:</strong> ' + escapeHtml(credential.llmContext || credential.selectionNotes) + '</p>',
|
|
1605
|
+
'<p class="panel-footnote"><strong>User context:</strong> ' + escapeHtml(credential.userContext || credential.llmContext || credential.selectionNotes) + '</p>',
|
|
1162
1606
|
'<p class="muted-copy mono">Domains: ' + escapeHtml(credential.allowedDomains.join(', ')) + '</p>',
|
|
1163
|
-
'<div class="panel-actions"><button class="button-secondary" type="button" data-credential-context-action="open" data-credential-context-id="' + escapeHtml(credential.id) + '">Edit
|
|
1164
|
-
'<details class="disclosure"><summary>More actions</summary><div class="panel-actions"><button class="button-secondary" type="button" data-credential-context-action="rename" data-credential-context-id="' + escapeHtml(credential.id) + '" data-credential-context-name="' + escapeHtml(credential.displayName) + '">Rename</button><button class="button-secondary" type="button" data-credential-context-action="retag" data-credential-context-id="' + escapeHtml(credential.id) + '" data-credential-context-tags="' + escapeHtml(credential.tags.join(', ')) + '">Retag</button><button class="button-secondary" type="button" data-credential-context-action="status" data-credential-context-id="' + escapeHtml(credential.id) + '" data-credential-context-status="' + escapeHtml(nextStatus) + '">' + statusActionLabel + '</button>' + (credential.owner === 'local' ? '<button class="button-danger" type="button" data-credential-context-action="delete" data-credential-context-id="' + escapeHtml(credential.id) + '" data-credential-context-name="' + escapeHtml(credential.displayName) + '">Delete</button>' : '') + '</div></details>',
|
|
1607
|
+
'<div class="panel-actions"><button class="button-secondary" type="button" data-credential-context-action="open" data-credential-context-id="' + escapeHtml(credential.id) + '">Edit token</button><button class="button-secondary" type="button" data-credential-context-action="test" data-credential-context-id="' + escapeHtml(credential.id) + '">Use in test</button><button class="button-secondary" type="button" data-credential-context-action="status" data-credential-context-id="' + escapeHtml(credential.id) + '" data-credential-context-status="' + escapeHtml(nextStatus) + '">' + statusActionLabel + '</button><button class="button-danger" type="button" data-credential-context-action="delete" data-credential-context-id="' + escapeHtml(credential.id) + '" data-credential-context-name="' + escapeHtml(credential.displayName) + '">Delete</button></div>',
|
|
1165
1608
|
'</article>'
|
|
1166
1609
|
].join('');
|
|
1167
1610
|
}
|
|
1168
1611
|
|
|
1169
|
-
const
|
|
1170
|
-
return credential.owner === 'local';
|
|
1171
|
-
}).sort(function(left, right) {
|
|
1612
|
+
const credentials = payload.credentials.slice().sort(function(left, right) {
|
|
1172
1613
|
if (left.id === state.lastCreatedCredentialId) {
|
|
1173
1614
|
return -1;
|
|
1174
1615
|
}
|
|
@@ -1181,22 +1622,12 @@ function renderCredentials() {
|
|
|
1181
1622
|
if (right.id === state.selectedCredentialId) {
|
|
1182
1623
|
return 1;
|
|
1183
1624
|
}
|
|
1625
|
+
if (left.owner !== right.owner) {
|
|
1626
|
+
return left.owner === 'local' ? -1 : 1;
|
|
1627
|
+
}
|
|
1184
1628
|
return left.displayName.localeCompare(right.displayName);
|
|
1185
1629
|
});
|
|
1186
|
-
|
|
1187
|
-
return credential.owner !== 'local';
|
|
1188
|
-
}).sort(function(left, right) {
|
|
1189
|
-
return left.displayName.localeCompare(right.displayName);
|
|
1190
|
-
});
|
|
1191
|
-
|
|
1192
|
-
return [
|
|
1193
|
-
ownCredentials.length
|
|
1194
|
-
? '<div class="stack-tight"><div><h3 style="margin:0 0 8px;">Your tokens</h3><p class="panel-footnote" style="margin-top:0;">These are the tokens you added yourself.</p></div>' + ownCredentials.map(renderCredentialCard).join('') + '</div>'
|
|
1195
|
-
: '<div class="empty-state">You have not added a token yet. When you save one, it will appear here with a clear token key.</div>',
|
|
1196
|
-
starterCredentials.length
|
|
1197
|
-
? '<div class="stack-tight" style="margin-top:16px;"><div><h3 style="margin:0 0 8px;">Included examples</h3><p class="panel-footnote" style="margin-top:0;">These example entries ship with the local setup. They are not the tokens you just added.</p></div>' + starterCredentials.map(renderCredentialCard).join('') + '</div>'
|
|
1198
|
-
: ''
|
|
1199
|
-
].join('');
|
|
1630
|
+
return '<div class="token-list">' + credentials.map(renderCredentialCard).join('') + '</div>';
|
|
1200
1631
|
});
|
|
1201
1632
|
|
|
1202
1633
|
if (!state.data.credentials) {
|
|
@@ -1204,24 +1635,10 @@ function renderCredentials() {
|
|
|
1204
1635
|
} else if (!state.data.credentials.ok) {
|
|
1205
1636
|
byId('credential-test-id').innerHTML = '<option value="">Credentials unavailable</option>';
|
|
1206
1637
|
} else {
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
return credential.owner !== 'local';
|
|
1212
|
-
});
|
|
1213
|
-
byId('credential-test-id').innerHTML = [
|
|
1214
|
-
ownCredentials.length
|
|
1215
|
-
? '<optgroup label="Your tokens">' + ownCredentials.map(function(credential) {
|
|
1216
|
-
return '<option value="' + escapeHtml(credential.id) + '">' + escapeHtml(credential.displayName + ' (' + credential.id + ')') + '</option>';
|
|
1217
|
-
}).join('') + '</optgroup>'
|
|
1218
|
-
: '',
|
|
1219
|
-
starterCredentials.length
|
|
1220
|
-
? '<optgroup label="Included examples">' + starterCredentials.map(function(credential) {
|
|
1221
|
-
return '<option value="' + escapeHtml(credential.id) + '">' + escapeHtml(credential.displayName + ' (' + credential.id + ')') + '</option>';
|
|
1222
|
-
}).join('') + '</optgroup>'
|
|
1223
|
-
: ''
|
|
1224
|
-
].join('');
|
|
1638
|
+
byId('credential-test-id').innerHTML = state.data.credentials.data.credentials.map(function(credential) {
|
|
1639
|
+
const label = credential.owner === 'local' ? 'saved' : 'example';
|
|
1640
|
+
return '<option value="' + escapeHtml(credential.id) + '">' + escapeHtml(credential.displayName + ' (' + credential.id + ', ' + label + ')') + '</option>';
|
|
1641
|
+
}).join('');
|
|
1225
1642
|
}
|
|
1226
1643
|
|
|
1227
1644
|
if (state.lastCredentialTest) {
|
|
@@ -1249,6 +1666,19 @@ function renderCredentials() {
|
|
|
1249
1666
|
renderCredentialContextManager();
|
|
1250
1667
|
}
|
|
1251
1668
|
|
|
1669
|
+
function renderCredentialTestError(message) {
|
|
1670
|
+
const friendly = humanizeErrorMessage(message);
|
|
1671
|
+
byId('credential-test-result').innerHTML = [
|
|
1672
|
+
'<div class="list-card" style="border-color: rgba(143, 45, 35, 0.2); background: rgba(143, 45, 35, 0.06);">',
|
|
1673
|
+
'<h3 style="margin:0 0 8px;">Token check failed</h3>',
|
|
1674
|
+
'<p><strong>Token:</strong> ' + escapeHtml(state.lastCredentialTestContext?.credentialId || 'the selected token') + '</p>',
|
|
1675
|
+
'<p><strong>URL:</strong> <span class="mono">' + escapeHtml(state.lastCredentialTestContext?.targetUrl || 'the selected URL') + '</span></p>',
|
|
1676
|
+
'<p>' + escapeHtml(friendly) + '</p>',
|
|
1677
|
+
'<div class="panel-footnote">Use <strong>Edit token</strong> to paste a replacement token and save changes.</div>',
|
|
1678
|
+
'</div>'
|
|
1679
|
+
].join('');
|
|
1680
|
+
}
|
|
1681
|
+
|
|
1252
1682
|
function visibleCredentials() {
|
|
1253
1683
|
return state.data.credentials && state.data.credentials.ok
|
|
1254
1684
|
? state.data.credentials.data.credentials
|
|
@@ -1265,14 +1695,28 @@ function selectedCredentialSummary() {
|
|
|
1265
1695
|
function credentialContextAssessment(payload) {
|
|
1266
1696
|
const errors = [];
|
|
1267
1697
|
const warnings = [];
|
|
1268
|
-
const
|
|
1269
|
-
const
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1698
|
+
const fieldErrors = {};
|
|
1699
|
+
const llmContext = String(payload.llmContext || payload.selectionNotes || '');
|
|
1700
|
+
const userContext = String(payload.userContext || '');
|
|
1701
|
+
const normalizedLlmContext = llmContext.trim().toLowerCase();
|
|
1702
|
+
if (!llmContext.trim()) {
|
|
1703
|
+
const message = 'LLM context is required. Explain when the agent should use this credential.';
|
|
1704
|
+
errors.push(message);
|
|
1705
|
+
fieldErrors['credential-llm-context'] = message;
|
|
1706
|
+
} else if (llmContext.trim().length < 16) {
|
|
1707
|
+
const message = 'LLM context is too short. Add enough detail for the agent to distinguish this credential from others.';
|
|
1708
|
+
errors.push(message);
|
|
1709
|
+
fieldErrors['credential-llm-context'] = message;
|
|
1710
|
+
} else if (llmContext.trim().length < 40) {
|
|
1711
|
+
warnings.push('LLM context is short. Add when-to-use guidance so the agent can choose this credential reliably.');
|
|
1712
|
+
}
|
|
1713
|
+
|
|
1714
|
+
if (!userContext.trim()) {
|
|
1715
|
+
const message = 'User context is required. Explain the human purpose of this credential.';
|
|
1716
|
+
errors.push(message);
|
|
1717
|
+
fieldErrors['credential-user-context'] = message;
|
|
1718
|
+
} else if (userContext.trim().length < 24) {
|
|
1719
|
+
warnings.push('User context is short. Add ownership, intent, or caveats so humans can understand why this credential exists.');
|
|
1276
1720
|
}
|
|
1277
1721
|
|
|
1278
1722
|
if (!payload.allowedDomains || !payload.allowedDomains.length) {
|
|
@@ -1283,45 +1727,57 @@ function credentialContextAssessment(payload) {
|
|
|
1283
1727
|
warnings.push('Permitted operations are empty. The preview should make the intended read or write capability explicit.');
|
|
1284
1728
|
}
|
|
1285
1729
|
|
|
1286
|
-
if (/^(use when needed|general use|general purpose|for api|api token|token for api|default token|main token)$/i.test(
|
|
1287
|
-
|
|
1730
|
+
if (/^(use when needed|general use|general purpose|for api|api token|token for api|default token|main token)$/i.test(normalizedLlmContext)) {
|
|
1731
|
+
const message = 'LLM context is too vague. Say what the credential is for, when the agent should choose it, and what it should avoid.';
|
|
1732
|
+
errors.push(message);
|
|
1733
|
+
fieldErrors['credential-llm-context'] = message;
|
|
1288
1734
|
}
|
|
1289
1735
|
|
|
1290
|
-
if (/(gh[pousr]_[A-Za-z0-9_]+|github_pat_|sk-[A-Za-z0-9_-]+|AKIA[0-9A-Z]{16})/.test(
|
|
1291
|
-
|
|
1736
|
+
if (/(gh[pousr]_[A-Za-z0-9_]+|github_pat_|sk-[A-Za-z0-9_-]+|AKIA[0-9A-Z]{16})/.test(llmContext + '\n' + userContext)) {
|
|
1737
|
+
const message = 'Context text looks like it may contain a secret. Keep raw tokens out of the human and agent-visible context.';
|
|
1738
|
+
errors.push(message);
|
|
1739
|
+
fieldErrors['credential-user-context'] = fieldErrors['credential-user-context'] || message;
|
|
1740
|
+
fieldErrors['credential-llm-context'] = fieldErrors['credential-llm-context'] || message;
|
|
1292
1741
|
}
|
|
1293
1742
|
|
|
1294
|
-
return { errors: errors, warnings: warnings };
|
|
1743
|
+
return { errors: errors, warnings: warnings, fieldErrors: fieldErrors };
|
|
1295
1744
|
}
|
|
1296
1745
|
|
|
1297
|
-
function
|
|
1298
|
-
const
|
|
1299
|
-
|
|
1746
|
+
function credentialGuidance() {
|
|
1747
|
+
const service = byId('credential-service')?.value.trim().toLowerCase();
|
|
1748
|
+
const operations = splitList(byId('credential-operations')?.value || '');
|
|
1749
|
+
const canWrite = operations.includes('http.post');
|
|
1750
|
+
if (service === 'github' && !canWrite) {
|
|
1300
1751
|
return {
|
|
1301
|
-
good: 'Use for GitHub repository metadata, issues, pull requests, and rate-limit reads. Never use it for write operations.',
|
|
1302
|
-
|
|
1752
|
+
good: 'LLM: Use for GitHub repository metadata, issues, pull requests, and rate-limit reads. Never use it for write operations.',
|
|
1753
|
+
user: 'User: Primary read-only GitHub token for routine repository lookups.',
|
|
1754
|
+
avoid: 'GitHub token'
|
|
1303
1755
|
};
|
|
1304
1756
|
}
|
|
1305
|
-
if (
|
|
1757
|
+
if (service === 'github' && canWrite) {
|
|
1306
1758
|
return {
|
|
1307
|
-
good: 'Use for GitHub workflows that need authenticated reads plus controlled writes such as issue comments, labels, or pull request updates. Prefer the read-only GitHub credential when writes are not needed.',
|
|
1759
|
+
good: 'LLM: Use for GitHub workflows that need authenticated reads plus controlled writes such as issue comments, labels, or pull request updates. Prefer the read-only GitHub credential when writes are not needed.',
|
|
1760
|
+
user: 'User: Higher-risk GitHub token for controlled write workflows.',
|
|
1308
1761
|
avoid: 'Main GitHub token'
|
|
1309
1762
|
};
|
|
1310
1763
|
}
|
|
1311
|
-
if (
|
|
1764
|
+
if (service === 'npm') {
|
|
1312
1765
|
return {
|
|
1313
|
-
good: 'Use for npm package metadata, dependency lookup, and registry read operations. Do not use it for publish workflows.',
|
|
1766
|
+
good: 'LLM: Use for npm package metadata, dependency lookup, and registry read operations. Do not use it for publish workflows.',
|
|
1767
|
+
user: 'User: Read-only npm registry token for package inspection.',
|
|
1314
1768
|
avoid: 'npm token'
|
|
1315
1769
|
};
|
|
1316
1770
|
}
|
|
1317
|
-
if (
|
|
1771
|
+
if (service && (service.includes('internal') || service.includes('private'))) {
|
|
1318
1772
|
return {
|
|
1319
|
-
good: 'Use only for the listed internal service domain when the task explicitly targets that API. Avoid unrelated external services.',
|
|
1773
|
+
good: 'LLM: Use only for the listed internal service domain when the task explicitly targets that API. Avoid unrelated external services.',
|
|
1774
|
+
user: 'User: Internal service credential scoped to one API or workflow.',
|
|
1320
1775
|
avoid: 'Internal token'
|
|
1321
1776
|
};
|
|
1322
1777
|
}
|
|
1323
1778
|
return {
|
|
1324
|
-
good: 'Describe the target service, the intended domain, when the agent should choose this credential, and what kinds of actions it should avoid.',
|
|
1779
|
+
good: 'LLM: Describe the target service, the intended domain, when the agent should choose this credential, and what kinds of actions it should avoid.',
|
|
1780
|
+
user: 'User: Describe why this token exists, who it is for, and any caveats for humans.',
|
|
1325
1781
|
avoid: 'Use when needed'
|
|
1326
1782
|
};
|
|
1327
1783
|
}
|
|
@@ -1331,11 +1787,12 @@ function renderCredentialGuidance() {
|
|
|
1331
1787
|
if (!node) {
|
|
1332
1788
|
return;
|
|
1333
1789
|
}
|
|
1334
|
-
const guidance =
|
|
1790
|
+
const guidance = credentialGuidance();
|
|
1335
1791
|
node.innerHTML = [
|
|
1336
|
-
'<div class="panel-footnote"><strong>Good context:</strong> ' + escapeHtml(guidance.good) + '</div>',
|
|
1792
|
+
'<div class="panel-footnote"><strong>Good LLM context:</strong> ' + escapeHtml(guidance.good) + '</div>',
|
|
1793
|
+
'<div class="panel-footnote"><strong>Good user context:</strong> ' + escapeHtml(guidance.user) + '</div>',
|
|
1337
1794
|
'<div class="panel-footnote"><strong>Avoid:</strong> ' + escapeHtml(guidance.avoid) + '</div>',
|
|
1338
|
-
'<div class="panel-footnote">
|
|
1795
|
+
'<div class="panel-footnote">LLM context should answer: when should the agent choose this credential, and what should it avoid doing? User context should explain the human purpose and ownership.</div>'
|
|
1339
1796
|
].join('');
|
|
1340
1797
|
}
|
|
1341
1798
|
|
|
@@ -1361,23 +1818,22 @@ function renderCredentialPreview() {
|
|
|
1361
1818
|
expiresAt: payload.expiresAt || null,
|
|
1362
1819
|
rotationPolicy: payload.rotationPolicy || 'Managed locally',
|
|
1363
1820
|
lastValidatedAt: null,
|
|
1364
|
-
|
|
1821
|
+
userContext: payload.userContext || '',
|
|
1822
|
+
llmContext: payload.llmContext || payload.selectionNotes || '',
|
|
1823
|
+
selectionNotes: payload.llmContext || payload.selectionNotes || '',
|
|
1365
1824
|
tags: payload.tags,
|
|
1366
1825
|
status: payload.status,
|
|
1367
1826
|
},
|
|
1368
1827
|
};
|
|
1369
1828
|
|
|
1370
1829
|
previewNode.innerHTML = '<pre>' + escapeHtml(prettyJson(preview)) + '</pre>';
|
|
1371
|
-
const assessment =
|
|
1830
|
+
const assessment = validateCredentialFormPayload(payload);
|
|
1372
1831
|
const messages = [];
|
|
1373
|
-
assessment.errors.forEach(function(message) {
|
|
1374
|
-
messages.push('<div class="error-state">' + escapeHtml(message) + '</div>');
|
|
1375
|
-
});
|
|
1376
1832
|
assessment.warnings.forEach(function(message) {
|
|
1377
1833
|
messages.push('<div class="panel-footnote">' + escapeHtml(message) + '</div>');
|
|
1378
1834
|
});
|
|
1379
1835
|
if (!messages.length) {
|
|
1380
|
-
messages.push('<div class="panel-footnote">This is the MCP-visible metadata shape. Secret storage details, binding refs, and raw token values do not appear here.</div>');
|
|
1836
|
+
messages.push('<div class="panel-footnote">This is the MCP-visible metadata shape. Secret storage details, binding refs, and raw token values do not appear here. KeyLore also mirrors <span class="mono">llmContext</span> into <span class="mono">selectionNotes</span> for older clients.</div>');
|
|
1381
1837
|
}
|
|
1382
1838
|
warningNode.innerHTML = messages.join('');
|
|
1383
1839
|
renderCredentialGuidance();
|
|
@@ -1404,7 +1860,9 @@ function renderContextPreview(previewNodeId, warningNodeId, payload, currentId)
|
|
|
1404
1860
|
expiresAt: null,
|
|
1405
1861
|
rotationPolicy: 'Managed separately',
|
|
1406
1862
|
lastValidatedAt: null,
|
|
1407
|
-
|
|
1863
|
+
userContext: payload.userContext || '',
|
|
1864
|
+
llmContext: payload.llmContext || payload.selectionNotes || '',
|
|
1865
|
+
selectionNotes: payload.llmContext || payload.selectionNotes || '',
|
|
1408
1866
|
tags: payload.tags,
|
|
1409
1867
|
status: payload.status || 'active',
|
|
1410
1868
|
},
|
|
@@ -1420,7 +1878,7 @@ function renderContextPreview(previewNodeId, warningNodeId, payload, currentId)
|
|
|
1420
1878
|
messages.push('<div class="panel-footnote">' + escapeHtml(message) + '</div>');
|
|
1421
1879
|
});
|
|
1422
1880
|
if (!messages.length) {
|
|
1423
|
-
messages.push('<div class="panel-footnote">This preview is
|
|
1881
|
+
messages.push('<div class="panel-footnote">This preview is metadata only. Secret bindings and raw tokens stay separate and are not editable here. Older clients still receive <span class="mono">selectionNotes</span> as a compatibility alias of <span class="mono">llmContext</span>.</div>');
|
|
1424
1882
|
}
|
|
1425
1883
|
warningNode.innerHTML = messages.join('');
|
|
1426
1884
|
}
|
|
@@ -1435,7 +1893,8 @@ function populateCredentialContextForm(credential) {
|
|
|
1435
1893
|
? 'http.get,http.post'
|
|
1436
1894
|
: 'http.get';
|
|
1437
1895
|
byId('credential-context-domains').value = credential.allowedDomains.join(', ');
|
|
1438
|
-
byId('credential-context-
|
|
1896
|
+
byId('credential-context-user-context').value = credential.userContext || credential.llmContext || credential.selectionNotes;
|
|
1897
|
+
byId('credential-context-llm-context').value = credential.llmContext || credential.selectionNotes;
|
|
1439
1898
|
byId('credential-context-tags').value = credential.tags.join(', ');
|
|
1440
1899
|
}
|
|
1441
1900
|
|
|
@@ -1449,7 +1908,9 @@ function serializeCredentialContextForm() {
|
|
|
1449
1908
|
status: byId('credential-context-status').value,
|
|
1450
1909
|
allowedDomains: splitList(byId('credential-context-domains').value),
|
|
1451
1910
|
permittedOperations: operations.length ? operations : ['http.get'],
|
|
1452
|
-
|
|
1911
|
+
userContext: byId('credential-context-user-context').value.trim(),
|
|
1912
|
+
llmContext: byId('credential-context-llm-context').value.trim(),
|
|
1913
|
+
selectionNotes: byId('credential-context-llm-context').value.trim(),
|
|
1453
1914
|
tags: splitList(byId('credential-context-tags').value),
|
|
1454
1915
|
};
|
|
1455
1916
|
}
|
|
@@ -1481,6 +1942,81 @@ function renderCredentialContextManager() {
|
|
|
1481
1942
|
);
|
|
1482
1943
|
}
|
|
1483
1944
|
|
|
1945
|
+
function applyCredentialDefaults() {
|
|
1946
|
+
byId('credential-storage').value = 'local';
|
|
1947
|
+
byId('credential-service').value = '';
|
|
1948
|
+
byId('credential-sensitivity').value = 'high';
|
|
1949
|
+
byId('credential-operations').value = 'http.get';
|
|
1950
|
+
byId('credential-tags').value = '';
|
|
1951
|
+
byId('credential-domains').value = '';
|
|
1952
|
+
byId('credential-user-context').value = '';
|
|
1953
|
+
byId('credential-llm-context').value = '';
|
|
1954
|
+
}
|
|
1955
|
+
|
|
1956
|
+
function resetCredentialFormForCreate() {
|
|
1957
|
+
state.credentialModalMode = 'create';
|
|
1958
|
+
state.credentialIdManuallyEdited = false;
|
|
1959
|
+
clearCredentialModalFeedback();
|
|
1960
|
+
clearCredentialFieldErrors();
|
|
1961
|
+
byId('credential-modal-title').textContent = 'Add token';
|
|
1962
|
+
byId('credential-modal-copy').textContent = 'Paste the token, add the human and AI context, and save it into KeyLore.';
|
|
1963
|
+
byId('credential-submit').dataset.idleLabel = 'Save token';
|
|
1964
|
+
byId('credential-submit').textContent = 'Save token';
|
|
1965
|
+
byId('credential-form').reset();
|
|
1966
|
+
applyCredentialDefaults();
|
|
1967
|
+
byId('credential-id').readOnly = false;
|
|
1968
|
+
byId('credential-secret-field').hidden = false;
|
|
1969
|
+
byId('credential-secret-label').textContent = 'Paste token';
|
|
1970
|
+
byId('credential-secret').value = '';
|
|
1971
|
+
byId('credential-secret').placeholder = 'Paste the raw token here. KeyLore stores it outside the searchable metadata catalogue.';
|
|
1972
|
+
byId('credential-secret').disabled = false;
|
|
1973
|
+
byId('credential-storage').disabled = false;
|
|
1974
|
+
syncCredentialSourceFields();
|
|
1975
|
+
renderCredentialPreview();
|
|
1976
|
+
}
|
|
1977
|
+
|
|
1978
|
+
function openCreateCredentialModal() {
|
|
1979
|
+
resetCredentialFormForCreate();
|
|
1980
|
+
showDialog('credential-modal');
|
|
1981
|
+
}
|
|
1982
|
+
|
|
1983
|
+
function openEditCredentialModal(credential) {
|
|
1984
|
+
state.credentialModalMode = 'edit';
|
|
1985
|
+
state.selectedCredentialId = credential.id;
|
|
1986
|
+
state.currentCredentialContext = credential;
|
|
1987
|
+
clearCredentialModalFeedback();
|
|
1988
|
+
clearCredentialFieldErrors();
|
|
1989
|
+
byId('credential-modal-title').textContent = 'Edit token';
|
|
1990
|
+
byId('credential-modal-copy').textContent = 'Update the token metadata and context. Stored secret material stays separate and is not shown here.';
|
|
1991
|
+
byId('credential-submit').dataset.idleLabel = 'Save changes';
|
|
1992
|
+
byId('credential-submit').textContent = 'Save changes';
|
|
1993
|
+
byId('credential-name').value = credential.displayName;
|
|
1994
|
+
byId('credential-id').value = credential.id;
|
|
1995
|
+
byId('credential-id').readOnly = true;
|
|
1996
|
+
byId('credential-service').value = credential.service;
|
|
1997
|
+
byId('credential-sensitivity').value = credential.sensitivity;
|
|
1998
|
+
byId('credential-operations').value = credential.permittedOperations.includes('http.post')
|
|
1999
|
+
? 'http.get,http.post'
|
|
2000
|
+
: 'http.get';
|
|
2001
|
+
byId('credential-domains').value = credential.allowedDomains.join(', ');
|
|
2002
|
+
byId('credential-user-context').value = credential.userContext || credential.llmContext || credential.selectionNotes;
|
|
2003
|
+
byId('credential-llm-context').value = credential.llmContext || credential.selectionNotes;
|
|
2004
|
+
byId('credential-tags').value = credential.tags.join(', ');
|
|
2005
|
+
const localOwner = credential.owner === 'local';
|
|
2006
|
+
byId('credential-storage').value = credential.binding?.adapter === 'env' ? 'env' : 'local';
|
|
2007
|
+
byId('credential-storage').disabled = true;
|
|
2008
|
+
byId('credential-secret-field').hidden = !localOwner;
|
|
2009
|
+
byId('credential-secret-label').textContent = 'Replace stored token (optional)';
|
|
2010
|
+
byId('credential-secret').value = '';
|
|
2011
|
+
byId('credential-secret').placeholder = localOwner
|
|
2012
|
+
? 'Paste a replacement token only if you want to update the stored secret.'
|
|
2013
|
+
: 'Secret replacement is only available for locally stored tokens.';
|
|
2014
|
+
byId('credential-secret').disabled = !localOwner;
|
|
2015
|
+
byId('credential-env-ref-field').hidden = true;
|
|
2016
|
+
renderCredentialPreview();
|
|
2017
|
+
showDialog('credential-modal');
|
|
2018
|
+
}
|
|
2019
|
+
|
|
1484
2020
|
async function openCredentialContext(credentialId) {
|
|
1485
2021
|
const result = await fetchJson('/v1/core/credentials/' + encodeURIComponent(credentialId) + '/context');
|
|
1486
2022
|
state.selectedCredentialId = credentialId;
|
|
@@ -1493,20 +2029,90 @@ function renderConnect() {
|
|
|
1493
2029
|
byId('codex-http-snippet').value = codexHttpSnippet();
|
|
1494
2030
|
byId('gemini-stdio-snippet').value = geminiStdioSnippet();
|
|
1495
2031
|
byId('gemini-http-snippet').value = geminiHttpSnippet();
|
|
2032
|
+
byId('claude-stdio-snippet').value = claudeStdioSnippet();
|
|
2033
|
+
byId('claude-http-snippet').value = claudeHttpSnippet();
|
|
1496
2034
|
byId('generic-http-snippet').value = genericHttpSnippet();
|
|
1497
2035
|
byId('mcp-token-export').value = "export KEYLORE_MCP_ACCESS_TOKEN='" + mcpHttpTokenValue() + "'";
|
|
1498
2036
|
byId('connect-client-id').value = state.localAdminBootstrap ? state.localAdminBootstrap.clientId : (state.sessionClientId || '');
|
|
1499
2037
|
if (state.localAdminBootstrap && !byId('connect-client-secret').value) {
|
|
1500
2038
|
byId('connect-client-secret').value = state.localAdminBootstrap.clientSecret;
|
|
1501
2039
|
}
|
|
1502
|
-
byId('
|
|
1503
|
-
? '<div class="state-active">stdio entry is available at <span class="mono">' + escapeHtml(config.stdioEntryPath) + '</span></div>'
|
|
1504
|
-
: '<div class="error-state">The stdio entry point was not found at <span class="mono">' + escapeHtml(config.stdioEntryPath) + '</span>.</div>';
|
|
1505
|
-
byId('codex-first-prompt').value = firstPromptForClient('Codex');
|
|
1506
|
-
byId('gemini-first-prompt').value = firstPromptForClient('Gemini');
|
|
2040
|
+
byId('shared-first-prompt').value = firstPrompt();
|
|
1507
2041
|
byId('connect-result').innerHTML = state.lastMcpConnection
|
|
1508
2042
|
? '<pre>' + escapeHtml(prettyJson(state.lastMcpConnection)) + '</pre>'
|
|
1509
|
-
: '<div class="empty-state">For local use,
|
|
2043
|
+
: '<div class="empty-state">For local use, choose a tool tab, copy the setup snippet or apply it directly, then restart your MCP client. For remote HTTP MCP, run the connection check here first.</div>';
|
|
2044
|
+
renderConnectTabs();
|
|
2045
|
+
}
|
|
2046
|
+
|
|
2047
|
+
function renderConnectTabs() {
|
|
2048
|
+
document.querySelectorAll('[data-connect-tab]').forEach(function(button) {
|
|
2049
|
+
const isActive = button.getAttribute('data-connect-tab') === state.connectTab;
|
|
2050
|
+
button.classList.toggle('is-active', isActive);
|
|
2051
|
+
button.setAttribute('aria-selected', isActive ? 'true' : 'false');
|
|
2052
|
+
});
|
|
2053
|
+
|
|
2054
|
+
document.querySelectorAll('[data-connect-panel]').forEach(function(panel) {
|
|
2055
|
+
const isActive = panel.getAttribute('data-connect-panel') === state.connectTab;
|
|
2056
|
+
panel.hidden = !isActive;
|
|
2057
|
+
});
|
|
2058
|
+
}
|
|
2059
|
+
|
|
2060
|
+
async function copySnippet(targetId, label) {
|
|
2061
|
+
const node = byId(targetId);
|
|
2062
|
+
if (!(node instanceof HTMLTextAreaElement || node instanceof HTMLInputElement)) {
|
|
2063
|
+
setNotice('error', 'Nothing to copy for ' + label + '.');
|
|
2064
|
+
return;
|
|
2065
|
+
}
|
|
2066
|
+
await navigator.clipboard.writeText(node.value);
|
|
2067
|
+
setNotice('info', label + ' copied to clipboard.');
|
|
2068
|
+
}
|
|
2069
|
+
|
|
2070
|
+
async function handleCopyAction(event) {
|
|
2071
|
+
const button = event.target instanceof Element ? event.target.closest('[data-copy-target]') : null;
|
|
2072
|
+
if (!button) {
|
|
2073
|
+
return;
|
|
2074
|
+
}
|
|
2075
|
+
const targetId = button.getAttribute('data-copy-target');
|
|
2076
|
+
const label = button.getAttribute('data-copy-label') || 'Snippet';
|
|
2077
|
+
if (!targetId) {
|
|
2078
|
+
return;
|
|
2079
|
+
}
|
|
2080
|
+
try {
|
|
2081
|
+
await copySnippet(targetId, label);
|
|
2082
|
+
} catch (error) {
|
|
2083
|
+
setNotice('error', error instanceof Error ? error.message : String(error));
|
|
2084
|
+
}
|
|
2085
|
+
}
|
|
2086
|
+
|
|
2087
|
+
async function handleApplyToolSetup(event) {
|
|
2088
|
+
const button = event.target instanceof Element ? event.target.closest('[data-apply-tool]') : null;
|
|
2089
|
+
if (!button) {
|
|
2090
|
+
return;
|
|
2091
|
+
}
|
|
2092
|
+
const tool = button.getAttribute('data-apply-tool');
|
|
2093
|
+
if (!tool) {
|
|
2094
|
+
return;
|
|
2095
|
+
}
|
|
2096
|
+
const result = await withAction('Applied local ' + tool + ' setup.', async function() {
|
|
2097
|
+
return fetchJson('/v1/core/tooling/apply', {
|
|
2098
|
+
method: 'POST',
|
|
2099
|
+
headers: {
|
|
2100
|
+
'content-type': 'application/json'
|
|
2101
|
+
},
|
|
2102
|
+
body: JSON.stringify({ tool: tool })
|
|
2103
|
+
});
|
|
2104
|
+
});
|
|
2105
|
+
setNotice('info', tool.charAt(0).toUpperCase() + tool.slice(1) + ' updated at ' + result.path + '. Restart the tool and try the prompt below.');
|
|
2106
|
+
}
|
|
2107
|
+
|
|
2108
|
+
function handleConnectTabClick(event) {
|
|
2109
|
+
const button = event.target instanceof Element ? event.target.closest('[data-connect-tab]') : null;
|
|
2110
|
+
if (!button) {
|
|
2111
|
+
return;
|
|
2112
|
+
}
|
|
2113
|
+
state.connectTab = button.getAttribute('data-connect-tab') || 'codex';
|
|
2114
|
+
persistSession();
|
|
2115
|
+
renderConnectTabs();
|
|
1510
2116
|
}
|
|
1511
2117
|
|
|
1512
2118
|
function renderTenants() {
|
|
@@ -1759,80 +2365,14 @@ function serializeAuthClientForm() {
|
|
|
1759
2365
|
};
|
|
1760
2366
|
}
|
|
1761
2367
|
|
|
1762
|
-
function applyCredentialTemplate() {
|
|
1763
|
-
const template = byId('credential-template').value;
|
|
1764
|
-
state.credentialIdManuallyEdited = false;
|
|
1765
|
-
if (template === 'github-readonly') {
|
|
1766
|
-
byId('credential-name').value = 'GitHub Read-Only Token';
|
|
1767
|
-
byId('credential-service').value = 'github';
|
|
1768
|
-
byId('credential-operations').value = 'http.get';
|
|
1769
|
-
byId('credential-domains').value = 'api.github.com';
|
|
1770
|
-
byId('credential-notes').value = 'Use for GitHub repository metadata, issues, pull requests, and rate-limit reads. Never use it for write operations.';
|
|
1771
|
-
byId('credential-tags').value = 'github,readonly';
|
|
1772
|
-
byId('credential-sensitivity').value = 'high';
|
|
1773
|
-
renderCredentialPreview();
|
|
1774
|
-
return;
|
|
1775
|
-
}
|
|
1776
|
-
|
|
1777
|
-
if (template === 'github-write') {
|
|
1778
|
-
byId('credential-name').value = 'GitHub Write Token';
|
|
1779
|
-
byId('credential-service').value = 'github';
|
|
1780
|
-
byId('credential-operations').value = 'http.get,http.post';
|
|
1781
|
-
byId('credential-domains').value = 'api.github.com';
|
|
1782
|
-
byId('credential-notes').value = 'Use for GitHub workflows that need authenticated reads plus controlled writes such as issue comments, pull request updates, labels, or status changes. Prefer the read-only GitHub credential when writes are not required.';
|
|
1783
|
-
byId('credential-tags').value = 'github,write';
|
|
1784
|
-
byId('credential-sensitivity').value = 'critical';
|
|
1785
|
-
renderCredentialPreview();
|
|
1786
|
-
return;
|
|
1787
|
-
}
|
|
1788
|
-
|
|
1789
|
-
if (template === 'npm-readonly') {
|
|
1790
|
-
byId('credential-name').value = 'npm Read-Only Token';
|
|
1791
|
-
byId('credential-service').value = 'npm';
|
|
1792
|
-
byId('credential-operations').value = 'http.get';
|
|
1793
|
-
byId('credential-domains').value = 'registry.npmjs.org';
|
|
1794
|
-
byId('credential-notes').value = 'Use for npm package metadata, dependency lookup, and registry read operations. Do not use this credential for publish or package mutation workflows.';
|
|
1795
|
-
byId('credential-tags').value = 'npm,readonly';
|
|
1796
|
-
byId('credential-sensitivity').value = 'high';
|
|
1797
|
-
renderCredentialPreview();
|
|
1798
|
-
return;
|
|
1799
|
-
}
|
|
1800
|
-
|
|
1801
|
-
if (template === 'internal-service') {
|
|
1802
|
-
byId('credential-name').value = 'Internal Service Token';
|
|
1803
|
-
byId('credential-service').value = 'internal_api';
|
|
1804
|
-
byId('credential-operations').value = 'http.get,http.post';
|
|
1805
|
-
byId('credential-domains').value = 'internal.example.com';
|
|
1806
|
-
byId('credential-notes').value = 'Use only for the listed internal service domain when the task explicitly targets that service. Keep this credential scoped to the documented internal API workflow and avoid unrelated external APIs.';
|
|
1807
|
-
byId('credential-tags').value = 'internal,bearer';
|
|
1808
|
-
byId('credential-sensitivity').value = 'critical';
|
|
1809
|
-
renderCredentialPreview();
|
|
1810
|
-
return;
|
|
1811
|
-
}
|
|
1812
|
-
|
|
1813
|
-
if (template === 'generic-bearer') {
|
|
1814
|
-
byId('credential-id').value = '';
|
|
1815
|
-
byId('credential-name').value = '';
|
|
1816
|
-
byId('credential-service').value = '';
|
|
1817
|
-
byId('credential-operations').value = 'http.get';
|
|
1818
|
-
byId('credential-domains').value = '';
|
|
1819
|
-
byId('credential-notes').value = '';
|
|
1820
|
-
byId('credential-tags').value = '';
|
|
1821
|
-
byId('credential-sensitivity').value = 'moderate';
|
|
1822
|
-
renderCredentialPreview();
|
|
1823
|
-
}
|
|
1824
|
-
|
|
1825
|
-
syncCredentialIdFromName(true);
|
|
1826
|
-
}
|
|
1827
|
-
|
|
1828
2368
|
function syncCredentialIdFromName(force) {
|
|
1829
2369
|
if (state.credentialIdManuallyEdited && !force) {
|
|
1830
2370
|
return;
|
|
1831
2371
|
}
|
|
1832
2372
|
|
|
1833
2373
|
const name = byId('credential-name').value.trim();
|
|
1834
|
-
const
|
|
1835
|
-
const fallback =
|
|
2374
|
+
const service = byId('credential-service').value.trim();
|
|
2375
|
+
const fallback = slugifyTokenKey(service || 'token');
|
|
1836
2376
|
byId('credential-id').value = slugifyTokenKey(name || fallback) + '-local';
|
|
1837
2377
|
renderCredentialPreview();
|
|
1838
2378
|
}
|
|
@@ -1847,6 +2387,7 @@ function syncCredentialSourceFields() {
|
|
|
1847
2387
|
function serializeCredentialForm() {
|
|
1848
2388
|
const operations = splitList(byId('credential-operations').value);
|
|
1849
2389
|
const adapter = byId('credential-storage').value;
|
|
2390
|
+
const secretValue = byId('credential-secret').value;
|
|
1850
2391
|
return {
|
|
1851
2392
|
credentialId: byId('credential-id').value.trim(),
|
|
1852
2393
|
displayName: byId('credential-name').value.trim(),
|
|
@@ -1856,7 +2397,9 @@ function serializeCredentialForm() {
|
|
|
1856
2397
|
sensitivity: byId('credential-sensitivity').value,
|
|
1857
2398
|
allowedDomains: splitList(byId('credential-domains').value),
|
|
1858
2399
|
permittedOperations: operations.length ? operations : ['http.get'],
|
|
1859
|
-
|
|
2400
|
+
userContext: byId('credential-user-context').value.trim(),
|
|
2401
|
+
llmContext: byId('credential-llm-context').value.trim(),
|
|
2402
|
+
selectionNotes: byId('credential-llm-context').value.trim(),
|
|
1860
2403
|
tags: splitList(byId('credential-tags').value),
|
|
1861
2404
|
authType: 'bearer',
|
|
1862
2405
|
headerName: 'Authorization',
|
|
@@ -1864,7 +2407,7 @@ function serializeCredentialForm() {
|
|
|
1864
2407
|
secretSource: adapter === 'local'
|
|
1865
2408
|
? {
|
|
1866
2409
|
adapter: 'local',
|
|
1867
|
-
secretValue:
|
|
2410
|
+
secretValue: secretValue
|
|
1868
2411
|
}
|
|
1869
2412
|
: {
|
|
1870
2413
|
adapter: 'env',
|
|
@@ -1873,6 +2416,39 @@ function serializeCredentialForm() {
|
|
|
1873
2416
|
};
|
|
1874
2417
|
}
|
|
1875
2418
|
|
|
2419
|
+
function validateCredentialFormPayload(payload) {
|
|
2420
|
+
const assessment = credentialContextAssessment(payload);
|
|
2421
|
+
const fieldErrors = { ...assessment.fieldErrors };
|
|
2422
|
+
|
|
2423
|
+
if (!payload.credentialId) {
|
|
2424
|
+
fieldErrors['credential-id'] = 'Token key is required.';
|
|
2425
|
+
}
|
|
2426
|
+
if (!payload.displayName) {
|
|
2427
|
+
fieldErrors['credential-name'] = 'Name shown in KeyLore is required.';
|
|
2428
|
+
}
|
|
2429
|
+
if (!payload.service) {
|
|
2430
|
+
fieldErrors['credential-service'] = 'Service name is required.';
|
|
2431
|
+
}
|
|
2432
|
+
if (!payload.allowedDomains?.length) {
|
|
2433
|
+
fieldErrors['credential-domains'] = 'Add at least one allowed domain so the token is clearly scoped.';
|
|
2434
|
+
}
|
|
2435
|
+
if (payload.secretSource?.adapter === 'local' && state.credentialModalMode === 'create' && !payload.secretSource.secretValue.trim()) {
|
|
2436
|
+
fieldErrors['credential-secret'] = 'Paste the token before saving.';
|
|
2437
|
+
}
|
|
2438
|
+
|
|
2439
|
+
const errorValues = Object.values(fieldErrors);
|
|
2440
|
+
return {
|
|
2441
|
+
errors: [
|
|
2442
|
+
...errorValues,
|
|
2443
|
+
...assessment.errors.filter(function(message) {
|
|
2444
|
+
return !errorValues.includes(message);
|
|
2445
|
+
}),
|
|
2446
|
+
],
|
|
2447
|
+
warnings: assessment.warnings,
|
|
2448
|
+
fieldErrors: fieldErrors,
|
|
2449
|
+
};
|
|
2450
|
+
}
|
|
2451
|
+
|
|
1876
2452
|
function syncCredentialTestDefaults(force) {
|
|
1877
2453
|
const credentials = state.data.credentials && state.data.credentials.ok
|
|
1878
2454
|
? state.data.credentials.data.credentials
|
|
@@ -1950,65 +2526,117 @@ async function handleLocalQuickstartLogin() {
|
|
|
1950
2526
|
async function handleCreateCredential(event) {
|
|
1951
2527
|
event.preventDefault();
|
|
1952
2528
|
const payload = serializeCredentialForm();
|
|
1953
|
-
const assessment =
|
|
2529
|
+
const assessment = validateCredentialFormPayload(payload);
|
|
1954
2530
|
if (assessment.errors.length) {
|
|
1955
2531
|
renderCredentialPreview();
|
|
2532
|
+
setCredentialFieldErrors(assessment.fieldErrors);
|
|
2533
|
+
setCredentialModalFeedback('error', assessment.errors[0]);
|
|
1956
2534
|
setNotice('error', assessment.errors[0]);
|
|
1957
2535
|
return;
|
|
1958
2536
|
}
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
|
|
1962
|
-
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
|
|
1966
|
-
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
|
|
2537
|
+
clearCredentialModalFeedback();
|
|
2538
|
+
clearCredentialFieldErrors();
|
|
2539
|
+
try {
|
|
2540
|
+
const result = await withAction(
|
|
2541
|
+
state.credentialModalMode === 'edit'
|
|
2542
|
+
? 'Token updated.'
|
|
2543
|
+
: 'Token created. Next: run Test Credential or connect your AI tool.'
|
|
2544
|
+
,
|
|
2545
|
+
async function() {
|
|
2546
|
+
if (state.credentialModalMode === 'edit') {
|
|
2547
|
+
const contextResult = await fetchJson('/v1/core/credentials/' + encodeURIComponent(payload.credentialId) + '/context', {
|
|
2548
|
+
method: 'PATCH',
|
|
2549
|
+
headers: {
|
|
2550
|
+
'content-type': 'application/json'
|
|
2551
|
+
},
|
|
2552
|
+
body: JSON.stringify({
|
|
2553
|
+
displayName: payload.displayName,
|
|
2554
|
+
service: payload.service,
|
|
2555
|
+
scopeTier: payload.scopeTier,
|
|
2556
|
+
sensitivity: payload.sensitivity,
|
|
2557
|
+
allowedDomains: payload.allowedDomains,
|
|
2558
|
+
permittedOperations: payload.permittedOperations,
|
|
2559
|
+
userContext: payload.userContext,
|
|
2560
|
+
llmContext: payload.llmContext,
|
|
2561
|
+
selectionNotes: payload.llmContext,
|
|
2562
|
+
tags: payload.tags,
|
|
2563
|
+
})
|
|
2564
|
+
});
|
|
2565
|
+
if (payload.secretSource.adapter === 'local' && payload.secretSource.secretValue.trim()) {
|
|
2566
|
+
await fetchJson('/v1/core/credentials/' + encodeURIComponent(payload.credentialId) + '/local-secret', {
|
|
2567
|
+
method: 'POST',
|
|
2568
|
+
headers: {
|
|
2569
|
+
'content-type': 'application/json'
|
|
2570
|
+
},
|
|
2571
|
+
body: JSON.stringify({
|
|
2572
|
+
secretValue: payload.secretSource.secretValue.trim()
|
|
2573
|
+
})
|
|
2574
|
+
});
|
|
2575
|
+
}
|
|
2576
|
+
return contextResult;
|
|
2577
|
+
}
|
|
2578
|
+
try {
|
|
2579
|
+
return await fetchJson('/v1/core/credentials', {
|
|
2580
|
+
method: 'POST',
|
|
2581
|
+
headers: {
|
|
2582
|
+
'content-type': 'application/json'
|
|
2583
|
+
},
|
|
2584
|
+
body: JSON.stringify(payload)
|
|
2585
|
+
});
|
|
2586
|
+
} catch (error) {
|
|
2587
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
2588
|
+
if (message.includes('already exists')) {
|
|
2589
|
+
const duplicateMessage = 'Token key "' + payload.credentialId + '" is already in use. Change the Token key field and save again.';
|
|
2590
|
+
setCredentialFieldErrors({ 'credential-id': duplicateMessage });
|
|
2591
|
+
throw new Error(duplicateMessage);
|
|
2592
|
+
}
|
|
2593
|
+
throw error;
|
|
2594
|
+
}
|
|
1972
2595
|
}
|
|
1973
|
-
|
|
2596
|
+
);
|
|
2597
|
+
state.lastCreatedCredentialId = result.credential.id;
|
|
2598
|
+
state.selectedCredentialId = result.credential.id;
|
|
2599
|
+
state.currentCredentialContext = result.credential;
|
|
2600
|
+
closeDialog('credential-modal');
|
|
2601
|
+
resetCredentialFormForCreate();
|
|
2602
|
+
syncCredentialTestDefaults(true);
|
|
2603
|
+
} catch (error) {
|
|
2604
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
2605
|
+
if (message.includes('already exists')) {
|
|
2606
|
+
setCredentialFieldErrors({ 'credential-id': message });
|
|
1974
2607
|
}
|
|
1975
|
-
|
|
1976
|
-
|
|
1977
|
-
state.selectedCredentialId = result.credential.id;
|
|
1978
|
-
state.currentCredentialContext = result.credential;
|
|
1979
|
-
state.credentialIdManuallyEdited = false;
|
|
1980
|
-
byId('credential-form').reset();
|
|
1981
|
-
byId('credential-template').value = 'github-readonly';
|
|
1982
|
-
byId('credential-storage').value = 'local';
|
|
1983
|
-
applyCredentialTemplate();
|
|
1984
|
-
syncCredentialSourceFields();
|
|
1985
|
-
renderCredentialPreview();
|
|
1986
|
-
syncCredentialTestDefaults(true);
|
|
2608
|
+
setCredentialModalFeedback('error', message);
|
|
2609
|
+
}
|
|
1987
2610
|
}
|
|
1988
2611
|
|
|
1989
2612
|
async function handleCredentialTest(event) {
|
|
1990
2613
|
event.preventDefault();
|
|
1991
2614
|
const credentialId = byId('credential-test-id').value.trim();
|
|
1992
2615
|
const targetUrl = byId('credential-test-url').value.trim();
|
|
1993
|
-
const result = await withAction('Token check completed. Review the summary below, then connect your AI tool.', async function() {
|
|
1994
|
-
return fetchJson('/v1/access/request', {
|
|
1995
|
-
method: 'POST',
|
|
1996
|
-
headers: {
|
|
1997
|
-
'content-type': 'application/json'
|
|
1998
|
-
},
|
|
1999
|
-
body: JSON.stringify({
|
|
2000
|
-
credentialId: credentialId,
|
|
2001
|
-
operation: 'http.get',
|
|
2002
|
-
targetUrl: targetUrl
|
|
2003
|
-
})
|
|
2004
|
-
});
|
|
2005
|
-
});
|
|
2006
2616
|
state.lastCredentialTestContext = {
|
|
2007
2617
|
credentialId: credentialId,
|
|
2008
2618
|
targetUrl: targetUrl
|
|
2009
2619
|
};
|
|
2010
|
-
|
|
2011
|
-
|
|
2620
|
+
try {
|
|
2621
|
+
const result = await withAction('Token check completed. Review the summary below, then connect your AI tool.', async function() {
|
|
2622
|
+
return fetchJson('/v1/access/request', {
|
|
2623
|
+
method: 'POST',
|
|
2624
|
+
headers: {
|
|
2625
|
+
'content-type': 'application/json'
|
|
2626
|
+
},
|
|
2627
|
+
body: JSON.stringify({
|
|
2628
|
+
credentialId: credentialId,
|
|
2629
|
+
operation: 'http.get',
|
|
2630
|
+
targetUrl: targetUrl
|
|
2631
|
+
})
|
|
2632
|
+
});
|
|
2633
|
+
});
|
|
2634
|
+
state.lastCredentialTest = result;
|
|
2635
|
+
renderCredentials();
|
|
2636
|
+
} catch (error) {
|
|
2637
|
+
state.lastCredentialTest = null;
|
|
2638
|
+
renderCredentialTestError(error instanceof Error ? error.message : String(error));
|
|
2639
|
+
}
|
|
2012
2640
|
}
|
|
2013
2641
|
|
|
2014
2642
|
async function handleCredentialContextAction(event) {
|
|
@@ -2027,11 +2655,21 @@ async function handleCredentialContextAction(event) {
|
|
|
2027
2655
|
setBusy(true);
|
|
2028
2656
|
clearNotice();
|
|
2029
2657
|
await openCredentialContext(credentialId);
|
|
2030
|
-
|
|
2658
|
+
openEditCredentialModal(state.currentCredentialContext);
|
|
2659
|
+
setNotice('info', 'Loaded the token for editing. Secret storage remains separate.');
|
|
2031
2660
|
setBusy(false);
|
|
2032
2661
|
return;
|
|
2033
2662
|
}
|
|
2034
2663
|
|
|
2664
|
+
if (action === 'test') {
|
|
2665
|
+
state.selectedCredentialId = credentialId;
|
|
2666
|
+
byId('credential-test-id').value = credentialId;
|
|
2667
|
+
syncCredentialTestDefaults(true);
|
|
2668
|
+
openSection('credentials-section');
|
|
2669
|
+
setNotice('info', 'Selected token loaded into Test credential.');
|
|
2670
|
+
return;
|
|
2671
|
+
}
|
|
2672
|
+
|
|
2035
2673
|
if (action === 'rename') {
|
|
2036
2674
|
const nextName = window.prompt('New display name', button.dataset.credentialContextName || '');
|
|
2037
2675
|
if (!nextName || !nextName.trim()) {
|
|
@@ -2112,6 +2750,7 @@ async function handleCredentialContextAction(event) {
|
|
|
2112
2750
|
state.lastCredentialTest = null;
|
|
2113
2751
|
state.lastCredentialTestContext = null;
|
|
2114
2752
|
}
|
|
2753
|
+
closeDialog('credential-modal');
|
|
2115
2754
|
renderCredentialContextManager();
|
|
2116
2755
|
return;
|
|
2117
2756
|
}
|
|
@@ -2459,24 +3098,40 @@ async function initialize() {
|
|
|
2459
3098
|
if (localQuickstartButton) {
|
|
2460
3099
|
localQuickstartButton.addEventListener('click', handleLocalQuickstartLogin);
|
|
2461
3100
|
}
|
|
3101
|
+
byId('open-credential-modal').addEventListener('click', openCreateCredentialModal);
|
|
2462
3102
|
byId('credential-form').addEventListener('submit', handleCreateCredential);
|
|
2463
3103
|
byId('credential-test-form').addEventListener('submit', handleCredentialTest);
|
|
2464
|
-
byId('credential-context-form').addEventListener('submit', handleCredentialContextSave);
|
|
2465
3104
|
byId('connect-form').addEventListener('submit', handleMcpConnectionCheck);
|
|
2466
|
-
byId('credential-template').addEventListener('change', applyCredentialTemplate);
|
|
2467
3105
|
byId('credential-name').addEventListener('input', function() {
|
|
2468
3106
|
syncCredentialIdFromName(false);
|
|
2469
3107
|
});
|
|
3108
|
+
byId('credential-service').addEventListener('input', function() {
|
|
3109
|
+
syncCredentialIdFromName(false);
|
|
3110
|
+
});
|
|
2470
3111
|
byId('credential-id').addEventListener('input', function() {
|
|
2471
3112
|
state.credentialIdManuallyEdited = true;
|
|
2472
3113
|
renderCredentialPreview();
|
|
2473
3114
|
});
|
|
2474
3115
|
byId('credential-storage').addEventListener('change', syncCredentialSourceFields);
|
|
2475
|
-
byId('credential-form').addEventListener('input',
|
|
3116
|
+
byId('credential-form').addEventListener('input', function(event) {
|
|
3117
|
+
if (event.target instanceof HTMLElement && event.target.id) {
|
|
3118
|
+
const container = event.target.closest('.field, .field-wide');
|
|
3119
|
+
const errorNode = byId(event.target.id + '-error');
|
|
3120
|
+
container?.classList.remove('has-error');
|
|
3121
|
+
if (errorNode) {
|
|
3122
|
+
errorNode.textContent = '';
|
|
3123
|
+
errorNode.classList.remove('is-visible');
|
|
3124
|
+
}
|
|
3125
|
+
}
|
|
3126
|
+
renderCredentialPreview();
|
|
3127
|
+
});
|
|
2476
3128
|
byId('credential-form').addEventListener('change', renderCredentialPreview);
|
|
2477
3129
|
byId('credential-test-id').addEventListener('change', function() {
|
|
2478
3130
|
syncCredentialTestDefaults(true);
|
|
2479
3131
|
});
|
|
3132
|
+
byId('connect-tabs').addEventListener('click', handleConnectTabClick);
|
|
3133
|
+
byId('connect-section').addEventListener('click', handleCopyAction);
|
|
3134
|
+
byId('connect-section').addEventListener('click', handleApplyToolSetup);
|
|
2480
3135
|
byId('tenant-form').addEventListener('submit', handleCreateTenant);
|
|
2481
3136
|
byId('auth-client-form').addEventListener('submit', handleCreateClient);
|
|
2482
3137
|
byId('refresh-dashboard').addEventListener('click', refreshDashboard);
|
|
@@ -2493,17 +3148,6 @@ async function initialize() {
|
|
|
2493
3148
|
byId('backup-inspect').addEventListener('click', handleBackupInspect);
|
|
2494
3149
|
byId('backup-restore').addEventListener('click', handleBackupRestore);
|
|
2495
3150
|
byId('backup-download').addEventListener('click', downloadBackup);
|
|
2496
|
-
byId('credential-context-form').addEventListener('input', function() {
|
|
2497
|
-
if (!state.selectedCredentialId) {
|
|
2498
|
-
return;
|
|
2499
|
-
}
|
|
2500
|
-
renderContextPreview(
|
|
2501
|
-
'credential-context-preview',
|
|
2502
|
-
'credential-context-preview-warnings',
|
|
2503
|
-
serializeCredentialContextForm(),
|
|
2504
|
-
state.selectedCredentialId,
|
|
2505
|
-
);
|
|
2506
|
-
});
|
|
2507
3151
|
byId('advanced-toggle').addEventListener('click', function() {
|
|
2508
3152
|
state.advancedVisible = !state.advancedVisible;
|
|
2509
3153
|
persistSession();
|
|
@@ -2516,6 +3160,17 @@ async function initialize() {
|
|
|
2516
3160
|
if (!(event.target instanceof Element)) {
|
|
2517
3161
|
return;
|
|
2518
3162
|
}
|
|
3163
|
+
const toastClose = event.target.closest('[data-toast-close]');
|
|
3164
|
+
if (toastClose) {
|
|
3165
|
+
const toastId = toastClose.getAttribute('data-toast-close');
|
|
3166
|
+
document.querySelector('[data-toast-id="' + toastId + '"]')?.remove();
|
|
3167
|
+
return;
|
|
3168
|
+
}
|
|
3169
|
+
const closeButton = event.target.closest('[data-dialog-close]');
|
|
3170
|
+
if (closeButton) {
|
|
3171
|
+
closeDialog(closeButton.getAttribute('data-dialog-close'));
|
|
3172
|
+
return;
|
|
3173
|
+
}
|
|
2519
3174
|
const button = event.target.closest('[data-nav-target]');
|
|
2520
3175
|
if (!button) {
|
|
2521
3176
|
return;
|
|
@@ -2541,7 +3196,7 @@ async function initialize() {
|
|
|
2541
3196
|
} else {
|
|
2542
3197
|
renderAll();
|
|
2543
3198
|
populateLoginDefaults(false);
|
|
2544
|
-
|
|
3199
|
+
applyCredentialDefaults();
|
|
2545
3200
|
syncCredentialSourceFields();
|
|
2546
3201
|
renderCredentialPreview();
|
|
2547
3202
|
if (state.localQuickstartEnabled) {
|
|
@@ -2554,7 +3209,7 @@ async function initialize() {
|
|
|
2554
3209
|
window.addEventListener('DOMContentLoaded', initialize);
|
|
2555
3210
|
`;
|
|
2556
3211
|
export function renderAdminPage(app) {
|
|
2557
|
-
const stdioEntryPath =
|
|
3212
|
+
const stdioEntryPath = resolveLocalStdioEntryPath();
|
|
2558
3213
|
const config = {
|
|
2559
3214
|
version: app.config.version,
|
|
2560
3215
|
baseUrl: app.config.publicBaseUrl,
|
|
@@ -2603,7 +3258,7 @@ export function renderAdminPage(app) {
|
|
|
2603
3258
|
<div>
|
|
2604
3259
|
<span class="eyebrow">Core Mode</span>
|
|
2605
3260
|
<h1>Save a token. Teach the AI when to use it. Keep the secret hidden.</h1>
|
|
2606
|
-
<p class="hero-copy">KeyLore is now centered on one beginner-friendly workflow: save a token, describe it in plain language, test it safely, and connect Codex or
|
|
3261
|
+
<p class="hero-copy">KeyLore is now centered on one beginner-friendly workflow: save a token, describe it in plain language, test it safely, and connect Codex, Gemini, or Claude without putting the raw secret into model-visible context.</p>
|
|
2607
3262
|
</div>
|
|
2608
3263
|
<div class="hero-meta">
|
|
2609
3264
|
<div class="meta-card">
|
|
@@ -2618,6 +3273,7 @@ export function renderAdminPage(app) {
|
|
|
2618
3273
|
</section>
|
|
2619
3274
|
|
|
2620
3275
|
<div id="notice" class="notice"></div>
|
|
3276
|
+
<div id="toast-console" class="toast-console" aria-live="polite" aria-atomic="false"></div>
|
|
2621
3277
|
|
|
2622
3278
|
<section id="login-panel" class="panel" style="margin-top: 24px;">
|
|
2623
3279
|
<div class="section-heading">
|
|
@@ -2667,179 +3323,220 @@ export function renderAdminPage(app) {
|
|
|
2667
3323
|
</section>
|
|
2668
3324
|
|
|
2669
3325
|
<div id="dashboard" class="dashboard" hidden>
|
|
2670
|
-
<
|
|
3326
|
+
<div class="utility-shell" aria-hidden="true">
|
|
3327
|
+
<button class="button-secondary" id="refresh-dashboard" type="button" data-busy-label="Refreshing..." data-idle-label="Refresh everything">Refresh everything</button>
|
|
3328
|
+
<button class="button-danger" id="logout" type="button">Clear session</button>
|
|
3329
|
+
<span id="session-status" class="state-warning">Not connected</span>
|
|
3330
|
+
<span id="session-client-id">anonymous token</span>
|
|
3331
|
+
<span id="session-tenant">global operator</span>
|
|
3332
|
+
<span id="session-scopes">not loaded</span>
|
|
3333
|
+
</div>
|
|
3334
|
+
<section id="quick-start-section" class="panel">
|
|
2671
3335
|
<div class="section-heading">
|
|
2672
3336
|
<div>
|
|
2673
|
-
<h2>
|
|
2674
|
-
<p>The
|
|
3337
|
+
<h2>Quick start</h2>
|
|
3338
|
+
<p>The shortest path is add token, test token, then connect your AI tool.</p>
|
|
2675
3339
|
</div>
|
|
2676
|
-
<div class="toolbar">
|
|
2677
|
-
<button class="button-secondary" id="refresh-dashboard" type="button" data-busy-label="Refreshing..." data-idle-label="Refresh everything">Refresh everything</button>
|
|
2678
|
-
<button class="button-danger" id="logout" type="button">Clear session</button>
|
|
2679
|
-
</div>
|
|
2680
|
-
</div>
|
|
2681
|
-
<div class="session-line">
|
|
2682
|
-
<span id="session-status" class="state-warning">Not connected</span>
|
|
2683
|
-
<span class="pill"><strong>Current path</strong> Core onboarding</span>
|
|
2684
3340
|
</div>
|
|
2685
|
-
<
|
|
2686
|
-
|
|
2687
|
-
<div class="list-meta">
|
|
2688
|
-
<span class="pill"><strong>Client</strong> <span id="session-client-id">anonymous token</span></span>
|
|
2689
|
-
<span class="pill"><strong>Tenant</strong> <span id="session-tenant">global operator</span></span>
|
|
2690
|
-
<span class="pill"><strong>Scopes</strong> <span id="session-scopes">not loaded</span></span>
|
|
2691
|
-
</div>
|
|
2692
|
-
</details>
|
|
2693
|
-
<div id="core-journey" style="margin-top: 18px;"></div>
|
|
2694
|
-
<div id="advanced-summary" class="advanced-summary"></div>
|
|
3341
|
+
<div id="core-journey"></div>
|
|
3342
|
+
<div id="advanced-summary" class="advanced-summary" style="margin-top:18px;"></div>
|
|
2695
3343
|
</section>
|
|
2696
3344
|
|
|
2697
3345
|
<section id="credentials-section" class="panel">
|
|
2698
3346
|
<div class="section-heading">
|
|
2699
3347
|
<div>
|
|
2700
|
-
<h2>
|
|
2701
|
-
<p>
|
|
3348
|
+
<h2>Your tokens</h2>
|
|
3349
|
+
<p>All saved tokens live here. Add one, edit it in a popup, test it, or remove it.</p>
|
|
2702
3350
|
</div>
|
|
2703
3351
|
</div>
|
|
2704
|
-
<div class="panel
|
|
2705
|
-
<div class="
|
|
3352
|
+
<div class="panel">
|
|
3353
|
+
<div class="token-toolbar">
|
|
3354
|
+
<div>
|
|
3355
|
+
<h2 style="font-size:1.4rem;">Saved tokens</h2>
|
|
3356
|
+
<p>All tokens are listed together here, including examples. Edit or delete any row directly.</p>
|
|
3357
|
+
</div>
|
|
3358
|
+
<button class="button" id="open-credential-modal" type="button">Add token</button>
|
|
3359
|
+
</div>
|
|
3360
|
+
<div id="credential-list"></div>
|
|
3361
|
+
</div>
|
|
3362
|
+
<div class="panel" style="margin-top:18px;">
|
|
3363
|
+
<div class="section-heading">
|
|
3364
|
+
<div>
|
|
3365
|
+
<h2 style="font-size:1.4rem;">Test credential</h2>
|
|
3366
|
+
<p>Run a real safe check. KeyLore will make an <code>http.get</code> call with the selected token and URL, without exposing the raw secret.</p>
|
|
3367
|
+
</div>
|
|
3368
|
+
</div>
|
|
3369
|
+
<form id="credential-test-form" class="form-grid">
|
|
3370
|
+
<div class="field"><label for="credential-test-id">Token to check</label><select id="credential-test-id"></select></div>
|
|
3371
|
+
<div class="field-wide"><label for="credential-test-url">URL to call with this token</label><input id="credential-test-url" type="url" placeholder="https://api.github.com/rate_limit" required /></div>
|
|
3372
|
+
<div class="panel-footnote field-wide" style="margin-top:-4px;">Success means the token, the target domain, and KeyLore policy all allowed the request.</div>
|
|
3373
|
+
<div class="form-actions field-wide"><button class="button-secondary" type="submit" data-busy-label="Testing credential..." data-idle-label="Check this token">Check this token</button></div>
|
|
3374
|
+
</form>
|
|
3375
|
+
<div id="credential-test-result" style="margin-top: 18px;"></div>
|
|
3376
|
+
</div>
|
|
3377
|
+
</section>
|
|
3378
|
+
|
|
3379
|
+
<dialog id="credential-modal" class="modal">
|
|
3380
|
+
<div class="modal-card">
|
|
3381
|
+
<div class="modal-header">
|
|
3382
|
+
<div>
|
|
3383
|
+
<h2 id="credential-modal-title" style="margin:0;">Add token</h2>
|
|
3384
|
+
<p id="credential-modal-copy" class="panel-footnote" style="margin-top:8px;">Paste the token, add the human and AI context, and save it into KeyLore.</p>
|
|
3385
|
+
</div>
|
|
3386
|
+
<button class="button-secondary" type="button" data-dialog-close="credential-modal">Close</button>
|
|
3387
|
+
</div>
|
|
3388
|
+
<div class="modal-body">
|
|
3389
|
+
<div id="credential-modal-feedback" class="modal-feedback" aria-live="assertive"></div>
|
|
2706
3390
|
<form id="credential-form" class="form-grid">
|
|
2707
|
-
<div class="field
|
|
2708
|
-
<div class="field"><label for="credential-
|
|
2709
|
-
<div class="field"><label for="credential-id">Token key</label><input id="credential-id" type="text" required placeholder="github-read-only-token-local" /></div>
|
|
3391
|
+
<div class="field"><label class="field-label" for="credential-name">Name shown in KeyLore <span class="info-glyph" tabindex="0" data-tooltip="The human-friendly name you will recognize later in the saved token list. Keep it short and specific.">i</span></label><input id="credential-name" type="text" required /><div id="credential-name-error" class="field-error" aria-live="polite"></div></div>
|
|
3392
|
+
<div class="field"><label class="field-label" for="credential-id">Token key <span class="info-glyph" tabindex="0" data-tooltip="The unique internal key for this token. KeyLore uses it when testing, selecting, and connecting the token. Change this if KeyLore says the key already exists.">i</span></label><input id="credential-id" type="text" required placeholder="github-read-only-token-local" /><div id="credential-id-error" class="field-error" aria-live="polite"></div></div>
|
|
2710
3393
|
<div class="field-wide panel-footnote" style="margin-top:-4px;">This is the unique key for the token. It appears in the saved-token list and is what you change if KeyLore says a token key already exists.</div>
|
|
2711
|
-
<div class="field"><label for="credential-
|
|
2712
|
-
<div
|
|
2713
|
-
<div class="field
|
|
3394
|
+
<div class="field"><label class="field-label" for="credential-storage">Where to store the token <span class="info-glyph" tabindex="0" data-tooltip="Local encrypted store is the simple default. Environment reference is only for advanced setups where another process already manages the secret.">i</span></label><select id="credential-storage"><option value="local">Local encrypted store</option><option value="env">Environment reference</option></select></div>
|
|
3395
|
+
<div class="field"><label class="field-label" for="credential-service">Service name <span class="info-glyph" tabindex="0" data-tooltip="The service this token belongs to, such as github, npm, stripe, or internal_api. This helps the AI identify the right token.">i</span></label><input id="credential-service" type="text" required placeholder="github" /><div id="credential-service-error" class="field-error" aria-live="polite"></div></div>
|
|
3396
|
+
<div class="field"><label class="field-label" for="credential-sensitivity">Risk level <span class="info-glyph" tabindex="0" data-tooltip="How damaging misuse would be. Use higher levels for broad access or write-capable tokens.">i</span></label><select id="credential-sensitivity"><option value="moderate">moderate</option><option value="high">high</option><option value="critical">critical</option></select></div>
|
|
3397
|
+
<div class="field"><label class="field-label" for="credential-operations">Allow writes? <span class="info-glyph" tabindex="0" data-tooltip="Choose whether this token should be treated as read-only or allowed to make controlled write requests.">i</span></label><select id="credential-operations"><option value="http.get">No, read only</option><option value="http.get,http.post">Yes, allow controlled writes</option></select></div>
|
|
3398
|
+
<div class="field"><label class="field-label" for="credential-tags">Tags <span class="info-glyph" tabindex="0" data-tooltip="Optional short labels like github, readonly, billing, or internal. These help search and later organization.">i</span></label><input id="credential-tags" type="text" placeholder="github,readonly" /></div>
|
|
3399
|
+
<div class="field"><label class="field-label" for="credential-domains">Where can it be used? <span class="info-glyph" tabindex="0" data-tooltip="List the domains this token is allowed to reach, such as api.github.com. This keeps the token scoped and helps the AI choose correctly.">i</span></label><textarea id="credential-domains" placeholder="api.github.com"></textarea><div id="credential-domains-error" class="field-error" aria-live="polite"></div></div>
|
|
3400
|
+
<div id="credential-secret-field" class="field-wide"><label id="credential-secret-label" class="field-label" for="credential-secret">Paste token <span class="info-glyph" tabindex="0" data-tooltip="Paste the raw token value here. KeyLore stores it separately from the searchable metadata so the AI does not see the secret itself.">i</span></label><textarea id="credential-secret" placeholder="Paste the raw token here. KeyLore stores it outside the searchable metadata catalogue."></textarea><div id="credential-secret-error" class="field-error" aria-live="polite"></div></div>
|
|
3401
|
+
<div id="credential-env-ref-field" class="field-wide" hidden><label class="field-label" for="credential-env-ref">Environment variable name <span class="info-glyph" tabindex="0" data-tooltip="Advanced mode only. Enter the environment variable that already contains the token, for example KEYLORE_SECRET_GITHUB_READONLY.">i</span></label><input id="credential-env-ref" type="text" placeholder="KEYLORE_SECRET_GITHUB_READONLY" /></div>
|
|
3402
|
+
<div class="field-wide"><label class="field-label" for="credential-user-context">Explain this token for people <span class="info-glyph" tabindex="0" data-tooltip="Human context: why this token exists, who it is for, and any caveats. Example: Primary read-only GitHub token for routine repository metadata lookups.">i</span></label><textarea id="credential-user-context" placeholder="Example: Primary read-only GitHub token for routine repository metadata lookups."></textarea><div id="credential-user-context-error" class="field-error" aria-live="polite"></div></div>
|
|
3403
|
+
<div class="field-wide"><label class="field-label" for="credential-llm-context">Tell the AI when to use this token <span class="info-glyph" tabindex="0" data-tooltip="AI context: be explicit about when to use this token and what to avoid. Good example: Use for GitHub repository metadata, issues, and pull requests. Never use it for write actions.">i</span></label><textarea id="credential-llm-context" placeholder="Example: Use this for GitHub repository metadata, issues, and pull requests. Do not use it for write actions."></textarea><div id="credential-llm-context-error" class="field-error" aria-live="polite"></div></div>
|
|
2714
3404
|
<div class="field-wide">
|
|
2715
|
-
<label for="credential-
|
|
2716
|
-
<div id="credential-guidance"></div>
|
|
2717
|
-
</div>
|
|
2718
|
-
<div class="field-wide">
|
|
2719
|
-
<label for="credential-mcp-preview">What the AI will see</label>
|
|
3405
|
+
<label class="field-label" for="credential-mcp-preview">What the AI will see <span class="info-glyph" tabindex="0" data-tooltip="This preview shows the metadata the AI can see. It never includes the raw secret value or the underlying secret binding details.">i</span></label>
|
|
2720
3406
|
<div id="credential-preview-warnings" style="margin-bottom: 12px;"></div>
|
|
2721
|
-
<
|
|
3407
|
+
<details class="disclosure">
|
|
3408
|
+
<summary>Show the AI-visible record</summary>
|
|
3409
|
+
<div id="credential-mcp-preview" style="margin-top: 12px;"></div>
|
|
3410
|
+
</details>
|
|
2722
3411
|
</div>
|
|
2723
|
-
<
|
|
2724
|
-
<summary>Advanced token settings</summary>
|
|
2725
|
-
<div class="form-grid">
|
|
2726
|
-
<div class="field"><label for="credential-storage">Where to store the token</label><select id="credential-storage"><option value="local">Local encrypted store</option><option value="env">Environment reference</option></select></div>
|
|
2727
|
-
<div class="field"><label for="credential-service">Service name</label><input id="credential-service" type="text" required /></div>
|
|
2728
|
-
<div class="field"><label for="credential-sensitivity">Risk level</label><select id="credential-sensitivity"><option value="moderate">moderate</option><option value="high">high</option><option value="critical">critical</option></select></div>
|
|
2729
|
-
<div class="field"><label for="credential-operations">Allow writes?</label><select id="credential-operations"><option value="http.get">No, read only</option><option value="http.get,http.post">Yes, allow controlled writes</option></select></div>
|
|
2730
|
-
<div class="field"><label for="credential-tags">Tags</label><input id="credential-tags" type="text" placeholder="github,readonly" /></div>
|
|
2731
|
-
<div id="credential-env-ref-field" class="field-wide" hidden><label for="credential-env-ref">Environment variable name</label><input id="credential-env-ref" type="text" placeholder="KEYLORE_SECRET_GITHUB_READONLY" /></div>
|
|
2732
|
-
</div>
|
|
2733
|
-
</details>
|
|
2734
|
-
<div class="form-actions field-wide"><button class="button" type="submit" data-busy-label="Saving token..." data-idle-label="Save token">Save token</button></div>
|
|
3412
|
+
<div class="form-actions field-wide"><button class="button" id="credential-submit" type="submit" data-busy-label="Saving token..." data-idle-label="Save token">Save token</button></div>
|
|
2735
3413
|
</form>
|
|
2736
3414
|
</div>
|
|
2737
|
-
<div class="span-5 code-stack">
|
|
2738
|
-
<div class="panel">
|
|
2739
|
-
<div class="section-heading">
|
|
2740
|
-
<div>
|
|
2741
|
-
<h2 style="font-size:1.4rem;">Saved tokens</h2>
|
|
2742
|
-
<p>Use these after you save something. The main next step is usually Test credential.</p>
|
|
2743
|
-
</div>
|
|
2744
|
-
</div>
|
|
2745
|
-
<div id="credential-list"></div>
|
|
2746
|
-
</div>
|
|
2747
|
-
<div class="panel">
|
|
2748
|
-
<div class="section-heading">
|
|
2749
|
-
<div>
|
|
2750
|
-
<h2 style="font-size:1.4rem;">Test credential</h2>
|
|
2751
|
-
<p>Run a real safe check. KeyLore will make an <code>http.get</code> call with the selected token and URL, without exposing the raw secret.</p>
|
|
2752
|
-
</div>
|
|
2753
|
-
</div>
|
|
2754
|
-
<form id="credential-test-form" class="form-grid">
|
|
2755
|
-
<div class="field"><label for="credential-test-id">Token to check</label><select id="credential-test-id"></select></div>
|
|
2756
|
-
<div class="field-wide"><label for="credential-test-url">URL to call with this token</label><input id="credential-test-url" type="url" placeholder="https://api.github.com/rate_limit" required /></div>
|
|
2757
|
-
<div class="panel-footnote field-wide" style="margin-top:-4px;">Success means the token, the target domain, and KeyLore policy all allowed the request.</div>
|
|
2758
|
-
<div class="form-actions field-wide"><button class="button-secondary" type="submit" data-busy-label="Testing credential..." data-idle-label="Check this token">Check this token</button></div>
|
|
2759
|
-
</form>
|
|
2760
|
-
<div id="credential-test-result" style="margin-top: 18px;"></div>
|
|
2761
|
-
</div>
|
|
2762
|
-
<details class="panel disclosure">
|
|
2763
|
-
<summary>Inspect or edit AI-facing context</summary>
|
|
2764
|
-
<div class="panel-footnote">This is metadata only. Secret storage and raw token values stay separate and are not shown here.</div>
|
|
2765
|
-
<div class="panel-grid" style="margin-top: 16px;">
|
|
2766
|
-
<div class="span-6 panel">
|
|
2767
|
-
<div class="section-heading"><div><h2 style="font-size:1.2rem;">Current AI-visible record</h2></div></div>
|
|
2768
|
-
<div id="credential-context-current"></div>
|
|
2769
|
-
</div>
|
|
2770
|
-
<div class="span-6 panel">
|
|
2771
|
-
<div class="section-heading"><div><h2 style="font-size:1.2rem;">Context editor</h2></div></div>
|
|
2772
|
-
<form id="credential-context-form" class="form-grid" hidden>
|
|
2773
|
-
<div class="field"><label for="credential-context-id">Credential ID</label><input id="credential-context-id" type="text" readonly /></div>
|
|
2774
|
-
<div class="field"><label for="credential-context-display-name">Display Name</label><input id="credential-context-display-name" type="text" required /></div>
|
|
2775
|
-
<div class="field"><label for="credential-context-service">Service</label><input id="credential-context-service" type="text" required /></div>
|
|
2776
|
-
<div class="field"><label for="credential-context-sensitivity">Risk level</label><select id="credential-context-sensitivity"><option value="moderate">moderate</option><option value="high">high</option><option value="critical">critical</option></select></div>
|
|
2777
|
-
<div class="field"><label for="credential-context-status">Lifecycle</label><select id="credential-context-status"><option value="active">active</option><option value="disabled">disabled</option></select></div>
|
|
2778
|
-
<div class="field"><label for="credential-context-operations">Allow writes?</label><select id="credential-context-operations"><option value="http.get">No, read only</option><option value="http.get,http.post">Yes, allow controlled writes</option></select></div>
|
|
2779
|
-
<div class="field-wide"><label for="credential-context-domains">Where can it be used?</label><textarea id="credential-context-domains"></textarea></div>
|
|
2780
|
-
<div class="field-wide"><label for="credential-context-notes">Tell the AI when to use this token</label><textarea id="credential-context-notes"></textarea></div>
|
|
2781
|
-
<div class="field-wide"><label for="credential-context-tags">Tags</label><input id="credential-context-tags" type="text" /></div>
|
|
2782
|
-
<div class="field-wide">
|
|
2783
|
-
<label for="credential-context-preview">Updated AI-visible preview</label>
|
|
2784
|
-
<div id="credential-context-preview-warnings" style="margin-bottom: 12px;"></div>
|
|
2785
|
-
<div id="credential-context-preview"></div>
|
|
2786
|
-
</div>
|
|
2787
|
-
<div class="form-actions field-wide"><button class="button-secondary" type="submit" data-busy-label="Saving context..." data-idle-label="Save context changes">Save context changes</button></div>
|
|
2788
|
-
</form>
|
|
2789
|
-
</div>
|
|
2790
|
-
</div>
|
|
2791
|
-
</details>
|
|
2792
|
-
</div>
|
|
2793
3415
|
</div>
|
|
2794
|
-
</
|
|
3416
|
+
</dialog>
|
|
2795
3417
|
|
|
2796
3418
|
<section id="connect-section" class="panel">
|
|
2797
3419
|
<div class="section-heading">
|
|
2798
3420
|
<div>
|
|
2799
3421
|
<h2>Connect your AI tool</h2>
|
|
2800
|
-
<p>
|
|
3422
|
+
<p>Follow the tool-specific steps below. Each one tells you where to put the config, what to restart, and what to try first.</p>
|
|
2801
3423
|
</div>
|
|
2802
3424
|
</div>
|
|
3425
|
+
<div id="connect-tabs" class="tab-row" role="tablist" aria-label="AI tool setup tabs">
|
|
3426
|
+
<button class="tab-button" type="button" data-connect-tab="codex" role="tab" aria-selected="true">Codex</button>
|
|
3427
|
+
<button class="tab-button" type="button" data-connect-tab="gemini" role="tab" aria-selected="false">Gemini CLI</button>
|
|
3428
|
+
<button class="tab-button" type="button" data-connect-tab="claude" role="tab" aria-selected="false">Claude CLI</button>
|
|
3429
|
+
</div>
|
|
2803
3430
|
<div class="panel-grid">
|
|
2804
|
-
<div class="span-
|
|
3431
|
+
<div class="span-12 code-stack" data-connect-panel="codex">
|
|
3432
|
+
<div class="panel">
|
|
3433
|
+
<div class="section-heading"><div><h2 style="font-size:1.4rem;">Codex</h2><p>Recommended for local use.</p></div></div>
|
|
3434
|
+
<ol class="panel-footnote">
|
|
3435
|
+
<li>Open or create <span class="mono">~/.codex/config.toml</span>.</li>
|
|
3436
|
+
<li>Paste the snippet below into that file under the top-level <span class="mono">mcp_servers</span> section.</li>
|
|
3437
|
+
<li>Save the file, then restart Codex.</li>
|
|
3438
|
+
<li>Use the first prompt after restart, or run <span class="mono">/mcp</span> inside Codex to confirm KeyLore is available.</li>
|
|
3439
|
+
</ol>
|
|
3440
|
+
<div class="snippet-stack">
|
|
3441
|
+
<div class="snippet-box">
|
|
3442
|
+
<button class="copy-glyph" type="button" data-copy-target="codex-stdio-snippet" data-copy-label="Codex setup snippet" aria-label="Copy Codex setup snippet">⧉</button>
|
|
3443
|
+
<textarea id="codex-stdio-snippet" style="width:100%; min-height: 130px;"></textarea>
|
|
3444
|
+
</div>
|
|
3445
|
+
</div>
|
|
3446
|
+
<div class="panel-actions" style="margin-top:16px;">
|
|
3447
|
+
<button class="button-secondary" type="button" data-apply-tool="codex">Apply to my Codex settings</button>
|
|
3448
|
+
</div>
|
|
3449
|
+
</div>
|
|
3450
|
+
</div>
|
|
3451
|
+
<div class="span-12 code-stack" data-connect-panel="gemini" hidden>
|
|
2805
3452
|
<div class="panel">
|
|
2806
|
-
<div class="section-heading"><div><h2 style="font-size:1.4rem;">
|
|
2807
|
-
<
|
|
2808
|
-
|
|
2809
|
-
|
|
2810
|
-
|
|
3453
|
+
<div class="section-heading"><div><h2 style="font-size:1.4rem;">Gemini CLI</h2><p>Recommended for local use.</p></div></div>
|
|
3454
|
+
<ol class="panel-footnote">
|
|
3455
|
+
<li>Open <span class="mono">~/.gemini/settings.json</span>.</li>
|
|
3456
|
+
<li>Merge the snippet below into the <span class="mono">mcpServers</span> object. If the file is empty, paste the whole snippet.</li>
|
|
3457
|
+
<li>Save the file, then restart Gemini CLI.</li>
|
|
3458
|
+
<li>Run <span class="mono">gemini mcp list</span> if you want to confirm KeyLore is connected, then use the first prompt below.</li>
|
|
3459
|
+
</ol>
|
|
3460
|
+
<div class="snippet-stack">
|
|
3461
|
+
<div class="snippet-box">
|
|
3462
|
+
<button class="copy-glyph" type="button" data-copy-target="gemini-stdio-snippet" data-copy-label="Gemini setup snippet" aria-label="Copy Gemini setup snippet">⧉</button>
|
|
3463
|
+
<textarea id="gemini-stdio-snippet" style="width:100%; min-height: 170px;"></textarea>
|
|
3464
|
+
</div>
|
|
3465
|
+
</div>
|
|
3466
|
+
<div class="panel-actions" style="margin-top:16px;">
|
|
3467
|
+
<button class="button-secondary" type="button" data-apply-tool="gemini">Apply to my Gemini settings</button>
|
|
3468
|
+
</div>
|
|
2811
3469
|
</div>
|
|
3470
|
+
</div>
|
|
3471
|
+
<div class="span-12 code-stack" data-connect-panel="claude" hidden>
|
|
2812
3472
|
<div class="panel">
|
|
2813
|
-
<div class="section-heading"><div><h2 style="font-size:1.4rem;">
|
|
2814
|
-
<
|
|
2815
|
-
|
|
2816
|
-
|
|
3473
|
+
<div class="section-heading"><div><h2 style="font-size:1.4rem;">Claude CLI</h2><p>Recommended for local use.</p></div></div>
|
|
3474
|
+
<ol class="panel-footnote">
|
|
3475
|
+
<li>Run the command below in your shell. It adds KeyLore to Claude's MCP config for you.</li>
|
|
3476
|
+
<li>Confirm the server appears with <span class="mono">claude mcp list</span>.</li>
|
|
3477
|
+
<li>Start or restart Claude CLI.</li>
|
|
3478
|
+
<li>Use the first prompt after restart.</li>
|
|
3479
|
+
</ol>
|
|
3480
|
+
<div class="snippet-stack">
|
|
3481
|
+
<div class="snippet-box">
|
|
3482
|
+
<button class="copy-glyph" type="button" data-copy-target="claude-stdio-snippet" data-copy-label="Claude setup command" aria-label="Copy Claude setup command">⧉</button>
|
|
3483
|
+
<textarea id="claude-stdio-snippet" style="width:100%; min-height: 160px;"></textarea>
|
|
3484
|
+
</div>
|
|
3485
|
+
</div>
|
|
3486
|
+
<div class="panel-actions" style="margin-top:16px;">
|
|
3487
|
+
<button class="button-secondary" type="button" data-apply-tool="claude">Apply to my Claude settings</button>
|
|
3488
|
+
</div>
|
|
2817
3489
|
</div>
|
|
2818
3490
|
</div>
|
|
2819
|
-
<div class="span-
|
|
2820
|
-
<div class="section-heading"
|
|
2821
|
-
|
|
2822
|
-
|
|
3491
|
+
<div class="span-12 panel">
|
|
3492
|
+
<div class="section-heading">
|
|
3493
|
+
<div>
|
|
3494
|
+
<h2 style="font-size:1.4rem;">First prompt to try</h2>
|
|
3495
|
+
<p>Use the same prompt after connecting any supported tool.</p>
|
|
3496
|
+
</div>
|
|
3497
|
+
</div>
|
|
3498
|
+
<div class="snippet-box">
|
|
3499
|
+
<button class="copy-glyph" type="button" data-copy-target="shared-first-prompt" data-copy-label="Shared first prompt" aria-label="Copy shared first prompt">⧉</button>
|
|
3500
|
+
<textarea id="shared-first-prompt" style="width:100%; min-height: 130px;"></textarea>
|
|
3501
|
+
</div>
|
|
2823
3502
|
</div>
|
|
2824
3503
|
<div class="span-12">
|
|
2825
3504
|
<details class="panel disclosure">
|
|
2826
3505
|
<summary>Remote or advanced connection options</summary>
|
|
2827
3506
|
<div class="panel-grid" style="margin-top: 16px;">
|
|
2828
|
-
<div class="span-
|
|
3507
|
+
<div class="span-4 code-stack">
|
|
2829
3508
|
<div class="panel">
|
|
2830
3509
|
<div class="section-heading"><div><h2 style="font-size:1.4rem;">Codex HTTP</h2></div></div>
|
|
2831
|
-
<
|
|
3510
|
+
<div class="snippet-box">
|
|
3511
|
+
<button class="copy-glyph" type="button" data-copy-target="codex-http-snippet" data-copy-label="Codex HTTP snippet" aria-label="Copy Codex HTTP snippet">⧉</button>
|
|
3512
|
+
<textarea id="codex-http-snippet" style="width:100%; min-height: 110px;"></textarea>
|
|
3513
|
+
</div>
|
|
2832
3514
|
</div>
|
|
2833
3515
|
<div class="panel">
|
|
2834
3516
|
<div class="section-heading"><div><h2 style="font-size:1.4rem;">Gemini HTTP</h2></div></div>
|
|
2835
|
-
<
|
|
3517
|
+
<div class="snippet-box">
|
|
3518
|
+
<button class="copy-glyph" type="button" data-copy-target="gemini-http-snippet" data-copy-label="Gemini HTTP snippet" aria-label="Copy Gemini HTTP snippet">⧉</button>
|
|
3519
|
+
<textarea id="gemini-http-snippet" style="width:100%; min-height: 190px;"></textarea>
|
|
3520
|
+
</div>
|
|
2836
3521
|
</div>
|
|
3522
|
+
<div class="panel">
|
|
3523
|
+
<div class="section-heading"><div><h2 style="font-size:1.4rem;">Claude HTTP</h2></div></div>
|
|
3524
|
+
<div class="snippet-box">
|
|
3525
|
+
<button class="copy-glyph" type="button" data-copy-target="claude-http-snippet" data-copy-label="Claude HTTP snippet" aria-label="Copy Claude HTTP snippet">⧉</button>
|
|
3526
|
+
<textarea id="claude-http-snippet" style="width:100%; min-height: 170px;"></textarea>
|
|
3527
|
+
</div>
|
|
3528
|
+
</div>
|
|
3529
|
+
</div>
|
|
3530
|
+
<div class="span-4 code-stack">
|
|
2837
3531
|
<div class="panel">
|
|
2838
3532
|
<div class="section-heading"><div><h2 style="font-size:1.4rem;">Generic HTTP</h2></div></div>
|
|
2839
|
-
<
|
|
3533
|
+
<div class="snippet-box">
|
|
3534
|
+
<button class="copy-glyph" type="button" data-copy-target="generic-http-snippet" data-copy-label="Generic HTTP snippet" aria-label="Copy generic HTTP snippet">⧉</button>
|
|
3535
|
+
<textarea id="generic-http-snippet" style="width:100%; min-height: 90px;"></textarea>
|
|
3536
|
+
</div>
|
|
2840
3537
|
</div>
|
|
2841
3538
|
</div>
|
|
2842
|
-
<div class="span-
|
|
3539
|
+
<div class="span-4 panel">
|
|
2843
3540
|
<div class="section-heading">
|
|
2844
3541
|
<div>
|
|
2845
3542
|
<h2 style="font-size:1.4rem;">HTTP MCP token and check</h2>
|
|
@@ -2849,7 +3546,7 @@ export function renderAdminPage(app) {
|
|
|
2849
3546
|
<form id="connect-form" class="form-grid">
|
|
2850
3547
|
<div class="field"><label for="connect-client-id">Client ID</label><input id="connect-client-id" type="text" placeholder="keylore-admin-local" /></div>
|
|
2851
3548
|
<div class="field"><label for="connect-client-secret">Client Secret</label><input id="connect-client-secret" type="password" placeholder="operator secret" /></div>
|
|
2852
|
-
<div class="field-wide"><label for="mcp-token-export">Export command</label><textarea id="mcp-token-export" style="width:100%; min-height: 90px;"></textarea></div>
|
|
3549
|
+
<div class="field-wide"><label for="mcp-token-export">Export command</label><div class="snippet-box"><button class="copy-glyph" type="button" data-copy-target="mcp-token-export" data-copy-label="MCP export command" aria-label="Copy MCP export command">⧉</button><textarea id="mcp-token-export" style="width:100%; min-height: 90px;"></textarea></div></div>
|
|
2853
3550
|
<div class="form-actions field-wide"><button class="button-secondary" type="submit" data-busy-label="Checking MCP..." data-idle-label="Mint token and verify HTTP MCP">Mint token and verify HTTP MCP</button></div>
|
|
2854
3551
|
</form>
|
|
2855
3552
|
<div id="connect-result" style="margin-top: 18px;"></div>
|