@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,19 @@
1
+ import type { Transcriber, TranscriberDef } from '@moxxy/sdk';
2
+ import { ActiveBackendRegistry } from './active-backend-registry.js';
3
+
4
+ /**
5
+ * Registry of speech-to-text backends:
6
+ * - plugins call `register(def)` at load time
7
+ * - the host/CLI calls `setActive(name, config)` once a backend is chosen
8
+ * - channels with audio input read `getActive()` to transcribe bytes
9
+ *
10
+ * At most one transcriber is *active* at a time, selected explicitly (no
11
+ * auto-adopt). `getActive()` throws when none is active, so call sites can
12
+ * degrade gracefully (e.g. Telegram falls back to "you sent a voice note but no
13
+ * transcriber is configured").
14
+ */
15
+ export class TranscriberRegistry extends ActiveBackendRegistry<TranscriberDef, Transcriber> {
16
+ constructor() {
17
+ super({ noun: 'Transcriber', build: (def, config) => def.createClient(config) });
18
+ }
19
+ }
@@ -0,0 +1,30 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import type { TunnelProviderDef } from '@moxxy/sdk';
3
+ import { TunnelProviderRegistry } from './tunnel-providers.js';
4
+
5
+ const mk = (name: string): TunnelProviderDef => ({
6
+ name,
7
+ open: () => Promise.resolve({ url: `http://${name}`, close: () => Promise.resolve() }),
8
+ });
9
+
10
+ describe('TunnelProviderRegistry', () => {
11
+ it('auto-activates first; duplicate throws', () => {
12
+ const r = new TunnelProviderRegistry();
13
+ expect(r.getActive()).toBeNull();
14
+ r.register(mk('localhost'));
15
+ expect(r.getActive()?.name).toBe('localhost');
16
+ expect(() => r.register(mk('localhost'))).toThrow(/already registered/);
17
+ });
18
+
19
+ it('replace + setActive + unregister-clears-active behave like the other registries', () => {
20
+ const r = new TunnelProviderRegistry();
21
+ r.register(mk('localhost'));
22
+ r.replace(mk('cloudflared'));
23
+ r.setActive('cloudflared');
24
+ expect(r.getActive()?.name).toBe('cloudflared');
25
+ expect(() => r.setActive('ngrok')).toThrow(/not registered/);
26
+ r.unregister('cloudflared');
27
+ expect(r.getActive()).toBeNull();
28
+ expect(r.list().map((p) => p.name)).toEqual(['localhost']);
29
+ });
30
+ });
@@ -0,0 +1,14 @@
1
+ import type { TunnelProviderDef } from '@moxxy/sdk';
2
+ import { ActiveDefRegistry } from './active-def-registry.js';
3
+
4
+ /**
5
+ * One active tunnel provider per session. Register throws on duplicate,
6
+ * auto-activates the first, and `unregister` clears the active slot. Core
7
+ * seeds the `localhost` provider so `getActive()` is non-null even when no
8
+ * tunnel plugin is installed. See {@link ActiveDefRegistry}.
9
+ */
10
+ export class TunnelProviderRegistry extends ActiveDefRegistry<TunnelProviderDef> {
11
+ constructor() {
12
+ super({ noun: 'Tunnel provider' });
13
+ }
14
+ }
@@ -0,0 +1,68 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import type { ViewRendererDef } from '@moxxy/sdk';
3
+ import { ViewRendererRegistry } from './view-renderers.js';
4
+
5
+ const mk = (name: string): ViewRendererDef => ({
6
+ name,
7
+ allowList: [],
8
+ parse: () => ({ ok: false, errors: [{ message: name }] }),
9
+ validate: () => [],
10
+ });
11
+
12
+ describe('ViewRendererRegistry', () => {
13
+ it('auto-activates the first registered renderer', () => {
14
+ const r = new ViewRendererRegistry();
15
+ expect(r.getActive()).toBeNull();
16
+ r.register(mk('a'));
17
+ expect(r.getActive()?.name).toBe('a');
18
+ r.register(mk('b'));
19
+ expect(r.getActive()?.name).toBe('a'); // still the first
20
+ });
21
+
22
+ it('throws on duplicate register', () => {
23
+ const r = new ViewRendererRegistry();
24
+ r.register(mk('a'));
25
+ expect(() => r.register(mk('a'))).toThrow(/already registered/);
26
+ });
27
+
28
+ it('replace overwrites without throwing and keeps active', () => {
29
+ const r = new ViewRendererRegistry();
30
+ r.register(mk('a'));
31
+ const replacement = mk('a');
32
+ r.replace(replacement);
33
+ expect(r.getActive()).toBe(replacement);
34
+ expect(r.list()).toHaveLength(1);
35
+ });
36
+
37
+ it('replace on empty registry auto-activates', () => {
38
+ const r = new ViewRendererRegistry();
39
+ r.replace(mk('a'));
40
+ expect(r.getActive()?.name).toBe('a');
41
+ });
42
+
43
+ it('setActive switches; throws for unknown', () => {
44
+ const r = new ViewRendererRegistry();
45
+ r.register(mk('a'));
46
+ r.register(mk('b'));
47
+ r.setActive('b');
48
+ expect(r.getActive()?.name).toBe('b');
49
+ expect(() => r.setActive('nope')).toThrow(/not registered/);
50
+ });
51
+
52
+ it('unregister clears the active slot rather than picking a successor', () => {
53
+ const r = new ViewRendererRegistry();
54
+ r.register(mk('a'));
55
+ r.register(mk('b'));
56
+ r.unregister('a');
57
+ expect(r.getActive()).toBeNull();
58
+ expect(r.list().map((x) => x.name)).toEqual(['b']);
59
+ });
60
+
61
+ it('unregistering a non-active renderer leaves active intact', () => {
62
+ const r = new ViewRendererRegistry();
63
+ r.register(mk('a'));
64
+ r.register(mk('b'));
65
+ r.unregister('b');
66
+ expect(r.getActive()?.name).toBe('a');
67
+ });
68
+ });
@@ -0,0 +1,15 @@
1
+ import type { ViewRendererDef } from '@moxxy/sdk';
2
+ import { ActiveDefRegistry } from './active-def-registry.js';
3
+
4
+ /**
5
+ * One active view-spec renderer per session. Register throws on duplicate,
6
+ * auto-activates the first, and `unregister` clears the active slot rather
7
+ * than picking an arbitrary successor. Core seeds a default renderer so
8
+ * `getActive()` is non-null even with no plugins. See
9
+ * {@link ActiveDefRegistry}.
10
+ */
11
+ export class ViewRendererRegistry extends ActiveDefRegistry<ViewRendererDef> {
12
+ constructor() {
13
+ super({ noun: 'View renderer' });
14
+ }
15
+ }
@@ -0,0 +1,53 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import type { WorkflowExecutorDef } from '@moxxy/sdk';
3
+ import { WorkflowExecutorRegistry } from './workflow-executors.js';
4
+
5
+ const mk = (name: string): WorkflowExecutorDef => ({
6
+ name,
7
+ run: async () => ({}) as never,
8
+ });
9
+
10
+ describe('WorkflowExecutorRegistry', () => {
11
+ it('auto-activates the first registered executor', () => {
12
+ const r = new WorkflowExecutorRegistry();
13
+ expect(r.getActive()).toBeNull();
14
+ r.register(mk('dag'));
15
+ expect(r.getActive()?.name).toBe('dag');
16
+ r.register(mk('linear'));
17
+ expect(r.getActive()?.name).toBe('dag'); // still the first
18
+ });
19
+
20
+ it('throws on duplicate register', () => {
21
+ const r = new WorkflowExecutorRegistry();
22
+ r.register(mk('dag'));
23
+ expect(() => r.register(mk('dag'))).toThrow(/already registered/);
24
+ });
25
+
26
+ it('replace overwrites without throwing and keeps active', () => {
27
+ const r = new WorkflowExecutorRegistry();
28
+ r.register(mk('dag'));
29
+ const replacement = mk('dag');
30
+ r.replace(replacement);
31
+ expect(r.getActive()).toBe(replacement);
32
+ expect(r.list()).toHaveLength(1);
33
+ });
34
+
35
+ it('setActive switches; throws for unknown', () => {
36
+ const r = new WorkflowExecutorRegistry();
37
+ r.register(mk('dag'));
38
+ r.register(mk('linear'));
39
+ r.setActive('linear');
40
+ expect(r.getActive()?.name).toBe('linear');
41
+ expect(() => r.setActive('nope')).toThrow(/not registered/);
42
+ });
43
+
44
+ it('unregister clears the active slot rather than picking a successor', () => {
45
+ const r = new WorkflowExecutorRegistry();
46
+ r.register(mk('dag'));
47
+ r.register(mk('linear'));
48
+ r.unregister('dag');
49
+ // getActive() returning null wrongly would silently disable workflows.
50
+ expect(r.getActive()).toBeNull();
51
+ expect(r.list().map((x) => x.name)).toEqual(['linear']);
52
+ });
53
+ });
@@ -0,0 +1,15 @@
1
+ import type { WorkflowExecutorDef } from '@moxxy/sdk';
2
+ import { ActiveDefRegistry } from './active-def-registry.js';
3
+
4
+ /**
5
+ * Registry of swappable workflow-execution strategies. Register throws on
6
+ * duplicate, auto-activates the first registration (so a session always has
7
+ * an executor once `@moxxy/plugin-workflows` loads its default `dag`), and
8
+ * `unregister` clears the active slot rather than silently picking a
9
+ * successor. See {@link ActiveDefRegistry}.
10
+ */
11
+ export class WorkflowExecutorRegistry extends ActiveDefRegistry<WorkflowExecutorDef> {
12
+ constructor() {
13
+ super({ noun: 'Workflow executor' });
14
+ }
15
+ }
@@ -0,0 +1,70 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { requirementSchema, type RequirementKind } from '@moxxy/sdk';
3
+ import { RequirementRegistry } from './requirements.js';
4
+ import { ToolRegistryImpl } from './registries/tools.js';
5
+ import { ProviderRegistry } from './registries/providers.js';
6
+ import { ModeRegistry } from './registries/modes.js';
7
+ import { CompactorRegistry } from './registries/compactors.js';
8
+ import { ChannelRegistryImpl } from './registries/channels.js';
9
+ import { AgentRegistry } from './registries/agents.js';
10
+ import { CommandRegistry } from './registries/commands.js';
11
+ import { TranscriberRegistry } from './registries/transcribers.js';
12
+ import { SynthesizerRegistry } from './registries/synthesizers.js';
13
+ import { silentLogger } from './logger.js';
14
+
15
+ const makeRequirements = () =>
16
+ new RequirementRegistry({
17
+ tools: new ToolRegistryImpl({ logger: silentLogger, cwd: '/tmp' }),
18
+ providers: new ProviderRegistry(),
19
+ modes: new ModeRegistry(),
20
+ compactors: new CompactorRegistry(),
21
+ channels: new ChannelRegistryImpl(),
22
+ agents: new AgentRegistry(),
23
+ commands: new CommandRegistry(),
24
+ transcribers: new TranscriberRegistry(),
25
+ synthesizers: new SynthesizerRegistry(),
26
+ });
27
+
28
+ // Drive the exhaustiveness sweep off the SDK's zod enum rather than a hand-kept
29
+ // list, so a kind added to the union (and its schema) is automatically exercised
30
+ // here. `targetInfo` is a table (TARGET_DESCRIPTORS) plus two special cases
31
+ // (plugin/runtime); the `Record<Exclude<RequirementKind,'plugin'|'runtime'>,_>`
32
+ // type already makes a missing table entry a compile error — this loop keeps the
33
+ // runtime smoke honest alongside that guard.
34
+ const ALL_KINDS: ReadonlyArray<RequirementKind> = requirementSchema.shape.kind.options;
35
+
36
+ describe('RequirementRegistry.targetInfo exhaustiveness', () => {
37
+ it('exercises every RequirementKind in the zod enum', () => {
38
+ // Sanity: the enum still spells out the full known set (catches an enum that
39
+ // accidentally shrank). Order-independent.
40
+ expect([...ALL_KINDS].sort()).toEqual(
41
+ [
42
+ 'agent',
43
+ 'channel',
44
+ 'command',
45
+ 'compactor',
46
+ 'mode',
47
+ 'plugin',
48
+ 'provider',
49
+ 'runtime',
50
+ 'synthesizer',
51
+ 'tool',
52
+ 'transcriber',
53
+ ].sort(),
54
+ );
55
+ });
56
+
57
+ it('resolves a check (never throws) for every RequirementKind', () => {
58
+ const requirements = makeRequirements();
59
+ for (const kind of ALL_KINDS) {
60
+ // Routed through the public `check` API since targetInfo is private.
61
+ // With nothing registered every kind reports a defined, blocking issue
62
+ // (missing / not_ready) rather than hitting the assertNever default — i.e.
63
+ // every kind has a table entry or special case backing it.
64
+ const check = requirements.check([{ kind, name: `nonexistent-${kind}` }]);
65
+ expect(check).toBeDefined();
66
+ expect(check.issues).toHaveLength(1);
67
+ expect(check.issues[0]?.requirement.kind).toBe(kind);
68
+ }
69
+ });
70
+ });
@@ -0,0 +1,111 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { defineProvider, defineTranscriber } from '@moxxy/sdk';
3
+ import { RequirementRegistry } from './requirements.js';
4
+ import { ToolRegistryImpl } from './registries/tools.js';
5
+ import { ProviderRegistry } from './registries/providers.js';
6
+ import { ModeRegistry } from './registries/modes.js';
7
+ import { CompactorRegistry } from './registries/compactors.js';
8
+ import { ChannelRegistryImpl } from './registries/channels.js';
9
+ import { AgentRegistry } from './registries/agents.js';
10
+ import { CommandRegistry } from './registries/commands.js';
11
+ import { TranscriberRegistry } from './registries/transcribers.js';
12
+ import { silentLogger } from './logger.js';
13
+
14
+ const makeRequirements = () => {
15
+ const tools = new ToolRegistryImpl({ logger: silentLogger, cwd: '/tmp' });
16
+ const providers = new ProviderRegistry();
17
+ const modes = new ModeRegistry();
18
+ const compactors = new CompactorRegistry();
19
+ const channels = new ChannelRegistryImpl();
20
+ const agents = new AgentRegistry();
21
+ const commands = new CommandRegistry();
22
+ const transcribers = new TranscriberRegistry();
23
+ const requirements = new RequirementRegistry({
24
+ tools,
25
+ providers,
26
+ modes,
27
+ compactors,
28
+ channels,
29
+ agents,
30
+ commands,
31
+ transcribers,
32
+ });
33
+ return { requirements, tools, providers, modes, compactors, transcribers };
34
+ };
35
+
36
+ describe('RequirementRegistry', () => {
37
+ it('reports missing registered requirements', () => {
38
+ const { requirements } = makeRequirements();
39
+
40
+ const check = requirements.check([{ kind: 'tool', name: 'web_fetch' }]);
41
+
42
+ expect(check).toMatchObject({
43
+ ready: false,
44
+ issues: [{ code: 'missing', message: 'Required tool is not registered: web_fetch' }],
45
+ });
46
+ });
47
+
48
+ it('reports inactive providers when active state is required', () => {
49
+ const { requirements, providers } = makeRequirements();
50
+ providers.register(
51
+ defineProvider({
52
+ name: 'openai-codex',
53
+ models: [],
54
+ createClient: () => ({ name: 'openai-codex', models: [], stream: async function* () {}, countTokens: async () => 0 }),
55
+ }),
56
+ );
57
+
58
+ const check = requirements.check([{ kind: 'provider', name: 'openai-codex', state: 'active' }]);
59
+
60
+ expect(check.ready).toBe(false);
61
+ expect(check.issues[0]).toMatchObject({
62
+ code: 'inactive',
63
+ message: 'Required provider is not active: openai-codex',
64
+ });
65
+ });
66
+
67
+ it('reports runtime requirements until they are marked ready', () => {
68
+ const { requirements } = makeRequirements();
69
+ const req = { kind: 'runtime' as const, name: 'auth:provider:openai-codex', state: 'ready' as const };
70
+
71
+ expect(requirements.check([req])).toMatchObject({
72
+ ready: false,
73
+ issues: [{ code: 'not_ready' }],
74
+ });
75
+
76
+ requirements.setRuntime('auth:provider:openai-codex', 'ready');
77
+
78
+ expect(requirements.check([req])).toEqual({ ready: true, issues: [] });
79
+ });
80
+
81
+ it('keeps optional requirements as diagnostics without blocking readiness', () => {
82
+ const { requirements } = makeRequirements();
83
+
84
+ const check = requirements.check([{ kind: 'runtime', name: 'browser:installed', state: 'ready', optional: true }]);
85
+
86
+ expect(check.ready).toBe(true);
87
+ expect(check.issues[0]).toMatchObject({ code: 'not_ready' });
88
+ });
89
+
90
+ it('isReady checks a single named target without composing per-def requirements', () => {
91
+ const { requirements, transcribers } = makeRequirements();
92
+ transcribers.register(
93
+ defineTranscriber({
94
+ name: 'openai-codex-transcribe',
95
+ createClient: () => ({ name: 'openai-codex-transcribe', transcribe: async () => ({ text: 'ok' }) }),
96
+ }),
97
+ );
98
+
99
+ expect(requirements.isReady('transcriber', 'openai-codex-transcribe')).toMatchObject({
100
+ ready: false,
101
+ issues: [{ code: 'inactive' }],
102
+ });
103
+
104
+ transcribers.setActive('openai-codex-transcribe');
105
+
106
+ expect(requirements.isReady('transcriber', 'openai-codex-transcribe')).toEqual({
107
+ ready: true,
108
+ issues: [],
109
+ });
110
+ });
111
+ });
@@ -0,0 +1,262 @@
1
+ import type {
2
+ MoxxyRequirement,
3
+ RequirementCheck,
4
+ RequirementIssue,
5
+ RequirementKind,
6
+ RequirementState,
7
+ AgentDef,
8
+ ChannelDef,
9
+ CommandDef,
10
+ CompactorDef,
11
+ ModeDef,
12
+ ProviderDef,
13
+ ToolDef,
14
+ TranscriberDef,
15
+ SynthesizerDef,
16
+ } from '@moxxy/sdk';
17
+
18
+ export interface RequirementChecker {
19
+ check(requirements: ReadonlyArray<MoxxyRequirement> | undefined): RequirementCheck;
20
+ }
21
+
22
+ export interface RequirementRegistryOptions {
23
+ readonly tools: {
24
+ get(name: string): ToolDef | undefined;
25
+ };
26
+ readonly providers: {
27
+ list(): ReadonlyArray<ProviderDef>;
28
+ getActiveName(): string | null;
29
+ };
30
+ readonly modes: {
31
+ list(): ReadonlyArray<ModeDef>;
32
+ getActive(): ModeDef;
33
+ };
34
+ readonly compactors: {
35
+ list(): ReadonlyArray<CompactorDef>;
36
+ getActive(): CompactorDef | null;
37
+ };
38
+ readonly channels: {
39
+ get(name: string): ChannelDef | undefined;
40
+ };
41
+ readonly agents: {
42
+ get(name: string): AgentDef | undefined;
43
+ };
44
+ readonly commands: {
45
+ get(name: string): CommandDef | undefined;
46
+ };
47
+ readonly transcribers: {
48
+ list(): ReadonlyArray<TranscriberDef>;
49
+ getActiveName(): string | null;
50
+ };
51
+ readonly synthesizers: {
52
+ list(): ReadonlyArray<SynthesizerDef>;
53
+ getActiveName(): string | null;
54
+ };
55
+ }
56
+
57
+ interface RegisteredPlugin {
58
+ readonly name: string;
59
+ readonly version: string;
60
+ }
61
+
62
+ interface TargetInfo {
63
+ readonly kind: RequirementKind;
64
+ readonly name: string;
65
+ readonly version?: string;
66
+ readonly active: boolean;
67
+ }
68
+
69
+ /**
70
+ * Per-kind recipe for {@link RequirementRegistry.targetInfo}. Every kind that
71
+ * resolves against a registry on `opts` differs only in two orthogonal aspects,
72
+ * so we table them rather than repeat the same `lookup -> {kind,name,active}`
73
+ * shape once per case:
74
+ * - `present` — does a def by this name exist? (`registry.get` or
75
+ * `list().some`, depending on the registry base)
76
+ * - `active` — is this the currently-active one? (a `getActiveName() === name`
77
+ * / `getActive()?.name === name` check, or always-true for kinds
78
+ * with no active/inactive distinction like tool/channel/agent)
79
+ *
80
+ * None of these kinds track a version (only `plugin` does, and it is
81
+ * special-cased — see {@link RequirementRegistry.targetInfo}), so a `version`
82
+ * aspect is deliberately omitted here. `runtime` is handled by `checkRuntime`
83
+ * before `targetInfo` is reached and so also has no descriptor.
84
+ */
85
+ interface TargetDescriptor {
86
+ /** Whether a def with `name` is registered for this kind. */
87
+ readonly present: (opts: RequirementRegistryOptions, name: string) => boolean;
88
+ /** Whether the registered def is the currently-active one. */
89
+ readonly active: (opts: RequirementRegistryOptions, name: string) => boolean;
90
+ }
91
+
92
+ /** Active for kinds with no active/inactive distinction (tool/channel/agent/command). */
93
+ const ALWAYS_ACTIVE = (): true => true;
94
+
95
+ export class RequirementRegistry {
96
+ private readonly runtime = new Map<string, RequirementState>();
97
+ private readonly plugins = new Map<string, RegisteredPlugin>();
98
+
99
+ constructor(private readonly opts: RequirementRegistryOptions) {}
100
+
101
+ registerPlugin(name: string, version: string): void {
102
+ this.plugins.set(name, { name, version });
103
+ }
104
+
105
+ unregisterPlugin(name: string): void {
106
+ this.plugins.delete(name);
107
+ }
108
+
109
+ setRuntime(name: string, state: RequirementState = 'ready'): void {
110
+ this.runtime.set(name, state);
111
+ }
112
+
113
+ clearRuntime(name: string): void {
114
+ this.runtime.delete(name);
115
+ }
116
+
117
+ check(requirements: ReadonlyArray<MoxxyRequirement> | undefined): RequirementCheck {
118
+ const issues: RequirementIssue[] = [];
119
+ for (const requirement of requirements ?? []) {
120
+ const issue = this.checkOne(requirement);
121
+ if (issue) issues.push(issue);
122
+ }
123
+ const blocking = issues.filter((issue) => !issue.requirement.optional);
124
+ return { ready: blocking.length === 0, issues };
125
+ }
126
+
127
+ /**
128
+ * Convenience: check whether a single named target is present and (if
129
+ * its kind has an active/inactive distinction) currently active.
130
+ * Equivalent to `check([{ kind, name, state: 'active' }])`.
131
+ */
132
+ isReady(kind: RequirementKind, name: string): RequirementCheck {
133
+ return this.check([{ kind, name, state: 'active' }]);
134
+ }
135
+
136
+ private checkOne(requirement: MoxxyRequirement): RequirementIssue | null {
137
+ if (requirement.kind === 'runtime') return this.checkRuntime(requirement);
138
+
139
+ const target = this.targetInfo(requirement.kind, requirement.name);
140
+ if (!target) {
141
+ return issue(requirement, 'missing', `Required ${label(requirement.kind)} is not registered: ${requirement.name}`);
142
+ }
143
+
144
+ if (requirement.version && target.version !== requirement.version) {
145
+ return issue(
146
+ requirement,
147
+ 'version_mismatch',
148
+ `Required ${label(requirement.kind)} has version ${target.version ?? '(unknown)'}, expected ${requirement.version}: ${requirement.name}`,
149
+ );
150
+ }
151
+
152
+ const state = requirement.state ?? 'registered';
153
+ if ((state === 'active' || state === 'ready') && !target.active) {
154
+ return issue(requirement, 'inactive', `Required ${label(requirement.kind)} is not active: ${requirement.name}`);
155
+ }
156
+
157
+ return null;
158
+ }
159
+
160
+ private checkRuntime(requirement: MoxxyRequirement): RequirementIssue | null {
161
+ const state = requirement.state ?? 'ready';
162
+ const actual = this.runtime.get(requirement.name);
163
+ if (actual !== state) {
164
+ return issue(requirement, 'not_ready', `Required runtime is not ready: ${requirement.name}`);
165
+ }
166
+ return null;
167
+ }
168
+
169
+ private targetInfo(kind: RequirementKind, name: string): TargetInfo | null {
170
+ // `plugin` resolves against this instance's private registration map (not a
171
+ // registry on `opts`) and is the only kind carrying a version, so it stays a
172
+ // special case rather than being contorted into the shared table.
173
+ if (kind === 'plugin') {
174
+ const plugin = this.plugins.get(name);
175
+ return plugin ? { kind, name, version: plugin.version, active: true } : null;
176
+ }
177
+ // `runtime` never reaches here — `checkOne` routes it to `checkRuntime` first.
178
+ if (kind === 'runtime') return null;
179
+
180
+ // Every remaining kind has a table entry: `TARGET_DESCRIPTORS` is a total
181
+ // `Record` over `Exclude<RequirementKind, 'plugin' | 'runtime'>`, so a kind
182
+ // added to the union without an entry is a compile error (the table-driven
183
+ // replacement for the old switch's `assertNever` default).
184
+ const descriptor = TARGET_DESCRIPTORS[kind];
185
+ if (!descriptor.present(this.opts, name)) return null;
186
+ return { kind, name, active: descriptor.active(this.opts, name) };
187
+ }
188
+ }
189
+
190
+ /**
191
+ * Lookup table backing {@link RequirementRegistry.targetInfo} for every
192
+ * registry-backed {@link RequirementKind}. `plugin` (private map + version) and
193
+ * `runtime` (handled before `targetInfo`) are intentionally absent and dealt
194
+ * with as special cases; the `Partial`-free `Record` keyed on the remaining
195
+ * kinds keeps the set exhaustive (a new kind is a compile error until listed).
196
+ */
197
+ const TARGET_DESCRIPTORS: Record<
198
+ Exclude<RequirementKind, 'plugin' | 'runtime'>,
199
+ TargetDescriptor
200
+ > = {
201
+ provider: {
202
+ present: (opts, name) => opts.providers.list().some((p) => p.name === name),
203
+ active: (opts, name) => opts.providers.getActiveName() === name,
204
+ },
205
+ tool: {
206
+ present: (opts, name) => opts.tools.get(name) !== undefined,
207
+ active: ALWAYS_ACTIVE,
208
+ },
209
+ transcriber: {
210
+ present: (opts, name) => opts.transcribers.list().some((t) => t.name === name),
211
+ active: (opts, name) => opts.transcribers.getActiveName() === name,
212
+ },
213
+ synthesizer: {
214
+ present: (opts, name) => opts.synthesizers.list().some((s) => s.name === name),
215
+ active: (opts, name) => opts.synthesizers.getActiveName() === name,
216
+ },
217
+ mode: {
218
+ present: (opts, name) => opts.modes.list().some((m) => m.name === name),
219
+ active: (opts, name) => activeModeName(opts.modes) === name,
220
+ },
221
+ compactor: {
222
+ present: (opts, name) => opts.compactors.list().some((c) => c.name === name),
223
+ active: (opts, name) => opts.compactors.getActive()?.name === name,
224
+ },
225
+ channel: {
226
+ present: (opts, name) => opts.channels.get(name) !== undefined,
227
+ active: ALWAYS_ACTIVE,
228
+ },
229
+ agent: {
230
+ present: (opts, name) => opts.agents.get(name) !== undefined,
231
+ active: ALWAYS_ACTIVE,
232
+ },
233
+ command: {
234
+ present: (opts, name) => opts.commands.get(name) !== undefined,
235
+ active: ALWAYS_ACTIVE,
236
+ },
237
+ };
238
+
239
+ function issue(
240
+ requirement: MoxxyRequirement,
241
+ code: RequirementIssue['code'],
242
+ message: string,
243
+ ): RequirementIssue {
244
+ return {
245
+ requirement,
246
+ code,
247
+ message,
248
+ ...(requirement.hint ? { hint: requirement.hint } : {}),
249
+ };
250
+ }
251
+
252
+ function label(kind: RequirementKind): string {
253
+ return kind;
254
+ }
255
+
256
+ function activeModeName(modes: RequirementRegistryOptions['modes']): string | null {
257
+ try {
258
+ return modes.getActive().name;
259
+ } catch {
260
+ return null;
261
+ }
262
+ }