@frockbot/plugin-settings 0.0.0 → 0.1.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.
Files changed (36) hide show
  1. package/frockbot.json +55 -0
  2. package/package.json +43 -6
  3. package/src/backend.test.ts +397 -0
  4. package/src/backend.ts +262 -0
  5. package/src/client/BotPanel.vue +57 -0
  6. package/src/client/BotSettingsSurface.vue +1076 -0
  7. package/src/client/BotSettingsTrigger.vue +26 -0
  8. package/src/client/ConnectionsSurface.vue +813 -0
  9. package/src/client/ModelsSurface.vue +419 -0
  10. package/src/client/PackageAccounts.vue +330 -0
  11. package/src/client/PackageCatalogSurface.vue +584 -0
  12. package/src/client/PackageSettingsForm.vue +150 -0
  13. package/src/client/PackageSettingsSection.vue +66 -0
  14. package/src/client/PluginsSurface.vue +412 -0
  15. package/src/client/PluginsTrigger.vue +62 -0
  16. package/src/client/UserProfileTrigger.vue +223 -0
  17. package/src/client/UserSettingsSurface.vue +242 -0
  18. package/src/client/assignment-operations.ts +22 -0
  19. package/src/client/bot-settings.test.ts +218 -0
  20. package/src/client/bot-settings.ts +150 -0
  21. package/src/client/index.test.ts +113 -0
  22. package/src/client/index.ts +78 -0
  23. package/src/client/package-settings.test.ts +63 -0
  24. package/src/client/package-settings.ts +77 -0
  25. package/src/client/package-surfaces.test.ts +147 -0
  26. package/src/client/package-surfaces.ts +93 -0
  27. package/src/client/user-display-name.test.ts +44 -0
  28. package/src/client/user-display-name.ts +16 -0
  29. package/src/env.d.ts +6 -0
  30. package/src/index.ts +1 -0
  31. package/src/manifest.ts +3 -0
  32. package/src/user.test.ts +1277 -0
  33. package/src/user.ts +1221 -0
  34. package/tsconfig.json +15 -0
  35. package/vite.config.ts +30 -0
  36. package/README.md +0 -3
@@ -0,0 +1,584 @@
1
+ <script setup lang="ts">
2
+ import { catalogSetupFieldKeyV1 } from "@frockbot/catalog-core";
3
+ import { UiButton, UiIcon } from "@frockbot/client-ui";
4
+ import {
5
+ frockBotWebDataKey,
6
+ type CatalogEntryV1,
7
+ type CatalogIndexEntryV1,
8
+ } from "@frockbot/plugin-shell/shared";
9
+ import { computed, inject, onMounted, ref } from "vue";
10
+
11
+ const providedWeb = inject(frockBotWebDataKey);
12
+ if (!providedWeb)
13
+ throw new Error("Package Catalog client data was not provided");
14
+ const web = providedWeb;
15
+
16
+ const search = ref("");
17
+ const openCatalogId = ref<string>();
18
+ const openCatalogEntry = ref<CatalogEntryV1>();
19
+ const catalogEntryLoading = ref(false);
20
+ const uninstallingPackageId = ref<string>();
21
+ const setupValues = ref<Record<string, string>>({});
22
+ const setupValuesFor = ref<string>();
23
+ const installingCatalogId = ref<string>();
24
+
25
+ const filteredCatalog = computed(() => {
26
+ const query = search.value.trim().toLocaleLowerCase();
27
+ if (!query) return web.value.packageCatalog;
28
+ return web.value.packageCatalog.filter(
29
+ (entry) =>
30
+ entry.displayName.toLocaleLowerCase().includes(query) ||
31
+ entry.packageId.toLocaleLowerCase().includes(query) ||
32
+ entry.description.toLocaleLowerCase().includes(query),
33
+ );
34
+ });
35
+
36
+ onMounted(() => void web.value.loadPackageCatalog());
37
+
38
+ function setupFieldsOf(entry: CatalogEntryV1 | undefined): Array<{
39
+ key: string;
40
+ title: string;
41
+ description?: string;
42
+ maxLength?: number;
43
+ }> {
44
+ return (entry?.setupFields ?? []).map((field, index) => ({
45
+ key: catalogSetupFieldKeyV1(field, index),
46
+ title: field.title ?? catalogSetupFieldKeyV1(field, index),
47
+ ...(field.description ? { description: field.description } : {}),
48
+ ...(field.maxLength === undefined ? {} : { maxLength: field.maxLength }),
49
+ }));
50
+ }
51
+
52
+ function entryUsesOAuth(entry: CatalogEntryV1 | undefined): boolean {
53
+ return (entry?.servers ?? []).some((server) => server.auth === "oauth");
54
+ }
55
+
56
+ function beginSetup(entry: CatalogEntryV1): void {
57
+ setupValuesFor.value = entry.catalogId;
58
+ setupValues.value = Object.fromEntries(
59
+ setupFieldsOf(entry).map((field) => [field.key, ""]),
60
+ );
61
+ }
62
+
63
+ async function connectCatalogServer(label: string, url: string): Promise<void> {
64
+ const redirect = await web.value.startMcpAuthorization({
65
+ label,
66
+ settings: { url, transport: "streamable-http" },
67
+ });
68
+ if (redirect) await web.value.openConnectionAuthorization(redirect);
69
+ }
70
+
71
+ async function installWithSetupValues(
72
+ index: CatalogIndexEntryV1,
73
+ entry: CatalogEntryV1,
74
+ ): Promise<void> {
75
+ installingCatalogId.value = index.catalogId;
76
+ try {
77
+ const values = Object.fromEntries(
78
+ Object.entries(setupValues.value)
79
+ .map(([key, value]) => [key, value.trim()] as const)
80
+ .filter(([, value]) => value.length > 0),
81
+ );
82
+ await web.value.installCatalogPackage(index, values);
83
+ setupValuesFor.value = undefined;
84
+ setupValues.value = {};
85
+ const server = (entry.servers ?? []).find(
86
+ (candidate) => candidate.auth === "oauth",
87
+ );
88
+ if (server) await connectCatalogServer(entry.displayName, server.url);
89
+ } catch (error) {
90
+ web.value.settingsError =
91
+ error instanceof Error ? error.message : "Could not install the Package";
92
+ } finally {
93
+ installingCatalogId.value = undefined;
94
+ }
95
+ }
96
+
97
+ async function toggleCatalogEntry(entry: CatalogIndexEntryV1): Promise<void> {
98
+ if (openCatalogId.value === entry.catalogId) {
99
+ openCatalogId.value = undefined;
100
+ openCatalogEntry.value = undefined;
101
+ return;
102
+ }
103
+ openCatalogId.value = entry.catalogId;
104
+ openCatalogEntry.value = undefined;
105
+ catalogEntryLoading.value = true;
106
+ try {
107
+ const detail = await web.value.loadCatalogEntry(entry.catalogId);
108
+ if (openCatalogId.value === entry.catalogId)
109
+ openCatalogEntry.value = detail;
110
+ } catch (error) {
111
+ web.value.settingsError =
112
+ error instanceof Error ? error.message : "Could not load the entry";
113
+ } finally {
114
+ catalogEntryLoading.value = false;
115
+ }
116
+ }
117
+
118
+ async function install(entry: CatalogIndexEntryV1): Promise<void> {
119
+ try {
120
+ await web.value.installCatalogPackage(entry);
121
+ } catch (error) {
122
+ web.value.settingsError =
123
+ error instanceof Error ? error.message : "Could not install the Package";
124
+ }
125
+ }
126
+
127
+ async function confirmUninstall(packageId: string): Promise<void> {
128
+ try {
129
+ await web.value.uninstallPackage(packageId);
130
+ uninstallingPackageId.value = undefined;
131
+ if (openCatalogEntry.value?.packageId === packageId) {
132
+ openCatalogId.value = undefined;
133
+ openCatalogEntry.value = undefined;
134
+ }
135
+ } catch (error) {
136
+ web.value.settingsError =
137
+ error instanceof Error
138
+ ? error.message
139
+ : "Could not uninstall the Package";
140
+ }
141
+ }
142
+
143
+ function isPackageInstalled(packageId: string): boolean {
144
+ return Boolean(
145
+ web.value.userSettings?.packages.some(
146
+ (item) => item.packageId === packageId && item.state === "installed",
147
+ ),
148
+ );
149
+ }
150
+ </script>
151
+
152
+ <template>
153
+ <div class="package-catalog-surface">
154
+ <p class="catalog-intro">
155
+ Browse published Packages. Installing one makes it available to every Bot
156
+ you own; each Bot still needs an explicit Assignment to use it.
157
+ </p>
158
+ <label class="catalog-search">
159
+ <UiIcon name="search" />
160
+ <input
161
+ v-model="search"
162
+ placeholder="Search the Catalog"
163
+ aria-label="Search the Package Catalog"
164
+ />
165
+ </label>
166
+ <p v-if="web.packageCatalogGeneration" class="catalog-generation">
167
+ Generation {{ web.packageCatalogGeneration }}
168
+ </p>
169
+ <div class="catalog-grid">
170
+ <article
171
+ v-for="entry in filteredCatalog"
172
+ :key="entry.catalogId"
173
+ class="catalog-card"
174
+ >
175
+ <button
176
+ type="button"
177
+ class="catalog-summary"
178
+ :aria-expanded="openCatalogId === entry.catalogId"
179
+ :aria-controls="`catalog-detail-${entry.catalogId}`"
180
+ @click="toggleCatalogEntry(entry)"
181
+ >
182
+ <span class="catalog-logo" aria-hidden="true">
183
+ {{ entry.displayName.slice(0, 1) }}
184
+ </span>
185
+ <span class="catalog-copy">
186
+ <strong>{{ entry.displayName }}</strong>
187
+ <small>{{ entry.description }}</small>
188
+ </span>
189
+ <span class="catalog-version">{{ entry.version }}</span>
190
+ <UiIcon
191
+ class="catalog-chevron"
192
+ :class="{
193
+ 'catalog-chevron--open': openCatalogId === entry.catalogId,
194
+ }"
195
+ name="chevrons-right"
196
+ size="sm"
197
+ />
198
+ </button>
199
+ <div
200
+ :id="`catalog-detail-${entry.catalogId}`"
201
+ class="catalog-collapse"
202
+ :class="{
203
+ 'catalog-collapse--open': openCatalogId === entry.catalogId,
204
+ }"
205
+ :inert="openCatalogId === entry.catalogId ? undefined : true"
206
+ >
207
+ <div class="catalog-collapse-inner">
208
+ <div class="catalog-detail">
209
+ <p v-if="catalogEntryLoading" class="catalog-note">Loading…</p>
210
+ <template v-else-if="openCatalogEntry">
211
+ <p class="catalog-note">{{ openCatalogEntry.description }}</p>
212
+ <dl class="catalog-facts">
213
+ <div>
214
+ <dt>Package</dt>
215
+ <dd>{{ openCatalogEntry.packageId }}</dd>
216
+ </div>
217
+ <div>
218
+ <dt>Version</dt>
219
+ <dd>{{ openCatalogEntry.version }}</dd>
220
+ </div>
221
+ <div v-if="openCatalogEntry.homepage">
222
+ <dt>Homepage</dt>
223
+ <dd>
224
+ <a
225
+ :href="openCatalogEntry.homepage"
226
+ rel="noreferrer noopener"
227
+ target="_blank"
228
+ >
229
+ {{ openCatalogEntry.homepage }}
230
+ </a>
231
+ </dd>
232
+ </div>
233
+ </dl>
234
+ </template>
235
+ <div
236
+ v-if="uninstallingPackageId === entry.packageId"
237
+ class="catalog-confirm"
238
+ role="alert"
239
+ >
240
+ <p>
241
+ Uninstalling {{ entry.displayName }} removes it from every
242
+ Bot. Its Assignments stay visible as unavailable so you can
243
+ repair or remove them. Connections and credentials are kept.
244
+ </p>
245
+ <div class="catalog-actions">
246
+ <UiButton @click="uninstallingPackageId = undefined">
247
+ Keep it
248
+ </UiButton>
249
+ <UiButton
250
+ variant="danger"
251
+ @click="confirmUninstall(entry.packageId)"
252
+ >
253
+ Uninstall
254
+ </UiButton>
255
+ </div>
256
+ </div>
257
+ <form
258
+ v-else-if="
259
+ setupValuesFor === entry.catalogId && openCatalogEntry
260
+ "
261
+ class="catalog-form"
262
+ @submit.prevent="
263
+ installWithSetupValues(entry, openCatalogEntry)
264
+ "
265
+ >
266
+ <label
267
+ v-for="field in setupFieldsOf(openCatalogEntry)"
268
+ :key="field.key"
269
+ >
270
+ <span>{{ field.title }}</span>
271
+ <input
272
+ v-model="setupValues[field.key]"
273
+ autocomplete="off"
274
+ :maxlength="field.maxLength ?? 2048"
275
+ />
276
+ <span v-if="field.description" class="catalog-hint">
277
+ {{ field.description }}
278
+ </span>
279
+ </label>
280
+ <p v-if="entryUsesOAuth(openCatalogEntry)" class="catalog-hint">
281
+ After installing, you will be sent to the connector to sign
282
+ in. FrockBot records the request; only you can complete it.
283
+ </p>
284
+ <div class="catalog-actions">
285
+ <UiButton @click="setupValuesFor = undefined"
286
+ >Cancel</UiButton
287
+ >
288
+ <UiButton
289
+ type="submit"
290
+ variant="primary"
291
+ :disabled="installingCatalogId === entry.catalogId"
292
+ >
293
+ {{
294
+ installingCatalogId === entry.catalogId
295
+ ? "Installing…"
296
+ : "Install and connect"
297
+ }}
298
+ </UiButton>
299
+ </div>
300
+ </form>
301
+ <div v-else class="catalog-actions">
302
+ <UiButton
303
+ v-if="isPackageInstalled(entry.packageId)"
304
+ variant="danger"
305
+ @click="uninstallingPackageId = entry.packageId"
306
+ >
307
+ Uninstall
308
+ </UiButton>
309
+ <UiButton
310
+ v-else-if="
311
+ openCatalogEntry &&
312
+ (setupFieldsOf(openCatalogEntry).length > 0 ||
313
+ entryUsesOAuth(openCatalogEntry))
314
+ "
315
+ variant="primary"
316
+ @click="beginSetup(openCatalogEntry)"
317
+ >
318
+ Set up
319
+ </UiButton>
320
+ <UiButton v-else variant="primary" @click="install(entry)">
321
+ Install
322
+ </UiButton>
323
+ </div>
324
+ </div>
325
+ </div>
326
+ </div>
327
+ </article>
328
+ </div>
329
+ <p
330
+ v-if="web.packageCatalog.length === 0 && !web.settingsError"
331
+ class="catalog-empty"
332
+ >
333
+ The Catalog has nothing to offer yet.
334
+ </p>
335
+ <p v-else-if="filteredCatalog.length === 0" class="catalog-empty">
336
+ No Catalog entry matches that search.
337
+ </p>
338
+ <p v-if="web.settingsError" class="settings-error" role="alert">
339
+ {{ web.settingsError }}
340
+ </p>
341
+ </div>
342
+ </template>
343
+
344
+ <style scoped>
345
+ .package-catalog-surface {
346
+ padding: 24px;
347
+ }
348
+
349
+ .catalog-intro,
350
+ .catalog-empty {
351
+ margin: 0 0 14px;
352
+ color: var(--frock-text-muted);
353
+ font-size: var(--frock-text-base);
354
+ }
355
+
356
+ .catalog-search {
357
+ display: flex;
358
+ height: 42px;
359
+ align-items: center;
360
+ gap: 9px;
361
+ padding: 0 13px;
362
+ border: 1px solid var(--frock-border);
363
+ border-radius: 999px;
364
+ color: var(--frock-text-subtle);
365
+ background: var(--frock-surface-raised);
366
+ }
367
+
368
+ .catalog-search:focus-within {
369
+ border-color: var(--frock-border-focus);
370
+ box-shadow: 0 0 0 3px var(--frock-focus-ring);
371
+ }
372
+
373
+ .catalog-search input {
374
+ min-width: 0;
375
+ flex: 1;
376
+ border: 0;
377
+ outline: 0;
378
+ color: var(--frock-text);
379
+ background: transparent;
380
+ }
381
+
382
+ .catalog-generation {
383
+ margin: 10px 0 0;
384
+ color: var(--frock-text-subtle);
385
+ font-size: var(--frock-text-sm);
386
+ }
387
+
388
+ .catalog-grid {
389
+ display: grid;
390
+ gap: 12px;
391
+ margin-top: 18px;
392
+ }
393
+
394
+ .catalog-card {
395
+ min-width: 0;
396
+ padding: 8px;
397
+ border: 1px solid var(--frock-border);
398
+ border-radius: var(--frock-radius-card);
399
+ background: var(--frock-surface-raised);
400
+ box-shadow: var(--frock-shadow-card);
401
+ }
402
+
403
+ .catalog-summary {
404
+ display: grid;
405
+ width: 100%;
406
+ min-width: 0;
407
+ grid-template-columns: 44px minmax(0, 1fr) auto 16px;
408
+ align-items: center;
409
+ gap: 12px;
410
+ padding: 8px;
411
+ border: 0;
412
+ border-radius: var(--frock-radius-control);
413
+ color: inherit;
414
+ background: transparent;
415
+ text-align: left;
416
+ cursor: pointer;
417
+ }
418
+
419
+ .catalog-summary:hover {
420
+ background: var(--frock-fill-hover);
421
+ }
422
+
423
+ .catalog-logo {
424
+ display: grid;
425
+ width: 44px;
426
+ height: 44px;
427
+ place-items: center;
428
+ border-radius: 12px;
429
+ color: var(--frock-action-secondary-text);
430
+ background: var(--frock-surface-accent);
431
+ font-weight: 800;
432
+ }
433
+
434
+ .catalog-copy {
435
+ min-width: 0;
436
+ }
437
+
438
+ .catalog-copy strong,
439
+ .catalog-copy small {
440
+ display: block;
441
+ overflow: hidden;
442
+ white-space: nowrap;
443
+ text-overflow: ellipsis;
444
+ }
445
+
446
+ .catalog-copy strong {
447
+ font-size: var(--frock-text-md);
448
+ font-weight: 600;
449
+ }
450
+
451
+ .catalog-copy small,
452
+ .catalog-version,
453
+ .catalog-note,
454
+ .catalog-hint {
455
+ color: var(--frock-text-muted);
456
+ font-size: var(--frock-text-sm);
457
+ }
458
+
459
+ .catalog-copy small {
460
+ margin-top: 4px;
461
+ }
462
+
463
+ .catalog-version {
464
+ white-space: nowrap;
465
+ }
466
+
467
+ .catalog-chevron {
468
+ color: var(--frock-text-subtle);
469
+ transition: transform var(--frock-motion-panel);
470
+ }
471
+
472
+ .catalog-chevron--open {
473
+ transform: rotate(90deg);
474
+ }
475
+
476
+ .catalog-collapse {
477
+ display: grid;
478
+ grid-template-rows: 0fr;
479
+ opacity: 0;
480
+ transition:
481
+ grid-template-rows var(--frock-motion-panel),
482
+ opacity var(--frock-motion-panel);
483
+ }
484
+
485
+ .catalog-collapse--open {
486
+ grid-template-rows: 1fr;
487
+ opacity: 1;
488
+ }
489
+
490
+ .catalog-collapse-inner {
491
+ min-height: 0;
492
+ overflow: hidden;
493
+ }
494
+
495
+ .catalog-detail {
496
+ display: grid;
497
+ gap: 12px;
498
+ padding: 12px 8px;
499
+ border-top: 1px solid var(--frock-border);
500
+ }
501
+
502
+ .catalog-note,
503
+ .catalog-hint {
504
+ margin: 0;
505
+ }
506
+
507
+ .catalog-facts {
508
+ display: grid;
509
+ gap: 6px;
510
+ margin: 0;
511
+ }
512
+
513
+ .catalog-facts div {
514
+ display: grid;
515
+ grid-template-columns: 90px minmax(0, 1fr);
516
+ gap: 8px;
517
+ }
518
+
519
+ .catalog-facts dt {
520
+ color: var(--frock-text-subtle);
521
+ font-size: var(--frock-text-sm);
522
+ }
523
+
524
+ .catalog-facts dd {
525
+ margin: 0;
526
+ overflow-wrap: anywhere;
527
+ font-size: var(--frock-text-sm);
528
+ }
529
+
530
+ .catalog-confirm {
531
+ display: grid;
532
+ gap: 10px;
533
+ padding: 12px;
534
+ border: 1px solid var(--frock-border);
535
+ border-radius: var(--frock-radius-control);
536
+ background: var(--frock-surface-subtle);
537
+ }
538
+
539
+ .catalog-confirm p {
540
+ margin: 0;
541
+ font-size: var(--frock-text-sm);
542
+ }
543
+
544
+ .catalog-form {
545
+ display: grid;
546
+ gap: 12px;
547
+ }
548
+
549
+ .catalog-form label {
550
+ display: grid;
551
+ gap: 6px;
552
+ }
553
+
554
+ .catalog-form label > span:first-child {
555
+ color: var(--frock-text-muted);
556
+ font-size: var(--frock-text-sm);
557
+ }
558
+
559
+ .catalog-form input {
560
+ min-width: 0;
561
+ padding: 8px 11px;
562
+ border: 1px solid var(--frock-border);
563
+ border-radius: 9px;
564
+ color: var(--frock-text);
565
+ background: var(--frock-surface-raised);
566
+ font-size: var(--frock-text-base);
567
+ }
568
+
569
+ .catalog-actions {
570
+ display: flex;
571
+ flex-wrap: wrap;
572
+ justify-content: flex-end;
573
+ gap: 8px;
574
+ }
575
+
576
+ .catalog-empty {
577
+ margin-top: 18px;
578
+ }
579
+
580
+ .settings-error {
581
+ color: var(--frock-danger-text);
582
+ font-size: var(--frock-text-sm);
583
+ }
584
+ </style>