@frockbot/plugin-settings 0.0.0 → 0.1.1

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
package/frockbot.json ADDED
@@ -0,0 +1,55 @@
1
+ {
2
+ "schemaVersion": 3,
3
+ "id": "settings",
4
+ "displayName": "FrockBot Settings",
5
+ "version": "0.0.1",
6
+ "compatibility": {
7
+ "frockbot": ">=0.0.1"
8
+ },
9
+ "dependencies": {
10
+ "auth": ">=0.0.1",
11
+ "shell": ">=0.0.1",
12
+ "ui-theme": ">=0.0.1"
13
+ },
14
+ "contributions": {
15
+ "backend": [
16
+ {
17
+ "entry": "./backend",
18
+ "host": "gateway"
19
+ },
20
+ {
21
+ "entry": "./user",
22
+ "host": "user"
23
+ }
24
+ ],
25
+ "client": {
26
+ "entry": "./client",
27
+ "mounts": [
28
+ {
29
+ "slot": "frockbot.sidebar-actions",
30
+ "order": 10
31
+ },
32
+ {
33
+ "slot": "frockbot.user-profile",
34
+ "order": 10
35
+ },
36
+ {
37
+ "slot": "frockbot.bot-actions",
38
+ "order": 10
39
+ },
40
+ {
41
+ "slot": "frockbot.right-panel",
42
+ "order": 10
43
+ }
44
+ ],
45
+ "outlets": [
46
+ "frockbot.user-settings-sections",
47
+ "frockbot.bot-settings-sections",
48
+ "frockbot.bot-settings-primary-sections",
49
+ "frockbot.bot-panel-sections",
50
+ "frockbot.bot-avatar-editor"
51
+ ]
52
+ }
53
+ },
54
+ "permissions": []
55
+ }
package/package.json CHANGED
@@ -1,14 +1,51 @@
1
1
  {
2
2
  "name": "@frockbot/plugin-settings",
3
- "version": "0.0.0",
4
- "description": "Placeholder reserving this name for trusted publishing. Superseded by the first release.",
5
- "license": "UNLICENSED",
3
+ "version": "0.1.1",
4
+ "private": false,
5
+ "type": "module",
6
+ "exports": {
7
+ ".": "./src/index.ts",
8
+ "./backend": "./src/backend.ts",
9
+ "./client": "./src/client/index.ts",
10
+ "./user": "./src/user.ts",
11
+ "./manifest": "./src/manifest.ts",
12
+ "./frockbot.json": "./frockbot.json",
13
+ "./package.json": "./package.json"
14
+ },
15
+ "frockbot": {
16
+ "manifest": "./frockbot.json"
17
+ },
18
+ "scripts": {
19
+ "test": "bun test src",
20
+ "build": "vite build",
21
+ "typecheck": "vue-tsc --noEmit -p tsconfig.json"
22
+ },
23
+ "dependencies": {
24
+ "@frockbot/catalog-core": "0.1.1",
25
+ "@frockbot/client-core": "0.1.1",
26
+ "@frockbot/client-ui": "0.1.1",
27
+ "@frockbot/configuration-core": "0.1.1",
28
+ "@frockbot/connection-core": "0.1.1",
29
+ "@frockbot/kernel-composition": "0.1.1",
30
+ "@frockbot/plugin-auth": "0.1.1",
31
+ "@frockbot/plugin-shell": "0.1.1",
32
+ "cordis": "4.0.0-rc.8",
33
+ "vue": "3.5.41"
34
+ },
35
+ "devDependencies": {
36
+ "@types/bun": "1.4.0",
37
+ "@types/node": "26.2.0",
38
+ "@vitejs/plugin-vue": "6.0.8",
39
+ "typescript": "5.9.3",
40
+ "vite": "8.2.2",
41
+ "vue-tsc": "3.3.10"
42
+ },
43
+ "publishConfig": {
44
+ "access": "public"
45
+ },
6
46
  "repository": {
7
47
  "type": "git",
8
48
  "url": "git+https://github.com/timoconnellaus/frockbot.git",
9
49
  "directory": "packages/plugin-settings"
10
- },
11
- "publishConfig": {
12
- "access": "public"
13
50
  }
14
51
  }
@@ -0,0 +1,397 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import type {
3
+ CompositionCommandReceiptV1,
4
+ CompositionGenerationViewV1,
5
+ RevertCompositionCommandV1,
6
+ } from "@frockbot/configuration-core";
7
+ import type {
8
+ ConnectionCommandReceiptV1,
9
+ ConnectionCommandV1,
10
+ } from "@frockbot/connection-core";
11
+ import { createSettingsBackendContribution } from "./backend.js";
12
+
13
+ const BOOTSTRAP_GENERATION = "2026-08-31T00:00:00.000Z:0123456789abcdef";
14
+ const AUTHORED_GENERATION = "2026-09-01T00:00:00.000Z:fedcba9876543210";
15
+ const REVERTED_GENERATION = "2026-09-02T00:00:00.000Z:0123456789abcdef";
16
+
17
+ function generationView(
18
+ generationId: string,
19
+ isCurrent: boolean,
20
+ ): CompositionGenerationViewV1 {
21
+ return {
22
+ schemaVersion: 1,
23
+ botId: "alpha",
24
+ generationId,
25
+ createdAt: generationId.slice(0, 24),
26
+ status: isCurrent ? "active" : "superseded",
27
+ isCurrent,
28
+ failures: [],
29
+ origin:
30
+ generationId === BOOTSTRAP_GENERATION
31
+ ? { kind: "bootstrap" }
32
+ : {
33
+ kind: "bot-authored",
34
+ runId: "run-1",
35
+ sessionId: "alice:alpha",
36
+ turnId: "turn-1",
37
+ },
38
+ members: [
39
+ {
40
+ packageId: "shell",
41
+ version: "0.0.1",
42
+ provenance: { kind: "first-party" },
43
+ },
44
+ ],
45
+ };
46
+ }
47
+
48
+ function host(receiptOverride?: unknown, compositionOverride?: unknown) {
49
+ const executed: ConnectionCommandV1[] = [];
50
+ const reverted: RevertCompositionCommandV1[] = [];
51
+ const receipts = new Map<string, ConnectionCommandReceiptV1>();
52
+ return {
53
+ executed,
54
+ reverted,
55
+ contribution: createSettingsBackendContribution({
56
+ listCompositionGenerations: (_userId, botId, query) =>
57
+ Promise.resolve(
58
+ (compositionOverride ?? {
59
+ schemaVersion: 1,
60
+ botId,
61
+ currentGenerationId: AUTHORED_GENERATION,
62
+ generations: [
63
+ generationView(AUTHORED_GENERATION, true),
64
+ generationView(BOOTSTRAP_GENERATION, false),
65
+ ].slice(0, query.limit),
66
+ ...(query.cursor ? { cursor: query.cursor } : {}),
67
+ }) as never,
68
+ ),
69
+ getCompositionGeneration: (_userId, _botId, generationId) =>
70
+ Promise.resolve(
71
+ generationId === BOOTSTRAP_GENERATION
72
+ ? generationView(BOOTSTRAP_GENERATION, false)
73
+ : undefined,
74
+ ),
75
+ revertComposition: (_userId, _botId, command) => {
76
+ reverted.push(command);
77
+ return Promise.resolve({
78
+ schemaVersion: 1,
79
+ commandId: command.commandId,
80
+ status: "applied",
81
+ generationId: REVERTED_GENERATION,
82
+ currentGenerationId: command.expectedGenerationId,
83
+ } satisfies CompositionCommandReceiptV1);
84
+ },
85
+ executeConnection: (_userId, command) => {
86
+ executed.push(command);
87
+ if (receiptOverride !== undefined) {
88
+ return Promise.resolve(receiptOverride as ConnectionCommandReceiptV1);
89
+ }
90
+ const receipt = {
91
+ schemaVersion: 1,
92
+ commandId: command.commandId,
93
+ connectionId:
94
+ "connectionId" in command
95
+ ? command.connectionId
96
+ : "connection-test",
97
+ status: "applied",
98
+ } satisfies ConnectionCommandReceiptV1;
99
+ receipts.set(command.commandId, receipt);
100
+ return Promise.resolve(receipt);
101
+ },
102
+ lookupConnectionCommand: (_userId, _packageId, commandId) =>
103
+ Promise.resolve(receipts.get(commandId)),
104
+ }),
105
+ };
106
+ }
107
+
108
+ function route(
109
+ contribution: ReturnType<typeof createSettingsBackendContribution>,
110
+ path: string,
111
+ init?: RequestInit & { userId?: string },
112
+ ) {
113
+ const url = new URL(`https://frockbot.test${path}`);
114
+ return contribution.route(new Request(url, init), url, {
115
+ userId: init?.userId ?? "alice",
116
+ client: "browser",
117
+ });
118
+ }
119
+
120
+ const createCommand = JSON.stringify({
121
+ schemaVersion: 1,
122
+ type: "connection/create-api-key",
123
+ commandId: "ollama-connect-1",
124
+ packageId: "provider-ollama-cloud",
125
+ connectionTypeId: "ollama-cloud-account",
126
+ label: "Work",
127
+ apiKey: "write-only-secret",
128
+ });
129
+
130
+ describe("Settings Connection gateway Contribution", () => {
131
+ test("owns only the provider-neutral Connection routes", async () => {
132
+ const { contribution } = host();
133
+ expect(await route(contribution, "/api/settings")).toBeUndefined();
134
+ expect(await route(contribution, "/api/bots")).toBeUndefined();
135
+ expect(
136
+ await route(contribution, "/api/connections", { userId: "" }),
137
+ ).toBeUndefined();
138
+ expect((await route(contribution, "/api/connections"))?.status).toBe(405);
139
+ expect(
140
+ (
141
+ await route(contribution, "/api/connection-commands", {
142
+ method: "POST",
143
+ })
144
+ )?.status,
145
+ ).toBe(405);
146
+ });
147
+
148
+ test("admits a Connection command and its receipt lookup", async () => {
149
+ const { contribution, executed } = host();
150
+ const response = await route(contribution, "/api/connections", {
151
+ method: "POST",
152
+ headers: { "content-type": "application/json" },
153
+ body: createCommand,
154
+ });
155
+ expect(response?.status).toBe(200);
156
+ expect((await response?.json()) as unknown).toEqual({
157
+ schemaVersion: 1,
158
+ commandId: "ollama-connect-1",
159
+ connectionId: "connection-test",
160
+ status: "applied",
161
+ });
162
+ expect(executed).toHaveLength(1);
163
+
164
+ const lookup = await route(
165
+ contribution,
166
+ "/api/connection-commands?packageId=provider-ollama-cloud&commandId=ollama-connect-1",
167
+ );
168
+ expect((await lookup?.json()) as unknown).toEqual({
169
+ schemaVersion: 1,
170
+ commandId: "ollama-connect-1",
171
+ connectionId: "connection-test",
172
+ status: "applied",
173
+ });
174
+
175
+ const missing = await route(
176
+ contribution,
177
+ "/api/connection-commands?packageId=provider-ollama-cloud&commandId=absent-1",
178
+ );
179
+ expect((await missing?.json()) as unknown).toBeNull();
180
+ });
181
+
182
+ test("rejects invalid command IDs and lookup queries", async () => {
183
+ const { contribution, executed } = host();
184
+ expect(
185
+ (
186
+ await route(contribution, "/api/connections", {
187
+ method: "POST",
188
+ headers: { "content-type": "application/json" },
189
+ body: JSON.stringify({
190
+ schemaVersion: 1,
191
+ type: "connection/refresh-models",
192
+ commandId: "lost response",
193
+ connectionId: "connection-test",
194
+ }),
195
+ })
196
+ )?.status,
197
+ ).toBe(400);
198
+ expect(executed).toHaveLength(0);
199
+ for (const query of [
200
+ "?packageId=provider-ollama-cloud",
201
+ "?packageId=provider-ollama-cloud&commandId=connect-1&extra=true",
202
+ "?packageId=provider-ollama-cloud&packageId=other&commandId=connect-1",
203
+ "?packageId=bad%2Fpackage&commandId=connect-1",
204
+ "?packageId=provider-ollama-cloud&commandId=lost%20response",
205
+ ]) {
206
+ expect(
207
+ (await route(contribution, `/api/connection-commands${query}`))?.status,
208
+ ).toBe(400);
209
+ }
210
+ });
211
+
212
+ test("rejects malformed receipts returned by the User authority", async () => {
213
+ const { contribution } = host({
214
+ schemaVersion: 1,
215
+ commandId: "ollama-connect-malformed",
216
+ connectionId: "connection-test",
217
+ status: "applied",
218
+ credential: "must-not-cross-the-seam",
219
+ });
220
+
221
+ expect(
222
+ (
223
+ await route(contribution, "/api/connections", {
224
+ method: "POST",
225
+ headers: { "content-type": "application/json" },
226
+ body: createCommand,
227
+ })
228
+ )?.status,
229
+ ).toBe(400);
230
+ });
231
+ });
232
+
233
+ const revertCommand = JSON.stringify({
234
+ schemaVersion: 1,
235
+ type: "composition/revert",
236
+ commandId: "composition-revert-1",
237
+ botId: "alpha",
238
+ toGenerationId: BOOTSTRAP_GENERATION,
239
+ expectedGenerationId: AUTHORED_GENERATION,
240
+ });
241
+
242
+ describe("Settings Composition gateway Contribution", () => {
243
+ test("lists a Bot's generations newest first", async () => {
244
+ const { contribution } = host();
245
+ const response = await route(
246
+ contribution,
247
+ "/api/bots/alpha/composition/generations",
248
+ );
249
+ expect(response?.status).toBe(200);
250
+ const body = (await response?.json()) as {
251
+ currentGenerationId: string;
252
+ generations: { generationId: string; isCurrent: boolean }[];
253
+ };
254
+ expect(body.currentGenerationId).toBe(AUTHORED_GENERATION);
255
+ expect(body.generations.map((entry) => entry.generationId)).toEqual([
256
+ AUTHORED_GENERATION,
257
+ BOOTSTRAP_GENERATION,
258
+ ]);
259
+ const paged = await route(
260
+ contribution,
261
+ "/api/bots/alpha/composition/generations?limit=1&cursor=composition%3Aindex%3Ax",
262
+ );
263
+ expect(
264
+ ((await paged?.json()) as { generations: unknown[] }).generations,
265
+ ).toHaveLength(1);
266
+ });
267
+
268
+ test("reads one generation and answers 404 for an unknown id", async () => {
269
+ const { contribution } = host();
270
+ const found = await route(
271
+ contribution,
272
+ `/api/bots/alpha/composition/generations/${encodeURIComponent(BOOTSTRAP_GENERATION)}`,
273
+ );
274
+ expect(found?.status).toBe(200);
275
+ expect(
276
+ ((await found?.json()) as { generationId: string }).generationId,
277
+ ).toBe(BOOTSTRAP_GENERATION);
278
+
279
+ const missing = await route(
280
+ contribution,
281
+ `/api/bots/alpha/composition/generations/${encodeURIComponent(AUTHORED_GENERATION)}`,
282
+ );
283
+ expect(missing?.status).toBe(404);
284
+ });
285
+
286
+ test("admits a revert command and returns its receipt", async () => {
287
+ const { contribution, reverted } = host();
288
+ const response = await route(
289
+ contribution,
290
+ "/api/bots/alpha/composition/revert",
291
+ {
292
+ method: "POST",
293
+ headers: { "content-type": "application/json" },
294
+ body: revertCommand,
295
+ },
296
+ );
297
+ expect(response?.status).toBe(200);
298
+ expect((await response?.json()) as unknown).toEqual({
299
+ schemaVersion: 1,
300
+ commandId: "composition-revert-1",
301
+ status: "applied",
302
+ generationId: REVERTED_GENERATION,
303
+ currentGenerationId: AUTHORED_GENERATION,
304
+ });
305
+ expect(reverted).toHaveLength(1);
306
+ expect(reverted[0]?.toGenerationId).toBe(BOOTSTRAP_GENERATION);
307
+ });
308
+
309
+ test("rejects wrong methods, mismatched Bots, and invalid queries", async () => {
310
+ const { contribution, reverted } = host();
311
+ expect(
312
+ (
313
+ await route(contribution, "/api/bots/alpha/composition/generations", {
314
+ method: "POST",
315
+ })
316
+ )?.status,
317
+ ).toBe(405);
318
+ expect(
319
+ (await route(contribution, "/api/bots/alpha/composition/revert"))?.status,
320
+ ).toBe(405);
321
+ expect(
322
+ await route(contribution, "/api/bots/alpha/composition/generations", {
323
+ userId: "",
324
+ }),
325
+ ).toBeUndefined();
326
+ for (const query of ["?limit=0", "?limit=500", "?limit=two", "?page=1"]) {
327
+ expect(
328
+ (
329
+ await route(
330
+ contribution,
331
+ `/api/bots/alpha/composition/generations${query}`,
332
+ )
333
+ )?.status,
334
+ ).toBe(400);
335
+ }
336
+ expect(
337
+ (
338
+ await route(contribution, "/api/bots/alpha/composition/revert", {
339
+ method: "POST",
340
+ headers: { "content-type": "application/json" },
341
+ body: JSON.stringify({
342
+ schemaVersion: 1,
343
+ type: "composition/revert",
344
+ commandId: "composition-revert-2",
345
+ botId: "beta",
346
+ toGenerationId: BOOTSTRAP_GENERATION,
347
+ expectedGenerationId: AUTHORED_GENERATION,
348
+ }),
349
+ })
350
+ )?.status,
351
+ ).toBe(400);
352
+ // The optimistic check is part of the decoder: a no-op revert never lands.
353
+ expect(
354
+ (
355
+ await route(contribution, "/api/bots/alpha/composition/revert", {
356
+ method: "POST",
357
+ headers: { "content-type": "application/json" },
358
+ body: JSON.stringify({
359
+ schemaVersion: 1,
360
+ type: "composition/revert",
361
+ commandId: "composition-revert-3",
362
+ botId: "alpha",
363
+ toGenerationId: AUTHORED_GENERATION,
364
+ expectedGenerationId: AUTHORED_GENERATION,
365
+ }),
366
+ })
367
+ )?.status,
368
+ ).toBe(400);
369
+ expect(reverted).toHaveLength(0);
370
+ });
371
+
372
+ test("rejects a generation view the Bot authority returns malformed", async () => {
373
+ const { contribution } = host(undefined, {
374
+ schemaVersion: 1,
375
+ botId: "alpha",
376
+ currentGenerationId: AUTHORED_GENERATION,
377
+ generations: [
378
+ {
379
+ ...generationView(AUTHORED_GENERATION, true),
380
+ members: [
381
+ {
382
+ packageId: "shell",
383
+ version: "0.0.1",
384
+ provenance: { kind: "first-party" },
385
+ artifactBytes: "must-not-cross-the-seam",
386
+ },
387
+ ],
388
+ },
389
+ ],
390
+ });
391
+
392
+ expect(
393
+ (await route(contribution, "/api/bots/alpha/composition/generations"))
394
+ ?.status,
395
+ ).toBe(400);
396
+ });
397
+ });