diff --git a/.gitignore b/.gitignore
index 78f591acc192171dee6b40944d2270825d456ff8..da546ac6ba07a6ee33b9a1572c3b9f6f113d321b 100644
--- a/.gitignore
+++ b/.gitignore
@@ -21,3 +21,7 @@
 # Go workspace file
 go.work
 
+# Binaries
+main
+a
+
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 9c4169971a7ae4e285670f4c339860a3b90c4757..a265bceadbc6b019db023fb06de7fc18ae41e069 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -1,5 +1,5 @@
 variables:
-  MAIN_PACKAGE: cmd/main.go
+  MAIN_PACKAGE: cmd/obsman/main.go
 include:
   - project: 'hubman/ops-man'
     file: /ci/flows/agent.yml
diff --git a/README.md b/README.md
index eff70ca0721ebde9184492ed15a75bf5b3b39b90..7ff7ee254e0565fbdc333b7341df7f961344e110 100644
--- a/README.md
+++ b/README.md
@@ -1,92 +1,16 @@
 # obs-man
 
+## Конфигурация
+Конфигурация определяет подключение к целевому хосту OBS-Websocket
 
 
-## Getting started
-
-To make it easy for you to get started with GitLab, here's a list of recommended next steps.
-
-Already a pro? Just edit this README.md and make it your own. Want to make it easy? [Use the template at the bottom](#editing-this-readme)!
-
-## Add your files
-
-- [ ] [Create](https://docs.gitlab.com/ee/user/project/repository/web_editor.html#create-a-file) or [upload](https://docs.gitlab.com/ee/user/project/repository/web_editor.html#upload-a-file) files
-- [ ] [Add files using the command line](https://docs.gitlab.com/ee/gitlab-basics/add-file.html#add-a-file-using-the-command-line) or push an existing Git repository with the following command:
-
-```
-cd existing_repo
-git remote add origin https://git.miem.hse.ru/hubman/obs-man.git
-git branch -M master
-git push -uf origin master
+```yaml
+obs:
+  host_port: 127.0.0.1:4455
+  password: eNYQzlPX48qqBDtw
 ```
 
-## Integrate with your tools
-
-- [ ] [Set up project integrations](https://git.miem.hse.ru/hubman/obs-man/-/settings/integrations)
-
-## Collaborate with your team
-
-- [ ] [Invite team members and collaborators](https://docs.gitlab.com/ee/user/project/members/)
-- [ ] [Create a new merge request](https://docs.gitlab.com/ee/user/project/merge_requests/creating_merge_requests.html)
-- [ ] [Automatically close issues from merge requests](https://docs.gitlab.com/ee/user/project/issues/managing_issues.html#closing-issues-automatically)
-- [ ] [Enable merge request approvals](https://docs.gitlab.com/ee/user/project/merge_requests/approvals/)
-- [ ] [Automatically merge when pipeline succeeds](https://docs.gitlab.com/ee/user/project/merge_requests/merge_when_pipeline_succeeds.html)
-
-## Test and Deploy
-
-Use the built-in continuous integration in GitLab.
-
-- [ ] [Get started with GitLab CI/CD](https://docs.gitlab.com/ee/ci/quick_start/index.html)
-- [ ] [Analyze your code for known vulnerabilities with Static Application Security Testing(SAST)](https://docs.gitlab.com/ee/user/application_security/sast/)
-- [ ] [Deploy to Kubernetes, Amazon EC2, or Amazon ECS using Auto Deploy](https://docs.gitlab.com/ee/topics/autodevops/requirements.html)
-- [ ] [Use pull-based deployments for improved Kubernetes management](https://docs.gitlab.com/ee/user/clusters/agent/)
-- [ ] [Set up protected environments](https://docs.gitlab.com/ee/ci/environments/protected_environments.html)
-
-***
-
-# Editing this README
-
-When you're ready to make this README your own, just edit this file and use the handy template below (or feel free to structure it however you want - this is just a starting point!). Thank you to [makeareadme.com](https://www.makeareadme.com/) for this template.
-
-## Suggestions for a good README
-Every project is different, so consider which of these sections apply to yours. The sections used in the template are suggestions for most open source projects. Also keep in mind that while a README can be too long and detailed, too long is better than too short. If you think your README is too long, consider utilizing another form of documentation rather than cutting out information.
-
-## Name
-Choose a self-explaining name for your project.
-
-## Description
-Let people know what your project can do specifically. Provide context and add a link to any reference visitors might be unfamiliar with. A list of Features or a Background subsection can also be added here. If there are alternatives to your project, this is a good place to list differentiating factors.
-
-## Badges
-On some READMEs, you may see small images that convey metadata, such as whether or not all the tests are passing for the project. You can use Shields to add some to your README. Many services also have instructions for adding a badge.
-
-## Visuals
-Depending on what you are making, it can be a good idea to include screenshots or even a video (you'll frequently see GIFs rather than actual videos). Tools like ttygif can help, but check out Asciinema for a more sophisticated method.
-
-## Installation
-Within a particular ecosystem, there may be a common way of installing things, such as using Yarn, NuGet, or Homebrew. However, consider the possibility that whoever is reading your README is a novice and would like more guidance. Listing specific steps helps remove ambiguity and gets people to using your project as quickly as possible. If it only runs in a specific context like a particular programming language version or operating system or has dependencies that have to be installed manually, also add a Requirements subsection.
-
-## Usage
-Use examples liberally, and show the expected output if you can. It's helpful to have inline the smallest example of usage that you can demonstrate, while providing links to more sophisticated examples if they are too long to reasonably include in the README.
-
-## Support
-Tell people where they can go to for help. It can be any combination of an issue tracker, a chat room, an email address, etc.
-
-## Roadmap
-If you have ideas for releases in the future, it is a good idea to list them in the README.
-
-## Contributing
-State if you are open to contributions and what your requirements are for accepting them.
-
-For people who want to make changes to your project, it's helpful to have some documentation on how to get started. Perhaps there is a script that they should run or some environment variables that they need to set. Make these steps explicit. These instructions could also be useful to your future self.
-
-You can also document commands to lint the code or run tests. These steps help to ensure high code quality and reduce the likelihood that the changes inadvertently break something. Having instructions for running tests is especially helpful if it requires external setup, such as starting a Selenium server for testing in a browser.
-
-## Authors and acknowledgment
-Show your appreciation to those who have contributed to the project.
-
-## License
-For open source projects, say how it is licensed.
-
-## Project status
-If you have run out of energy or time for your project, put a note at the top of the README saying that development has slowed down or stopped completely. Someone may choose to fork your project or volunteer to step in as a maintainer or owner, allowing your project to keep going. You can also make an explicit request for maintainers.
+## Нюансы Fault-Tolerance
+В случае отключения соединения по ws - агент не завершает работу.
+При любом обновлении конфига с новым X-Request-Id будет предпринята попытка
+переустановить подключение
\ No newline at end of file
diff --git a/cmd/main.go b/cmd/main.go
deleted file mode 100644
index 594330983db7cdfe6b24156cc96488d9a1c27582..0000000000000000000000000000000000000000
--- a/cmd/main.go
+++ /dev/null
@@ -1,80 +0,0 @@
-package main
-
-import (
-	"encoding/json"
-	"flag"
-	"git.miem.hse.ru/hubman/hubman-lib"
-	"git.miem.hse.ru/hubman/hubman-lib/core"
-	"git.miem.hse.ru/hubman/hubman-lib/executor"
-	"go.uber.org/zap"
-	"gopkg.in/yaml.v3"
-	"log"
-	"net"
-	"obs-man/internal"
-	"os"
-	"time"
-)
-
-func main() {
-	var cfgPath string
-	flag.StringVar(&cfgPath, "config-path", "conf.yaml", "specify config path")
-	flag.Parse()
-
-	c := obsman.Conf{}
-	f, err := os.Open(cfgPath)
-	if err != nil {
-		log.Fatal(err)
-	}
-	decoder := yaml.NewDecoder(f)
-	err = decoder.Decode(&c)
-	if err != nil {
-		log.Fatal(err)
-	}
-
-	obsManager, err := obsman.NewManager(c.ObsConf, zap.Must(zap.NewProduction()))
-
-	if err != nil {
-		log.Fatal(err)
-	}
-
-	app := hubman.NewAgentApp(
-		core.AgentConfiguration{
-			System: &core.SystemConfig{
-				Server: &core.InterfaceConfig{
-					IP:   net.IPv4(127, 0, 0, 1),
-					Port: c.Agent.ExecutorPort,
-				},
-				RedisUrl: c.Agent.RedisUrl,
-			},
-			User: &c.ObsConf,
-			ParseUserConfig: func(jsonBytes []byte) (core.Configuration, error) {
-				newConf := obsman.ObsConf{}
-				err := json.Unmarshal(jsonBytes, &newConf)
-				return &newConf, err
-			},
-		},
-		hubman.WithExecutor(
-			hubman.WithCommand(obsman.SetScene{}, func(command core.SerializedCommand, parser executor.CommandParser) {
-				cmd := obsman.SetScene{}
-				parser(&cmd)
-				obsManager.DoSetScene(cmd)
-			}),
-			hubman.WithCommand(obsman.StartRecord{}, func(command core.SerializedCommand, parser executor.CommandParser) {
-				cmd := obsman.StartRecord{}
-				parser(&cmd)
-				obsManager.DoStartRecord(cmd)
-			}),
-			hubman.WithCommand(obsman.StopRecord{}, func(command core.SerializedCommand, parser executor.CommandParser) {
-				cmd := obsman.StopRecord{}
-				parser(&cmd)
-				obsManager.DoStopRecord(cmd)
-			}),
-		),
-		hubman.WithOnConfigRefresh(func(c core.AgentConfiguration) {
-			<-time.After(time.Second * 1)
-		}),
-	)
-
-	<-app.WaitShutdown()
-	obsManager.Close()
-}
diff --git a/cmd/obsman/main.go b/cmd/obsman/main.go
new file mode 100644
index 0000000000000000000000000000000000000000..e0c4d7859b56566a43c128022bc52dc028d042c4
--- /dev/null
+++ b/cmd/obsman/main.go
@@ -0,0 +1,82 @@
+package main
+
+import (
+	"encoding/json"
+	"log"
+	obsman "obs-man/pkg"
+	ocmd "obs-man/pkg/command"
+	osig "obs-man/pkg/signal"
+
+	"git.miem.hse.ru/hubman/hubman-lib"
+	"git.miem.hse.ru/hubman/hubman-lib/core"
+	hcore "git.miem.hse.ru/hubman/hubman-lib/core"
+	"git.miem.hse.ru/hubman/hubman-lib/executor"
+	"git.miem.hse.ru/hubman/hubman-lib/manipulator"
+	"go.uber.org/zap"
+)
+
+func main() {
+	systemConfig := &hcore.SystemConfig{}
+	userConfig := &obsman.Conf{}
+
+	err := hcore.ReadConfig(systemConfig, userConfig)
+	if err != nil {
+		log.Fatalf("error while init config: %v", err)
+	}
+
+	conf := hcore.AgentConfiguration{
+		System: systemConfig,
+		User:   userConfig,
+		ParseUserConfig: func(jsonBuf []byte) (hcore.Configuration, error) {
+			var conf *obsman.Conf
+			err := json.Unmarshal(jsonBuf, &conf)
+			return conf, err
+		},
+	}
+	signalsCh := make(chan hcore.Signal)
+
+	app := hcore.NewContainer(conf.System.Logging)
+
+	obsManager, err := obsman.NewManager(userConfig.ObsConf, app.Logger(), signalsCh)
+	if err != nil {
+		app.Logger().Fatal("Unable to create obs manager", zap.Error(err))
+	}
+	defer obsManager.Close()
+
+	executorCommands := append(make([]func(executor.Executor), 0), ocmd.ProvideRecordCommands(obsManager, app.Logger())...)
+	executorCommands = append(executorCommands, ocmd.ProvideSceneCommands(obsManager, app.Logger())...)
+	executorCommands = append(executorCommands, ocmd.ProvideSourceCommands(obsManager, app.Logger())...)
+	executorCommands = append(executorCommands, ocmd.ProvideStreamCommands(obsManager, app.Logger())...)
+	executorCommands = append(executorCommands, ocmd.ProvideUiCommands(obsManager, app.Logger())...)
+
+	manipulatorSignals := append(make([]func(manipulator.Manipulator), 0), hubman.WithChannel(signalsCh))
+	manipulatorSignals = append(manipulatorSignals, osig.CurrentSceneSignals...)
+	manipulatorSignals = append(manipulatorSignals, osig.InputSignals...)
+	manipulatorSignals = append(manipulatorSignals, osig.RecordSignals...)
+	manipulatorSignals = append(manipulatorSignals, osig.ReplayBufferSignals...)
+	manipulatorSignals = append(manipulatorSignals, osig.SceneItemSignals...)
+	manipulatorSignals = append(manipulatorSignals, osig.ScreenshotSignals...)
+	manipulatorSignals = append(manipulatorSignals, osig.StreamSignals...)
+	manipulatorSignals = append(manipulatorSignals, osig.TransitionSignals...)
+	manipulatorSignals = append(manipulatorSignals, osig.UISignals...)
+	manipulatorSignals = append(manipulatorSignals, osig.VirtualCamSignals...)
+
+	app.RegisterPlugin(hubman.NewAgentPlugin(app.Logger(),
+		conf,
+		hubman.WithExecutor(
+			executorCommands...,
+		),
+		hubman.WithManipulator(
+			manipulatorSignals...,
+		),
+		hubman.WithOnConfigRefresh(func(c core.AgentConfiguration) {
+			newConf, ok := c.User.(*obsman.Conf)
+			if !ok {
+				log.Fatal("Invalid new config provided", zap.Any("newConf", c.User))
+			}
+			obsManager.UpdateConn(newConf.ObsConf)
+		}),
+	))
+
+	<-app.WaitShutdown()
+}
diff --git a/conf.yaml b/conf.yaml
deleted file mode 100644
index 60634411970b1582d78fd4ec61792846022de6a7..0000000000000000000000000000000000000000
--- a/conf.yaml
+++ /dev/null
@@ -1,6 +0,0 @@
-agent:
-  server_port: 8082
-  redis_url: redis://@127.0.0.1:6379
-obs:
-  host_port: 127.0.0.1:4455
-  password: eNYQzlPX48qqBDtw
diff --git a/go.mod b/go.mod
index 00a931ffcfd1ccd296a2b515010de9345ce433af..21519c85a55adcd7d0ee73dc84facf34d801cadc 100644
--- a/go.mod
+++ b/go.mod
@@ -1,16 +1,23 @@
 module obs-man
 
-go 1.20
+go 1.21
+
+toolchain go1.21.5
+
+require (
+	git.miem.hse.ru/hubman/hubman-lib v0.0.21
+	github.com/andreykaipov/goobs v0.12.1
+	go.uber.org/zap v1.26.0
+)
 
 require (
-	git.miem.hse.ru/hubman/hubman-lib v0.0.17 // indirect
-	github.com/andreykaipov/goobs v0.12.1 // indirect
 	github.com/apapsch/go-jsonmerge/v2 v2.0.0 // indirect
 	github.com/bahlo/generic-list-go v0.2.0 // indirect
 	github.com/buger/jsonparser v1.1.1 // indirect
 	github.com/cespare/xxhash/v2 v2.2.0 // indirect
 	github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
 	github.com/diegoholiveira/jsonlogic/v3 v3.3.0 // indirect
+	github.com/go-chi/chi v1.5.4 // indirect
 	github.com/go-chi/chi/v5 v5.0.10 // indirect
 	github.com/google/uuid v1.3.1 // indirect
 	github.com/gorilla/websocket v1.5.0 // indirect
@@ -24,6 +31,6 @@ require (
 	github.com/redis/go-redis/v9 v9.1.0 // indirect
 	github.com/wk8/go-ordered-map/v2 v2.1.8 // indirect
 	go.uber.org/multierr v1.11.0 // indirect
-	go.uber.org/zap v1.26.0 // indirect
 	gopkg.in/yaml.v3 v3.0.1 // indirect
+	moul.io/chizap v1.0.3 // indirect
 )
diff --git a/go.sum b/go.sum
index af5616d910756be5899db87fa6aea73c83f18790..4946cf667304173cc2433393177e730ffbea6fa8 100644
--- a/go.sum
+++ b/go.sum
@@ -1,26 +1,36 @@
-git.miem.hse.ru/hubman/hubman-lib v0.0.14 h1:UEPP6SPZOzODer52q14RLh8tsRKUhWfmtHU6Awu03h8=
-git.miem.hse.ru/hubman/hubman-lib v0.0.14/go.mod h1:o+a+T60NtRc+bfehxGi2eQQfeGXgUuyWFL5q+KS/zhI=
-git.miem.hse.ru/hubman/hubman-lib v0.0.16 h1:D/X/l+Q70GaBOIkMiVyyva8vg8N4Tq9TV5/VJkGlRy0=
-git.miem.hse.ru/hubman/hubman-lib v0.0.16/go.mod h1:o+a+T60NtRc+bfehxGi2eQQfeGXgUuyWFL5q+KS/zhI=
-git.miem.hse.ru/hubman/hubman-lib v0.0.17 h1:1A/SVZrmj6IYwiYUUG1q5K8EFb4V1tVD05s1TLwXYF0=
-git.miem.hse.ru/hubman/hubman-lib v0.0.17/go.mod h1:9m3Jlu/oABdSHwRyo6zuCz1JVK1eok/iud9mUtbk+S8=
+git.miem.hse.ru/hubman/hubman-lib v0.0.21 h1:6TSv15paUMFSgTLO978X3reLQdYUzRl19pef0+bXsmU=
+git.miem.hse.ru/hubman/hubman-lib v0.0.21/go.mod h1:dqzfxhYcgHY3Akz8SmEYUR9eauwgsDDJfef6zfiqPeI=
 github.com/RaveNoX/go-jsoncommentstrip v1.0.0/go.mod h1:78ihd09MekBnJnxpICcwzCMzGrKSKYe4AqU6PDYYpjk=
+github.com/alicebob/gopher-json v0.0.0-20200520072559-a9ecdc9d1d3a h1:HbKu58rmZpUGpz5+4FfNmIU+FmZg2P3Xaj2v2bfNWmk=
+github.com/alicebob/gopher-json v0.0.0-20200520072559-a9ecdc9d1d3a/go.mod h1:SGnFV6hVsYE877CKEZ6tDNTjaSXYUk6QqoIK6PrAtcc=
+github.com/alicebob/miniredis/v2 v2.30.5 h1:3r6kTHdKnuP4fkS8k2IrvSfxpxUTcW1SOL0wN7b7Dt0=
+github.com/alicebob/miniredis/v2 v2.30.5/go.mod h1:b25qWj4fCEsBeAAR2mlb0ufImGC6uH3VlUfb/HS5zKg=
 github.com/andreykaipov/goobs v0.12.1 h1:KfVHfuvGFHg1MDi1muaKCwRxek3O4tzKcqBZmLDF8EA=
 github.com/andreykaipov/goobs v0.12.1/go.mod h1:9lCSWI7uZScJx05Hc0KnRtIItGjU8VpGV5dhONiiUgg=
 github.com/apapsch/go-jsonmerge/v2 v2.0.0 h1:axGnT1gRIfimI7gJifB699GoE/oq+F2MU7Dml6nw9rQ=
 github.com/apapsch/go-jsonmerge/v2 v2.0.0/go.mod h1:lvDnEdqiQrp0O42VQGgmlKpxL1AP2+08jFMw88y4klk=
 github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk=
 github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg=
+github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
 github.com/bmatcuk/doublestar v1.1.1/go.mod h1:UD6OnuiIn0yFxxA2le/rnRU1G4RaI4UvFv1sNto9p6w=
+github.com/bsm/ginkgo/v2 v2.9.5 h1:rtVBYPs3+TC5iLUVOis1B9tjLTup7Cj5IfzosKtvTJ0=
+github.com/bsm/ginkgo/v2 v2.9.5/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c=
+github.com/bsm/gomega v1.26.0 h1:LhQm+AFcgV2M0WyKroMASzAzCAJVpAxQXv4SaI9a69Y=
+github.com/bsm/gomega v1.26.0/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0=
 github.com/buger/jsonparser v1.1.1 h1:2PnMjfWD7wBILjqQbt530v576A/cAbQvEW9gGIpYMUs=
 github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0=
 github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
 github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
+github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
+github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
 github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
 github.com/diegoholiveira/jsonlogic/v3 v3.3.0 h1:XdIxQ+ICFcQB9tVf46cmiCkc5K9MN8Sh/x+XDHL+iXM=
 github.com/diegoholiveira/jsonlogic/v3 v3.3.0/go.mod h1:9oE8z9G+0OMxOoLHF3fhek3KuqD5CBqM0B6XFL08MSg=
+github.com/go-chi/chi v1.5.4 h1:QHdzF2szwjqVV4wmByUnTcsbIg7UGaQ0tPF2t5GcAIs=
+github.com/go-chi/chi v1.5.4/go.mod h1:uaf8YgoFazUOkPBG7fxPftUylNumIev9awIWOENIuEg=
 github.com/go-chi/chi/v5 v5.0.10 h1:rLz5avzKpjqxrYwXNfmjkrYYXOyLJd37pz53UFHC6vk=
 github.com/go-chi/chi/v5 v5.0.10/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
 github.com/google/uuid v1.3.1 h1:KjJaJ9iWZ3jOFZIf1Lqf4laDRCasjl0BCmnEGxkdLb4=
@@ -33,6 +43,13 @@ github.com/invopop/jsonschema v0.12.0 h1:6ovsNSuvn9wEQVOyc72aycBMVQFKz7cPdMJn10C
 github.com/invopop/jsonschema v0.12.0/go.mod h1:ffZ5Km5SWWRAIN6wbDXItl95euhFz2uON45H2qjYt+0=
 github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
 github.com/juju/gnuflag v0.0.0-20171113085948-2ce1bb71843d/go.mod h1:2PavIy+JPciBPrBUjwbNvtwB6RQlve+hkpll6QSNmOE=
+github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
+github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI=
+github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
+github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
+github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
+github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
+github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
 github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
 github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
 github.com/mitchellh/copystructure v1.0.0 h1:Laisrj+bAB6b/yJwB5Bt3ITZhGJdqmxquMKeZ+mmkFQ=
@@ -43,20 +60,82 @@ github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d h1:VhgPp6v9qf9Agr/
 github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d/go.mod h1:YUTz3bUH2ZwIWBy3CJBeOBEugqcmXREj14T+iG/4k4U=
 github.com/oapi-codegen/runtime v1.0.0 h1:P4rqFX5fMFWqRzY9M/3YF9+aPSPPB06IzP2P7oOxrWo=
 github.com/oapi-codegen/runtime v1.0.0/go.mod h1:LmCUMQuPB4M/nLXilQXhHw+BLZdDb18B34OO356yJ/A=
+github.com/pkg/diff v0.0.0-20200914180035-5b29258ca4f7/go.mod h1:zO8QMzTeZd5cpnIkz/Gn6iK0jDfGicM1nynOkkPIl28=
+github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
+github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
+github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
 github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
 github.com/redis/go-redis/v9 v9.1.0 h1:137FnGdk+EQdCbye1FW+qOEcY5S+SpY9T0NiuqvtfMY=
 github.com/redis/go-redis/v9 v9.1.0/go.mod h1:urWj3He21Dj5k4TK1y59xH8Uj6ATueP8AH1cY3lZl4c=
+github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
 github.com/spkg/bom v0.0.0-20160624110644-59b7046e48ad/go.mod h1:qLr4V1qq6nMqFKkMo8ZTx3f+BZEkzsRUY10Xsm2mwU0=
 github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
 github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
+github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
 github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
+github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
+github.com/tailscale/depaware v0.0.0-20210622194025-720c4b409502/go.mod h1:p9lPsd+cx33L3H9nNoecRRxPssFKUwwI50I3pZ0yT+8=
 github.com/wk8/go-ordered-map/v2 v2.1.8 h1:5h/BUHu93oj4gIdvHHHGsScSTMijfx5PeYkE/fJgbpc=
 github.com/wk8/go-ordered-map/v2 v2.1.8/go.mod h1:5nJHM5DyteebpVlHnWMV0rPz6Zp7+xBAnxjb1X5vnTw=
+github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
+github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
+github.com/yuin/gopher-lua v1.1.0 h1:BojcDhfyDWgU2f2TOzYK/g5p2gxMrku8oupLDqlnSqE=
+github.com/yuin/gopher-lua v1.1.0/go.mod h1:GBR0iDaNXjAgGg9zfCvksxSRnQx76gclCIb7kdAd1Pw=
+go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
+go.uber.org/atomic v1.8.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
+go.uber.org/goleak v1.1.11-0.20210813005559-691160354723/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ=
+go.uber.org/goleak v1.2.0 h1:xqgm/S+aQvhWFTtR0XK3Jvg7z8kGV8P4X14IzwN3Eqk=
+go.uber.org/goleak v1.2.0/go.mod h1:XJYK+MuIchqpmGmUSAzotztawfKvYLUIgg7guXrwVUo=
+go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU=
+go.uber.org/multierr v1.7.0/go.mod h1:7EAYxJLBy9rStEaz58O2t4Uvip6FSURkq8/ppBp95ak=
 go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
 go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
+go.uber.org/zap v1.19.1/go.mod h1:j3DNczoxDZroyBnOT1L/Q79cfUMGZxlv/9dzN7SM1rI=
 go.uber.org/zap v1.26.0 h1:sI7k6L95XOKS281NhVKOFCUNIvv9e0w4BF8N3u+tCRo=
 go.uber.org/zap v1.26.0/go.mod h1:dtElttAiwGvoJ/vj4IwHBS/gXsEu/pZ50mUIRWuG0so=
+golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
+golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
+golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
+golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
+golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
+golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
+golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
+golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
+golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
+golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
+golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
+golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
+golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
+golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20201211185031-d93e913c1a58/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
+golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
+golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
+gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
+gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
 gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
 gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
 gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+moul.io/chizap v1.0.3 h1:mliXvvuS5HVo3QP8qPXczWtRM5dQ9UmK3bBVIkZo6ek=
+moul.io/chizap v1.0.3/go.mod h1:pq4R9kGLwz4XjBc4hodQYuoE7Yc9RUabLBFyyi2uErk=
diff --git a/internal/manager.go b/internal/manager.go
deleted file mode 100644
index 01968c67374db78c004f71359978e6901e81955f..0000000000000000000000000000000000000000
--- a/internal/manager.go
+++ /dev/null
@@ -1,42 +0,0 @@
-package obsman
-
-import (
-	"github.com/andreykaipov/goobs"
-	"github.com/andreykaipov/goobs/api/requests/scenes"
-	"go.uber.org/zap"
-	"io"
-)
-
-type Manager interface {
-	io.Closer
-	DoSetScene(SetScene)
-	DoStartRecord(StartRecord)
-	DoStopRecord(StopRecord)
-}
-
-type manager struct {
-	logger *zap.Logger
-	client *goobs.Client
-}
-
-func (m *manager) Close() error {
-	_ = m.client.Disconnect()
-	return nil
-}
-
-func (m *manager) DoSetScene(scene SetScene) {
-	_, _ = m.client.Scenes.SetCurrentProgramScene(&scenes.SetCurrentProgramSceneParams{SceneName: scene.SceneName})
-}
-
-func (m *manager) DoStartRecord(cmd StartRecord) {
-	m.client.Record.StartRecord()
-}
-
-func (m *manager) DoStopRecord(cmd StopRecord) {
-	m.client.Record.StopRecord()
-}
-
-func NewManager(c ObsConf, logger *zap.Logger) (*manager, error) {
-	client, err := goobs.New(c.HostPort, goobs.WithPassword(c.Password))
-	return &manager{client: client, logger: logger.Named("OBSManager")}, err
-}
diff --git a/internal/obs_command.go b/internal/obs_command.go
deleted file mode 100644
index e3e106dbdd157e19242a55e72f70b6c3e774fc37..0000000000000000000000000000000000000000
--- a/internal/obs_command.go
+++ /dev/null
@@ -1,61 +0,0 @@
-package obsman
-
-import (
-	"errors"
-)
-
-type AgentConf struct {
-	ExecutorPort uint16 `yaml:"server_port"`
-	RedisUrl     string `yaml:"redis_url"`
-}
-
-type ObsConf struct {
-	HostPort string `yaml:"host_port"`
-	Password string `yaml:"password"`
-}
-
-type Conf struct {
-	Agent   AgentConf `yaml:"agent"`
-	ObsConf ObsConf   `yaml:"obs"`
-}
-
-func (o ObsConf) Validate() error {
-	if o.HostPort == "" {
-		return errors.New("empty host port")
-	}
-	return nil
-}
-
-type SetScene struct {
-	SceneName string `hubman:"scene_name"`
-}
-
-func (s SetScene) Code() string {
-	return "SetScene"
-}
-
-func (s SetScene) Description() string {
-	return "Sets scene with given name active in obs"
-}
-
-type StartRecord struct {
-}
-
-func (s StartRecord) Code() string {
-	return "StartRecord"
-}
-
-func (s StartRecord) Description() string {
-	return "Toggles record, if it is started - is noop. Similar to start record button"
-}
-
-type StopRecord struct {
-}
-
-func (s StopRecord) Code() string {
-	return "StopRecord"
-}
-
-func (s StopRecord) Description() string {
-	return "Toggles off record, if it is off - is noop. Similar to stop record button"
-}
diff --git a/pkg/command/record.go b/pkg/command/record.go
new file mode 100644
index 0000000000000000000000000000000000000000..3289b1f726ae7e0a87a1174f677228e180362210
--- /dev/null
+++ b/pkg/command/record.go
@@ -0,0 +1,176 @@
+package command
+
+import (
+	"git.miem.hse.ru/hubman/hubman-lib"
+	"git.miem.hse.ru/hubman/hubman-lib/core"
+	ex "git.miem.hse.ru/hubman/hubman-lib/executor"
+	"go.uber.org/zap"
+)
+
+func ProvideRecordCommands(obsProvider ObsProvider, l *zap.Logger) []func(ex.Executor) {
+	return []func(ex.Executor){
+		hubman.WithCommand(StartRecord{}, func(command core.SerializedCommand, cp ex.CommandParser) {
+			cmd := StartRecord{}
+			cp(&cmd)
+			cmd.Run(obsProvider, l.Named(cmd.Code()))
+		}),
+		hubman.WithCommand(StopRecord{}, func(command core.SerializedCommand, cp ex.CommandParser) {
+			cmd := StopRecord{}
+			cp(&cmd)
+			cmd.Run(obsProvider, l.Named(cmd.Code()))
+		}),
+		hubman.WithCommand(ToggleRecord{}, func(command core.SerializedCommand, cp ex.CommandParser) {
+			cmd := PauseRecord{}
+			cp(&cmd)
+			cmd.Run(obsProvider, l.Named(cmd.Code()))
+		}),
+
+		hubman.WithCommand(PauseRecord{}, func(command core.SerializedCommand, cp ex.CommandParser) {
+			cmd := PauseRecord{}
+			cp(&cmd)
+			cmd.Run(obsProvider, l.Named(cmd.Code()))
+		}),
+		hubman.WithCommand(ResumeRecord{}, func(command core.SerializedCommand, cp ex.CommandParser) {
+			cmd := ResumeRecord{}
+			cp(&cmd)
+			cmd.Run(obsProvider, l.Named(cmd.Code()))
+		}),
+		hubman.WithCommand(ToggleRecordPause{}, func(command core.SerializedCommand, cp ex.CommandParser) {
+			cmd := ToggleRecordPause{}
+			cp(&cmd)
+			cmd.Run(obsProvider, l.Named(cmd.Code()))
+		}),
+	}
+}
+
+/*----------------------------- Start/Stop/Toggle Record ---------------------*/
+
+var _ RunnableCommand = &StartRecord{}
+var _ RunnableCommand = &StopRecord{}
+var _ RunnableCommand = &ToggleRecord{}
+
+type StartRecord struct {
+}
+
+func (s StartRecord) Code() string {
+	return "StartRecord"
+}
+
+func (s StartRecord) Description() string {
+	return "Starts record, if it is already started - is noop. Similar to start record button"
+}
+
+func (s StartRecord) Run(p ObsProvider, _ *zap.Logger) error {
+	obsClient, err := p.Provide()
+	if err != nil {
+		return err
+	}
+	_, err = obsClient.Record.StartRecord()
+	return err
+}
+
+type StopRecord struct {
+}
+
+func (s StopRecord) Code() string {
+	return "StopRecord"
+}
+
+func (s StopRecord) Description() string {
+	return "Toggles off record, if it is off - is noop. Similar to stop record button"
+}
+
+func (s StopRecord) Run(p ObsProvider, _ *zap.Logger) error {
+	obsClient, err := p.Provide()
+	if err != nil {
+		return err
+	}
+	_, err = obsClient.Record.StopRecord()
+	return err
+}
+
+type ToggleRecord struct {
+}
+
+func (t ToggleRecord) Code() string {
+	return "ToggleRecord"
+}
+
+func (t ToggleRecord) Description() string {
+	return "Toggles Record, ex recording -> stop off, no recording -> start it"
+}
+
+func (t ToggleRecord) Run(p ObsProvider, _ *zap.Logger) error {
+	obsClient, err := p.Provide()
+	if err != nil {
+		return err
+	}
+	_, err = obsClient.Record.ToggleRecord()
+	return err
+}
+
+/*----------------------------- Pause/Resume/Toggle PauseRecord --------------*/
+
+var _ RunnableCommand = &PauseRecord{}
+var _ RunnableCommand = &ResumeRecord{}
+var _ RunnableCommand = &ToggleRecordPause{}
+
+type PauseRecord struct {
+}
+
+func (p PauseRecord) Code() string {
+	return "PauseRecord"
+}
+
+func (p PauseRecord) Description() string {
+	return "Pauses current recording, no-op if obs is not recording now"
+}
+
+func (p PauseRecord) Run(pr ObsProvider, _ *zap.Logger) error {
+	obsClient, err := pr.Provide()
+	if err != nil {
+		return err
+	}
+	_, err = obsClient.Record.PauseRecord()
+	return err
+}
+
+type ResumeRecord struct {
+}
+
+func (r ResumeRecord) Code() string {
+	return "ResumeRecord"
+}
+
+func (r ResumeRecord) Description() string {
+	return "Resumes Record"
+}
+
+func (r ResumeRecord) Run(pr ObsProvider, _ *zap.Logger) error {
+	obsClient, err := pr.Provide()
+	if err != nil {
+		return err
+	}
+	_, err = obsClient.Record.ResumeRecord()
+	return err
+}
+
+type ToggleRecordPause struct {
+}
+
+func (t ToggleRecordPause) Code() string {
+	return "ToggleRecordPause"
+}
+
+func (t ToggleRecordPause) Description() string {
+	return "Toggles RecordPause"
+}
+
+func (t ToggleRecordPause) Run(pr ObsProvider, _ *zap.Logger) error {
+	obsClient, err := pr.Provide()
+	if err != nil {
+		return err
+	}
+	_, err = obsClient.Record.ToggleRecordPause()
+	return err
+}
diff --git a/pkg/command/runnable_command.go b/pkg/command/runnable_command.go
new file mode 100644
index 0000000000000000000000000000000000000000..d00ff3509196de8aaf1983c225f9842f72ffdac7
--- /dev/null
+++ b/pkg/command/runnable_command.go
@@ -0,0 +1,14 @@
+package command
+
+import (
+	"github.com/andreykaipov/goobs"
+	"go.uber.org/zap"
+)
+
+type RunnableCommand interface {
+	Run(ObsProvider, *zap.Logger) error
+}
+
+type ObsProvider interface {
+	Provide() (*goobs.Client, error)
+}
diff --git a/pkg/command/scene.go b/pkg/command/scene.go
new file mode 100644
index 0000000000000000000000000000000000000000..0cc849de3c1f7a3642c869dae3140b81d91da830
--- /dev/null
+++ b/pkg/command/scene.go
@@ -0,0 +1,77 @@
+package command
+
+import (
+	"git.miem.hse.ru/hubman/hubman-lib"
+	"git.miem.hse.ru/hubman/hubman-lib/core"
+	ex "git.miem.hse.ru/hubman/hubman-lib/executor"
+	"github.com/andreykaipov/goobs/api/requests/scenes"
+	"go.uber.org/zap"
+)
+
+func ProvideSceneCommands(obsManager ObsProvider, l *zap.Logger) []func(ex.Executor) {
+	return []func(ex.Executor){
+		hubman.WithCommand(SetCurrentPreviewScene{}, func(_ core.SerializedCommand, cp ex.CommandParser) {
+			cmd := SetCurrentPreviewScene{}
+			cp(&cmd)
+			cmd.Run(obsManager, l.Named(cmd.Code()))
+		}),
+		hubman.WithCommand(SetCurrentProgramScene{}, func(_ core.SerializedCommand, cp ex.CommandParser) {
+			cmd := SetCurrentProgramScene{}
+			cp(&cmd)
+			cmd.Run(obsManager, l.Named(cmd.Code()))
+		}),
+	}
+}
+
+/*----------------------------- Set Preview/Current --------------------------*/
+
+var _ RunnableCommand = &SetCurrentProgramScene{}
+var _ RunnableCommand = &SetCurrentPreviewScene{}
+
+type SetCurrentProgramScene struct {
+	ProgramSceneName string `hubman:"program_scene_name"`
+}
+
+func (s SetCurrentProgramScene) Run(p ObsProvider, _ *zap.Logger) error {
+	obsClient, err := p.Provide()
+	if err != nil {
+		return err
+	}
+	_, err = obsClient.Scenes.SetCurrentProgramScene(&scenes.SetCurrentProgramSceneParams{
+		SceneName: s.ProgramSceneName,
+	})
+	return err
+}
+
+func (s SetCurrentProgramScene) Code() string {
+	return "SetCurrentProgramScene"
+}
+
+func (s SetCurrentProgramScene) Description() string {
+	return "Sets current Program Scene"
+}
+
+type SetCurrentPreviewScene struct {
+	PreviewSceneName string `hubman:"preview_scene_name"`
+}
+
+func (s SetCurrentPreviewScene) Run(p ObsProvider, _ *zap.Logger) error {
+	obsClient, err := p.Provide()
+	if err != nil {
+		return err
+	}
+	_, err = obsClient.Scenes.SetCurrentPreviewScene(&scenes.SetCurrentPreviewSceneParams{
+		SceneName: s.PreviewSceneName,
+	})
+	return err
+}
+
+func (s SetCurrentPreviewScene) Code() string {
+	return "SetCurrentPreviewScene"
+}
+
+func (s SetCurrentPreviewScene) Description() string {
+	return "Sets current Preview Scene"
+}
+
+/*----------------------------- Set SceneItem --------------------------------*/
diff --git a/pkg/command/source.go b/pkg/command/source.go
new file mode 100644
index 0000000000000000000000000000000000000000..8451bb9212d43118a1196eeea0d7cc081fa74172
--- /dev/null
+++ b/pkg/command/source.go
@@ -0,0 +1,123 @@
+package command
+
+import (
+	"errors"
+
+	"git.miem.hse.ru/hubman/hubman-lib"
+	"git.miem.hse.ru/hubman/hubman-lib/core"
+	ex "git.miem.hse.ru/hubman/hubman-lib/executor"
+	"github.com/andreykaipov/goobs/api/requests/inputs"
+	"github.com/andreykaipov/goobs/api/requests/sceneitems"
+	"go.uber.org/zap"
+)
+
+func ProvideSourceCommands(obsManager ObsProvider, l *zap.Logger) []func(ex.Executor) {
+	return []func(ex.Executor){
+		hubman.WithCommand(SetInputMute{}, func(_ core.SerializedCommand, cp ex.CommandParser) {
+			cmd := SetInputMute{}
+			cp(&cmd)
+			cmd.Run(obsManager, l.Named(cmd.Code()))
+		}),
+		hubman.WithCommand(ToggleInputMute{}, func(_ core.SerializedCommand, cp ex.CommandParser) {
+			cmd := ToggleInputMute{}
+			cp(&cmd)
+			cmd.Run(obsManager, l.Named(cmd.Code()))
+		}),
+		hubman.WithCommand(ToggleSceneItemEnabled{}, func(_ core.SerializedCommand, cp ex.CommandParser) {
+			cmd := ToggleSceneItemEnabled{}
+			cp(&cmd)
+			cmd.Run(obsManager, l.Named(cmd.Code()))
+		}),
+	}
+}
+
+var _ RunnableCommand = &SetInputMute{}
+var _ RunnableCommand = &ToggleInputMute{}
+var _ RunnableCommand = &ToggleSceneItemEnabled{}
+
+type SetInputMute struct {
+	InputName string `hubman:"input_name"`
+	Muted     bool   `hubman:"muted"`
+}
+
+func (s SetInputMute) Run(p ObsProvider, _ *zap.Logger) error {
+	obsClient, err := p.Provide()
+	if err != nil {
+		return err
+	}
+	_, err = obsClient.Inputs.SetInputMute(&inputs.SetInputMuteParams{
+		InputName: s.InputName,
+	})
+	return err
+}
+
+func (s SetInputMute) Code() string {
+	return "SetInputMute"
+}
+
+func (s SetInputMute) Description() string {
+	return "Sets the audio mute state of an input with given muted property"
+}
+
+type ToggleInputMute struct {
+	InputName string `hubman:"input_name"`
+}
+
+func (t ToggleInputMute) Run(p ObsProvider, _ *zap.Logger) error {
+	obsClient, err := p.Provide()
+	if err != nil {
+		return err
+	}
+	_, err = obsClient.Inputs.ToggleInputMute(&inputs.ToggleInputMuteParams{
+		InputName: t.InputName,
+	})
+	return err
+}
+
+func (t ToggleInputMute) Code() string {
+	return "ToggleInputMute"
+}
+
+func (t ToggleInputMute) Description() string {
+	return "Toggles the audio mute state of a given input. Ex true->false, false->true"
+}
+
+type ToggleSceneItemEnabled struct {
+	SceneItemName string `hubman:"scene_item_name"`
+	SceneName     string `hubman:"scene_name"`
+}
+
+func (t ToggleSceneItemEnabled) Run(p ObsProvider, _ *zap.Logger) error {
+	obsClient, err := p.Provide()
+	if err != nil {
+		return err
+	}
+	items, err := obsClient.SceneItems.GetSceneItemList(&sceneitems.GetSceneItemListParams{
+		SceneName: t.SceneName,
+	})
+	if err != nil {
+		return err
+	}
+	for _, item := range items.SceneItems {
+		if item.SourceName == t.SceneItemName {
+			enabled := !item.SceneItemEnabled
+			_, err = obsClient.SceneItems.SetSceneItemEnabled(
+				&sceneitems.SetSceneItemEnabledParams{
+					SceneName:        t.SceneName,
+					SceneItemId:      float64(item.SceneItemID),
+					SceneItemEnabled: &enabled,
+				},
+			)
+			return err
+		}
+	}
+	return errors.New("not found scene item")
+}
+
+func (t ToggleSceneItemEnabled) Code() string {
+	return "ToggleSceneItemEnabled"
+}
+
+func (t ToggleSceneItemEnabled) Description() string {
+	return "Toggles the scene item enabled state, searches for it using current scene. Ex true->false, false->true"
+}
diff --git a/pkg/command/stream.go b/pkg/command/stream.go
new file mode 100644
index 0000000000000000000000000000000000000000..021fda5ea648bfb9c80b31357760e7c67b76162f
--- /dev/null
+++ b/pkg/command/stream.go
@@ -0,0 +1,126 @@
+package command
+
+import (
+	"git.miem.hse.ru/hubman/hubman-lib"
+	"git.miem.hse.ru/hubman/hubman-lib/core"
+	ex "git.miem.hse.ru/hubman/hubman-lib/executor"
+	"github.com/andreykaipov/goobs/api/requests/stream"
+	"go.uber.org/zap"
+)
+
+func ProvideStreamCommands(obsManager ObsProvider, l *zap.Logger) []func(ex.Executor) {
+	return []func(ex.Executor){
+		hubman.WithCommand(ToggleStream{}, func(_ core.SerializedCommand, cp ex.CommandParser) {
+			cmd := ToggleStream{}
+			cp(&cmd)
+			cmd.Run(obsManager, l.Named(cmd.Code()))
+		}),
+		hubman.WithCommand(StartStream{}, func(_ core.SerializedCommand, cp ex.CommandParser) {
+			cmd := StartStream{}
+			cp(&cmd)
+			cmd.Run(obsManager, l.Named(cmd.Code()))
+		}),
+		hubman.WithCommand(StopStream{}, func(_ core.SerializedCommand, cp ex.CommandParser) {
+			cmd := StopStream{}
+			cp(&cmd)
+			cmd.Run(obsManager, l.Named(cmd.Code()))
+		}),
+		hubman.WithCommand(SendStreamCaption{}, func(_ core.SerializedCommand, cp ex.CommandParser) {
+			cmd := SendStreamCaption{}
+			cp(&cmd)
+			cmd.Run(obsManager, l.Named(cmd.Code()))
+		}),
+	}
+}
+
+/*----------------------------- Toggle/Start/Stop Stream -------------------*/
+
+var _ RunnableCommand = &ToggleStream{}
+var _ RunnableCommand = &StartStream{}
+var _ RunnableCommand = &StopStream{}
+var _ RunnableCommand = &SendStreamCaption{}
+
+type ToggleStream struct {
+}
+
+func (t ToggleStream) Code() string {
+	return "ToggleStream"
+}
+
+func (t ToggleStream) Description() string {
+	return "Toggles Stream, ex: streaming -> stop, stop -> streaming"
+}
+
+func (t ToggleStream) Run(p ObsProvider, _ *zap.Logger) error {
+	obsClient, err := p.Provide()
+	if err != nil {
+		return err
+	}
+	_, err = obsClient.Stream.ToggleStream()
+	return err
+}
+
+type StartStream struct {
+}
+
+func (s StartStream) Code() string {
+	return "StartStream"
+}
+
+func (s StartStream) Description() string {
+	return "Starts Stream, if it is already running is no-op"
+}
+
+func (s StartStream) Run(p ObsProvider, _ *zap.Logger) error {
+	obsClient, err := p.Provide()
+	if err != nil {
+		return err
+	}
+	_, err = obsClient.Stream.StartStream()
+	return err
+}
+
+type StopStream struct {
+}
+
+func (s StopStream) Code() string {
+	return "StopStream"
+}
+
+func (s StopStream) Description() string {
+	return "Stops Stream, if it is off - is no-op"
+}
+
+func (s StopStream) Run(p ObsProvider, _ *zap.Logger) error {
+	obsClient, err := p.Provide()
+	if err != nil {
+		return err
+	}
+	_, err = obsClient.Stream.StopStream()
+	return err
+}
+
+/*----------------------------- SendCaption for stream -----------------------*/
+
+type SendStreamCaption struct {
+	StreamCaption string `hubman:"stream_caption"`
+}
+
+func (s SendStreamCaption) Code() string {
+	return "SendStreamCaption"
+}
+
+func (s SendStreamCaption) Description() string {
+	return "Sends StreamCaption"
+}
+
+func (s SendStreamCaption) Run(p ObsProvider, _ *zap.Logger) error {
+	obsClient, err := p.Provide()
+	if err != nil {
+		return err
+	}
+	_, err = obsClient.Stream.SendStreamCaption(&stream.SendStreamCaptionParams{
+		CaptionText: s.StreamCaption,
+	})
+	return err
+}
diff --git a/pkg/command/transition.go b/pkg/command/transition.go
new file mode 100644
index 0000000000000000000000000000000000000000..c4eafbb02bb6304698ba60dece80e984fc15af4f
--- /dev/null
+++ b/pkg/command/transition.go
@@ -0,0 +1,154 @@
+package command
+
+import (
+	"git.miem.hse.ru/hubman/hubman-lib"
+	"git.miem.hse.ru/hubman/hubman-lib/core"
+	ex "git.miem.hse.ru/hubman/hubman-lib/executor"
+	"github.com/andreykaipov/goobs/api/requests/transitions"
+	"go.uber.org/zap"
+)
+
+func ProvideTransitionCommands(obsManager ObsProvider, l *zap.Logger) []func(ex.Executor) {
+	return []func(ex.Executor){
+		hubman.WithCommand(TriggerStudioModeTransition{}, func(_ core.SerializedCommand, cp ex.CommandParser) {
+			cmd := TriggerStudioModeTransition{}
+			cp(&cmd)
+			cmd.Run(obsManager, l.Named(cmd.Code()))
+		}),
+		hubman.WithCommand(SetCurrentSceneTransition{}, func(_ core.SerializedCommand, cp ex.CommandParser) {
+			cmd := SetCurrentSceneTransition{}
+			cp(&cmd)
+			cmd.Run(obsManager, l.Named(cmd.Code()))
+		}),
+		hubman.WithCommand(TriggerStudioModeTransitionWithName{}, func(_ core.SerializedCommand, cp ex.CommandParser) {
+			cmd := TriggerStudioModeTransitionWithName{}
+			cp(&cmd)
+			cmd.Run(obsManager, l.Named(cmd.Code()))
+		}),
+	}
+}
+
+var _ RunnableCommand = &SetCurrentSceneTransition{}
+var _ RunnableCommand = &TriggerStudioModeTransition{}
+var _ RunnableCommand = &TriggerStudioModeTransitionWithName{}
+
+// TODO add commands to executor
+type SetSceneSceneTransitionOverride struct {
+	SceneName          string  `hubman:"scene_name"`
+	TransitionDuration float64 `hubman:"transition_duration"`
+	TransitionName     string  `hubman:"transition_name"`
+}
+
+func (s SetSceneSceneTransitionOverride) Code() string {
+	return "SetSceneSceneTransitionOverride"
+}
+
+func (s SetSceneSceneTransitionOverride) Description() string {
+	return "Sets SceneScene Transition Override"
+}
+
+type SetCurrentSceneTransition struct {
+	TransitionName string `hubman:"transition_name"`
+}
+
+func (s SetCurrentSceneTransition) Code() string {
+	return "SetCurrentSceneTransition"
+}
+
+func (s SetCurrentSceneTransition) Description() string {
+	return "Sets Current Scene Transition"
+}
+
+func (s SetCurrentSceneTransition) Run(p ObsProvider, _ *zap.Logger) error {
+	obsClient, err := p.Provide()
+	if err != nil {
+		return err
+	}
+	_, err = obsClient.Transitions.SetCurrentSceneTransition(&transitions.SetCurrentSceneTransitionParams{
+		TransitionName: s.TransitionName,
+	})
+	return err
+}
+
+type TriggerStudioModeTransition struct {
+}
+
+func (s TriggerStudioModeTransition) Code() string {
+	return "TriggerStudioModeTransition"
+}
+
+func (s TriggerStudioModeTransition) Description() string {
+	return "Triggers selected in OBS studio mode transition"
+}
+
+func (s TriggerStudioModeTransition) Run(p ObsProvider, _ *zap.Logger) error {
+	obsClient, err := p.Provide()
+	if err != nil {
+		return err
+	}
+	_, err = obsClient.Transitions.TriggerStudioModeTransition()
+	return err
+}
+
+type TriggerStudioModeTransitionWithName struct {
+	TransitionName string `hubman:"transition_name"`
+}
+
+func (s TriggerStudioModeTransitionWithName) Code() string {
+	return "TriggerStudioModeTransitionWithName"
+}
+
+func (s TriggerStudioModeTransitionWithName) Description() string {
+	return "Triggers studio mode transition with name included in command"
+}
+
+func (s TriggerStudioModeTransitionWithName) Run(p ObsProvider, l *zap.Logger) error {
+	obsClient, err := p.Provide()
+	if err != nil {
+		return err
+	}
+	curTransition, err := obsClient.Transitions.GetCurrentSceneTransition()
+	if err != nil {
+		return err
+	}
+	_, err = obsClient.Transitions.SetCurrentSceneTransition(&transitions.SetCurrentSceneTransitionParams{
+		TransitionName: s.TransitionName,
+	})
+	if err != nil {
+		return err
+	}
+	_, err = obsClient.Transitions.TriggerStudioModeTransition()
+	if err != nil {
+		return err
+	}
+	_, err = obsClient.Transitions.SetCurrentSceneTransition(&transitions.SetCurrentSceneTransitionParams{
+		TransitionName: curTransition.TransitionName,
+	})
+	return err
+}
+
+type SetCurrentSceneCollection struct {
+	SceneCollectionName string `hubman:"scene_collection_name"`
+}
+
+func (s SetCurrentSceneCollection) Code() string {
+	return "SetCurrentSceneCollection"
+}
+
+func (s SetCurrentSceneCollection) Description() string {
+	return "Sets Current Scene Collection"
+}
+
+type SetSceneItemBlendMode struct {
+	SceneItemBlendMode string  `hubman:"scene_name"`
+	SceneItemId        float64 `hubman:"scene_item_id"`
+	SceneName          string  `hubman:"scene_name"`
+}
+
+func (s SetSceneItemBlendMode) Code() string {
+	return "SetSceneItemBlendMode"
+}
+
+func (s SetSceneItemBlendMode) Description() string {
+	return "Sets Scene ItemBlendMode"
+}
diff --git a/pkg/command/ui.go b/pkg/command/ui.go
new file mode 100644
index 0000000000000000000000000000000000000000..f97fa73e62eabbb7363e8e396544a755d7b70457
--- /dev/null
+++ b/pkg/command/ui.go
@@ -0,0 +1,42 @@
+package command
+
+import (
+	"git.miem.hse.ru/hubman/hubman-lib"
+	"git.miem.hse.ru/hubman/hubman-lib/core"
+	ex "git.miem.hse.ru/hubman/hubman-lib/executor"
+	"github.com/andreykaipov/goobs/api/requests/ui"
+	"go.uber.org/zap"
+)
+
+func ProvideUiCommands(obsProvider ObsProvider, l *zap.Logger) []func(ex.Executor) {
+	return []func(ex.Executor){
+		hubman.WithCommand(SetStudioModeEnabled{}, func(_ core.SerializedCommand, cp ex.CommandParser) {
+			cmd := SetStudioModeEnabled{}
+			cp(&cmd)
+			cmd.Run(obsProvider, l.Named(cmd.Code()))
+		}),
+	}
+}
+
+type SetStudioModeEnabled struct {
+	UseStudioMode bool `hubman:"use_studio_mode"`
+}
+
+func (s SetStudioModeEnabled) Run(p ObsProvider, _ *zap.Logger) error {
+	obsClient, err := p.Provide()
+	if err != nil {
+		return err
+	}
+	_, err = obsClient.Ui.SetStudioModeEnabled(&ui.SetStudioModeEnabledParams{
+		StudioModeEnabled: &s.UseStudioMode,
+	})
+	return err
+}
+
+func (s SetStudioModeEnabled) Description() string {
+	return "Enables or disables studio mode with given property"
+}
+
+func (s SetStudioModeEnabled) Code() string {
+	return "SetStudioModeEnabled"
+}
diff --git a/pkg/command/virtual_cam.go b/pkg/command/virtual_cam.go
new file mode 100644
index 0000000000000000000000000000000000000000..506e0c7c0d68098d888e5eff56f9e974e36eae52
--- /dev/null
+++ b/pkg/command/virtual_cam.go
@@ -0,0 +1,92 @@
+package command
+
+import (
+	"git.miem.hse.ru/hubman/hubman-lib"
+	"git.miem.hse.ru/hubman/hubman-lib/core"
+	ex "git.miem.hse.ru/hubman/hubman-lib/executor"
+	"go.uber.org/zap"
+)
+
+func ProvideVirtualCameraCommands(obsManager ObsProvider, l *zap.Logger) []func(ex.Executor) {
+	return []func(ex.Executor){
+		hubman.WithCommand(ToggleVirtualCam{}, func(_ core.SerializedCommand, cp ex.CommandParser) {
+			cmd := ToggleVirtualCam{}
+			cp(&cmd)
+			cmd.Run(obsManager, l.Named(cmd.Code()))
+		}),
+		hubman.WithCommand(StopVirtualCam{}, func(_ core.SerializedCommand, cp ex.CommandParser) {
+			cmd := StopVirtualCam{}
+			cp(&cmd)
+			cmd.Run(obsManager, l.Named(cmd.Code()))
+		}),
+		hubman.WithCommand(StartVirtualCam{}, func(_ core.SerializedCommand, cp ex.CommandParser) {
+			cmd := StartVirtualCam{}
+			cp(&cmd)
+			cmd.Run(obsManager, l.Named(cmd.Code()))
+		}),
+	}
+}
+
+var _ RunnableCommand = &ToggleVirtualCam{}
+var _ RunnableCommand = &StopVirtualCam{}
+var _ RunnableCommand = &StartVirtualCam{}
+
+type ToggleVirtualCam struct {
+}
+
+func (t ToggleVirtualCam) Code() string {
+	return "ToggleVirtualCam"
+}
+
+func (t ToggleVirtualCam) Description() string {
+	return "Toggles VirtualCam, ex: enabled -> disable, disabled -> enable"
+}
+
+func (t ToggleVirtualCam) Run(p ObsProvider, _ *zap.Logger) error {
+	obsClient, err := p.Provide()
+	if err != nil {
+		return err
+	}
+	_, err = obsClient.Outputs.ToggleVirtualCam()
+	return err
+}
+
+type StopVirtualCam struct {
+}
+
+func (s StopVirtualCam) Code() string {
+	return "StopVirtualCam"
+}
+
+func (s StopVirtualCam) Description() string {
+	return "Stops VirtualCam, if it is not running - is no-op"
+}
+
+func (s StopVirtualCam) Run(p ObsProvider, _ *zap.Logger) error {
+	obsClient, err := p.Provide()
+	if err != nil {
+		return err
+	}
+	_, err = obsClient.Outputs.StopVirtualCam()
+	return err
+}
+
+type StartVirtualCam struct {
+}
+
+func (s StartVirtualCam) Code() string {
+	return "StartVirtualCam"
+}
+
+func (s StartVirtualCam) Description() string {
+	return "Starts VirtualCam, if it is already running - is no-op"
+}
+
+func (s StartVirtualCam) Run(p ObsProvider, _ *zap.Logger) error {
+	obsClient, err := p.Provide()
+	if err != nil {
+		return err
+	}
+	_, err = obsClient.Outputs.StartVirtualCam()
+	return err
+}
diff --git a/pkg/config.go b/pkg/config.go
new file mode 100644
index 0000000000000000000000000000000000000000..f2447d2ebbdeac6e27a5b93266559b8017cbb268
--- /dev/null
+++ b/pkg/config.go
@@ -0,0 +1,20 @@
+package obsman
+
+import (
+	hcore "git.miem.hse.ru/hubman/hubman-lib/core"
+)
+
+type ObsConf struct {
+	HostPort string `yaml:"host_port" json:"host_port"`
+	Password string `yaml:"password" json:"password"`
+}
+
+type Conf struct {
+	ObsConf ObsConf `yaml:"obs" json:"obs"`
+}
+
+var _ hcore.Configuration = &Conf{}
+
+func (o Conf) Validate() error {
+	return nil
+}
diff --git a/pkg/manager.go b/pkg/manager.go
new file mode 100644
index 0000000000000000000000000000000000000000..37d15fbd3479b2301d65ad5c23839621806e8f61
--- /dev/null
+++ b/pkg/manager.go
@@ -0,0 +1,176 @@
+package obsman
+
+import (
+	"context"
+	"errors"
+	cmd "obs-man/pkg/command"
+	osig "obs-man/pkg/signal"
+	"reflect"
+
+	"git.miem.hse.ru/hubman/hubman-lib/core"
+	"github.com/andreykaipov/goobs"
+	obsevents "github.com/andreykaipov/goobs/api/events"
+	"go.uber.org/zap"
+)
+
+var _ cmd.ObsProvider = &manager{}
+
+type manager struct {
+	conf   ObsConf
+	logger *zap.Logger
+	client *goobs.Client
+
+	connected    bool
+	listenCtx    context.Context
+	cancelListen context.CancelFunc
+	signals      chan<- core.Signal
+}
+
+func (m *manager) listenEvents() {
+	for {
+		select {
+		case ev := <-m.client.IncomingEvents:
+			m.logger.Debug(
+				"Received event from obs",
+				zap.Any("event", ev),
+				zap.String("event_type", reflect.TypeOf(ev).String()),
+			)
+			m.processObsEvent(ev)
+		case <-m.listenCtx.Done():
+			return
+		}
+	}
+}
+
+func (m *manager) processObsEvent(event interface{}) {
+	switch e := event.(type) {
+	case *obsevents.CurrentPreviewSceneChanged:
+		m.signals <- &osig.CurrentPreviewSceneChanged{
+			SceneName: e.SceneName,
+		}
+	case obsevents.CurrentProgramSceneChanged:
+		m.signals <- &osig.CurrentProgramSceneChanged{
+			SceneName: e.SceneName,
+		}
+	case obsevents.InputMuteStateChanged:
+		m.signals <- &osig.InputMuteStateChanged{
+			InputName:  e.InputName,
+			InputMuted: e.InputMuted,
+		}
+	case obsevents.InputVolumeChanged:
+		m.signals <- &osig.InputVolumeChanged{
+			InputName:      e.InputName,
+			InputVolumeMul: e.InputVolumeMul,
+			InputVolumeDb:  e.InputVolumeDb,
+		}
+	case obsevents.RecordStateChanged:
+		m.signals <- &osig.RecordStateChanged{
+			OutputActive: e.OutputActive,
+			OutputState:  e.OutputState,
+			OutputPath:   e.OutputPath,
+		}
+	case obsevents.ReplayBufferStateChanged:
+		m.signals <- &osig.ReplayBufferStateChanged{
+			OutputActive: e.OutputActive,
+			OutputState:  e.OutputState,
+		}
+	case obsevents.ReplayBufferSaved:
+		m.signals <- &osig.ReplayBufferSaved{
+			SavedReplayPath: e.SavedReplayPath,
+		}
+	case obsevents.SceneItemEnableStateChanged:
+		m.signals <- &osig.SceneItemEnableStateChanged{
+			SceneName:        e.SceneName,
+			SceneItemId:      int(e.SceneItemId),
+			SceneItemEnabled: e.SceneItemEnabled,
+		}
+	case obsevents.ScreenshotSaved:
+		m.signals <- &osig.ScreenshotSaved{
+			SavedScreenshotPath: e.SavedScreenshotPath,
+		}
+	case obsevents.StreamStateChanged:
+		m.signals <- &osig.StreamStateChanged{
+			OutputActive: e.OutputActive,
+			OutputState:  e.OutputState,
+		}
+	case obsevents.SceneTransitionStarted:
+		m.signals <- &osig.SceneTransitionStarted{
+			TransitionName: e.TransitionName,
+		}
+	case obsevents.SceneTransitionEnded:
+		m.signals <- &osig.SceneTransitionEnded{
+			TransitionName: e.TransitionName,
+		}
+	case obsevents.SceneTransitionVideoEnded:
+		m.signals <- &osig.SceneTransitionVideoEnded{
+			TransitionName: e.TransitionName,
+		}
+	case obsevents.StudioModeStateChanged:
+		m.signals <- &osig.StudioModeStateChanged{
+			StudioModeEnabled: e.StudioModeEnabled,
+		}
+	case obsevents.VirtualcamStateChanged:
+		m.signals <- &osig.VirtualCamStateChanged{
+			OutputActive: e.OutputActive,
+			OutputState:  e.OutputState,
+		}
+	case *obsevents.ExitStarted:
+		m.Close()
+	}
+}
+
+func (m *manager) Provide() (*goobs.Client, error) {
+	if m.client == nil {
+		return nil, errors.New("no opened obs connection")
+	}
+	return m.client, nil
+}
+
+func (m *manager) Close() error {
+	m.connected = false
+	m.cancelListen()
+	_ = m.client.Disconnect()
+	return nil
+}
+
+func (m *manager) UpdateConn(c ObsConf) error {
+	ctxlog := m.logger.With(zap.Any("config", c))
+	ctxlog.Debug("Updating obs connection")
+	if c == m.conf && m.connected {
+		ctxlog.Debug("Already connected")
+		return nil
+	}
+
+	client, err := goobs.New(c.HostPort, goobs.WithPassword(c.Password))
+	if err != nil {
+		ctxlog.Error("Failed to connect to obs", zap.Error(err))
+		return err
+	}
+
+	m.cancelListen()
+	m.client = client
+	m.listenCtx, m.cancelListen = context.WithCancel(context.Background())
+
+	go m.listenEvents()
+	m.connected = true
+
+	return nil
+}
+
+func NewManager(c ObsConf, logger *zap.Logger, signalsCh chan<- core.Signal) (*manager, error) {
+	client, err := goobs.New(c.HostPort, goobs.WithPassword(c.Password))
+	if err != nil {
+		return nil, err
+	}
+	m := &manager{
+		client:    client,
+		logger:    logger.Named("OBSManager"),
+		signals:   signalsCh,
+		connected: true,
+	}
+
+	m.listenCtx, m.cancelListen = context.WithCancel(context.Background())
+	go m.listenEvents()
+
+	return m, nil
+}
diff --git a/pkg/signal/current_scene.go b/pkg/signal/current_scene.go
new file mode 100644
index 0000000000000000000000000000000000000000..8f081f97de2d08882e28995d9b08f7e32879eb71
--- /dev/null
+++ b/pkg/signal/current_scene.go
@@ -0,0 +1,35 @@
+package signal
+
+import (
+	"git.miem.hse.ru/hubman/hubman-lib"
+	"git.miem.hse.ru/hubman/hubman-lib/manipulator"
+)
+
+var CurrentSceneSignals []func(manipulator.Manipulator) = []func(manipulator.Manipulator){
+	hubman.WithSignal[CurrentProgramSceneChanged](),
+	hubman.WithSignal[CurrentPreviewSceneChanged](),
+}
+
+type CurrentProgramSceneChanged struct {
+	SceneName string `hubman:"scene_name"`
+}
+
+func (c CurrentProgramSceneChanged) Code() string {
+	return "CurrentProgramSceneChanged"
+}
+
+func (c CurrentProgramSceneChanged) Description() string {
+	return "Sent when current program scene changes to included scene"
+}
+
+type CurrentPreviewSceneChanged struct {
+	SceneName string `hubman:"scene_name"`
+}
+
+func (c CurrentPreviewSceneChanged) Code() string {
+	return "CurrentPreviewSceneChanged"
+}
+
+func (c CurrentPreviewSceneChanged) Description() string {
+	return "Sent when current preview scene changes to included scene"
+}
diff --git a/pkg/signal/input.go b/pkg/signal/input.go
new file mode 100644
index 0000000000000000000000000000000000000000..59ca3256d527f6bcde767f7927a87da348e33f70
--- /dev/null
+++ b/pkg/signal/input.go
@@ -0,0 +1,38 @@
+package signal
+
+import (
+	"git.miem.hse.ru/hubman/hubman-lib"
+	"git.miem.hse.ru/hubman/hubman-lib/manipulator"
+)
+
+var InputSignals []func(manipulator.Manipulator) = []func(manipulator.Manipulator){
+	hubman.WithSignal[InputMuteStateChanged](),
+	hubman.WithSignal[InputVolumeChanged](),
+}
+
+type InputMuteStateChanged struct {
+	InputName  string `hubman:"input_name"`
+	InputMuted bool   `hubman:"input_muted"`
+}
+
+func (i InputMuteStateChanged) Code() string {
+	return "InputMuteStateChanged"
+}
+
+func (i InputMuteStateChanged) Description() string {
+	return "Sent when input with included name mutes or unmutes"
+}
+
+type InputVolumeChanged struct {
+	InputName      string  `hubman:"input_name"`
+	InputVolumeMul float64 `hubman:"input_volume_mul"`
+	InputVolumeDb  float64 `hubman:"input_volume_db"`
+}
+
+func (i InputVolumeChanged) Code() string {
+	return "InputVolumeChanged"
+}
+
+func (i InputVolumeChanged) Description() string {
+	return "Sent when input with included name changes its volume"
+}
diff --git a/pkg/signal/record.go b/pkg/signal/record.go
new file mode 100644
index 0000000000000000000000000000000000000000..fc040d3bcae494dbd844f7fbbe7afb1aeb270912
--- /dev/null
+++ b/pkg/signal/record.go
@@ -0,0 +1,24 @@
+package signal
+
+import (
+	"git.miem.hse.ru/hubman/hubman-lib"
+	"git.miem.hse.ru/hubman/hubman-lib/manipulator"
+)
+
+var RecordSignals []func(manipulator.Manipulator) = []func(manipulator.Manipulator){
+	hubman.WithSignal[RecordStateChanged](),
+}
+
+type RecordStateChanged struct {
+	OutputActive bool   `hubman:"output_active"`
+	OutputState  string `hubman:"output_state"`
+	OutputPath   string `hubman:"output_path"`
+}
+
+func (r RecordStateChanged) Code() string {
+	return "RecordStateChanged"
+}
+
+func (r RecordStateChanged) Description() string {
+	return "Sent when record output state changes, for OutputState values see obs docs"
+}
diff --git a/pkg/signal/replay_buffer.go b/pkg/signal/replay_buffer.go
new file mode 100644
index 0000000000000000000000000000000000000000..2a8f19831690f8e176a0dc7c43f33e3621678dac
--- /dev/null
+++ b/pkg/signal/replay_buffer.go
@@ -0,0 +1,36 @@
+package signal
+
+import (
+	"git.miem.hse.ru/hubman/hubman-lib"
+	"git.miem.hse.ru/hubman/hubman-lib/manipulator"
+)
+
+var ReplayBufferSignals []func(manipulator.Manipulator) = []func(manipulator.Manipulator){
+	hubman.WithSignal[ReplayBufferSaved](),
+	hubman.WithSignal[ReplayBufferStateChanged](),
+}
+
+type ReplayBufferStateChanged struct {
+	OutputActive bool   `hubman:"output_active"`
+	OutputState  string `hubman:"output_state"`
+}
+
+func (r ReplayBufferStateChanged) Code() string {
+	return "ReplayBufferStateChanged"
+}
+
+func (r ReplayBufferStateChanged) Description() string {
+	return "Sent when the replay buffer state changes, for OutputState values see obs docs"
+}
+
+type ReplayBufferSaved struct {
+	SavedReplayPath string `hubman:"saved_replay_path"`
+}
+
+func (r ReplayBufferSaved) Code() string {
+	return "ReplayBufferSaved"
+}
+
+func (r ReplayBufferSaved) Description() string {
+	return "Sent when the replay buffer has been saved to included path"
+}
diff --git a/pkg/signal/scene_item.go b/pkg/signal/scene_item.go
new file mode 100644
index 0000000000000000000000000000000000000000..15fe8c92b799506c032af05307084174d6aa4a4c
--- /dev/null
+++ b/pkg/signal/scene_item.go
@@ -0,0 +1,24 @@
+package signal
+
+import (
+	"git.miem.hse.ru/hubman/hubman-lib"
+	"git.miem.hse.ru/hubman/hubman-lib/manipulator"
+)
+
+var SceneItemSignals []func(manipulator.Manipulator) = []func(manipulator.Manipulator){
+	hubman.WithSignal[SceneItemEnableStateChanged](),
+}
+
+type SceneItemEnableStateChanged struct {
+	SceneName        string `hubman:"scene_name"`
+	SceneItemId      int    `hubman:"scene_item_id"`
+	SceneItemEnabled bool   `hubman:"scene_item_enabled"`
+}
+
+func (s SceneItemEnableStateChanged) Code() string {
+	return "SceneItemEnableStateChanged"
+}
+
+func (s SceneItemEnableStateChanged) Description() string {
+	return "Sent when item's enabled state has changed"
+}
diff --git a/pkg/signal/screenshot.go b/pkg/signal/screenshot.go
new file mode 100644
index 0000000000000000000000000000000000000000..4c3da9d01fd1ce9099865f74d6097e2ec6413d22
--- /dev/null
+++ b/pkg/signal/screenshot.go
@@ -0,0 +1,22 @@
+package signal
+
+import (
+	"git.miem.hse.ru/hubman/hubman-lib"
+	"git.miem.hse.ru/hubman/hubman-lib/manipulator"
+)
+
+var ScreenshotSignals []func(manipulator.Manipulator) = []func(manipulator.Manipulator){
+	hubman.WithSignal[ScreenshotSaved](),
+}
+
+type ScreenshotSaved struct {
+	SavedScreenshotPath string `hubman:"saved_screenshot_path"`
+}
+
+func (s ScreenshotSaved) Code() string {
+	return "ScreenshotSaved"
+}
+
+func (s ScreenshotSaved) Description() string {
+	return "Sent when a screenshot has been saved"
+}
diff --git a/pkg/signal/stream.go b/pkg/signal/stream.go
new file mode 100644
index 0000000000000000000000000000000000000000..c59cd7817b9fa972d2b0135882239a42963caae5
--- /dev/null
+++ b/pkg/signal/stream.go
@@ -0,0 +1,23 @@
+package signal
+
+import (
+	"git.miem.hse.ru/hubman/hubman-lib"
+	"git.miem.hse.ru/hubman/hubman-lib/manipulator"
+)
+
+var StreamSignals []func(manipulator.Manipulator) = []func(manipulator.Manipulator){
+	hubman.WithSignal[StreamStateChanged](),
+}
+
+type StreamStateChanged struct {
+	OutputActive bool   `hubman:"output_active"`
+	OutputState  string `hubman:"output_state"`
+}
+
+func (s StreamStateChanged) Code() string {
+	return "StreamStateChanged"
+}
+
+func (s StreamStateChanged) Description() string {
+	return "Sent when stream output state changes, for OutputState values see obs docs"
+}
diff --git a/pkg/signal/transition.go b/pkg/signal/transition.go
new file mode 100644
index 0000000000000000000000000000000000000000..f8e080cb5969515fec1133185757bd024db58751
--- /dev/null
+++ b/pkg/signal/transition.go
@@ -0,0 +1,48 @@
+package signal
+
+import (
+	"git.miem.hse.ru/hubman/hubman-lib"
+	"git.miem.hse.ru/hubman/hubman-lib/manipulator"
+)
+
+var TransitionSignals []func(manipulator.Manipulator) = []func(manipulator.Manipulator){
+	hubman.WithSignal[SceneTransitionEnded](),
+	hubman.WithSignal[SceneTransitionStarted](),
+	hubman.WithSignal[SceneTransitionVideoEnded](),
+}
+
+type SceneTransitionEnded struct {
+	TransitionName string `hubman:"transition_name"`
+}
+
+func (c SceneTransitionEnded) Code() string {
+	return "SceneTransitionEnded"
+}
+
+func (c SceneTransitionEnded) Description() string {
+	return "Sent when scene transition has completed fully, i.e. not interrupted by user"
+}
+
+type SceneTransitionStarted struct {
+	TransitionName string `hubman:"transition_name"`
+}
+
+func (c SceneTransitionStarted) Code() string {
+	return "SceneTransitionStarted"
+}
+
+func (c SceneTransitionStarted) Description() string {
+	return "Sent when scene transition has started"
+}
+
+type SceneTransitionVideoEnded struct {
+	TransitionName string `hubman:"transition_name"`
+}
+
+func (c SceneTransitionVideoEnded) Code() string {
+	return "SceneTransitionStarted"
+}
+
+func (c SceneTransitionVideoEnded) Description() string {
+	return "Sent when scene transition video complets fully, see obs docs for concrete explanation"
+}
diff --git a/pkg/signal/ui.go b/pkg/signal/ui.go
new file mode 100644
index 0000000000000000000000000000000000000000..692c6c35b63b7937353066cb5495557ab9ac3505
--- /dev/null
+++ b/pkg/signal/ui.go
@@ -0,0 +1,22 @@
+package signal
+
+import (
+	"git.miem.hse.ru/hubman/hubman-lib"
+	"git.miem.hse.ru/hubman/hubman-lib/manipulator"
+)
+
+var UISignals []func(manipulator.Manipulator) = []func(manipulator.Manipulator){
+	hubman.WithSignal[StudioModeStateChanged](),
+}
+
+type StudioModeStateChanged struct {
+	StudioModeEnabled bool `hubman:"studio_mode_enabled"`
+}
+
+func (s StudioModeStateChanged) Code() string {
+	return "StudioModeStateChanged"
+}
+
+func (s StudioModeStateChanged) Description() string {
+	return "Sent when studio mode has been enabled or disabled"
+}
diff --git a/pkg/signal/virtual_cam.go b/pkg/signal/virtual_cam.go
new file mode 100644
index 0000000000000000000000000000000000000000..6afdf14be0bf776db7c13bde8cae53aae6da7c47
--- /dev/null
+++ b/pkg/signal/virtual_cam.go
@@ -0,0 +1,23 @@
+package signal
+
+import (
+	"git.miem.hse.ru/hubman/hubman-lib"
+	"git.miem.hse.ru/hubman/hubman-lib/manipulator"
+)
+
+var VirtualCamSignals []func(manipulator.Manipulator) = []func(manipulator.Manipulator){
+	hubman.WithSignal[VirtualCamStateChanged](),
+}
+
+type VirtualCamStateChanged struct {
+	OutputActive bool   `hubman:"output_active"`
+	OutputState  string `hubman:"output_state"`
+}
+
+func (v VirtualCamStateChanged) Code() string {
+	return "VirtualCamStateChanged"
+}
+
+func (v VirtualCamStateChanged) Description() string {
+	return "Sent when the virtual cam state changes, for OutputState values see obs docs"
+}
diff --git a/system-config.yaml b/system-config.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..389cfcedf525ffa063b8913d770b37ef7b433d95
--- /dev/null
+++ b/system-config.yaml
@@ -0,0 +1,6 @@
+logging:
+  level: DEBUG
+redis_url: redis://127.0.0.1:6379
+server:
+  ip: 127.0.0.1
+  port: 8094
\ No newline at end of file
diff --git a/user-config.yaml b/user-config.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..dd43663497f4cef44fdee1abbeab75b20f870fcc
--- /dev/null
+++ b/user-config.yaml
@@ -0,0 +1,3 @@
+obs:
+  host_port: 127.0.0.1:4455
+  password: eNYQzlPX48qqBDtw