@moxxy/config 0.27.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 (60) hide show
  1. package/LICENSE +21 -0
  2. package/dist/config-writer.d.ts +30 -0
  3. package/dist/config-writer.d.ts.map +1 -0
  4. package/dist/config-writer.js +57 -0
  5. package/dist/config-writer.js.map +1 -0
  6. package/dist/define.d.ts +17 -0
  7. package/dist/define.d.ts.map +1 -0
  8. package/dist/define.js +18 -0
  9. package/dist/define.js.map +1 -0
  10. package/dist/index.d.ts +9 -0
  11. package/dist/index.d.ts.map +1 -0
  12. package/dist/index.js +11 -0
  13. package/dist/index.js.map +1 -0
  14. package/dist/loader.d.ts +28 -0
  15. package/dist/loader.d.ts.map +1 -0
  16. package/dist/loader.js +210 -0
  17. package/dist/loader.js.map +1 -0
  18. package/dist/merge.d.ts +9 -0
  19. package/dist/merge.d.ts.map +1 -0
  20. package/dist/merge.js +45 -0
  21. package/dist/merge.js.map +1 -0
  22. package/dist/plugin-settings-schema.d.ts +24 -0
  23. package/dist/plugin-settings-schema.d.ts.map +1 -0
  24. package/dist/plugin-settings-schema.js +17 -0
  25. package/dist/plugin-settings-schema.js.map +1 -0
  26. package/dist/plugin.d.ts +21 -0
  27. package/dist/plugin.d.ts.map +1 -0
  28. package/dist/plugin.js +329 -0
  29. package/dist/plugin.js.map +1 -0
  30. package/dist/plugins-tree-schema.d.ts +444 -0
  31. package/dist/plugins-tree-schema.d.ts.map +1 -0
  32. package/dist/plugins-tree-schema.js +93 -0
  33. package/dist/plugins-tree-schema.js.map +1 -0
  34. package/dist/schema.d.ts +1200 -0
  35. package/dist/schema.d.ts.map +1 -0
  36. package/dist/schema.js +198 -0
  37. package/dist/schema.js.map +1 -0
  38. package/dist/user-config.d.ts +77 -0
  39. package/dist/user-config.d.ts.map +1 -0
  40. package/dist/user-config.js +265 -0
  41. package/dist/user-config.js.map +1 -0
  42. package/package.json +56 -0
  43. package/src/config-writer.test.ts +52 -0
  44. package/src/config-writer.ts +88 -0
  45. package/src/define.ts +19 -0
  46. package/src/index.ts +64 -0
  47. package/src/loader.test.ts +122 -0
  48. package/src/loader.ts +232 -0
  49. package/src/loader.yaml.test.ts +149 -0
  50. package/src/merge.test.ts +78 -0
  51. package/src/merge.ts +44 -0
  52. package/src/plugin-settings-schema.ts +18 -0
  53. package/src/plugin.test.ts +310 -0
  54. package/src/plugin.ts +360 -0
  55. package/src/plugins-tree-schema.test.ts +25 -0
  56. package/src/plugins-tree-schema.ts +103 -0
  57. package/src/schema.ts +218 -0
  58. package/src/security-config.test.ts +30 -0
  59. package/src/user-config.test.ts +113 -0
  60. package/src/user-config.ts +359 -0
package/src/plugin.ts ADDED
@@ -0,0 +1,360 @@
1
+ import { promises as fs } from 'node:fs';
2
+ import * as path from 'node:path';
3
+ import { z, createMutex, defineTool, definePlugin, type Plugin } from '@moxxy/sdk';
4
+ import { moxxyPath, writeFileAtomic } from '@moxxy/sdk/server';
5
+ import { findUpward, loadConfig } from './loader.js';
6
+ import { moxxyConfigSchema, type MoxxyConfig } from './schema.js';
7
+ import { setConfigValue } from './config-writer.js';
8
+
9
+ /**
10
+ * Optional callback that the CLI (or any session host) can provide to apply
11
+ * config changes to a live session without a restart. The applier receives the
12
+ * full validated config snapshot AFTER the write; it should diff against its
13
+ * own cached state and update the parts it can apply safely.
14
+ *
15
+ * Return a list of changed paths that were reflected at runtime, plus any
16
+ * that need a session restart to take effect.
17
+ */
18
+ export interface ConfigApplyResult {
19
+ readonly applied: ReadonlyArray<string>;
20
+ readonly pending: ReadonlyArray<string>;
21
+ }
22
+ export type ConfigApplier = (snapshot: MoxxyConfig) => Promise<ConfigApplyResult>;
23
+
24
+ const scopeSchema = z.enum(['user', 'project']);
25
+ type Scope = z.infer<typeof scopeSchema>;
26
+ // Default scope when the model omits it. Project-local is the safer
27
+ // default for read tools — touching the user-global file is usually an
28
+ // explicit operator action, not an inferred one.
29
+ const scopeSchemaOptional = scopeSchema.optional().default('project');
30
+
31
+ const USER_YAML = (): string => moxxyPath('config.yaml');
32
+
33
+ // The editor tools only read/write YAML configs (`doc.setIn` then re-serialize),
34
+ // so the project-scope walk deliberately matches ONLY the YAML names — never the
35
+ // .ts/.js configs `loadConfig` also honors, which these tools can't safely edit.
36
+ // The upward-walk traversal itself is shared with loader.ts (findUpward) so the
37
+ // depth bound can't drift; only this name list differs, by design.
38
+ const PROJECT_YAML_NAMES = ['moxxy.config.yaml', 'moxxy.config.yml'] as const;
39
+
40
+ async function findScopePath(scope: Scope, cwd: string): Promise<string | null> {
41
+ if (scope === 'user') {
42
+ const yaml = USER_YAML();
43
+ try {
44
+ await fs.access(yaml);
45
+ return yaml;
46
+ } catch {
47
+ return null;
48
+ }
49
+ }
50
+ // Project scope: walk upward looking for moxxy.config.yaml first, .yml second.
51
+ return findUpward(cwd, PROJECT_YAML_NAMES);
52
+ }
53
+
54
+ function scopeDefaultPath(scope: Scope, cwd: string): string {
55
+ return scope === 'user' ? USER_YAML() : path.join(cwd, 'moxxy.config.yaml');
56
+ }
57
+
58
+ /**
59
+ * True when a resolved project-scope target lives ABOVE cwd. The upward walk
60
+ * (findScopePath → findUpward) can resolve a config file in an ancestor dir, so
61
+ * a write/init may silently mutate a parent monorepo's or home-dir config. We
62
+ * surface this in the tool result so the operator can see the edit left the
63
+ * project root rather than discovering it after the fact. User scope is always
64
+ * the explicit ~/.moxxy path, so it never counts as an ancestor write.
65
+ */
66
+ function isOutsideCwd(target: string, cwd: string): boolean {
67
+ const rel = path.relative(path.resolve(cwd), path.resolve(target));
68
+ return rel.startsWith('..') || path.isAbsolute(rel);
69
+ }
70
+
71
+ async function readDoc(filePath: string): Promise<{ doc: import('yaml').Document.Parsed; text: string }> {
72
+ const text = await fs.readFile(filePath, 'utf8').catch(() => '');
73
+ const yamlMod = (await import('yaml')) as typeof import('yaml');
74
+ const doc = yamlMod.parseDocument(text);
75
+ return { doc, text };
76
+ }
77
+
78
+ function parseDotPath(p: string): Array<string | number> {
79
+ if (!p) return [];
80
+ return p.split('.').map((seg) => (/^\d+$/.test(seg) ? Number(seg) : seg));
81
+ }
82
+
83
+ // True only when `cursor` is a plain object or array that has `seg` as an OWN
84
+ // (not inherited) property. Keeps config_get's dot-path traversal inside the
85
+ // parsed config instead of letting it climb the prototype chain or index into
86
+ // scalar values.
87
+ function isIndexableOwn(cursor: unknown, seg: string | number): boolean {
88
+ if (cursor === null || typeof cursor !== 'object') return false;
89
+ if (seg === '__proto__' || seg === 'constructor' || seg === 'prototype') return false;
90
+ return Object.prototype.hasOwnProperty.call(cursor, seg);
91
+ }
92
+
93
+ function parseValue(raw: string): unknown {
94
+ // Try JSON first (allows arrays, numbers, booleans, strings, objects).
95
+ try {
96
+ return JSON.parse(raw);
97
+ } catch {
98
+ return raw;
99
+ }
100
+ }
101
+
102
+ // Capability fs globs shared by the tools below. The project-scope config can
103
+ // resolve in an ANCESTOR directory (findUpward's bounded walk), so these
104
+ // anchor on the well-known basenames rather than `$cwd` — the cap matcher
105
+ // tests them against absolute paths. The editor tools only ever touch YAML;
106
+ // reload/validate go through loadConfig, which also honors executable
107
+ // configs (config.ts/js in ~/.moxxy, moxxy.config.ts/js in the project).
108
+ const YAML_CONFIG_GLOBS = [
109
+ '~/.moxxy/config.yaml',
110
+ '/**/moxxy.config.yaml',
111
+ '/**/moxxy.config.yml',
112
+ ] as const;
113
+ const LOADER_CONFIG_GLOBS = ['~/.moxxy/config.*', '/**/moxxy.config.*'] as const;
114
+
115
+ export function buildConfigPlugin(
116
+ opts: { cwd: string; applier?: ConfigApplier } = { cwd: process.cwd() },
117
+ ): Plugin {
118
+ const cwd = opts.cwd;
119
+ const applier = opts.applier;
120
+
121
+ // Per-instance mutex serializing the read-modify-write of the config file.
122
+ // writeFileAtomic prevents torn writes but not lost updates: two concurrent
123
+ // config_set calls (the model can fire tools in parallel) would each apply
124
+ // their edit on top of the same stale doc, and the last atomic rename would
125
+ // clobber the other. Mirrors provider-admin/store.ts.
126
+ const writeMutex = createMutex();
127
+
128
+ return definePlugin({
129
+ name: '@moxxy/plugin-config',
130
+ version: '0.0.0',
131
+ tools: [
132
+ defineTool({
133
+ name: 'config_path',
134
+ description:
135
+ 'Return the resolved file path for the moxxy config at a given scope ' +
136
+ '(defaults to "project" — the moxxy.config.yaml in the current dir). ' +
137
+ 'Returns null if no file exists yet.',
138
+ inputSchema: z.object({ scope: scopeSchemaOptional }),
139
+ isolation: {
140
+ capabilities: {
141
+ fs: { read: [...YAML_CONFIG_GLOBS] },
142
+ net: { mode: 'none' },
143
+ timeMs: 10_000,
144
+ },
145
+ },
146
+ handler: async ({ scope }) => {
147
+ const found = await findScopePath(scope, cwd);
148
+ return { scope, path: found, defaultPath: scopeDefaultPath(scope, cwd) };
149
+ },
150
+ }),
151
+ defineTool({
152
+ name: 'config_show',
153
+ description:
154
+ 'Return the raw text of the moxxy config at the given scope (defaults to "project"). ' +
155
+ 'Useful when the agent needs to inspect or edit it.',
156
+ inputSchema: z.object({ scope: scopeSchemaOptional }),
157
+ isolation: {
158
+ capabilities: {
159
+ fs: { read: [...YAML_CONFIG_GLOBS] },
160
+ net: { mode: 'none' },
161
+ timeMs: 10_000,
162
+ },
163
+ },
164
+ handler: async ({ scope }) => {
165
+ const found = await findScopePath(scope, cwd);
166
+ if (!found) return { scope, path: null, text: '' };
167
+ const text = await fs.readFile(found, 'utf8');
168
+ return { scope, path: found, text };
169
+ },
170
+ }),
171
+ defineTool({
172
+ name: 'config_get',
173
+ description:
174
+ 'Read a single value from the config by dot-path (e.g. "provider.model"). Returns the parsed JSON value.',
175
+ inputSchema: z.object({ scope: scopeSchemaOptional, path: z.string().min(1) }),
176
+ // `$cwd/*`: the `path` INPUT is a dot-path ("provider.model"), not a
177
+ // file, but the cap checker's key heuristic treats it as one and
178
+ // resolves it against cwd — cover that single-segment resolution so
179
+ // legitimate reads aren't denied. The real fs surface is the globs.
180
+ isolation: {
181
+ capabilities: {
182
+ fs: { read: [...YAML_CONFIG_GLOBS, '$cwd/*'] },
183
+ net: { mode: 'none' },
184
+ timeMs: 10_000,
185
+ },
186
+ },
187
+ handler: async ({ scope, path: dotPath }) => {
188
+ const found = await findScopePath(scope, cwd);
189
+ if (!found) return null;
190
+ const yamlMod = (await import('yaml')) as typeof import('yaml');
191
+ const text = await fs.readFile(found, 'utf8');
192
+ const parsed = yamlMod.parse(text) ?? {};
193
+ const segs = parseDotPath(dotPath);
194
+ let cursor: unknown = parsed;
195
+ for (const seg of segs) {
196
+ // Only descend into a plain object or array, and only into an OWN
197
+ // property — otherwise a path like `constructor.prototype.toString`
198
+ // would walk the prototype chain and leak built-ins, and indexing a
199
+ // string value would return characters by number.
200
+ if (!isIndexableOwn(cursor, seg)) return null;
201
+ cursor = (cursor as Record<string | number, unknown>)[seg];
202
+ }
203
+ return cursor ?? null;
204
+ },
205
+ }),
206
+ defineTool({
207
+ name: 'config_set',
208
+ description:
209
+ 'Set a value at a dot-path in the moxxy config. Creates the file if missing. Value is JSON-parsed (so pass `"sonnet"`, `42`, `["a","b"]`, etc).',
210
+ inputSchema: z.object({
211
+ scope: scopeSchema,
212
+ path: z.string().min(1),
213
+ value: z.string(),
214
+ }),
215
+ permission: { action: 'prompt' },
216
+ // Comment-preserving read-modify-write of the YAML config. `$cwd/*`
217
+ // covers the dot-path `path` input, which the cap checker's key
218
+ // heuristic resolves against cwd (see config_get). The live applier
219
+ // is a host-owned closure; its runtime effects are the host's.
220
+ isolation: {
221
+ capabilities: {
222
+ fs: { read: [...YAML_CONFIG_GLOBS, '$cwd/*'], write: [...YAML_CONFIG_GLOBS] },
223
+ net: { mode: 'none' },
224
+ timeMs: 30_000,
225
+ },
226
+ },
227
+ handler: async ({ scope, path: dotPath, value }) => {
228
+ // ONE write implementation for every surface: the shared
229
+ // schema-validated, comment-preserving, mutex-serialized writer
230
+ // (config-writer.ts) — the TUI /settings panel writes through the
231
+ // same function, so tool and UI writes can't interleave or drift.
232
+ const written = await setConfigValue({
233
+ scope,
234
+ cwd,
235
+ path: dotPath,
236
+ value: parseValue(value),
237
+ });
238
+
239
+ // If a runtime applier is wired, try to reflect the change live.
240
+ let runtime: ConfigApplyResult = { applied: [], pending: [] };
241
+ if (applier) {
242
+ try {
243
+ runtime = await applier(written.config);
244
+ } catch (err) {
245
+ runtime = {
246
+ applied: [],
247
+ pending: [`reload-failed: ${err instanceof Error ? err.message : String(err)}`],
248
+ };
249
+ }
250
+ }
251
+
252
+ return {
253
+ path: written.path,
254
+ outsideCwd: scope === 'project' && isOutsideCwd(written.path, cwd),
255
+ runtime,
256
+ };
257
+ },
258
+ }),
259
+ defineTool({
260
+ name: 'config_reload',
261
+ description:
262
+ 'Re-read the merged config from disk and apply the safe subset of changes (mode, compactor, plugin enable/disable) to the active session. Anything outside that subset is reported in `pending` and requires a restart.',
263
+ inputSchema: z.object({}),
264
+ // loadConfig may execute a .ts/.js config via jiti, whose compile
265
+ // cache lands under node_modules/.cache. The applier is a host-owned
266
+ // closure; its runtime effects (plugin load/unload) are the host's.
267
+ isolation: {
268
+ capabilities: {
269
+ fs: {
270
+ read: [...LOADER_CONFIG_GLOBS],
271
+ write: ['/**/node_modules/.cache/**', '/tmp/**'],
272
+ },
273
+ net: { mode: 'none' },
274
+ timeMs: 30_000,
275
+ },
276
+ },
277
+ handler: async () => {
278
+ if (!applier) {
279
+ return { applied: [], pending: ['(no runtime applier configured)'] };
280
+ }
281
+ const { config: fresh } = await loadConfig({ cwd });
282
+ return await applier(fresh);
283
+ },
284
+ }),
285
+ defineTool({
286
+ name: 'config_init',
287
+ description:
288
+ 'Create a starter moxxy config file at the given scope (yaml format), if one does not already exist.',
289
+ inputSchema: z.object({ scope: scopeSchema }),
290
+ permission: { action: 'prompt' },
291
+ isolation: {
292
+ capabilities: {
293
+ fs: {
294
+ read: [...YAML_CONFIG_GLOBS],
295
+ write: ['~/.moxxy/config.yaml', '$cwd/moxxy.config.yaml'],
296
+ },
297
+ net: { mode: 'none' },
298
+ timeMs: 30_000,
299
+ },
300
+ },
301
+ handler: async ({ scope }) =>
302
+ writeMutex.run(async () => {
303
+ const existing = await findScopePath(scope, cwd);
304
+ if (existing) {
305
+ return {
306
+ path: existing,
307
+ created: false,
308
+ outsideCwd: scope === 'project' && isOutsideCwd(existing, cwd),
309
+ };
310
+ }
311
+ const target = scopeDefaultPath(scope, cwd);
312
+ await fs.mkdir(path.dirname(target), { recursive: true });
313
+ const template = `# moxxy config (${scope} scope)
314
+ # Documentation: https://docs.moxxy.ai
315
+ plugins:
316
+ provider:
317
+ default: anthropic
318
+ items:
319
+ anthropic:
320
+ model: claude-sonnet-4-6
321
+ mode:
322
+ default: default
323
+ `;
324
+ await writeFileAtomic(target, template);
325
+ return {
326
+ path: target,
327
+ created: true,
328
+ outsideCwd: scope === 'project' && isOutsideCwd(target, cwd),
329
+ };
330
+ }),
331
+ }),
332
+ defineTool({
333
+ name: 'config_validate',
334
+ description:
335
+ 'Re-run schema validation on the merged config (user + project) without applying any changes. Returns ok or the list of issues.',
336
+ inputSchema: z.object({}),
337
+ // Same surface as config_reload: loadConfig may execute a .ts/.js
338
+ // config via jiti (compile cache under node_modules/.cache).
339
+ isolation: {
340
+ capabilities: {
341
+ fs: {
342
+ read: [...LOADER_CONFIG_GLOBS],
343
+ write: ['/**/node_modules/.cache/**', '/tmp/**'],
344
+ },
345
+ net: { mode: 'none' },
346
+ timeMs: 30_000,
347
+ },
348
+ },
349
+ handler: async () => {
350
+ try {
351
+ await loadConfig({ cwd });
352
+ return { ok: true };
353
+ } catch (err) {
354
+ return { ok: false, error: err instanceof Error ? err.message : String(err) };
355
+ }
356
+ },
357
+ }),
358
+ ],
359
+ });
360
+ }
@@ -0,0 +1,25 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { PLUGIN_CATEGORY_KEYS, pluginsTreeSchema } from './plugins-tree-schema.js';
3
+
4
+ describe('plugins-tree schema — reflector category', () => {
5
+ it('lists reflector as an active-def category key', () => {
6
+ expect(PLUGIN_CATEGORY_KEYS).toContain('reflector');
7
+ });
8
+
9
+ it('round-trips plugins.reflector.default', () => {
10
+ const tree = { reflector: { default: 'default' } };
11
+ const parsed = pluginsTreeSchema.parse(tree);
12
+ expect(parsed.reflector?.default).toBe('default');
13
+ });
14
+
15
+ it('accepts a per-item option bag on the reflector slot', () => {
16
+ const parsed = pluginsTreeSchema.parse({
17
+ reflector: { default: 'default', items: { default: { window: 'session' } } },
18
+ });
19
+ expect(parsed.reflector?.items?.default).toEqual({ window: 'session' });
20
+ });
21
+
22
+ it('rejects an unknown key inside the reflector slot (.strict())', () => {
23
+ expect(() => pluginsTreeSchema.parse({ reflector: { nope: true } })).toThrow();
24
+ });
25
+ });
@@ -0,0 +1,103 @@
1
+ import { z } from 'zod';
2
+ import { pluginSettingsSchema } from './plugin-settings-schema.js';
3
+
4
+ /**
5
+ * The unified `plugins:` tree — the single source of truth for what's
6
+ * installed/enabled and what's the active default per category. Two axes:
7
+ *
8
+ * - `packages` — the install/enable ledger, keyed by **npm package name**.
9
+ * Enabling a package makes all of its contributions available at once.
10
+ * - per-category slots — the swap axis, keyed by **contribution name**
11
+ * (`provider.default: anthropic`). The no-package core floors
12
+ * (markdown/localhost/tfidf/jsonl/none) can only be named this way, and a
13
+ * multi-contribution package needs to say *which* one is active. A package
14
+ * name is also accepted as a `default` alias and resolved to its sole
15
+ * contribution of the kind (ambiguous when the package contributes several).
16
+ *
17
+ * Clean slate: this fully replaces the legacy flat `provider`/`mode`/`compactor`/
18
+ * `workflowExecutor` keys, the old package-keyed `plugins:` map, and
19
+ * `preferences.json`. There is no back-compat normalizer.
20
+ */
21
+
22
+ /** Per-item options for a provider contribution. */
23
+ export const providerItemSchema = z
24
+ .object({
25
+ /** Default model id for this provider. */
26
+ model: z.string().optional(),
27
+ /** Provider-specific client config (baseURL, headers, …). */
28
+ config: z.record(z.string(), z.unknown()).optional(),
29
+ /** Disable this specific provider contribution without removing the package. */
30
+ enabled: z.boolean().optional(),
31
+ })
32
+ .strict();
33
+ export type ProviderItem = z.infer<typeof providerItemSchema>;
34
+
35
+ /** The provider category slot (special: ordered credential fallbacks + typed items). */
36
+ export const providerSlotSchema = z
37
+ .object({
38
+ /** Active provider contribution name (or a package-name alias). */
39
+ default: z.string().optional(),
40
+ /** Ordered provider names to fall back to when the primary's key fails. */
41
+ fallbacks: z.array(z.string()).optional(),
42
+ items: z.record(z.string(), providerItemSchema).optional(),
43
+ })
44
+ .strict();
45
+ export type ProviderSlot = z.infer<typeof providerSlotSchema>;
46
+
47
+ /** A generic active-def category slot: which contribution is the default + per-item options. */
48
+ export const categorySlotSchema = z
49
+ .object({
50
+ /** Active contribution name (or a package-name alias). */
51
+ default: z.string().optional(),
52
+ /** Per-contribution option bags, keyed by contribution name. */
53
+ items: z.record(z.string(), z.record(z.string(), z.unknown())).optional(),
54
+ })
55
+ .strict();
56
+ export type CategorySlot = z.infer<typeof categorySlotSchema>;
57
+
58
+ /**
59
+ * The `plugins:` tree. Reserved keys = `packages` + the active-def category
60
+ * names; the schema is closed (`.strict()`) so a typo'd key is a clear error
61
+ * rather than a silently-ignored setting.
62
+ */
63
+ export const pluginsTreeSchema = z
64
+ .object({
65
+ /** Install/enable ledger keyed by npm package name. */
66
+ packages: z.record(z.string(), pluginSettingsSchema).optional(),
67
+ // Swap axis — one slot per ActiveDef registry kind.
68
+ provider: providerSlotSchema.optional(),
69
+ mode: categorySlotSchema.optional(),
70
+ compactor: categorySlotSchema.optional(),
71
+ cacheStrategy: categorySlotSchema.optional(),
72
+ workflowExecutor: categorySlotSchema.optional(),
73
+ transcriber: categorySlotSchema.optional(),
74
+ synthesizer: categorySlotSchema.optional(),
75
+ embedder: categorySlotSchema.optional(),
76
+ isolator: categorySlotSchema.optional(),
77
+ viewRenderer: categorySlotSchema.optional(),
78
+ tunnelProvider: categorySlotSchema.optional(),
79
+ eventStore: categorySlotSchema.optional(),
80
+ reflector: categorySlotSchema.optional(),
81
+ channel: categorySlotSchema.optional(),
82
+ })
83
+ .strict();
84
+ export type PluginsTree = z.infer<typeof pluginsTreeSchema>;
85
+
86
+ /** The set of category keys that carry an active-def `default` (i.e. every key but `packages`). */
87
+ export const PLUGIN_CATEGORY_KEYS = [
88
+ 'provider',
89
+ 'mode',
90
+ 'compactor',
91
+ 'cacheStrategy',
92
+ 'workflowExecutor',
93
+ 'transcriber',
94
+ 'synthesizer',
95
+ 'embedder',
96
+ 'isolator',
97
+ 'viewRenderer',
98
+ 'tunnelProvider',
99
+ 'eventStore',
100
+ 'reflector',
101
+ 'channel',
102
+ ] as const;
103
+ export type PluginCategoryKey = (typeof PLUGIN_CATEGORY_KEYS)[number];