@moxxy/core 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +125 -0
- package/dist/events/factory.d.ts +6 -0
- package/dist/events/factory.d.ts.map +1 -0
- package/dist/events/factory.js +14 -0
- package/dist/events/factory.js.map +1 -0
- package/dist/events/log.d.ts +92 -0
- package/dist/events/log.d.ts.map +1 -0
- package/dist/events/log.js +244 -0
- package/dist/events/log.js.map +1 -0
- package/dist/index.d.ts +40 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +41 -0
- package/dist/index.js.map +1 -0
- package/dist/logger.d.ts +16 -0
- package/dist/logger.d.ts.map +1 -0
- package/dist/logger.js +27 -0
- package/dist/logger.js.map +1 -0
- package/dist/permissions/engine.d.ts +94 -0
- package/dist/permissions/engine.d.ts.map +1 -0
- package/dist/permissions/engine.js +203 -0
- package/dist/permissions/engine.js.map +1 -0
- package/dist/permissions/resolvers.d.ts +8 -0
- package/dist/permissions/resolvers.d.ts.map +1 -0
- package/dist/permissions/resolvers.js +8 -0
- package/dist/permissions/resolvers.js.map +1 -0
- package/dist/plugins/discovery.d.ts +9 -0
- package/dist/plugins/discovery.d.ts.map +1 -0
- package/dist/plugins/discovery.js +112 -0
- package/dist/plugins/discovery.js.map +1 -0
- package/dist/plugins/host-options.d.ts +72 -0
- package/dist/plugins/host-options.d.ts.map +1 -0
- package/dist/plugins/host-options.js +2 -0
- package/dist/plugins/host-options.js.map +1 -0
- package/dist/plugins/host.d.ts +53 -0
- package/dist/plugins/host.d.ts.map +1 -0
- package/dist/plugins/host.js +269 -0
- package/dist/plugins/host.js.map +1 -0
- package/dist/plugins/lifecycle.d.ts +31 -0
- package/dist/plugins/lifecycle.d.ts.map +1 -0
- package/dist/plugins/lifecycle.js +100 -0
- package/dist/plugins/lifecycle.js.map +1 -0
- package/dist/plugins/loader.d.ts +7 -0
- package/dist/plugins/loader.d.ts.map +1 -0
- package/dist/plugins/loader.js +68 -0
- package/dist/plugins/loader.js.map +1 -0
- package/dist/plugins/package-requirements.d.ts +16 -0
- package/dist/plugins/package-requirements.d.ts.map +1 -0
- package/dist/plugins/package-requirements.js +52 -0
- package/dist/plugins/package-requirements.js.map +1 -0
- package/dist/plugins/registry-kinds.d.ts +58 -0
- package/dist/plugins/registry-kinds.d.ts.map +1 -0
- package/dist/plugins/registry-kinds.js +139 -0
- package/dist/plugins/registry-kinds.js.map +1 -0
- package/dist/plugins/toposort.d.ts +24 -0
- package/dist/plugins/toposort.d.ts.map +1 -0
- package/dist/plugins/toposort.js +67 -0
- package/dist/plugins/toposort.js.map +1 -0
- package/dist/preferences.d.ts +40 -0
- package/dist/preferences.d.ts.map +1 -0
- package/dist/preferences.js +58 -0
- package/dist/preferences.js.map +1 -0
- package/dist/registries/active-backend-registry.d.ts +65 -0
- package/dist/registries/active-backend-registry.d.ts.map +1 -0
- package/dist/registries/active-backend-registry.js +108 -0
- package/dist/registries/active-backend-registry.js.map +1 -0
- package/dist/registries/active-def-registry.d.ts +59 -0
- package/dist/registries/active-def-registry.d.ts.map +1 -0
- package/dist/registries/active-def-registry.js +63 -0
- package/dist/registries/active-def-registry.js.map +1 -0
- package/dist/registries/agents.d.ts +16 -0
- package/dist/registries/agents.d.ts.map +1 -0
- package/dist/registries/agents.js +17 -0
- package/dist/registries/agents.js.map +1 -0
- package/dist/registries/cache-strategies.d.ts +12 -0
- package/dist/registries/cache-strategies.d.ts.map +1 -0
- package/dist/registries/cache-strategies.js +13 -0
- package/dist/registries/cache-strategies.js.map +1 -0
- package/dist/registries/channels.d.ts +16 -0
- package/dist/registries/channels.d.ts.map +1 -0
- package/dist/registries/channels.js +35 -0
- package/dist/registries/channels.js.map +1 -0
- package/dist/registries/commands.d.ts +29 -0
- package/dist/registries/commands.d.ts.map +1 -0
- package/dist/registries/commands.js +72 -0
- package/dist/registries/commands.js.map +1 -0
- package/dist/registries/compactors.d.ts +12 -0
- package/dist/registries/compactors.d.ts.map +1 -0
- package/dist/registries/compactors.js +13 -0
- package/dist/registries/compactors.js.map +1 -0
- package/dist/registries/def-map-registry.d.ts +33 -0
- package/dist/registries/def-map-registry.d.ts.map +1 -0
- package/dist/registries/def-map-registry.js +36 -0
- package/dist/registries/def-map-registry.js.map +1 -0
- package/dist/registries/embedders.d.ts +16 -0
- package/dist/registries/embedders.d.ts.map +1 -0
- package/dist/registries/embedders.js +17 -0
- package/dist/registries/embedders.js.map +1 -0
- package/dist/registries/isolators.d.ts +21 -0
- package/dist/registries/isolators.d.ts.map +1 -0
- package/dist/registries/isolators.js +32 -0
- package/dist/registries/isolators.js.map +1 -0
- package/dist/registries/modes.d.ts +28 -0
- package/dist/registries/modes.d.ts.map +1 -0
- package/dist/registries/modes.js +81 -0
- package/dist/registries/modes.js.map +1 -0
- package/dist/registries/providers.d.ts +44 -0
- package/dist/registries/providers.d.ts.map +1 -0
- package/dist/registries/providers.js +95 -0
- package/dist/registries/providers.js.map +1 -0
- package/dist/registries/skills.d.ts +24 -0
- package/dist/registries/skills.d.ts.map +1 -0
- package/dist/registries/skills.js +81 -0
- package/dist/registries/skills.js.map +1 -0
- package/dist/registries/surfaces.d.ts +13 -0
- package/dist/registries/surfaces.d.ts.map +1 -0
- package/dist/registries/surfaces.js +14 -0
- package/dist/registries/surfaces.js.map +1 -0
- package/dist/registries/synthesizers.d.ts +26 -0
- package/dist/registries/synthesizers.d.ts.map +1 -0
- package/dist/registries/synthesizers.js +34 -0
- package/dist/registries/synthesizers.js.map +1 -0
- package/dist/registries/tools.d.ts +49 -0
- package/dist/registries/tools.d.ts.map +1 -0
- package/dist/registries/tools.js +98 -0
- package/dist/registries/tools.js.map +1 -0
- package/dist/registries/transcribers.d.ts +17 -0
- package/dist/registries/transcribers.d.ts.map +1 -0
- package/dist/registries/transcribers.js +18 -0
- package/dist/registries/transcribers.js.map +1 -0
- package/dist/registries/tunnel-providers.d.ts +12 -0
- package/dist/registries/tunnel-providers.d.ts.map +1 -0
- package/dist/registries/tunnel-providers.js +13 -0
- package/dist/registries/tunnel-providers.js.map +1 -0
- package/dist/registries/view-renderers.d.ts +13 -0
- package/dist/registries/view-renderers.d.ts.map +1 -0
- package/dist/registries/view-renderers.js +14 -0
- package/dist/registries/view-renderers.js.map +1 -0
- package/dist/registries/workflow-executors.d.ts +13 -0
- package/dist/registries/workflow-executors.d.ts.map +1 -0
- package/dist/registries/workflow-executors.js +14 -0
- package/dist/registries/workflow-executors.js.map +1 -0
- package/dist/requirements.d.ts +59 -0
- package/dist/requirements.d.ts.map +1 -0
- package/dist/requirements.js +149 -0
- package/dist/requirements.js.map +1 -0
- package/dist/run-turn.d.ts +6 -0
- package/dist/run-turn.d.ts.map +1 -0
- package/dist/run-turn.js +141 -0
- package/dist/run-turn.js.map +1 -0
- package/dist/session-runtime.d.ts +67 -0
- package/dist/session-runtime.d.ts.map +1 -0
- package/dist/session-runtime.js +2 -0
- package/dist/session-runtime.js.map +1 -0
- package/dist/session.d.ts +203 -0
- package/dist/session.d.ts.map +1 -0
- package/dist/session.js +399 -0
- package/dist/session.js.map +1 -0
- package/dist/sessions/persistence.d.ts +184 -0
- package/dist/sessions/persistence.d.ts.map +1 -0
- package/dist/sessions/persistence.js +442 -0
- package/dist/sessions/persistence.js.map +1 -0
- package/dist/setup-agent.d.ts +91 -0
- package/dist/setup-agent.d.ts.map +1 -0
- package/dist/setup-agent.js +138 -0
- package/dist/setup-agent.js.map +1 -0
- package/dist/skills/index.d.ts +4 -0
- package/dist/skills/index.d.ts.map +1 -0
- package/dist/skills/index.js +4 -0
- package/dist/skills/index.js.map +1 -0
- package/dist/skills/loader.d.ts +16 -0
- package/dist/skills/loader.d.ts.map +1 -0
- package/dist/skills/loader.js +99 -0
- package/dist/skills/loader.js.map +1 -0
- package/dist/skills/parse.d.ts +5 -0
- package/dist/skills/parse.d.ts.map +1 -0
- package/dist/skills/parse.js +8 -0
- package/dist/skills/parse.js.map +1 -0
- package/dist/skills/synthesize-draft.d.ts +8 -0
- package/dist/skills/synthesize-draft.d.ts.map +1 -0
- package/dist/skills/synthesize-draft.js +32 -0
- package/dist/skills/synthesize-draft.js.map +1 -0
- package/dist/skills/synthesize.d.ts +33 -0
- package/dist/skills/synthesize.d.ts.map +1 -0
- package/dist/skills/synthesize.js +241 -0
- package/dist/skills/synthesize.js.map +1 -0
- package/dist/subagents/events.d.ts +20 -0
- package/dist/subagents/events.d.ts.map +1 -0
- package/dist/subagents/events.js +146 -0
- package/dist/subagents/events.js.map +1 -0
- package/dist/subagents/registry.d.ts +40 -0
- package/dist/subagents/registry.d.ts.map +1 -0
- package/dist/subagents/registry.js +42 -0
- package/dist/subagents/registry.js.map +1 -0
- package/dist/subagents/run-child.d.ts +43 -0
- package/dist/subagents/run-child.d.ts.map +1 -0
- package/dist/subagents/run-child.js +276 -0
- package/dist/subagents/run-child.js.map +1 -0
- package/dist/subagents/spawn.d.ts +8 -0
- package/dist/subagents/spawn.d.ts.map +1 -0
- package/dist/subagents/spawn.js +8 -0
- package/dist/subagents/spawn.js.map +1 -0
- package/dist/subagents/tools.d.ts +4 -0
- package/dist/subagents/tools.d.ts.map +1 -0
- package/dist/subagents/tools.js +13 -0
- package/dist/subagents/tools.js.map +1 -0
- package/dist/subagents.d.ts +3 -0
- package/dist/subagents.d.ts.map +1 -0
- package/dist/subagents.js +3 -0
- package/dist/subagents.js.map +1 -0
- package/dist/surfaces/host.d.ts +53 -0
- package/dist/surfaces/host.d.ts.map +1 -0
- package/dist/surfaces/host.js +177 -0
- package/dist/surfaces/host.js.map +1 -0
- package/dist/tunnel/localhost.d.ts +7 -0
- package/dist/tunnel/localhost.d.ts.map +1 -0
- package/dist/tunnel/localhost.js +21 -0
- package/dist/tunnel/localhost.js.map +1 -0
- package/dist/usage-stats.d.ts +42 -0
- package/dist/usage-stats.d.ts.map +1 -0
- package/dist/usage-stats.js +83 -0
- package/dist/usage-stats.js.map +1 -0
- package/dist/view/default-renderer.d.ts +7 -0
- package/dist/view/default-renderer.d.ts.map +1 -0
- package/dist/view/default-renderer.js +14 -0
- package/dist/view/default-renderer.js.map +1 -0
- package/dist/view/parse.d.ts +7 -0
- package/dist/view/parse.d.ts.map +1 -0
- package/dist/view/parse.js +479 -0
- package/dist/view/parse.js.map +1 -0
- package/package.json +61 -0
- package/src/events/factory.ts +27 -0
- package/src/events/log.test.ts +470 -0
- package/src/events/log.ts +256 -0
- package/src/index.ts +102 -0
- package/src/logger.ts +46 -0
- package/src/permissions/engine.test.ts +206 -0
- package/src/permissions/engine.ts +247 -0
- package/src/permissions/resolvers.test.ts +40 -0
- package/src/permissions/resolvers.ts +17 -0
- package/src/plugins/discovery.test.ts +115 -0
- package/src/plugins/discovery.ts +124 -0
- package/src/plugins/host-options.ts +73 -0
- package/src/plugins/host-view-tunnel.test.ts +86 -0
- package/src/plugins/host.test.ts +434 -0
- package/src/plugins/host.ts +327 -0
- package/src/plugins/lifecycle.test.ts +187 -0
- package/src/plugins/lifecycle.ts +136 -0
- package/src/plugins/loader.ts +79 -0
- package/src/plugins/package-requirements.ts +50 -0
- package/src/plugins/registry-kinds.test.ts +162 -0
- package/src/plugins/registry-kinds.ts +210 -0
- package/src/plugins/toposort.test.ts +66 -0
- package/src/plugins/toposort.ts +69 -0
- package/src/preferences.test.ts +105 -0
- package/src/preferences.ts +84 -0
- package/src/registries/active-backend-registry.ts +142 -0
- package/src/registries/active-def-registry.test.ts +103 -0
- package/src/registries/active-def-registry.ts +97 -0
- package/src/registries/agents.ts +18 -0
- package/src/registries/cache-strategies.ts +14 -0
- package/src/registries/channels.ts +42 -0
- package/src/registries/commands.test.ts +71 -0
- package/src/registries/commands.ts +78 -0
- package/src/registries/compactors.ts +14 -0
- package/src/registries/def-map-registry.test.ts +67 -0
- package/src/registries/def-map-registry.ts +59 -0
- package/src/registries/embedders.test.ts +54 -0
- package/src/registries/embedders.ts +18 -0
- package/src/registries/isolators.test.ts +43 -0
- package/src/registries/isolators.ts +38 -0
- package/src/registries/modes.test.ts +66 -0
- package/src/registries/modes.ts +80 -0
- package/src/registries/providers.test.ts +50 -0
- package/src/registries/providers.ts +97 -0
- package/src/registries/semantics.test.ts +117 -0
- package/src/registries/skills.ts +87 -0
- package/src/registries/surfaces.ts +18 -0
- package/src/registries/synthesizers.test.ts +63 -0
- package/src/registries/synthesizers.ts +36 -0
- package/src/registries/tools.test.ts +142 -0
- package/src/registries/tools.ts +145 -0
- package/src/registries/transcribers.test.ts +70 -0
- package/src/registries/transcribers.ts +19 -0
- package/src/registries/tunnel-providers.test.ts +30 -0
- package/src/registries/tunnel-providers.ts +14 -0
- package/src/registries/view-renderers.test.ts +68 -0
- package/src/registries/view-renderers.ts +15 -0
- package/src/registries/workflow-executors.test.ts +53 -0
- package/src/registries/workflow-executors.ts +15 -0
- package/src/requirements.targetinfo.test.ts +70 -0
- package/src/requirements.test.ts +111 -0
- package/src/requirements.ts +262 -0
- package/src/run-turn.test.ts +178 -0
- package/src/run-turn.ts +156 -0
- package/src/session-runtime.ts +75 -0
- package/src/session.test.ts +216 -0
- package/src/session.ts +491 -0
- package/src/sessions/persistence.flush.test.ts +100 -0
- package/src/sessions/persistence.ready.test.ts +91 -0
- package/src/sessions/persistence.test.ts +274 -0
- package/src/sessions/persistence.ts +498 -0
- package/src/setup-agent.test.ts +72 -0
- package/src/setup-agent.ts +189 -0
- package/src/skills/index.ts +14 -0
- package/src/skills/loader.test.ts +102 -0
- package/src/skills/loader.ts +114 -0
- package/src/skills/parse.test.ts +59 -0
- package/src/skills/parse.ts +11 -0
- package/src/skills/synthesize-draft.test.ts +79 -0
- package/src/skills/synthesize-draft.ts +46 -0
- package/src/skills/synthesize.test.ts +170 -0
- package/src/skills/synthesize.ts +304 -0
- package/src/subagents/events.test.ts +56 -0
- package/src/subagents/events.ts +194 -0
- package/src/subagents/registry.ts +62 -0
- package/src/subagents/run-child.test.ts +185 -0
- package/src/subagents/run-child.ts +425 -0
- package/src/subagents/spawn.ts +12 -0
- package/src/subagents/tools.ts +19 -0
- package/src/subagents.ts +2 -0
- package/src/surfaces/host.test.ts +330 -0
- package/src/surfaces/host.ts +192 -0
- package/src/tunnel/localhost.test.ts +22 -0
- package/src/tunnel/localhost.ts +21 -0
- package/src/usage-stats.test.ts +77 -0
- package/src/usage-stats.ts +119 -0
- package/src/view/default-renderer.test.ts +29 -0
- package/src/view/default-renderer.ts +14 -0
- package/src/view/parse-extended.test.ts +255 -0
- package/src/view/parse.test.ts +232 -0
- package/src/view/parse.ts +520 -0
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import type { CommandDef } from '@moxxy/sdk';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Registry of `/<name>` slash commands shared across every channel.
|
|
5
|
+
* Plugins contribute via `PluginSpec.commands`; channels read with
|
|
6
|
+
* `list()` / `get()` / `match()` to render their picker and dispatch
|
|
7
|
+
* matching user input.
|
|
8
|
+
*
|
|
9
|
+
* Naming conflicts throw at registration time so two plugins can't
|
|
10
|
+
* silently shadow the same name — use `replace()` for explicit
|
|
11
|
+
* overrides (e.g. user-config tweaks of a built-in).
|
|
12
|
+
*/
|
|
13
|
+
export class CommandRegistry {
|
|
14
|
+
private readonly commands = new Map<string, CommandDef>();
|
|
15
|
+
/** Reverse-lookup table: alias → primary name. */
|
|
16
|
+
private readonly aliases = new Map<string, string>();
|
|
17
|
+
|
|
18
|
+
register(cmd: CommandDef): void {
|
|
19
|
+
if (this.commands.has(cmd.name)) {
|
|
20
|
+
throw new Error(`Command already registered: /${cmd.name}`);
|
|
21
|
+
}
|
|
22
|
+
if (this.aliases.has(cmd.name)) {
|
|
23
|
+
throw new Error(`Command name already in use as an alias: /${cmd.name}`);
|
|
24
|
+
}
|
|
25
|
+
this.commands.set(cmd.name, cmd);
|
|
26
|
+
for (const alias of cmd.aliases ?? []) {
|
|
27
|
+
if (this.aliases.has(alias) || this.commands.has(alias)) {
|
|
28
|
+
throw new Error(`Command alias already in use: /${alias}`);
|
|
29
|
+
}
|
|
30
|
+
this.aliases.set(alias, cmd.name);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
replace(cmd: CommandDef): void {
|
|
35
|
+
// Clean up any aliases of the prior definition before re-adding.
|
|
36
|
+
const prior = this.commands.get(cmd.name);
|
|
37
|
+
if (prior) {
|
|
38
|
+
for (const alias of prior.aliases ?? []) this.aliases.delete(alias);
|
|
39
|
+
}
|
|
40
|
+
this.commands.set(cmd.name, cmd);
|
|
41
|
+
for (const alias of cmd.aliases ?? []) {
|
|
42
|
+
this.aliases.set(alias, cmd.name);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
unregister(name: string): void {
|
|
47
|
+
const cmd = this.commands.get(name);
|
|
48
|
+
if (!cmd) return;
|
|
49
|
+
for (const alias of cmd.aliases ?? []) this.aliases.delete(alias);
|
|
50
|
+
this.commands.delete(name);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
get(name: string): CommandDef | undefined {
|
|
54
|
+
const direct = this.commands.get(name);
|
|
55
|
+
if (direct) return direct;
|
|
56
|
+
const aliased = this.aliases.get(name);
|
|
57
|
+
return aliased ? this.commands.get(aliased) : undefined;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
has(name: string): boolean {
|
|
61
|
+
return this.get(name) !== undefined;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
list(): ReadonlyArray<CommandDef> {
|
|
65
|
+
return [...this.commands.values()];
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Filter to commands visible in `channel`. Commands with no
|
|
70
|
+
* `channels` field are visible everywhere; commands with a list are
|
|
71
|
+
* visible only when `channel` appears in it.
|
|
72
|
+
*/
|
|
73
|
+
listForChannel(channel: string): ReadonlyArray<CommandDef> {
|
|
74
|
+
return this.list().filter(
|
|
75
|
+
(c) => !c.channels || c.channels.includes(channel),
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { CompactorDef } from '@moxxy/sdk';
|
|
2
|
+
import { ActiveDefRegistry } from './active-def-registry.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* One active compaction strategy per session. Register throws on duplicate
|
|
6
|
+
* (use `replace()` to overwrite) and auto-activates the first registration;
|
|
7
|
+
* `unregister` clears the active slot rather than picking an arbitrary
|
|
8
|
+
* successor. See {@link ActiveDefRegistry}.
|
|
9
|
+
*/
|
|
10
|
+
export class CompactorRegistry extends ActiveDefRegistry<CompactorDef> {
|
|
11
|
+
constructor() {
|
|
12
|
+
super({ noun: 'Compactor' });
|
|
13
|
+
}
|
|
14
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { DefMapRegistry } from './def-map-registry.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Contract test for the shared base behind agents, channels and surfaces.
|
|
6
|
+
* Covers the default `name`-keyed shape and a `kind`-keyed variant (surfaces).
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
interface NamedThing {
|
|
10
|
+
name: string;
|
|
11
|
+
v?: number;
|
|
12
|
+
}
|
|
13
|
+
const named = (name: string, v?: number): NamedThing => ({ name, v });
|
|
14
|
+
|
|
15
|
+
interface KindThing {
|
|
16
|
+
kind: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
describe('DefMapRegistry (shared base)', () => {
|
|
20
|
+
const make = () =>
|
|
21
|
+
new DefMapRegistry<NamedThing>({ noun: 'Thing', keyOf: (d) => d.name });
|
|
22
|
+
|
|
23
|
+
it('register / get / has / list round-trip', () => {
|
|
24
|
+
const r = make();
|
|
25
|
+
expect(r.list()).toEqual([]);
|
|
26
|
+
expect(r.get('a')).toBeUndefined();
|
|
27
|
+
expect(r.has('a')).toBe(false);
|
|
28
|
+
const a = named('a');
|
|
29
|
+
r.register(a);
|
|
30
|
+
expect(r.get('a')).toBe(a);
|
|
31
|
+
expect(r.has('a')).toBe(true);
|
|
32
|
+
r.register(named('b'));
|
|
33
|
+
expect(r.list().map((x) => x.name)).toEqual(['a', 'b']);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('throws the exact noun-prefixed message on duplicate register', () => {
|
|
37
|
+
const r = make();
|
|
38
|
+
r.register(named('a'));
|
|
39
|
+
expect(() => r.register(named('a'))).toThrow('Thing already registered: a');
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('replace overwrites without throwing', () => {
|
|
43
|
+
const r = make();
|
|
44
|
+
r.register(named('a', 1));
|
|
45
|
+
r.replace(named('a', 2));
|
|
46
|
+
expect(r.get('a')?.v).toBe(2);
|
|
47
|
+
expect(r.list()).toHaveLength(1);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('unregister removes by key', () => {
|
|
51
|
+
const r = make();
|
|
52
|
+
r.register(named('a'));
|
|
53
|
+
r.register(named('b'));
|
|
54
|
+
r.unregister('a');
|
|
55
|
+
expect(r.has('a')).toBe(false);
|
|
56
|
+
expect(r.list().map((x) => x.name)).toEqual(['b']);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it('supports a non-name key field (kind) and interpolates it in the error', () => {
|
|
60
|
+
const r = new DefMapRegistry<KindThing>({ noun: 'Surface', keyOf: (d) => d.kind });
|
|
61
|
+
r.register({ kind: 'terminal' });
|
|
62
|
+
expect(r.get('terminal')).toEqual({ kind: 'terminal' });
|
|
63
|
+
expect(() => r.register({ kind: 'terminal' })).toThrow('Surface already registered: terminal');
|
|
64
|
+
r.unregister('terminal');
|
|
65
|
+
expect(r.has('terminal')).toBe(false);
|
|
66
|
+
});
|
|
67
|
+
});
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared base for the "flat name→def, no active slot" registries (agents,
|
|
3
|
+
* channels, surfaces). A `Map<key, def>` exposing register (throws on
|
|
4
|
+
* duplicate), replace (overwrite), unregister, list, get and has. The three
|
|
5
|
+
* registries differ only in their noun and key field (`name` vs `kind`),
|
|
6
|
+
* supplied via {@link DefMapRegistryOptions}.
|
|
7
|
+
*
|
|
8
|
+
* `ChannelRegistryImpl` extends this and adds `listWithAvailability`;
|
|
9
|
+
* `SurfaceRegistryImpl` keys on `kind` rather than `name`.
|
|
10
|
+
*/
|
|
11
|
+
export interface DefMapRegistryOptions<TDef, K> {
|
|
12
|
+
/** Capitalised singular noun for error messages, e.g. `'Agent'`. */
|
|
13
|
+
readonly noun: string;
|
|
14
|
+
/** Extract the map key from a def, e.g. `(d) => d.name` or `(d) => d.kind`. */
|
|
15
|
+
readonly keyOf: (def: TDef) => K;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export class DefMapRegistry<TDef, K = string> {
|
|
19
|
+
protected readonly defs = new Map<K, TDef>();
|
|
20
|
+
private readonly noun: string;
|
|
21
|
+
private readonly keyOf: (def: TDef) => K;
|
|
22
|
+
|
|
23
|
+
constructor(opts: DefMapRegistryOptions<TDef, K>) {
|
|
24
|
+
this.noun = opts.noun;
|
|
25
|
+
this.keyOf = opts.keyOf;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Register a definition. Throws on duplicate so two plugins can't silently
|
|
30
|
+
* shadow each other — use `replace()` when you really want to override.
|
|
31
|
+
*/
|
|
32
|
+
register(def: TDef): void {
|
|
33
|
+
const key = this.keyOf(def);
|
|
34
|
+
if (this.defs.has(key)) {
|
|
35
|
+
throw new Error(`${this.noun} already registered: ${String(key)}`);
|
|
36
|
+
}
|
|
37
|
+
this.defs.set(key, def);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
replace(def: TDef): void {
|
|
41
|
+
this.defs.set(this.keyOf(def), def);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
unregister(key: K): void {
|
|
45
|
+
this.defs.delete(key);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
list(): ReadonlyArray<TDef> {
|
|
49
|
+
return [...this.defs.values()];
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
get(key: K): TDef | undefined {
|
|
53
|
+
return this.defs.get(key);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
has(key: K): boolean {
|
|
57
|
+
return this.defs.has(key);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import type { EmbedderDef, EmbeddingProvider } from '@moxxy/sdk';
|
|
2
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
3
|
+
import { EmbedderRegistry } from './embedders.js';
|
|
4
|
+
|
|
5
|
+
function fakeEmbedder(name: string): EmbeddingProvider {
|
|
6
|
+
return { name, dim: 3, embed: async (t) => t.map(() => [0, 0, 0]) };
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function def(name: string, create: () => EmbeddingProvider): EmbedderDef {
|
|
10
|
+
return { name, createClient: create };
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
describe('EmbedderRegistry', () => {
|
|
14
|
+
it('does not call createClient until the embedder is activated (lazy)', () => {
|
|
15
|
+
const reg = new EmbedderRegistry();
|
|
16
|
+
const create = vi.fn(() => fakeEmbedder('x'));
|
|
17
|
+
reg.register(def('x', create));
|
|
18
|
+
expect(create).not.toHaveBeenCalled();
|
|
19
|
+
expect(reg.has('x')).toBe(true);
|
|
20
|
+
|
|
21
|
+
const inst = reg.setActive('x');
|
|
22
|
+
expect(create).toHaveBeenCalledTimes(1);
|
|
23
|
+
expect(inst.name).toBe('x');
|
|
24
|
+
// Re-activating reuses the cached instance.
|
|
25
|
+
reg.setActive('x');
|
|
26
|
+
expect(create).toHaveBeenCalledTimes(1);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('has no active embedder until one is selected (opt-in)', () => {
|
|
30
|
+
const reg = new EmbedderRegistry();
|
|
31
|
+
reg.register(def('x', () => fakeEmbedder('x')));
|
|
32
|
+
expect(reg.tryGetActive()).toBeNull();
|
|
33
|
+
expect(reg.getActiveName()).toBeNull();
|
|
34
|
+
reg.setActive('x');
|
|
35
|
+
expect(reg.tryGetActive()?.name).toBe('x');
|
|
36
|
+
expect(reg.getActiveName()).toBe('x');
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('throws on duplicate registration and on activating an unknown name', () => {
|
|
40
|
+
const reg = new EmbedderRegistry();
|
|
41
|
+
reg.register(def('x', () => fakeEmbedder('x')));
|
|
42
|
+
expect(() => reg.register(def('x', () => fakeEmbedder('x')))).toThrow(/already registered/);
|
|
43
|
+
expect(() => reg.setActive('nope')).toThrow(/not registered/);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('unregister clears the active slot', () => {
|
|
47
|
+
const reg = new EmbedderRegistry();
|
|
48
|
+
reg.register(def('x', () => fakeEmbedder('x')));
|
|
49
|
+
reg.setActive('x');
|
|
50
|
+
reg.unregister('x');
|
|
51
|
+
expect(reg.has('x')).toBe(false);
|
|
52
|
+
expect(reg.tryGetActive()).toBeNull();
|
|
53
|
+
});
|
|
54
|
+
});
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { EmbedderDef, EmbeddingProvider } from '@moxxy/sdk';
|
|
2
|
+
import { ActiveBackendRegistry } from './active-backend-registry.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Registry of text-embedding backends:
|
|
6
|
+
* - plugins call `register(def)` at load time (via `PluginSpec.embedders`)
|
|
7
|
+
* - the host/CLI calls `setActive(name, config)` once an embedder is chosen
|
|
8
|
+
* - @moxxy/plugin-memory reads `getActive()` / `tryGetActive()` for recall
|
|
9
|
+
*
|
|
10
|
+
* At most one embedder is active at a time, selected explicitly. `createClient`
|
|
11
|
+
* is called lazily on first activation, so a registered-but-unselected embedder
|
|
12
|
+
* (e.g. the heavy transformers one) never instantiates its runtime.
|
|
13
|
+
*/
|
|
14
|
+
export class EmbedderRegistry extends ActiveBackendRegistry<EmbedderDef, EmbeddingProvider> {
|
|
15
|
+
constructor() {
|
|
16
|
+
super({ noun: 'Embedder', build: (def, config) => def.createClient(config) });
|
|
17
|
+
}
|
|
18
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type { Isolator } from '@moxxy/sdk';
|
|
2
|
+
import { describe, expect, it } from 'vitest';
|
|
3
|
+
import { IsolatorRegistry } from './isolators.js';
|
|
4
|
+
|
|
5
|
+
function fakeIsolator(name: string): Isolator {
|
|
6
|
+
return {
|
|
7
|
+
name,
|
|
8
|
+
strength: 'none',
|
|
9
|
+
run: async (_call, bound, _caps) => bound(undefined),
|
|
10
|
+
} as unknown as Isolator;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
describe('IsolatorRegistry (core contribution collection)', () => {
|
|
14
|
+
it('registers, looks up, lists, and unregisters by name', () => {
|
|
15
|
+
const reg = new IsolatorRegistry();
|
|
16
|
+
expect(reg.has('docker')).toBe(false);
|
|
17
|
+
reg.register(fakeIsolator('docker'));
|
|
18
|
+
expect(reg.has('docker')).toBe(true);
|
|
19
|
+
expect(reg.get('docker')?.name).toBe('docker');
|
|
20
|
+
expect(reg.list().map((i) => i.name)).toEqual(['docker']);
|
|
21
|
+
reg.unregister('docker');
|
|
22
|
+
expect(reg.has('docker')).toBe(false);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('overwrites by name (an isolator may arrive via more than one path)', () => {
|
|
26
|
+
const reg = new IsolatorRegistry();
|
|
27
|
+
const first = fakeIsolator('worker');
|
|
28
|
+
const second = fakeIsolator('worker');
|
|
29
|
+
reg.register(first);
|
|
30
|
+
reg.register(second);
|
|
31
|
+
expect(reg.list()).toHaveLength(1);
|
|
32
|
+
expect(reg.get('worker')).toBe(second);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('has no concept of an active isolator (selection stays with the security layer)', () => {
|
|
36
|
+
const reg = new IsolatorRegistry();
|
|
37
|
+
reg.register(fakeIsolator('wasm'));
|
|
38
|
+
// The registry is a plain collection — merely registering never activates
|
|
39
|
+
// anything; the security layer picks one by `security.isolator` config.
|
|
40
|
+
expect('getActive' in reg).toBe(false);
|
|
41
|
+
expect('setActive' in reg).toBe(false);
|
|
42
|
+
});
|
|
43
|
+
});
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { Isolator } from '@moxxy/sdk';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Collection of capability isolators contributed by plugins via
|
|
5
|
+
* `PluginSpec.isolators`. Unlike the single-active registries, this is just the
|
|
6
|
+
* set of AVAILABLE isolators — selection (and ownership of the security
|
|
7
|
+
* boundary) stays with the active security layer (`@moxxy/plugin-security`),
|
|
8
|
+
* which reads these and picks one by `security.isolator` config.
|
|
9
|
+
*
|
|
10
|
+
* A contributed isolator is therefore NEVER auto-activated: registration only
|
|
11
|
+
* makes it available; the user must opt in by name, so a rogue plugin can't
|
|
12
|
+
* silently weaken isolation just by being installed.
|
|
13
|
+
*/
|
|
14
|
+
export class IsolatorRegistry {
|
|
15
|
+
private readonly impls = new Map<string, Isolator>();
|
|
16
|
+
|
|
17
|
+
register(iso: Isolator): void {
|
|
18
|
+
// Overwrite by name: an isolator may arrive via more than one path (a
|
|
19
|
+
// bundled built-in AND a discovered copy). Same name → same role; last wins.
|
|
20
|
+
this.impls.set(iso.name, iso);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
unregister(name: string): void {
|
|
24
|
+
this.impls.delete(name);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
get(name: string): Isolator | undefined {
|
|
28
|
+
return this.impls.get(name);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
has(name: string): boolean {
|
|
32
|
+
return this.impls.has(name);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
list(): ReadonlyArray<Isolator> {
|
|
36
|
+
return [...this.impls.values()];
|
|
37
|
+
}
|
|
38
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { defineMode } from '@moxxy/sdk';
|
|
3
|
+
import { ModeRegistry } from './modes.js';
|
|
4
|
+
|
|
5
|
+
const mode = (name: string) => defineMode({ name, run: async function* () {} });
|
|
6
|
+
|
|
7
|
+
describe('ModeRegistry legacy-name migration', () => {
|
|
8
|
+
it('resolves a legacy mode name to the current one on setActive', () => {
|
|
9
|
+
const reg = new ModeRegistry();
|
|
10
|
+
reg.register(mode('default'));
|
|
11
|
+
reg.register(mode('research'));
|
|
12
|
+
|
|
13
|
+
// Old names persisted in config/preferences/RPCs must not crash.
|
|
14
|
+
reg.setActive('tool-use');
|
|
15
|
+
expect(reg.getActive().name).toBe('default');
|
|
16
|
+
|
|
17
|
+
reg.setActive('deep-research');
|
|
18
|
+
expect(reg.getActive().name).toBe('research');
|
|
19
|
+
|
|
20
|
+
// Removed modes fall back to the default mode rather than throwing.
|
|
21
|
+
reg.setActive('plan-execute');
|
|
22
|
+
expect(reg.getActive().name).toBe('default');
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('still throws on a genuinely unknown mode (not a known legacy name)', () => {
|
|
26
|
+
const reg = new ModeRegistry();
|
|
27
|
+
reg.register(mode('default'));
|
|
28
|
+
expect(() => reg.setActive('totally-made-up')).toThrow(/Mode not registered: totally-made-up/);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it('passes a current name through unchanged', () => {
|
|
32
|
+
const reg = new ModeRegistry();
|
|
33
|
+
reg.register(mode('default'));
|
|
34
|
+
reg.register(mode('goal'));
|
|
35
|
+
reg.setActive('goal');
|
|
36
|
+
expect(reg.getActive().name).toBe('goal');
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('notifies onActiveChange when the ACTIVE mode def is replaced in place', () => {
|
|
40
|
+
// u42-3: a hot-reloaded active mode must tell observers its behaviour
|
|
41
|
+
// swapped, even though the active name is unchanged.
|
|
42
|
+
const reg = new ModeRegistry();
|
|
43
|
+
reg.register(mode('default')); // auto-active
|
|
44
|
+
let fired = 0;
|
|
45
|
+
reg.onActiveChange(() => {
|
|
46
|
+
fired += 1;
|
|
47
|
+
});
|
|
48
|
+
const replacement = mode('default');
|
|
49
|
+
reg.replace(replacement);
|
|
50
|
+
expect(fired).toBe(1);
|
|
51
|
+
// getActive() returns the new def reference.
|
|
52
|
+
expect(reg.getActive()).toBe(replacement);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('does NOT notify onActiveChange when replacing a NON-active mode', () => {
|
|
56
|
+
const reg = new ModeRegistry();
|
|
57
|
+
reg.register(mode('default')); // auto-active
|
|
58
|
+
reg.register(mode('goal'));
|
|
59
|
+
let fired = 0;
|
|
60
|
+
reg.onActiveChange(() => {
|
|
61
|
+
fired += 1;
|
|
62
|
+
});
|
|
63
|
+
reg.replace(mode('goal')); // not the active mode
|
|
64
|
+
expect(fired).toBe(0);
|
|
65
|
+
});
|
|
66
|
+
});
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { migrateModeName, type ModeDef } from '@moxxy/sdk';
|
|
2
|
+
|
|
3
|
+
export class ModeRegistry {
|
|
4
|
+
private readonly modes = new Map<string, ModeDef>();
|
|
5
|
+
private active: string | null = null;
|
|
6
|
+
private readonly changeListeners = new Set<() => void>();
|
|
7
|
+
|
|
8
|
+
/** Observe active-mode changes — used by the runner to broadcast
|
|
9
|
+
* InfoChanged so remote clients track a mode switch (whether it came from
|
|
10
|
+
* a `setMode` RPC or a mode handing off to another mode mid-session). */
|
|
11
|
+
onActiveChange(fn: () => void): () => void {
|
|
12
|
+
this.changeListeners.add(fn);
|
|
13
|
+
return () => this.changeListeners.delete(fn);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Register a mode. Throws on duplicate — use `replace()` for
|
|
18
|
+
* overwrite. Auto-activates on first registration (modes need a default
|
|
19
|
+
* for any session to work).
|
|
20
|
+
*/
|
|
21
|
+
register(mode: ModeDef): void {
|
|
22
|
+
if (this.modes.has(mode.name)) {
|
|
23
|
+
throw new Error(`Mode already registered: ${mode.name}`);
|
|
24
|
+
}
|
|
25
|
+
this.modes.set(mode.name, mode);
|
|
26
|
+
if (!this.active) this.activate(mode);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
replace(mode: ModeDef): void {
|
|
30
|
+
this.modes.set(mode.name, mode);
|
|
31
|
+
if (!this.active) {
|
|
32
|
+
this.activate(mode);
|
|
33
|
+
} else if (this.active === mode.name) {
|
|
34
|
+
// The active mode's def was swapped in place (e.g. a hot-reloaded mode
|
|
35
|
+
// plugin). `getActive()` already returns the new def, but observers of
|
|
36
|
+
// `onActiveChange` (the runner broadcasting InfoChanged) must be told the
|
|
37
|
+
// active behaviour changed under them, or remote clients keep driving the
|
|
38
|
+
// stale def. `activate()` would early-return on the name match, so notify
|
|
39
|
+
// directly.
|
|
40
|
+
for (const fn of this.changeListeners) fn();
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Remove a mode. If it was active, the active slot is cleared —
|
|
46
|
+
* callers must `setActive()` explicitly rather than silently picking
|
|
47
|
+
* some arbitrary "next" mode.
|
|
48
|
+
*/
|
|
49
|
+
unregister(name: string): void {
|
|
50
|
+
this.modes.delete(name);
|
|
51
|
+
if (this.active === name) this.active = null;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
list(): ReadonlyArray<ModeDef> {
|
|
55
|
+
return [...this.modes.values()];
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
setActive(name: string): void {
|
|
59
|
+
// Prefer the literal name; only when it isn't registered fall back to the
|
|
60
|
+
// legacy-name map (e.g. a persisted "tool-use" → "default"). This never
|
|
61
|
+
// overrides a validly-registered name and keeps an old config / preference
|
|
62
|
+
// / setMode RPC value from crashing a session with "Mode not registered".
|
|
63
|
+
const mode = this.modes.get(name) ?? this.modes.get(migrateModeName(name));
|
|
64
|
+
if (!mode) throw new Error(`Mode not registered: ${name}`);
|
|
65
|
+
this.activate(mode);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
getActive(): ModeDef {
|
|
69
|
+
if (!this.active) throw new Error('No active mode registered.');
|
|
70
|
+
const mode = this.modes.get(this.active);
|
|
71
|
+
if (!mode) throw new Error(`Active mode missing: ${this.active}`);
|
|
72
|
+
return mode;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
private activate(mode: ModeDef): void {
|
|
76
|
+
if (this.active === mode.name) return;
|
|
77
|
+
this.active = mode.name;
|
|
78
|
+
for (const fn of this.changeListeners) fn();
|
|
79
|
+
}
|
|
80
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import type { LLMProvider, ProviderDef } from '@moxxy/sdk';
|
|
3
|
+
import { ProviderRegistry } from './providers.js';
|
|
4
|
+
|
|
5
|
+
function def(name: string): ProviderDef {
|
|
6
|
+
return {
|
|
7
|
+
name,
|
|
8
|
+
models: [{ id: `${name}-model`, contextWindow: 1000 }],
|
|
9
|
+
createClient: () => ({ name, models: [] }) as unknown as LLMProvider,
|
|
10
|
+
} as ProviderDef;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
describe('ProviderRegistry enable/disable', () => {
|
|
14
|
+
it('providers are enabled by default; disabling blocks setActive', () => {
|
|
15
|
+
const reg = new ProviderRegistry();
|
|
16
|
+
reg.register(def('a'));
|
|
17
|
+
expect(reg.isEnabled('a')).toBe(true);
|
|
18
|
+
|
|
19
|
+
reg.setEnabled('a', false);
|
|
20
|
+
expect(reg.isEnabled('a')).toBe(false);
|
|
21
|
+
expect(() => reg.setActive('a')).toThrow(/disabled/);
|
|
22
|
+
|
|
23
|
+
reg.setEnabled('a', true);
|
|
24
|
+
expect(() => reg.setActive('a')).not.toThrow();
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('refuses to disable the ACTIVE provider', () => {
|
|
28
|
+
const reg = new ProviderRegistry();
|
|
29
|
+
reg.register(def('a'));
|
|
30
|
+
reg.setActive('a');
|
|
31
|
+
expect(() => reg.setEnabled('a', false)).toThrow(/active provider/i);
|
|
32
|
+
// Still enabled after the refused toggle.
|
|
33
|
+
expect(reg.isEnabled('a')).toBe(true);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('accepts seeding unknown names before their defs register (boot order)', () => {
|
|
37
|
+
const reg = new ProviderRegistry();
|
|
38
|
+
reg.setEnabled('later', false);
|
|
39
|
+
reg.register(def('later'));
|
|
40
|
+
expect(reg.isEnabled('later')).toBe(false);
|
|
41
|
+
expect(() => reg.setActive('later')).toThrow(/disabled/);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('keeps disabled providers listed (visible but inactive)', () => {
|
|
45
|
+
const reg = new ProviderRegistry();
|
|
46
|
+
reg.register(def('a'));
|
|
47
|
+
reg.setEnabled('a', false);
|
|
48
|
+
expect(reg.list().map((d) => d.name)).toContain('a');
|
|
49
|
+
});
|
|
50
|
+
});
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import type { LLMProvider, ProviderDef } from '@moxxy/sdk';
|
|
2
|
+
|
|
3
|
+
export class ProviderRegistry {
|
|
4
|
+
private readonly defs = new Map<string, ProviderDef>();
|
|
5
|
+
private readonly instances = new Map<string, LLMProvider>();
|
|
6
|
+
private active: string | null = null;
|
|
7
|
+
/**
|
|
8
|
+
* Names the user disabled. Kept name-based (not def-based) so it can be
|
|
9
|
+
* seeded from preferences BEFORE the plugins register their defs — boot
|
|
10
|
+
* order doesn't matter. A disabled provider stays registered/listable but
|
|
11
|
+
* can't be activated.
|
|
12
|
+
*/
|
|
13
|
+
private readonly disabled = new Set<string>();
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Register a provider def. Throws on duplicate — use `replace()` for
|
|
17
|
+
* explicit overwrite. Matches the semantics of `tools` and `channels`.
|
|
18
|
+
*/
|
|
19
|
+
register(def: ProviderDef, instance?: LLMProvider): void {
|
|
20
|
+
if (this.defs.has(def.name)) {
|
|
21
|
+
throw new Error(`Provider already registered: ${def.name}`);
|
|
22
|
+
}
|
|
23
|
+
this.defs.set(def.name, def);
|
|
24
|
+
if (instance) this.instances.set(def.name, instance);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Overwrite an existing def (also drops the cached instance so the new
|
|
29
|
+
* createClient gets called).
|
|
30
|
+
*
|
|
31
|
+
* Invariant: replacing the ACTIVE provider's def leaves `active` pointing at
|
|
32
|
+
* it but with no cached instance, so `getActive()` throws until the caller
|
|
33
|
+
* rebuilds the instance. Callers replacing the active provider MUST follow
|
|
34
|
+
* with `setActive(name, config)` (replace can't rebuild itself — it has no
|
|
35
|
+
* config). The sole production caller does exactly that.
|
|
36
|
+
*/
|
|
37
|
+
replace(def: ProviderDef, instance?: LLMProvider): void {
|
|
38
|
+
this.defs.set(def.name, def);
|
|
39
|
+
this.instances.delete(def.name);
|
|
40
|
+
if (instance) this.instances.set(def.name, instance);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
unregister(name: string): void {
|
|
44
|
+
this.defs.delete(name);
|
|
45
|
+
this.instances.delete(name);
|
|
46
|
+
if (this.active === name) this.active = null;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
list(): ReadonlyArray<ProviderDef> {
|
|
50
|
+
return [...this.defs.values()];
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Enable/disable a provider by name. Disabling the ACTIVE provider is
|
|
55
|
+
* refused — switch first, then disable — so a session never ends up with an
|
|
56
|
+
* active-but-disabled provider. Unknown names are accepted (the set seeds
|
|
57
|
+
* from preferences before plugins register), except when disabling via a
|
|
58
|
+
* live toggle is meaningless because the provider is active.
|
|
59
|
+
*/
|
|
60
|
+
setEnabled(name: string, enabled: boolean): void {
|
|
61
|
+
if (!enabled && this.active === name) {
|
|
62
|
+
throw new Error(`Cannot disable the active provider "${name}" — switch providers first.`);
|
|
63
|
+
}
|
|
64
|
+
if (enabled) this.disabled.delete(name);
|
|
65
|
+
else this.disabled.add(name);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
isEnabled(name: string): boolean {
|
|
69
|
+
return !this.disabled.has(name);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
setActive(name: string, config?: Record<string, unknown>): LLMProvider {
|
|
73
|
+
const def = this.defs.get(name);
|
|
74
|
+
if (!def) throw new Error(`Provider not registered: ${name}`);
|
|
75
|
+
if (this.disabled.has(name)) {
|
|
76
|
+
throw new Error(`Provider "${name}" is disabled — enable it first.`);
|
|
77
|
+
}
|
|
78
|
+
let instance = this.instances.get(name);
|
|
79
|
+
if (!instance) {
|
|
80
|
+
instance = def.createClient(config ?? {});
|
|
81
|
+
this.instances.set(name, instance);
|
|
82
|
+
}
|
|
83
|
+
this.active = name;
|
|
84
|
+
return instance;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
getActive(): LLMProvider {
|
|
88
|
+
if (!this.active) throw new Error('No active provider. Call setActive(name) first.');
|
|
89
|
+
const inst = this.instances.get(this.active);
|
|
90
|
+
if (!inst) throw new Error(`Active provider has no instance: ${this.active}`);
|
|
91
|
+
return inst;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
getActiveName(): string | null {
|
|
95
|
+
return this.active;
|
|
96
|
+
}
|
|
97
|
+
}
|