@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,86 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { definePlugin, defineTunnelProvider, defineViewRenderer } from '@moxxy/sdk';
3
+ import { silentLogger } from '../logger.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 { CacheStrategyRegistry } from '../registries/cache-strategies.js';
9
+ import { ViewRendererRegistry } from '../registries/view-renderers.js';
10
+ import { TunnelProviderRegistry } from '../registries/tunnel-providers.js';
11
+ import { ChannelRegistryImpl } from '../registries/channels.js';
12
+ import { AgentRegistry } from '../registries/agents.js';
13
+ import { CommandRegistry } from '../registries/commands.js';
14
+ import { TranscriberRegistry } from '../registries/transcribers.js';
15
+ import { HookDispatcherImpl } from './lifecycle.js';
16
+ import { PluginHost } from './host.js';
17
+ import { RequirementRegistry } from '../requirements.js';
18
+
19
+ function makeHost() {
20
+ const tools = new ToolRegistryImpl({ logger: silentLogger, cwd: '/tmp' });
21
+ const providers = new ProviderRegistry();
22
+ const modes = new ModeRegistry();
23
+ const compactors = new CompactorRegistry();
24
+ const cacheStrategies = new CacheStrategyRegistry();
25
+ const viewRenderers = new ViewRendererRegistry();
26
+ const tunnelProviders = new TunnelProviderRegistry();
27
+ const channels = new ChannelRegistryImpl();
28
+ const agents = new AgentRegistry();
29
+ const commands = new CommandRegistry();
30
+ const transcribers = new TranscriberRegistry();
31
+ const requirements = new RequirementRegistry({ tools, providers, modes, compactors, channels, agents, commands, transcribers });
32
+ const dispatcher = new HookDispatcherImpl({ logger: silentLogger });
33
+ const host = new PluginHost({
34
+ cwd: '/tmp',
35
+ logger: silentLogger,
36
+ tools,
37
+ providers,
38
+ modes,
39
+ compactors,
40
+ cacheStrategies,
41
+ viewRenderers,
42
+ tunnelProviders,
43
+ channels,
44
+ agents,
45
+ commands,
46
+ transcribers,
47
+ requirements,
48
+ dispatcher,
49
+ });
50
+ return { host, viewRenderers, tunnelProviders };
51
+ }
52
+
53
+ const vr = defineViewRenderer({ name: 'custom-renderer', allowList: [], parse: () => ({ ok: false, errors: [] }), validate: () => [] });
54
+ const tp = defineTunnelProvider({ name: 'ngrok', open: () => Promise.resolve({ url: 'http://x', close: () => Promise.resolve() }) });
55
+
56
+ describe('PluginHost — view-renderer + tunnel-provider blocks', () => {
57
+ it('registers both block kinds on registerStatic', () => {
58
+ const { host, viewRenderers, tunnelProviders } = makeHost();
59
+ host.registerStatic(definePlugin({ name: 'blocks-demo', viewRenderers: [vr], tunnelProviders: [tp] }));
60
+ expect(viewRenderers.list().map((r) => r.name)).toContain('custom-renderer');
61
+ expect(viewRenderers.getActive()?.name).toBe('custom-renderer'); // first → auto-active
62
+ expect(tunnelProviders.list().map((p) => p.name)).toContain('ngrok');
63
+ expect(tunnelProviders.getActive()?.name).toBe('ngrok');
64
+ });
65
+
66
+ it('unregisters both block kinds on unload', async () => {
67
+ const { host, viewRenderers, tunnelProviders } = makeHost();
68
+ host.registerStatic(definePlugin({ name: 'blocks-demo', viewRenderers: [vr], tunnelProviders: [tp] }));
69
+ await host.unload('blocks-demo');
70
+ expect(viewRenderers.list()).toHaveLength(0);
71
+ expect(viewRenderers.getActive()).toBeNull();
72
+ expect(tunnelProviders.list()).toHaveLength(0);
73
+ expect(tunnelProviders.getActive()).toBeNull();
74
+ });
75
+
76
+ it('replace semantics: a second renderer plugin overwrites without throwing', () => {
77
+ const { host, viewRenderers } = makeHost();
78
+ host.registerStatic(definePlugin({ name: 'a', viewRenderers: [vr] }));
79
+ const vr2 = defineViewRenderer({ name: 'custom-renderer', allowList: [], parse: () => ({ ok: false, errors: [] }), validate: () => [] });
80
+ // host.applyPlugin uses registry.replace for view renderers, so a same-named
81
+ // renderer from another plugin overwrites instead of throwing.
82
+ host.registerStatic(definePlugin({ name: 'b', viewRenderers: [vr2] }));
83
+ expect(viewRenderers.list()).toHaveLength(1);
84
+ expect(viewRenderers.getActive()).toBe(vr2);
85
+ });
86
+ });
@@ -0,0 +1,434 @@
1
+ import { afterEach, describe, expect, it, vi } from 'vitest';
2
+ import { promises as fs } from 'node:fs';
3
+ import * as os from 'node:os';
4
+ import * as path from 'node:path';
5
+ import { z } from 'zod';
6
+ import type { Plugin, ResolvedPluginManifest } from '@moxxy/sdk';
7
+ import {
8
+ defineCompactor,
9
+ defineMode,
10
+ definePlugin,
11
+ defineProvider,
12
+ defineTool,
13
+ defineTranscriber,
14
+ } from '@moxxy/sdk';
15
+ import { silentLogger } from '../logger.js';
16
+ import { ToolRegistryImpl } from '../registries/tools.js';
17
+ import { ProviderRegistry } from '../registries/providers.js';
18
+ import { ModeRegistry } from '../registries/modes.js';
19
+ import { CompactorRegistry } from '../registries/compactors.js';
20
+ import { ChannelRegistryImpl } from '../registries/channels.js';
21
+ import { AgentRegistry } from '../registries/agents.js';
22
+ import { CommandRegistry } from '../registries/commands.js';
23
+ import { TranscriberRegistry } from '../registries/transcribers.js';
24
+ import { HookDispatcherImpl } from './lifecycle.js';
25
+ import { PluginHost } from './host.js';
26
+ import { RequirementRegistry } from '../requirements.js';
27
+
28
+ const makeHost = () => {
29
+ const tools = new ToolRegistryImpl({ logger: silentLogger, cwd: '/tmp' });
30
+ const providers = new ProviderRegistry();
31
+ const modes = new ModeRegistry();
32
+ const compactors = new CompactorRegistry();
33
+ const channels = new ChannelRegistryImpl();
34
+ const agents = new AgentRegistry();
35
+ const commands = new CommandRegistry();
36
+ const transcribers = new TranscriberRegistry();
37
+ const requirements = new RequirementRegistry({
38
+ tools,
39
+ providers,
40
+ modes,
41
+ compactors,
42
+ channels,
43
+ agents,
44
+ commands,
45
+ transcribers,
46
+ });
47
+ const dispatcher = new HookDispatcherImpl({ logger: silentLogger });
48
+ const host = new PluginHost({
49
+ cwd: '/tmp',
50
+ logger: silentLogger,
51
+ tools,
52
+ providers,
53
+ modes,
54
+ compactors,
55
+ channels,
56
+ agents,
57
+ commands,
58
+ transcribers,
59
+ requirements,
60
+ dispatcher,
61
+ });
62
+ return { host, tools, providers, modes, compactors, channels, agents, commands, transcribers, requirements, dispatcher };
63
+ };
64
+
65
+ describe('PluginHost', () => {
66
+ it('registerStatic wires up tools, providers, modes, compactors', () => {
67
+ const { host, tools, providers, modes, compactors } = makeHost();
68
+ const tool = defineTool({
69
+ name: 'echo',
70
+ description: '',
71
+ inputSchema: z.any(),
72
+ handler: () => null,
73
+ });
74
+ const provider = defineProvider({
75
+ name: 'fake',
76
+ models: [],
77
+ createClient: () => ({ name: 'fake', models: [], stream: async function* () {}, countTokens: async () => 0 }),
78
+ });
79
+ const strategy = defineMode({ name: 'fake-loop', run: async function* () {} });
80
+ const compactor = defineCompactor({
81
+ name: 'fake-compact',
82
+ shouldCompact: () => false,
83
+ compact: async () => ({ type: 'compaction', compactor: 'fake-compact', replacedRange: [0, 0], summary: '', tokensSaved: 0, sessionId: '' as never, turnId: '' as never, source: 'compactor' }),
84
+ });
85
+ const plugin = definePlugin({
86
+ name: 'demo',
87
+ tools: [tool],
88
+ providers: [provider],
89
+ modes: [strategy],
90
+ compactors: [compactor],
91
+ });
92
+
93
+ host.registerStatic(plugin);
94
+ expect(tools.has('echo')).toBe(true);
95
+ expect(providers.list()).toHaveLength(1);
96
+ expect(modes.list()).toHaveLength(1);
97
+ expect(compactors.list()).toHaveLength(1);
98
+ expect(host.list()).toEqual([
99
+ { name: 'demo', version: '0.0.0', loaded: true, kinds: ['provider', 'mode', 'compactor', 'tool'] },
100
+ ]);
101
+ });
102
+
103
+ it('rejects double registration', () => {
104
+ const { host } = makeHost();
105
+ const p = definePlugin({ name: 'x' });
106
+ host.registerStatic(p);
107
+ expect(() => host.registerStatic(p)).toThrow(/already registered/);
108
+ });
109
+
110
+ it('rejects plugins whose required plugin is missing without partial registration', () => {
111
+ const { host, tools } = makeHost();
112
+ const plugin = definePlugin({
113
+ name: 'needs-codex',
114
+ tools: [
115
+ defineTool({
116
+ name: 'should-not-register',
117
+ description: '',
118
+ inputSchema: z.any(),
119
+ handler: () => null,
120
+ }),
121
+ ],
122
+ });
123
+
124
+ expect(() =>
125
+ host.registerStatic(plugin, {
126
+ requirements: [{ kind: 'plugin', name: '@moxxy/plugin-provider-openai-codex' }],
127
+ }),
128
+ ).toThrow(/Required plugin is not registered: @moxxy\/plugin-provider-openai-codex/);
129
+ expect(tools.has('should-not-register')).toBe(false);
130
+ expect(host.list()).toEqual([]);
131
+ expect(host.listSkipped()).toMatchObject([
132
+ {
133
+ pluginName: 'needs-codex',
134
+ source: 'static',
135
+ reason: 'unmet_requirements',
136
+ message: 'Required plugin is not registered: @moxxy/plugin-provider-openai-codex',
137
+ },
138
+ ]);
139
+ });
140
+
141
+ it('keeps optional plugin requirements as diagnostics without skipping registration', () => {
142
+ const { host, tools } = makeHost();
143
+ host.registerStatic(
144
+ definePlugin({
145
+ name: 'optional-codex',
146
+ tools: [
147
+ defineTool({
148
+ name: 'optional-tool',
149
+ description: '',
150
+ inputSchema: z.any(),
151
+ handler: () => null,
152
+ }),
153
+ ],
154
+ }),
155
+ {
156
+ requirements: [
157
+ {
158
+ kind: 'plugin',
159
+ name: '@moxxy/plugin-provider-openai-codex',
160
+ optional: true,
161
+ hint: 'Enable Codex for richer behavior.',
162
+ },
163
+ ],
164
+ },
165
+ );
166
+
167
+ expect(tools.has('optional-tool')).toBe(true);
168
+ expect(host.listSkipped()).toEqual([]);
169
+ });
170
+
171
+ it('clears a previous skip once the same plugin registers successfully', () => {
172
+ const { host } = makeHost();
173
+ const plugin = definePlugin({ name: 'needs-codex' });
174
+ const opts = {
175
+ requirements: [{ kind: 'plugin' as const, name: '@moxxy/plugin-provider-openai-codex' }],
176
+ };
177
+
178
+ expect(() => host.registerStatic(plugin, opts)).toThrow(/Required plugin is not registered/);
179
+ expect(host.listSkipped()).toHaveLength(1);
180
+
181
+ host.registerStatic(definePlugin({ name: '@moxxy/plugin-provider-openai-codex' }));
182
+ host.registerStatic(plugin, opts);
183
+
184
+ expect(host.listSkipped()).toEqual([]);
185
+ });
186
+
187
+ it('records discovered plugin skips with discovered source metadata', () => {
188
+ const { host } = makeHost();
189
+ const plugin = definePlugin({ name: '@demo/discovered' });
190
+
191
+ expect(() =>
192
+ host.registerDiscovered(plugin, {
193
+ entry: './dist/index.js',
194
+ packageName: '@demo/discovered',
195
+ packageVersion: '1.0.0',
196
+ packagePath: '/tmp/discovered',
197
+ requirements: [{ kind: 'plugin', name: '@demo/base' }],
198
+ }),
199
+ ).toThrow(/Required plugin is not registered/);
200
+
201
+ expect(host.listSkipped()).toMatchObject([
202
+ {
203
+ pluginName: '@demo/discovered',
204
+ source: 'discovered',
205
+ packageName: '@demo/discovered',
206
+ },
207
+ ]);
208
+ });
209
+
210
+ it('allows plugin registration after required plugin is loaded', () => {
211
+ const { host, tools } = makeHost();
212
+ host.registerStatic(definePlugin({ name: '@moxxy/plugin-provider-openai-codex' }));
213
+ host.registerStatic(
214
+ definePlugin({
215
+ name: 'needs-codex',
216
+ tools: [
217
+ defineTool({
218
+ name: 'registers-after-requirements',
219
+ description: '',
220
+ inputSchema: z.any(),
221
+ handler: () => null,
222
+ }),
223
+ ],
224
+ }),
225
+ {
226
+ requirements: [{ kind: 'plugin', name: '@moxxy/plugin-provider-openai-codex' }],
227
+ },
228
+ );
229
+
230
+ expect(tools.has('registers-after-requirements')).toBe(true);
231
+ });
232
+
233
+ it('unload removes contributions', async () => {
234
+ const { host, tools } = makeHost();
235
+ const tool = defineTool({
236
+ name: 'e',
237
+ description: '',
238
+ inputSchema: z.any(),
239
+ handler: () => null,
240
+ });
241
+ const plugin = definePlugin({ name: 'demo', tools: [tool] });
242
+ host.registerStatic(plugin);
243
+ expect(tools.has('e')).toBe(true);
244
+ await host.unload('demo');
245
+ expect(tools.has('e')).toBe(false);
246
+ });
247
+
248
+ it('keys a discovered plugin by package name, so unload(packageName) works even when it differs from the declared name', async () => {
249
+ const { host, tools } = makeHost();
250
+ const tool = defineTool({
251
+ name: 'dt',
252
+ description: '',
253
+ inputSchema: z.any(),
254
+ handler: () => null,
255
+ });
256
+ // Declared plugin name intentionally differs from the package name.
257
+ const plugin = definePlugin({ name: 'weird-internal-name', tools: [tool] });
258
+ host.registerDiscovered(plugin, {
259
+ entry: './dist/index.js',
260
+ packageName: '@scope/pkg',
261
+ packageVersion: '1.0.0',
262
+ packagePath: '/tmp/pkg',
263
+ });
264
+ expect(tools.has('dt')).toBe(true);
265
+ // Callers (self-update/config/plugins-admin) unload by PACKAGE name.
266
+ await host.unload('@scope/pkg');
267
+ expect(tools.has('dt')).toBe(false);
268
+ });
269
+
270
+ it('satisfies a kind:plugin requirement by PACKAGE name even when the dep\'s declared name differs', () => {
271
+ const { host, tools, requirements } = makeHost();
272
+ // The dependency: declared plugin.name intentionally differs from its
273
+ // package name. toposort resolves kind:'plugin' deps by packageName, so the
274
+ // readiness gate must register/match by packageName too — otherwise the
275
+ // dependent's requirement (keyed on the dep's packageName) silently fails.
276
+ host.registerDiscovered(definePlugin({ name: 'weird-internal-name' }), {
277
+ entry: './dist/index.js',
278
+ packageName: '@scope/base-pkg',
279
+ packageVersion: '1.0.0',
280
+ packagePath: '/tmp/base',
281
+ });
282
+ // The readiness gate is keyed by the PACKAGE name.
283
+ expect(requirements.isReady('plugin', '@scope/base-pkg').ready).toBe(true);
284
+
285
+ // A dependent that requires the base by its PACKAGE name must load cleanly.
286
+ const dependent = definePlugin({
287
+ name: 'dependent',
288
+ tools: [
289
+ defineTool({
290
+ name: 'dep-tool',
291
+ description: '',
292
+ inputSchema: z.any(),
293
+ handler: () => null,
294
+ }),
295
+ ],
296
+ });
297
+ expect(() =>
298
+ host.registerDiscovered(dependent, {
299
+ entry: './dist/index.js',
300
+ packageName: '@scope/dependent-pkg',
301
+ packageVersion: '1.0.0',
302
+ packagePath: '/tmp/dependent',
303
+ requirements: [{ kind: 'plugin', name: '@scope/base-pkg' }],
304
+ }),
305
+ ).not.toThrow();
306
+ expect(tools.has('dep-tool')).toBe(true);
307
+ });
308
+
309
+ it('discoverAndLoad without loader warns and returns nothing', async () => {
310
+ const { host } = makeHost();
311
+ const warn = vi.spyOn(silentLogger, 'warn');
312
+ const result = await host.discoverAndLoad();
313
+ expect(result).toEqual([]);
314
+ warn.mockRestore();
315
+ });
316
+
317
+ describe('reload with userPaths', () => {
318
+ const tempDirs: string[] = [];
319
+ afterEach(async () => {
320
+ await Promise.all(tempDirs.splice(0).map((d) => fs.rm(d, { recursive: true, force: true })));
321
+ });
322
+
323
+ const makeHostWithUserPaths = (
324
+ userPaths: ReadonlyArray<string>,
325
+ isDisabled?: (packageName: string) => boolean,
326
+ ) => {
327
+ const base = makeHost();
328
+ const loader = {
329
+ load: async (m: ResolvedPluginManifest): Promise<Plugin> =>
330
+ definePlugin({
331
+ name: m.packageName,
332
+ tools: [
333
+ defineTool({
334
+ name: `${m.packageName}_tool`,
335
+ description: '',
336
+ inputSchema: z.any(),
337
+ handler: () => null,
338
+ }),
339
+ ],
340
+ }),
341
+ };
342
+ const host = new PluginHost({
343
+ cwd: '/tmp',
344
+ logger: silentLogger,
345
+ tools: base.tools,
346
+ providers: base.providers,
347
+ modes: base.modes,
348
+ compactors: base.compactors,
349
+ channels: base.channels,
350
+ agents: base.agents,
351
+ commands: base.commands,
352
+ transcribers: base.transcribers,
353
+ requirements: base.requirements,
354
+ dispatcher: base.dispatcher,
355
+ loader,
356
+ userPaths,
357
+ ...(isDisabled ? { isDisabled } : {}),
358
+ });
359
+ return { host, tools: base.tools };
360
+ };
361
+
362
+ const writeUserPlugin = async (root: string, name: string): Promise<void> => {
363
+ const dir = path.join(root, name);
364
+ await fs.mkdir(dir, { recursive: true });
365
+ await fs.writeFile(
366
+ path.join(dir, 'package.json'),
367
+ JSON.stringify({ name, version: '0.0.0', moxxy: { plugin: { entry: './index.mjs' } } }),
368
+ 'utf8',
369
+ );
370
+ };
371
+
372
+ it('preserves static builtins and (re)discovers user-path plugins', async () => {
373
+ const root = await fs.mkdtemp(path.join(os.tmpdir(), 'moxxy-host-'));
374
+ tempDirs.push(root);
375
+ const { host, tools } = makeHostWithUserPaths([root]);
376
+
377
+ // A statically-registered builtin has no manifest — it must survive reload.
378
+ host.registerStatic(
379
+ definePlugin({
380
+ name: 'builtin',
381
+ tools: [defineTool({ name: 'builtin_tool', description: '', inputSchema: z.any(), handler: () => null })],
382
+ }),
383
+ );
384
+ await writeUserPlugin(root, 'userplug');
385
+
386
+ await host.reload();
387
+ expect(tools.has('builtin_tool')).toBe(true); // static preserved
388
+ expect(tools.has('userplug_tool')).toBe(true); // user-path discovered
389
+
390
+ // Remove the user plugin from disk → reload unloads it, keeps the builtin.
391
+ await fs.rm(path.join(root, 'userplug'), { recursive: true, force: true });
392
+ await host.reload();
393
+ expect(tools.has('userplug_tool')).toBe(false);
394
+ expect(tools.has('builtin_tool')).toBe(true);
395
+ });
396
+
397
+ it('never loads a disabled discovered plugin, and a runtime toggle takes effect on reload', async () => {
398
+ const root = await fs.mkdtemp(path.join(os.tmpdir(), 'moxxy-host-'));
399
+ tempDirs.push(root);
400
+ const disabled = new Set<string>(['userplug']);
401
+ const { host, tools } = makeHostWithUserPaths([root], (pkg) => disabled.has(pkg));
402
+ await writeUserPlugin(root, 'userplug');
403
+
404
+ // Disabled on disk → reload must NOT load it.
405
+ await host.reload();
406
+ expect(tools.has('userplug_tool')).toBe(false);
407
+
408
+ // Runtime enable (predicate flips) → next reload discovers + loads it.
409
+ disabled.delete('userplug');
410
+ await host.reload();
411
+ expect(tools.has('userplug_tool')).toBe(true);
412
+
413
+ // Runtime disable of a currently-loaded plugin → reload unloads it and
414
+ // does not resurrect it (the bug this predicate fixes).
415
+ disabled.add('userplug');
416
+ await host.reload();
417
+ expect(tools.has('userplug_tool')).toBe(false);
418
+ });
419
+ });
420
+
421
+ it('registerStatic + unload roundtrip transcribers', async () => {
422
+ const { host, transcribers } = makeHost();
423
+ const t = defineTranscriber({
424
+ name: 'fake-stt',
425
+ createClient: () => ({ name: 'fake-stt', transcribe: async () => ({ text: '' }) }),
426
+ });
427
+ host.registerStatic(definePlugin({ name: 'stt-demo', transcribers: [t] }));
428
+ expect(transcribers.has('fake-stt')).toBe(true);
429
+ expect(transcribers.list()).toHaveLength(1);
430
+
431
+ await host.unload('stt-demo');
432
+ expect(transcribers.has('fake-stt')).toBe(false);
433
+ });
434
+ });