@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.
@@ -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: block;
299
+ display: none;
275
300
  }
276
301
 
277
- .notice.is-info {
278
- background: rgba(19, 93, 74, 0.08);
279
- color: var(--accent);
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
- .notice.is-error {
283
- background: rgba(143, 45, 35, 0.09);
284
- color: var(--danger);
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
- .notice.is-warning {
288
- background: rgba(157, 75, 20, 0.1);
289
- color: var(--warning);
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 firstPromptForClient(clientName) {
1118
+ function firstPrompt() {
815
1119
  const credential = state.currentCredentialContext || selectedCredentialSummary() || visibleCredentials()[0];
816
1120
  if (!credential) {
817
- return 'After you create a credential, ask ' + clientName + ': "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."';
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 'Ask ' + clientName + ': "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."';
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
- const node = byId('notice');
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
- const node = byId('notice');
886
- node.className = 'notice';
887
- node.textContent = '';
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 a snippet, and try the first prompt.'
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;">What to do next</h2><p>Follow the short path. Everything else can wait until later.</p></div></div>',
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
- '<article class="step-card"><span class="' + (state.token ? 'state-active' : '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="' + escapeHtml(state.token ? 'session-section' : 'login-panel') + '">Go there</button></div></article>',
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 credentials are visible yet. Start with a template, save a token, then use Test Credential as the next step.</div>';
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="list-card">',
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 AI notes</button></div>',
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 ownCredentials = payload.credentials.filter(function(credential) {
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
- const starterCredentials = payload.credentials.filter(function(credential) {
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
- const ownCredentials = state.data.credentials.data.credentials.filter(function(credential) {
1208
- return credential.owner === 'local';
1209
- });
1210
- const starterCredentials = state.data.credentials.data.credentials.filter(function(credential) {
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 notes = String(payload.selectionNotes || '');
1269
- const normalizedNotes = notes.trim().toLowerCase();
1270
- if (!notes.trim()) {
1271
- errors.push('Selection notes are required. Explain when the agent should use this credential.');
1272
- } else if (notes.trim().length < 16) {
1273
- errors.push('Selection notes are too short. Add enough detail for the agent to distinguish this credential from others.');
1274
- } else if (notes.trim().length < 40) {
1275
- warnings.push('Selection notes are short. Add when-to-use guidance so the agent can choose this credential reliably.');
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(normalizedNotes)) {
1287
- errors.push('Selection notes are too vague. Say what the credential is for, when the agent should choose it, and what it should avoid.');
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(notes)) {
1291
- errors.push('Selection notes look like they may contain a secret. Keep raw tokens out of the agent-visible context.');
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 credentialGuidanceForTemplate() {
1298
- const template = byId('credential-template').value;
1299
- if (template === 'github-readonly') {
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
- avoid: 'GitHub token'
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 (template === 'github-write') {
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 (template === 'npm-readonly') {
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 (template === 'internal-service') {
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 = credentialGuidanceForTemplate();
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">Good notes should answer: what service is this for, when should the agent choose it, and what should it avoid doing?</div>'
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
- selectionNotes: payload.selectionNotes || '',
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 = credentialContextAssessment(payload);
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
- selectionNotes: payload.selectionNotes || '',
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 agent-facing metadata only. Secret bindings and raw tokens stay separate and are not editable here.</div>');
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-notes').value = credential.selectionNotes;
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
- selectionNotes: byId('credential-context-notes').value.trim(),
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('connect-stdio-status').innerHTML = config.stdioAvailable
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, copy a stdio snippet and restart your MCP client. For remote HTTP MCP, run the connection check here first.</div>';
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 template = byId('credential-template').value;
1835
- const fallback = template === 'generic-bearer' ? 'token' : template.replace(/[^a-z0-9]+/g, '-');
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
- selectionNotes: byId('credential-notes').value.trim(),
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: byId('credential-secret').value
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 = credentialContextAssessment(payload);
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
- const result = await withAction('Credential created. Next: run Test Credential or inspect the saved context.', async function() {
1960
- try {
1961
- return await fetchJson('/v1/core/credentials', {
1962
- method: 'POST',
1963
- headers: {
1964
- 'content-type': 'application/json'
1965
- },
1966
- body: JSON.stringify(payload)
1967
- });
1968
- } catch (error) {
1969
- const message = error instanceof Error ? error.message : String(error);
1970
- if (message.includes('already exists')) {
1971
- throw new Error('Token key "' + payload.credentialId + '" is already in use. Change the Token key field and save again.');
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
- throw error;
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
- state.lastCreatedCredentialId = result.credential.id;
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
- state.lastCredentialTest = result;
2011
- renderCredentials();
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
- setNotice('info', 'Loaded the current MCP-visible context. Secret storage remains separate.');
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', renderCredentialPreview);
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
- applyCredentialTemplate();
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 = path.resolve(process.cwd(), "dist/index.js");
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 Gemini without putting the raw secret into model-visible context.</p>
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
- <section id="session-section" class="panel">
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>You&apos;re connected</h2>
2674
- <p>The only things you need next are save token, test token, and connect your AI tool.</p>
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
- <details class="disclosure">
2686
- <summary>Session details</summary>
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>Save a token</h2>
2701
- <p>Start with the basics. Paste the token, say when the AI should use it, and let KeyLore keep the raw secret out of the catalogue.</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-grid">
2705
- <div class="span-7 panel">
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-wide"><label for="credential-template">What is this token for?</label><select id="credential-template"><option value="github-readonly">GitHub read-only</option><option value="github-write">GitHub write-capable</option><option value="npm-readonly">npm read-only</option><option value="internal-service">Internal service token</option><option value="generic-bearer">Generic bearer API</option></select></div>
2708
- <div class="field"><label for="credential-name">Name shown in KeyLore</label><input id="credential-name" type="text" required /></div>
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-domains">Where can it be used?</label><textarea id="credential-domains" placeholder="api.github.com"></textarea></div>
2712
- <div id="credential-secret-field" class="field-wide"><label for="credential-secret">Paste token</label><textarea id="credential-secret" placeholder="Paste the raw token here. KeyLore stores it outside the searchable metadata catalogue."></textarea></div>
2713
- <div class="field-wide"><label for="credential-notes">Tell the AI when to use this token</label><textarea id="credential-notes" placeholder="Example: Use this for GitHub repository metadata, issues, and pull requests. Do not use it for write actions."></textarea></div>
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-guidance">Writing help</label>
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
- <div id="credential-mcp-preview"></div>
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
- <details class="disclosure field-wide">
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
- </section>
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>For most local users, copy the local snippet below, restart the AI tool, and try the suggested first prompt.</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-6 code-stack">
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;">Codex local setup</h2><p>Recommended for local use.</p></div></div>
2807
- <textarea id="codex-stdio-snippet" style="width:100%; min-height: 130px;"></textarea>
2808
- <div class="panel-footnote">Local stdio uses the KeyLore process on this machine and avoids extra token setup.</div>
2809
- <div class="section-heading" style="margin-top: 16px;"><div><h2 style="font-size:1.2rem;">First prompt to try in Codex</h2></div></div>
2810
- <textarea id="codex-first-prompt" style="width:100%; min-height: 130px;"></textarea>
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;">Gemini local setup</h2><p>Recommended for local use.</p></div></div>
2814
- <textarea id="gemini-stdio-snippet" style="width:100%; min-height: 170px;"></textarea>
2815
- <div class="section-heading" style="margin-top: 16px;"><div><h2 style="font-size:1.2rem;">First prompt to try in Gemini</h2></div></div>
2816
- <textarea id="gemini-first-prompt" style="width:100%; min-height: 130px;"></textarea>
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-6 panel">
2820
- <div class="section-heading"><div><h2 style="font-size:1.4rem;">Local connection check</h2></div></div>
2821
- <div id="connect-stdio-status"></div>
2822
- <div class="panel-footnote">If the local entry point is present, copy one of the local setup snippets above and restart your AI tool.</div>
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-6 code-stack">
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
- <textarea id="codex-http-snippet" style="width:100%; min-height: 110px;"></textarea>
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
- <textarea id="gemini-http-snippet" style="width:100%; min-height: 190px;"></textarea>
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
- <textarea id="generic-http-snippet" style="width:100%; min-height: 90px;"></textarea>
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-6 panel">
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>