@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.
Files changed (332) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +125 -0
  3. package/dist/events/factory.d.ts +6 -0
  4. package/dist/events/factory.d.ts.map +1 -0
  5. package/dist/events/factory.js +14 -0
  6. package/dist/events/factory.js.map +1 -0
  7. package/dist/events/log.d.ts +92 -0
  8. package/dist/events/log.d.ts.map +1 -0
  9. package/dist/events/log.js +244 -0
  10. package/dist/events/log.js.map +1 -0
  11. package/dist/index.d.ts +40 -0
  12. package/dist/index.d.ts.map +1 -0
  13. package/dist/index.js +41 -0
  14. package/dist/index.js.map +1 -0
  15. package/dist/logger.d.ts +16 -0
  16. package/dist/logger.d.ts.map +1 -0
  17. package/dist/logger.js +27 -0
  18. package/dist/logger.js.map +1 -0
  19. package/dist/permissions/engine.d.ts +94 -0
  20. package/dist/permissions/engine.d.ts.map +1 -0
  21. package/dist/permissions/engine.js +203 -0
  22. package/dist/permissions/engine.js.map +1 -0
  23. package/dist/permissions/resolvers.d.ts +8 -0
  24. package/dist/permissions/resolvers.d.ts.map +1 -0
  25. package/dist/permissions/resolvers.js +8 -0
  26. package/dist/permissions/resolvers.js.map +1 -0
  27. package/dist/plugins/discovery.d.ts +9 -0
  28. package/dist/plugins/discovery.d.ts.map +1 -0
  29. package/dist/plugins/discovery.js +112 -0
  30. package/dist/plugins/discovery.js.map +1 -0
  31. package/dist/plugins/host-options.d.ts +72 -0
  32. package/dist/plugins/host-options.d.ts.map +1 -0
  33. package/dist/plugins/host-options.js +2 -0
  34. package/dist/plugins/host-options.js.map +1 -0
  35. package/dist/plugins/host.d.ts +53 -0
  36. package/dist/plugins/host.d.ts.map +1 -0
  37. package/dist/plugins/host.js +269 -0
  38. package/dist/plugins/host.js.map +1 -0
  39. package/dist/plugins/lifecycle.d.ts +31 -0
  40. package/dist/plugins/lifecycle.d.ts.map +1 -0
  41. package/dist/plugins/lifecycle.js +100 -0
  42. package/dist/plugins/lifecycle.js.map +1 -0
  43. package/dist/plugins/loader.d.ts +7 -0
  44. package/dist/plugins/loader.d.ts.map +1 -0
  45. package/dist/plugins/loader.js +68 -0
  46. package/dist/plugins/loader.js.map +1 -0
  47. package/dist/plugins/package-requirements.d.ts +16 -0
  48. package/dist/plugins/package-requirements.d.ts.map +1 -0
  49. package/dist/plugins/package-requirements.js +52 -0
  50. package/dist/plugins/package-requirements.js.map +1 -0
  51. package/dist/plugins/registry-kinds.d.ts +58 -0
  52. package/dist/plugins/registry-kinds.d.ts.map +1 -0
  53. package/dist/plugins/registry-kinds.js +139 -0
  54. package/dist/plugins/registry-kinds.js.map +1 -0
  55. package/dist/plugins/toposort.d.ts +24 -0
  56. package/dist/plugins/toposort.d.ts.map +1 -0
  57. package/dist/plugins/toposort.js +67 -0
  58. package/dist/plugins/toposort.js.map +1 -0
  59. package/dist/preferences.d.ts +40 -0
  60. package/dist/preferences.d.ts.map +1 -0
  61. package/dist/preferences.js +58 -0
  62. package/dist/preferences.js.map +1 -0
  63. package/dist/registries/active-backend-registry.d.ts +65 -0
  64. package/dist/registries/active-backend-registry.d.ts.map +1 -0
  65. package/dist/registries/active-backend-registry.js +108 -0
  66. package/dist/registries/active-backend-registry.js.map +1 -0
  67. package/dist/registries/active-def-registry.d.ts +59 -0
  68. package/dist/registries/active-def-registry.d.ts.map +1 -0
  69. package/dist/registries/active-def-registry.js +63 -0
  70. package/dist/registries/active-def-registry.js.map +1 -0
  71. package/dist/registries/agents.d.ts +16 -0
  72. package/dist/registries/agents.d.ts.map +1 -0
  73. package/dist/registries/agents.js +17 -0
  74. package/dist/registries/agents.js.map +1 -0
  75. package/dist/registries/cache-strategies.d.ts +12 -0
  76. package/dist/registries/cache-strategies.d.ts.map +1 -0
  77. package/dist/registries/cache-strategies.js +13 -0
  78. package/dist/registries/cache-strategies.js.map +1 -0
  79. package/dist/registries/channels.d.ts +16 -0
  80. package/dist/registries/channels.d.ts.map +1 -0
  81. package/dist/registries/channels.js +35 -0
  82. package/dist/registries/channels.js.map +1 -0
  83. package/dist/registries/commands.d.ts +29 -0
  84. package/dist/registries/commands.d.ts.map +1 -0
  85. package/dist/registries/commands.js +72 -0
  86. package/dist/registries/commands.js.map +1 -0
  87. package/dist/registries/compactors.d.ts +12 -0
  88. package/dist/registries/compactors.d.ts.map +1 -0
  89. package/dist/registries/compactors.js +13 -0
  90. package/dist/registries/compactors.js.map +1 -0
  91. package/dist/registries/def-map-registry.d.ts +33 -0
  92. package/dist/registries/def-map-registry.d.ts.map +1 -0
  93. package/dist/registries/def-map-registry.js +36 -0
  94. package/dist/registries/def-map-registry.js.map +1 -0
  95. package/dist/registries/embedders.d.ts +16 -0
  96. package/dist/registries/embedders.d.ts.map +1 -0
  97. package/dist/registries/embedders.js +17 -0
  98. package/dist/registries/embedders.js.map +1 -0
  99. package/dist/registries/isolators.d.ts +21 -0
  100. package/dist/registries/isolators.d.ts.map +1 -0
  101. package/dist/registries/isolators.js +32 -0
  102. package/dist/registries/isolators.js.map +1 -0
  103. package/dist/registries/modes.d.ts +28 -0
  104. package/dist/registries/modes.d.ts.map +1 -0
  105. package/dist/registries/modes.js +81 -0
  106. package/dist/registries/modes.js.map +1 -0
  107. package/dist/registries/providers.d.ts +44 -0
  108. package/dist/registries/providers.d.ts.map +1 -0
  109. package/dist/registries/providers.js +95 -0
  110. package/dist/registries/providers.js.map +1 -0
  111. package/dist/registries/skills.d.ts +24 -0
  112. package/dist/registries/skills.d.ts.map +1 -0
  113. package/dist/registries/skills.js +81 -0
  114. package/dist/registries/skills.js.map +1 -0
  115. package/dist/registries/surfaces.d.ts +13 -0
  116. package/dist/registries/surfaces.d.ts.map +1 -0
  117. package/dist/registries/surfaces.js +14 -0
  118. package/dist/registries/surfaces.js.map +1 -0
  119. package/dist/registries/synthesizers.d.ts +26 -0
  120. package/dist/registries/synthesizers.d.ts.map +1 -0
  121. package/dist/registries/synthesizers.js +34 -0
  122. package/dist/registries/synthesizers.js.map +1 -0
  123. package/dist/registries/tools.d.ts +49 -0
  124. package/dist/registries/tools.d.ts.map +1 -0
  125. package/dist/registries/tools.js +98 -0
  126. package/dist/registries/tools.js.map +1 -0
  127. package/dist/registries/transcribers.d.ts +17 -0
  128. package/dist/registries/transcribers.d.ts.map +1 -0
  129. package/dist/registries/transcribers.js +18 -0
  130. package/dist/registries/transcribers.js.map +1 -0
  131. package/dist/registries/tunnel-providers.d.ts +12 -0
  132. package/dist/registries/tunnel-providers.d.ts.map +1 -0
  133. package/dist/registries/tunnel-providers.js +13 -0
  134. package/dist/registries/tunnel-providers.js.map +1 -0
  135. package/dist/registries/view-renderers.d.ts +13 -0
  136. package/dist/registries/view-renderers.d.ts.map +1 -0
  137. package/dist/registries/view-renderers.js +14 -0
  138. package/dist/registries/view-renderers.js.map +1 -0
  139. package/dist/registries/workflow-executors.d.ts +13 -0
  140. package/dist/registries/workflow-executors.d.ts.map +1 -0
  141. package/dist/registries/workflow-executors.js +14 -0
  142. package/dist/registries/workflow-executors.js.map +1 -0
  143. package/dist/requirements.d.ts +59 -0
  144. package/dist/requirements.d.ts.map +1 -0
  145. package/dist/requirements.js +149 -0
  146. package/dist/requirements.js.map +1 -0
  147. package/dist/run-turn.d.ts +6 -0
  148. package/dist/run-turn.d.ts.map +1 -0
  149. package/dist/run-turn.js +141 -0
  150. package/dist/run-turn.js.map +1 -0
  151. package/dist/session-runtime.d.ts +67 -0
  152. package/dist/session-runtime.d.ts.map +1 -0
  153. package/dist/session-runtime.js +2 -0
  154. package/dist/session-runtime.js.map +1 -0
  155. package/dist/session.d.ts +203 -0
  156. package/dist/session.d.ts.map +1 -0
  157. package/dist/session.js +399 -0
  158. package/dist/session.js.map +1 -0
  159. package/dist/sessions/persistence.d.ts +184 -0
  160. package/dist/sessions/persistence.d.ts.map +1 -0
  161. package/dist/sessions/persistence.js +442 -0
  162. package/dist/sessions/persistence.js.map +1 -0
  163. package/dist/setup-agent.d.ts +91 -0
  164. package/dist/setup-agent.d.ts.map +1 -0
  165. package/dist/setup-agent.js +138 -0
  166. package/dist/setup-agent.js.map +1 -0
  167. package/dist/skills/index.d.ts +4 -0
  168. package/dist/skills/index.d.ts.map +1 -0
  169. package/dist/skills/index.js +4 -0
  170. package/dist/skills/index.js.map +1 -0
  171. package/dist/skills/loader.d.ts +16 -0
  172. package/dist/skills/loader.d.ts.map +1 -0
  173. package/dist/skills/loader.js +99 -0
  174. package/dist/skills/loader.js.map +1 -0
  175. package/dist/skills/parse.d.ts +5 -0
  176. package/dist/skills/parse.d.ts.map +1 -0
  177. package/dist/skills/parse.js +8 -0
  178. package/dist/skills/parse.js.map +1 -0
  179. package/dist/skills/synthesize-draft.d.ts +8 -0
  180. package/dist/skills/synthesize-draft.d.ts.map +1 -0
  181. package/dist/skills/synthesize-draft.js +32 -0
  182. package/dist/skills/synthesize-draft.js.map +1 -0
  183. package/dist/skills/synthesize.d.ts +33 -0
  184. package/dist/skills/synthesize.d.ts.map +1 -0
  185. package/dist/skills/synthesize.js +241 -0
  186. package/dist/skills/synthesize.js.map +1 -0
  187. package/dist/subagents/events.d.ts +20 -0
  188. package/dist/subagents/events.d.ts.map +1 -0
  189. package/dist/subagents/events.js +146 -0
  190. package/dist/subagents/events.js.map +1 -0
  191. package/dist/subagents/registry.d.ts +40 -0
  192. package/dist/subagents/registry.d.ts.map +1 -0
  193. package/dist/subagents/registry.js +42 -0
  194. package/dist/subagents/registry.js.map +1 -0
  195. package/dist/subagents/run-child.d.ts +43 -0
  196. package/dist/subagents/run-child.d.ts.map +1 -0
  197. package/dist/subagents/run-child.js +276 -0
  198. package/dist/subagents/run-child.js.map +1 -0
  199. package/dist/subagents/spawn.d.ts +8 -0
  200. package/dist/subagents/spawn.d.ts.map +1 -0
  201. package/dist/subagents/spawn.js +8 -0
  202. package/dist/subagents/spawn.js.map +1 -0
  203. package/dist/subagents/tools.d.ts +4 -0
  204. package/dist/subagents/tools.d.ts.map +1 -0
  205. package/dist/subagents/tools.js +13 -0
  206. package/dist/subagents/tools.js.map +1 -0
  207. package/dist/subagents.d.ts +3 -0
  208. package/dist/subagents.d.ts.map +1 -0
  209. package/dist/subagents.js +3 -0
  210. package/dist/subagents.js.map +1 -0
  211. package/dist/surfaces/host.d.ts +53 -0
  212. package/dist/surfaces/host.d.ts.map +1 -0
  213. package/dist/surfaces/host.js +177 -0
  214. package/dist/surfaces/host.js.map +1 -0
  215. package/dist/tunnel/localhost.d.ts +7 -0
  216. package/dist/tunnel/localhost.d.ts.map +1 -0
  217. package/dist/tunnel/localhost.js +21 -0
  218. package/dist/tunnel/localhost.js.map +1 -0
  219. package/dist/usage-stats.d.ts +42 -0
  220. package/dist/usage-stats.d.ts.map +1 -0
  221. package/dist/usage-stats.js +83 -0
  222. package/dist/usage-stats.js.map +1 -0
  223. package/dist/view/default-renderer.d.ts +7 -0
  224. package/dist/view/default-renderer.d.ts.map +1 -0
  225. package/dist/view/default-renderer.js +14 -0
  226. package/dist/view/default-renderer.js.map +1 -0
  227. package/dist/view/parse.d.ts +7 -0
  228. package/dist/view/parse.d.ts.map +1 -0
  229. package/dist/view/parse.js +479 -0
  230. package/dist/view/parse.js.map +1 -0
  231. package/package.json +61 -0
  232. package/src/events/factory.ts +27 -0
  233. package/src/events/log.test.ts +470 -0
  234. package/src/events/log.ts +256 -0
  235. package/src/index.ts +102 -0
  236. package/src/logger.ts +46 -0
  237. package/src/permissions/engine.test.ts +206 -0
  238. package/src/permissions/engine.ts +247 -0
  239. package/src/permissions/resolvers.test.ts +40 -0
  240. package/src/permissions/resolvers.ts +17 -0
  241. package/src/plugins/discovery.test.ts +115 -0
  242. package/src/plugins/discovery.ts +124 -0
  243. package/src/plugins/host-options.ts +73 -0
  244. package/src/plugins/host-view-tunnel.test.ts +86 -0
  245. package/src/plugins/host.test.ts +434 -0
  246. package/src/plugins/host.ts +327 -0
  247. package/src/plugins/lifecycle.test.ts +187 -0
  248. package/src/plugins/lifecycle.ts +136 -0
  249. package/src/plugins/loader.ts +79 -0
  250. package/src/plugins/package-requirements.ts +50 -0
  251. package/src/plugins/registry-kinds.test.ts +162 -0
  252. package/src/plugins/registry-kinds.ts +210 -0
  253. package/src/plugins/toposort.test.ts +66 -0
  254. package/src/plugins/toposort.ts +69 -0
  255. package/src/preferences.test.ts +105 -0
  256. package/src/preferences.ts +84 -0
  257. package/src/registries/active-backend-registry.ts +142 -0
  258. package/src/registries/active-def-registry.test.ts +103 -0
  259. package/src/registries/active-def-registry.ts +97 -0
  260. package/src/registries/agents.ts +18 -0
  261. package/src/registries/cache-strategies.ts +14 -0
  262. package/src/registries/channels.ts +42 -0
  263. package/src/registries/commands.test.ts +71 -0
  264. package/src/registries/commands.ts +78 -0
  265. package/src/registries/compactors.ts +14 -0
  266. package/src/registries/def-map-registry.test.ts +67 -0
  267. package/src/registries/def-map-registry.ts +59 -0
  268. package/src/registries/embedders.test.ts +54 -0
  269. package/src/registries/embedders.ts +18 -0
  270. package/src/registries/isolators.test.ts +43 -0
  271. package/src/registries/isolators.ts +38 -0
  272. package/src/registries/modes.test.ts +66 -0
  273. package/src/registries/modes.ts +80 -0
  274. package/src/registries/providers.test.ts +50 -0
  275. package/src/registries/providers.ts +97 -0
  276. package/src/registries/semantics.test.ts +117 -0
  277. package/src/registries/skills.ts +87 -0
  278. package/src/registries/surfaces.ts +18 -0
  279. package/src/registries/synthesizers.test.ts +63 -0
  280. package/src/registries/synthesizers.ts +36 -0
  281. package/src/registries/tools.test.ts +142 -0
  282. package/src/registries/tools.ts +145 -0
  283. package/src/registries/transcribers.test.ts +70 -0
  284. package/src/registries/transcribers.ts +19 -0
  285. package/src/registries/tunnel-providers.test.ts +30 -0
  286. package/src/registries/tunnel-providers.ts +14 -0
  287. package/src/registries/view-renderers.test.ts +68 -0
  288. package/src/registries/view-renderers.ts +15 -0
  289. package/src/registries/workflow-executors.test.ts +53 -0
  290. package/src/registries/workflow-executors.ts +15 -0
  291. package/src/requirements.targetinfo.test.ts +70 -0
  292. package/src/requirements.test.ts +111 -0
  293. package/src/requirements.ts +262 -0
  294. package/src/run-turn.test.ts +178 -0
  295. package/src/run-turn.ts +156 -0
  296. package/src/session-runtime.ts +75 -0
  297. package/src/session.test.ts +216 -0
  298. package/src/session.ts +491 -0
  299. package/src/sessions/persistence.flush.test.ts +100 -0
  300. package/src/sessions/persistence.ready.test.ts +91 -0
  301. package/src/sessions/persistence.test.ts +274 -0
  302. package/src/sessions/persistence.ts +498 -0
  303. package/src/setup-agent.test.ts +72 -0
  304. package/src/setup-agent.ts +189 -0
  305. package/src/skills/index.ts +14 -0
  306. package/src/skills/loader.test.ts +102 -0
  307. package/src/skills/loader.ts +114 -0
  308. package/src/skills/parse.test.ts +59 -0
  309. package/src/skills/parse.ts +11 -0
  310. package/src/skills/synthesize-draft.test.ts +79 -0
  311. package/src/skills/synthesize-draft.ts +46 -0
  312. package/src/skills/synthesize.test.ts +170 -0
  313. package/src/skills/synthesize.ts +304 -0
  314. package/src/subagents/events.test.ts +56 -0
  315. package/src/subagents/events.ts +194 -0
  316. package/src/subagents/registry.ts +62 -0
  317. package/src/subagents/run-child.test.ts +185 -0
  318. package/src/subagents/run-child.ts +425 -0
  319. package/src/subagents/spawn.ts +12 -0
  320. package/src/subagents/tools.ts +19 -0
  321. package/src/subagents.ts +2 -0
  322. package/src/surfaces/host.test.ts +330 -0
  323. package/src/surfaces/host.ts +192 -0
  324. package/src/tunnel/localhost.test.ts +22 -0
  325. package/src/tunnel/localhost.ts +21 -0
  326. package/src/usage-stats.test.ts +77 -0
  327. package/src/usage-stats.ts +119 -0
  328. package/src/view/default-renderer.test.ts +29 -0
  329. package/src/view/default-renderer.ts +14 -0
  330. package/src/view/parse-extended.test.ts +255 -0
  331. package/src/view/parse.test.ts +232 -0
  332. 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
+ }