diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 52e52ffac5f6ec850a16e2673b05709c8bce785c..8c272bef56d344559053762e98a227a9a7630432 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -45,6 +45,7 @@ appdirs = "1.2.0"
 kstore = "0.9.1"
 kmpIo = "0.1.5"
 kotlinSemver = "2.0.0"
+jansi = "2.4.1"
 temerity = "[0.1.0-dev0z+41180a5,0.1.0]"
 
 kotlinx-dataframe = "0.14.2"
@@ -89,7 +90,7 @@ kotlinx-coroutines-debug = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-
 kotlinx-coroutines-swing = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-swing", version.ref = "coroutines" }
 kotlinx-coroutines-slf4j = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-slf4j", version.ref = "coroutines" }
 kotlinx-datetime = { module = "org.jetbrains.kotlinx:kotlinx-datetime", version.ref = "datetime" }
-kotlinx-io = { module = "org.jetbrains.kotlinx:kotlinx-io", version.ref = "kotlinx-io" }
+kotlinx-io = { module = "org.jetbrains.kotlinx:kotlinx-io-core", version.ref = "kotlinx-io" }
 
 ktorfit-lib = { module = "de.jensklingenberg.ktorfit:ktorfit-lib-light", version.ref = "ktorfit" }
 
@@ -102,6 +103,7 @@ kstore = { module = "io.github.xxfast:kstore", version.ref = "kstore" }
 kstore-file = { module = "io.github.xxfast:kstore-file", version.ref = "kstore" }
 kmpIo = { module = "io.github.skolson:kmp-io", version.ref = "kmpIo" }
 kotlinSemver = { module = "io.github.z4kn4fein:semver", version.ref = "kotlinSemver" }
+jansi = { module = "org.fusesource.jansi:jansi", version.ref = "jansi" }
 temerity = { module = "edu.ucsc.its:temerity", version.ref = "temerity" }
 
 sandwich = { module = "com.github.skydoves:sandwich", version.ref = "sandwich" }
diff --git a/temerity/build.gradle.kts b/temerity/build.gradle.kts
index 399af7dc7a0ac163b8357e687b3a69feb61f6a8d..0deccaa3ec015bfdcd317699b9291df1d6666df1 100644
--- a/temerity/build.gradle.kts
+++ b/temerity/build.gradle.kts
@@ -104,11 +104,13 @@ kotlin {
             dependencies {
                 implementation(libs.ktor.client.java)
                 implementation(libs.slf4j.api)
+                implementation(libs.jansi)
             }
         }
         val androidMain by getting {
             dependencies {
                 implementation(libs.ktor.client.android)
+                // TODO: Add slf4f-api
             }
         }
         val jvmTest by getting {
diff --git a/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/core/JsonFactory.kt b/temerity/src/androidMain/kotlin/edu/ucsc/its/temerity/core/LoggerFactory.android.kt
similarity index 72%
rename from temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/core/JsonFactory.kt
rename to temerity/src/androidMain/kotlin/edu/ucsc/its/temerity/core/LoggerFactory.android.kt
index 02f61f54d0c020a81d018f20a3fb4642f3659449..32d677f1394f211c1431f9a53e52d769553bb1d8 100644
--- a/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/core/JsonFactory.kt
+++ b/temerity/src/androidMain/kotlin/edu/ucsc/its/temerity/core/LoggerFactory.android.kt
@@ -17,15 +17,13 @@
  */
 package edu.ucsc.its.temerity.core
 
-import kotlinx.serialization.json.Json
+import co.touchlab.kermit.Logger
+import edu.ucsc.its.temerity.TemClientConfig
 
-/**
- * Factory for creating a Ktor [Json] object.
- * Adapted from the tmdb-api project: https://github.com/MoviebaseApp/tmdb-kotlin/raw/refs/heads/main/tmdb-api/src/commonMain/kotlin/app/moviebase/tmdb/core/JsonFactory.kt
- */
-internal object JsonFactory {
-
-  fun buildJson(): Json = Json {
-    coerceInputValues = true
-  }
+internal actual fun createLoggerCommon(
+  tag: String?,
+  config: TemClientConfig?,
+  supportKtxNotebook: Boolean,
+): Logger {
+  TODO("Not yet implemented")
 }
diff --git a/temerity/src/androidMain/kotlin/edu/ucsc/its/temerity/Actual.kt b/temerity/src/androidMain/kotlin/edu/ucsc/its/temerity/di/PlatformModule.android.kt
similarity index 93%
rename from temerity/src/androidMain/kotlin/edu/ucsc/its/temerity/Actual.kt
rename to temerity/src/androidMain/kotlin/edu/ucsc/its/temerity/di/PlatformModule.android.kt
index a15413e1183d5d6c9a4185c071c5fa179f022f89..4c5f5da5d897af76a30dc3872693c611e554948e 100644
--- a/temerity/src/androidMain/kotlin/edu/ucsc/its/temerity/Actual.kt
+++ b/temerity/src/androidMain/kotlin/edu/ucsc/its/temerity/di/PlatformModule.android.kt
@@ -15,9 +15,9 @@
  *     License along with this library; if not, write to the Free Software
  *     Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  */
-package edu.ucsc.its.temerity
+package edu.ucsc.its.temerity.di
 
-import edu.ucsc.its.temerity.core.DispatcherFactory.createDispatcher
+import edu.ucsc.its.temerity.extensions.coroutines.createDispatcher
 import io.ktor.client.engine.android.Android
 import kotlinx.coroutines.CoroutineDispatcher
 import kotlinx.coroutines.Dispatchers
diff --git a/temerity/src/androidMain/kotlin/edu/ucsc/its/temerity/extensions/log/Filesystem.android.kt b/temerity/src/androidMain/kotlin/edu/ucsc/its/temerity/extensions/log/Filesystem.android.kt
new file mode 100644
index 0000000000000000000000000000000000000000..094b3d7530deffde589ca3ccadd23ad63130c62f
--- /dev/null
+++ b/temerity/src/androidMain/kotlin/edu/ucsc/its/temerity/extensions/log/Filesystem.android.kt
@@ -0,0 +1,23 @@
+/*
+ *     Designed and developed in 2022-2024 by William Walker (wnwalker@ucsc.edu)
+ *     Copyright 2022-2024 The Regents of the University of California. All rights reserved.
+ *
+ *     This library is free software; you can redistribute it and/or
+ *     modify it under the terms of the GNU Lesser General Public
+ *     License as published by the Free Software Foundation; version 2.1 of the License.
+ *
+ *     This library is distributed in the hope that it will be useful,
+ *     but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ *     Lesser General Public License for more details.
+ *
+ *     You should have received a copy of the GNU Lesser General Public
+ *     License along with this library; if not, write to the Free Software
+ *     Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
+ */
+package edu.ucsc.its.temerity.extensions.log
+
+import kotlinx.io.files.FileSystem
+import kotlinx.io.files.SystemFileSystem
+
+internal actual fun fileSystem(): FileSystem = SystemFileSystem
diff --git a/temerity/src/androidMain/kotlin/edu/ucsc/its/temerity/extensions/log/FilesystemActual.kt b/temerity/src/androidMain/kotlin/edu/ucsc/its/temerity/extensions/log/FilesystemActual.kt
deleted file mode 100644
index 4cba6149f39b8faeefff8e9c0c915242c8fc07a6..0000000000000000000000000000000000000000
--- a/temerity/src/androidMain/kotlin/edu/ucsc/its/temerity/extensions/log/FilesystemActual.kt
+++ /dev/null
@@ -1,6 +0,0 @@
-package edu.ucsc.its.temerity.extensions.log
-
-import kotlinx.io.files.FileSystem
-import kotlinx.io.files.SystemFileSystem
-
-internal actual fun fileSystem() : FileSystem = SystemFileSystem
diff --git a/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/TemerityApi.kt b/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/TemerityApi.kt
index b3908d6ea163ba77148fc0dbdf9125828e1d6f69..93adda8dca388bae8e967d6cc332868bf3aaf126 100644
--- a/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/TemerityApi.kt
+++ b/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/TemerityApi.kt
@@ -17,6 +17,7 @@
  */
 package edu.ucsc.its.temerity
 
+import edu.ucsc.its.temerity.extensions.datetime.DateTimeExt.currentDate
 import edu.ucsc.its.temerity.model.AuditLogEntry
 import edu.ucsc.its.temerity.model.Course
 import edu.ucsc.its.temerity.model.Device
@@ -52,7 +53,7 @@ public interface TemerityApi {
    * @return The version of the client library as a semantic versioning formatted string.
    */
   @Suppress("PropertyName")
-  public val VERSION: String
+  public val version: String
 
   /**
    * Fetches the list of users from the platform.
diff --git a/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/Util.kt b/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/Util.kt
deleted file mode 100644
index d4c36dc88ca2946ce633d7c28e90060a7ad32344..0000000000000000000000000000000000000000
--- a/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/Util.kt
+++ /dev/null
@@ -1,42 +0,0 @@
-/*
- *     Designed and developed in 2022-2024 by William Walker (wnwalker@ucsc.edu)
- *     Copyright 2022-2024 The Regents of the University of California. All rights reserved.
- *
- *     This library is free software; you can redistribute it and/or
- *     modify it under the terms of the GNU Lesser General Public
- *     License as published by the Free Software Foundation; version 2.1 of the License.
- *
- *     This library is distributed in the hope that it will be useful,
- *     but WITHOUT ANY WARRANTY; without even the implied warranty of
- *     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
- *     Lesser General Public License for more details.
- *
- *     You should have received a copy of the GNU Lesser General Public
- *     License along with this library; if not, write to the Free Software
- *     Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
- */
-package edu.ucsc.its.temerity
-
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.Job
-import kotlinx.coroutines.SupervisorJob
-import kotlinx.datetime.Clock
-import kotlinx.datetime.LocalDate
-import kotlinx.datetime.LocalTime
-import kotlinx.datetime.TimeZone
-import kotlinx.datetime.toLocalDateTime
-import kotlinx.datetime.todayIn
-import kotlin.coroutines.CoroutineContext
-
-internal fun currentTz(): TimeZone = TimeZone.currentSystemDefault()
-
-internal fun thisInstant() = Clock.System.now()
-
-public fun currentDate(): LocalDate = Clock.System.todayIn(currentTz())
-
-internal fun currentTime(): LocalTime = thisInstant().toLocalDateTime(currentTz()).time
-
-internal fun createJobScope(coroutineContext: CoroutineContext, allowIndependentFailure: Boolean = false): CoroutineScope {
-  val parentJob = if (allowIndependentFailure) SupervisorJob() else Job()
-  return CoroutineScope(coroutineContext + parentJob)
-}
diff --git a/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/core/DispatcherFactory.kt b/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/core/DispatcherFactory.kt
deleted file mode 100644
index 576562b997e701fb8ed6c78cde804b9cd9102812..0000000000000000000000000000000000000000
--- a/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/core/DispatcherFactory.kt
+++ /dev/null
@@ -1,50 +0,0 @@
-/*
- *     Designed and developed in 2022-2024 by William Walker (wnwalker@ucsc.edu)
- *     Copyright 2022-2024 The Regents of the University of California. All rights reserved.
- *
- *     This library is free software; you can redistribute it and/or
- *     modify it under the terms of the GNU Lesser General Public
- *     License as published by the Free Software Foundation; version 2.1 of the License.
- *
- *     This library is distributed in the hope that it will be useful,
- *     but WITHOUT ANY WARRANTY; without even the implied warranty of
- *     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
- *     Lesser General Public License for more details.
- *
- *     You should have received a copy of the GNU Lesser General Public
- *     License along with this library; if not, write to the Free Software
- *     Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
- */
-package edu.ucsc.its.temerity.core
-
-import edu.ucsc.its.temerity.core.Temerity.Companion.DEFAULT_MINIMUM_THREAD_COUNT
-import edu.ucsc.its.temerity.createJobScope
-import kotlinx.coroutines.CoroutineDispatcher
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.Dispatchers
-
-internal object DispatcherFactory {
-
-  internal fun createDispatcher(dispatcherThreadPool: CoroutineDispatcher, threadCount: Int, dispatcherName: String): CoroutineDispatcher = dispatcherThreadPool.limitedParallelism(threadCount, dispatcherName)
-
-  internal fun createLibraryScope(dispatcher: CoroutineDispatcher): CoroutineScope = createJobScope(dispatcher, allowIndependentFailure = true)
-
-  private fun availableThreads() = Runtime.getRuntime().availableProcessors().minus(1)
-
-  internal fun calculateMaxThreads(defaultMinimumThreadCount: Int) = availableThreads().coerceAtLeast(defaultMinimumThreadCount)
-
-  internal fun setThreadCount(maximumThreadCount: Int? = null): Int = when (maximumThreadCount) {
-    null -> calculateMaxThreads(DEFAULT_MINIMUM_THREAD_COUNT)
-    else -> {
-      if (maximumThreadCount > availableThreads()) {
-        // TODO: Log warning that the configured thread count is higher than the number of available threads
-        calculateMaxThreads(DEFAULT_MINIMUM_THREAD_COUNT)
-      }
-      if (maximumThreadCount < DEFAULT_MINIMUM_THREAD_COUNT) {
-        // TODO: Log warning that the configured thread count is lower than the default minimum thread pool size
-        calculateMaxThreads(DEFAULT_MINIMUM_THREAD_COUNT)
-      }
-      maxOf(maximumThreadCount, calculateMaxThreads(DEFAULT_MINIMUM_THREAD_COUNT))
-    }
-  }
-}
diff --git a/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/core/LoggerFactory.kt b/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/core/LoggerFactory.kt
index 1a3902befd259c65dcc2f55ec4d3ed22007c55bc..1dfefa8148eac95f8e868928369ec90cf27ea0a5 100644
--- a/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/core/LoggerFactory.kt
+++ b/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/core/LoggerFactory.kt
@@ -1,46 +1,27 @@
+/*
+ *     Designed and developed in 2022-2024 by William Walker (wnwalker@ucsc.edu)
+ *     Copyright 2022-2024 The Regents of the University of California. All rights reserved.
+ *
+ *     This library is free software; you can redistribute it and/or
+ *     modify it under the terms of the GNU Lesser General Public
+ *     License as published by the Free Software Foundation; version 2.1 of the License.
+ *
+ *     This library is distributed in the hope that it will be useful,
+ *     but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ *     Lesser General Public License for more details.
+ *
+ *     You should have received a copy of the GNU Lesser General Public
+ *     License along with this library; if not, write to the Free Software
+ *     Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
+ */
 package edu.ucsc.its.temerity.core
 
-import co.touchlab.kermit.CommonWriter
 import co.touchlab.kermit.Logger
-import co.touchlab.kermit.NoTagFormatter
-import co.touchlab.kermit.Severity
-import co.touchlab.kermit.loggerConfigInit
-import co.touchlab.kermit.platformLogWriter
 import edu.ucsc.its.temerity.TemClientConfig
 
 internal object LoggerFactory {
-  internal fun createLogger(tag: String?, config: TemClientConfig? = null, supportKtxNotebook: Boolean = false): Logger =
-    when (config){
-      null -> {
-        Logger(
-          config = loggerConfigInit(
-            if (supportKtxNotebook) CommonWriter(NoTagFormatter) else platformLogWriter(NoTagFormatter),
-            minSeverity = Severity.Debug,
-          ),
-          tag = tag ?: "TemerityLib",
-        )
-      }
-      else -> {
-        when (config.optDebugEnabled) {
-          // If debug logging is enabled, create a logger which color prints to the console
-          true -> {
-            Logger(
-              config = loggerConfigInit(
-                if (supportKtxNotebook) CommonWriter(NoTagFormatter) else platformLogWriter(NoTagFormatter),
-                minSeverity = Severity.Debug,
-              ),
-              tag = tag ?: "TemerityLib",
-            )
-          }
-          // If debug logging is disabled, create a logger which prints to a log file
-          false -> {
-            Logger(
-              config = loggerConfigInit(
-              
-              )
-            )
-          }
-        }
-      }
-    }
+  internal fun createLogger(tag: String?, config: TemClientConfig? = null, supportKtxNotebook: Boolean = false) = createLoggerCommon(tag = tag, config = config, supportKtxNotebook = supportKtxNotebook)
 }
+
+internal expect fun createLoggerCommon(tag: String?, config: TemClientConfig? = null, supportKtxNotebook: Boolean = false): Logger
diff --git a/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/core/Temerity.kt b/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/core/Temerity.kt
index 0139a0a052296ce7e756278e7330a3584d72d95d..9083faa08333c07d28683ec0f38cf3f4a0f41c64 100644
--- a/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/core/Temerity.kt
+++ b/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/core/Temerity.kt
@@ -20,22 +20,24 @@ package edu.ucsc.its.temerity.core
 import co.touchlab.stately.collections.ConcurrentMutableMap
 import com.skydoves.sandwich.ApiResponse
 import com.skydoves.sandwich.StatusCode
+import com.skydoves.sandwich.StatusCode.NoContent
 import com.skydoves.sandwich.getOrThrow
 import com.skydoves.sandwich.ktor.executeApiResponse
 import com.skydoves.sandwich.ktor.statusCode
-import com.skydoves.sandwich.ktorfit.ApiResponseConverterFactory
 import com.skydoves.sandwich.onSuccess
-import de.jensklingenberg.ktorfit.ktorfit
 import edu.ucsc.its.temerity.AuditLogSortOrder
+import edu.ucsc.its.temerity.AuditLogSortOrder.NEW_FIRST
 import edu.ucsc.its.temerity.BuildConfig
 import edu.ucsc.its.temerity.TemClientConfig
 import edu.ucsc.its.temerity.TemerityApi
 import edu.ucsc.its.temerity.api.PlatformApi
-import edu.ucsc.its.temerity.api.createPlatformApi
-import edu.ucsc.its.temerity.core.DispatcherFactory.createLibraryScope
-import edu.ucsc.its.temerity.core.JsonFactory.buildJson
 import edu.ucsc.its.temerity.core.Temerity.Companion.DEFAULT_WEB_TIMEOUT
-import edu.ucsc.its.temerity.createJobScope
+import edu.ucsc.its.temerity.di.libModule
+import edu.ucsc.its.temerity.di.platformModule
+import edu.ucsc.its.temerity.extensions.coroutines.calculateMaxThreads
+import edu.ucsc.its.temerity.extensions.coroutines.createJobScope
+import edu.ucsc.its.temerity.extensions.coroutines.setThreadCount
+import edu.ucsc.its.temerity.extensions.datetime.DateTimeExt
 import edu.ucsc.its.temerity.extensions.time.applyAuditLogFormat
 import edu.ucsc.its.temerity.extensions.time.applyScheduledSessionDateFormat
 import edu.ucsc.its.temerity.model.AuditLogEntry
@@ -44,13 +46,13 @@ import edu.ucsc.its.temerity.model.Device
 import edu.ucsc.its.temerity.model.DeviceRecordingSession
 import edu.ucsc.its.temerity.model.EventType
 import edu.ucsc.its.temerity.model.FolderPermissions
+import edu.ucsc.its.temerity.model.FolderPermissions.Action
 import edu.ucsc.its.temerity.model.Group
 import edu.ucsc.its.temerity.model.NewUser
 import edu.ucsc.its.temerity.model.User
 import edu.ucsc.its.temerity.model.UserGroup
 import edu.ucsc.its.temerity.model.UserRecordingSession
 import edu.ucsc.its.temerity.model.UserUpdate
-import edu.ucsc.its.temerity.platformModule
 import edu.ucsc.its.temerity.sortByCreationDate
 import io.ktor.client.HttpClient
 import io.ktor.client.HttpClientConfig
@@ -58,18 +60,21 @@ import io.ktor.client.engine.HttpClientEngine
 import io.ktor.client.plugins.HttpTimeout
 import io.ktor.client.plugins.defaultRequest
 import io.ktor.client.plugins.logging.LogLevel
+import io.ktor.client.plugins.logging.LogLevel.ALL
 import io.ktor.client.plugins.logging.Logging
 import io.ktor.client.request.header
 import io.ktor.client.statement.HttpResponse
 import io.ktor.http.URLProtocol
 import io.ktor.util.cio.toByteArray
 import io.ktor.utils.io.ByteReadChannel
+import kotlinx.coroutines.CoroutineDispatcher
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.async
 import kotlinx.coroutines.awaitAll
 import kotlinx.coroutines.delay
 import kotlinx.datetime.DateTimeUnit
 import kotlinx.datetime.LocalDate
+import kotlinx.datetime.LocalTime
 import kotlinx.datetime.minus
 import kotlinx.serialization.SerializationException
 import kotlinx.serialization.encodeToString
@@ -84,15 +89,15 @@ import org.koin.core.logger.Level.ERROR
 import org.koin.core.logger.Level.INFO
 import org.koin.core.logger.Level.NONE
 import org.koin.core.logger.Level.WARNING
+import org.koin.core.logger.Logger
 import org.koin.core.logger.MESSAGE
 import org.koin.core.parameter.parametersOf
 import org.koin.dsl.koinApplication
-import org.koin.dsl.module
 import kotlin.time.Duration
 import kotlin.time.Duration.Companion.minutes
 import kotlin.time.DurationUnit
-import kotlinx.coroutines.CoroutineDispatcher
-import org.koin.core.logger.Logger
+import kotlin.time.DurationUnit.MILLISECONDS
+import kotlinx.datetime.DateTimeUnit.Companion
 import co.touchlab.kermit.Logger as KermitLogger
 import io.ktor.client.plugins.logging.Logger as KtorLogger
 
@@ -114,32 +119,21 @@ public class Temerity internal constructor(
     // TODO: Use this as cache4k expiration time
     public val DEFAULT_CACHE_EXPIRATION: Duration = 15.minutes
     internal const val DEFAULT_MINIMUM_THREAD_COUNT: Int = 2
-  }
+    
+    @JvmStatic
+    internal fun createLogger(): KermitLogger = LoggerFactory.createLogger(
+      tag = TODO(),
+      config = TODO (),
+      supportKtxNotebook = TODO()
+    )
 
-  /**
-   * This function provides the Koin module for the Temerity library.
-   * It includes the platform module and provides factories for [HttpClient] and [PlatformApi].
-   */
-  internal val libModule = module {
-    factory { (config: TemClientConfig, kermit: co.touchlab.kermit.Logger) ->
-      buildHttpClient(
-        httpClientEngine = get( parameters = { parametersOf(libraryCoroutineDispatcher) }),
-        config = config,
-        logger = kermit,
-      )
-    }
-    factory { (config: TemClientConfig) ->
-      val client: HttpClient = get { parametersOf(config, createLogger(tag = "Temerity Library Web Request engine", config = config)) }
-      val ktorfit = ktorfit {
-        config.serviceUrl?.let { baseUrl(it) }
-        httpClient(client)
-        converterFactories(ApiResponseConverterFactory.create())
-      }
-      ktorfit.createPlatformApi()
-    }
-    single { (dispatcher: CoroutineDispatcher) ->
-      createLibraryScope(dispatcher)
-    }
+    @JvmStatic
+    @Suppress("MemberVisibilityCanBePrivate")
+    public fun currentDate(): LocalDate = DateTimeExt.currentDate()
+
+    @JvmStatic
+    @Suppress("MemberVisibilityCanBePrivate")
+    public fun currentTime(): LocalTime = DateTimeExt.currentTime()
   }
 
   /**
@@ -148,7 +142,7 @@ public class Temerity internal constructor(
    * via factories defined as part of the lib Module.
    */
   private fun createKoinApp() = object : TemerityKoinContext() {
-    override val logger = createLogger(tag = "TemerityLib Koin DI Context")
+    override val logger = createLoggerCommon(tag = "TemerityLib Koin DI Context")
     override val koinApp = koinApplication {
       logger(object : Logger() {
         override fun display(level: Level, msg: MESSAGE) {
@@ -162,8 +156,8 @@ public class Temerity internal constructor(
         }
       })
       modules(
-        libModule,
-        platformModule()
+        libModule(),
+        platformModule(),
       )
     }
     override val koin: Koin = koinApp.koin
@@ -175,10 +169,12 @@ public class Temerity internal constructor(
     abstract val logger: KermitLogger
   }
 
-  @Suppress("PropertyName")
-  override val VERSION: String = BuildConfig.LIB_VERSION
+  override val version: String = BuildConfig.LIB_VERSION
 
-  private val json: Json = buildJson()
+  // Kotlinx-serialization [Json] encoder/decoder object used for serializing/deserializing JSON object responses
+  private val json: Json = Json {
+    coerceInputValues = true
+  }
   internal val koinContext = createKoinApp()
   override fun getKoin(): Koin = koinContext.koin
 
@@ -199,8 +195,8 @@ public class Temerity internal constructor(
     libraryCoroutineDispatcher = get<CoroutineDispatcher> {
       val dispatcherName = "Temerity Library Dispatcher"
       when (val optThreadCount = config.threadCount) {
-        null -> parametersOf(DispatcherFactory.calculateMaxThreads(DEFAULT_MINIMUM_THREAD_COUNT), dispatcherName)
-        else -> parametersOf(DispatcherFactory.setThreadCount(optThreadCount), dispatcherName)
+        null -> parametersOf(calculateMaxThreads(DEFAULT_MINIMUM_THREAD_COUNT), dispatcherName)
+        else -> parametersOf(setThreadCount(optThreadCount), dispatcherName)
       }
     }
     libraryCoroutineScope = get<CoroutineScope> {
@@ -230,7 +226,7 @@ public class Temerity internal constructor(
         // If the API returns a 204 No Content status code, we should throw a BreakException to stop the page iteration loop
         // As of Cashew release, this is only used in getGroups() to stop the loop when no more groups are returned
         // May be used in the future for other paginated endpoints (such as getAuditLogEntries)
-        StatusCode.NoContent -> throw BreakException(page - 1)
+        NoContent -> throw BreakException(page - 1)
 
         else -> Unit
       }
@@ -246,7 +242,7 @@ public class Temerity internal constructor(
       when (exception) {
         is SerializationException -> {
           // TODO: Implement platform API version checking
-          get<KermitLogger>{ parametersOf("TemerityLib") }.d { "Returned JSON object: ${exception.message}" }
+          get<KermitLogger> { parametersOf("TemerityLib") }.d { "Returned JSON object: ${exception.message}" }
           error("Encountered error decoding response from platform API. You likely need to choose a Temerity release that supports the API version implemented by your instance. \nPlatform API response data : $apiResponsePayload")
         }
 
@@ -309,11 +305,11 @@ public class Temerity internal constructor(
     TODO("Not yet implemented")
   }
 
-  public override suspend fun addFolderPermission(perms: FolderPermissions.Action): HttpResponse {
+  public override suspend fun addFolderPermission(perms: Action): HttpResponse {
     TODO("Not yet implemented")
   }
 
-  public override suspend fun removeFolderPermission(perms: FolderPermissions.Action) {
+  public override suspend fun removeFolderPermission(perms: Action) {
     TODO("Not yet implemented")
   }
 
@@ -346,13 +342,15 @@ public class Temerity internal constructor(
         }
         return returnedGroups
       }
+
       else -> {
         return emptyList()
       }
     }
   }
 
-  public override suspend fun getCourse(courseCode: String): Course = decodeResponseCatching(platformApi.getCourse(courseCode).executeApiResponse<String>())
+  public override suspend fun getCourse(courseCode: String): Course =
+    decodeResponseCatching(platformApi.getCourse(courseCode).executeApiResponse<String>())
 
   public override suspend fun getCourses(paginated: Boolean): List<Course> {
     return when (paginated) {
@@ -411,7 +409,7 @@ public class Temerity internal constructor(
     results.forEach { returnedEntries.addAll(it) }
 
     // Apply passed sortOrder, or default to NEW_FIRST TODO: Read from user settings
-    returnedEntries.applyOrDefault(sortOrder ?: AuditLogSortOrder.NEW_FIRST)
+    returnedEntries.applyOrDefault(sortOrder ?: NEW_FIRST)
     return returnedEntries
   }
 
@@ -430,10 +428,10 @@ public class Temerity internal constructor(
     when (paginated) {
       true -> {
         val returnedEntries = ArrayList<AuditLogEntry>()
-        var windowStart = if (endTime.minus(1, DateTimeUnit.Companion.MONTH) < startTime) {
+        var windowStart = if (endTime.minus(1, DateTimeUnit.MONTH) < startTime) {
           startTime
         } else {
-          endTime.minus(1, DateTimeUnit.Companion.MONTH)
+          endTime.minus(1, DateTimeUnit.MONTH)
         }
         var windowEnd = endTime
         while (windowStart < windowEnd) {
@@ -445,7 +443,8 @@ public class Temerity internal constructor(
                 entryOffset = x * 75,
                 eventType = eventType.value,
               )
-              val logEventEntries: List<AuditLogEntry> = decodeResponseCatching(endpointRequest.executeApiResponse<String>(), page = x)
+              val logEventEntries: List<AuditLogEntry> =
+                decodeResponseCatching(endpointRequest.executeApiResponse<String>(), page = x)
               returnedEntries.addAll(logEventEntries)
             }
           } catch (e: Exception) {
@@ -461,29 +460,31 @@ public class Temerity internal constructor(
             }
           }
           windowEnd = windowStart
-          val backStep = windowStart.minus(1, DateTimeUnit.Companion.MONTH)
+          val backStep = windowStart.minus(1, DateTimeUnit.MONTH)
           windowStart = if (backStep < startTime) startTime else backStep
         }
         // Apply passed sortOrder, or default to NEW_FIRST TODO: Read from user settings
-        returnedEntries.applyOrDefault(sortOrder ?: AuditLogSortOrder.NEW_FIRST)
+        returnedEntries.applyOrDefault(sortOrder ?: NEW_FIRST)
         return returnedEntries
       }
+
       false -> {
         TODO("Implement non-paginated audit log entry retrieval")
       }
     }
   }
 
-  private fun ArrayList<AuditLogEntry>.applyOrDefault(sortOrder: AuditLogSortOrder?): ArrayList<AuditLogEntry> = when (sortOrder) {
-    null -> {
-      // Currently reads from prefs, falling back to BuildConfig constant. Can be overridden by passing a sort order as a parameter.
-      // TODO: Implement a way to pass a clientDefaultSortOrder to Builder
-      // TODO: Persist user-defined default sort order for subsequent runs
-      apply { sortByCreationDate(AuditLogSortOrder.NEW_FIRST) }
-    }
+  private fun ArrayList<AuditLogEntry>.applyOrDefault(sortOrder: AuditLogSortOrder?): ArrayList<AuditLogEntry> =
+    when (sortOrder) {
+      null -> {
+        // Currently reads from prefs, falling back to BuildConfig constant. Can be overridden by passing a sort order as a parameter.
+        // TODO: Implement a way to pass a clientDefaultSortOrder to Builder
+        // TODO: Persist user-defined default sort order for subsequent runs
+        apply { sortByCreationDate(NEW_FIRST) }
+      }
 
-    else -> apply { sortByCreationDate(sortOrder) }
-  }
+      else -> apply { sortByCreationDate(sortOrder) }
+    }
 
   // TODO: Implement setUser() function
 
@@ -520,7 +521,8 @@ public class Temerity internal constructor(
     )
 
   public override suspend fun getStorageAnalyticsReport(groupId: Long): ByteArray =
-    platformApi.getStorageAnalyticsReport(groupId).executeApiResponse<ByteReadChannel>().getOrThrow().toByteArray(limit = 2000000000) // Limit file downloads to 2 GB
+    platformApi.getStorageAnalyticsReport(groupId).executeApiResponse<ByteReadChannel>().getOrThrow()
+      .toByteArray(limit = 2000000000) // Limit file downloads to 2 GB
 }
 
 /**
@@ -563,7 +565,7 @@ internal fun buildHttpClient(
                 logger.d(message)
               }
             }
-            level = LogLevel.ALL
+            level = ALL
             sanitizeHeader { headerKey ->
               headerKey == "authToken"
             }
@@ -580,8 +582,8 @@ internal fun buildHttpClient(
     expectSuccess = config.expectSuccess
 
     if (config.useWebTimeout) {
-      val defaultWebTimeout = DEFAULT_WEB_TIMEOUT.toLong(DurationUnit.MILLISECONDS)
-      val configuredWebTimeout = config.webTimeout?.toLong(DurationUnit.MILLISECONDS)
+      val defaultWebTimeout = DEFAULT_WEB_TIMEOUT.toLong(MILLISECONDS)
+      val configuredWebTimeout = config.webTimeout?.toLong(MILLISECONDS)
       install(HttpTimeout) {
         connectTimeoutMillis = configuredWebTimeout ?: defaultWebTimeout
         requestTimeoutMillis = configuredWebTimeout ?: defaultWebTimeout
diff --git a/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/di/LibModule.kt b/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/di/LibModule.kt
new file mode 100644
index 0000000000000000000000000000000000000000..f823724a757def6756911d2b8119e53444ac9b78
--- /dev/null
+++ b/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/di/LibModule.kt
@@ -0,0 +1,59 @@
+/*
+ *     Designed and developed in 2022-2024 by William Walker (wnwalker@ucsc.edu)
+ *     Copyright 2022-2024 The Regents of the University of California. All rights reserved.
+ *
+ *     This library is free software; you can redistribute it and/or
+ *     modify it under the terms of the GNU Lesser General Public
+ *     License as published by the Free Software Foundation; version 2.1 of the License.
+ *
+ *     This library is distributed in the hope that it will be useful,
+ *     but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ *     Lesser General Public License for more details.
+ *
+ *     You should have received a copy of the GNU Lesser General Public
+ *     License along with this library; if not, write to the Free Software
+ *     Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
+ */
+package edu.ucsc.its.temerity.di
+
+import com.skydoves.sandwich.ktorfit.ApiResponseConverterFactory
+import de.jensklingenberg.ktorfit.ktorfit
+import edu.ucsc.its.temerity.TemClientConfig
+import edu.ucsc.its.temerity.api.PlatformApi
+import edu.ucsc.its.temerity.api.createPlatformApi
+import edu.ucsc.its.temerity.core.buildHttpClient
+import edu.ucsc.its.temerity.core.createLoggerCommon
+import edu.ucsc.its.temerity.extensions.coroutines.createLibraryScope
+import io.ktor.client.HttpClient
+import kotlinx.coroutines.CoroutineDispatcher
+import org.koin.core.parameter.parametersOf
+import org.koin.core.qualifier.named
+import org.koin.dsl.module
+
+/**
+ * This function provides the Koin module for the Temerity library.
+ * It includes the platform module and provides factories for [HttpClient] and [PlatformApi].
+ */
+internal fun libModule() = module {
+  factory { (config: TemClientConfig, kermit: co.touchlab.kermit.Logger) ->
+    buildHttpClient(
+      httpClientEngine = get(parameters = { parametersOf(get<CoroutineDispatcher>(named("libraryCoroutineDispatcher"))) }),
+      config = config,
+      logger = kermit,
+    )
+  }
+  factory { (config: TemClientConfig) ->
+    val client: HttpClient =
+      get { parametersOf(config, createLoggerCommon(tag = "Temerity Library Web Request engine", config = config)) }
+    val ktorfit = ktorfit {
+      config.serviceUrl?.let { baseUrl(it) }
+      httpClient(client)
+      converterFactories(ApiResponseConverterFactory.create())
+    }
+    ktorfit.createPlatformApi()
+  }
+  single { (dispatcher: CoroutineDispatcher) ->
+    createLibraryScope(dispatcher)
+  }
+}
diff --git a/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/Expect.kt b/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/di/PlatformModule.kt
similarity index 97%
rename from temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/Expect.kt
rename to temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/di/PlatformModule.kt
index 13146c7d67077590e2b5109a6f3f1c7e369ee3ee..06086bdd73c13f50739d89aafbd5708313d113ee 100644
--- a/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/Expect.kt
+++ b/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/di/PlatformModule.kt
@@ -15,7 +15,7 @@
  *     License along with this library; if not, write to the Free Software
  *     Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  */
-package edu.ucsc.its.temerity
+package edu.ucsc.its.temerity.di
 
 import org.koin.core.module.Module
 
diff --git a/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/extensions/coroutines/CoroutinesExt.kt b/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/extensions/coroutines/CoroutinesExt.kt
new file mode 100644
index 0000000000000000000000000000000000000000..90e834b2b2707be5926024ddb8620d1df20b2a56
--- /dev/null
+++ b/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/extensions/coroutines/CoroutinesExt.kt
@@ -0,0 +1,54 @@
+/*
+ *     Designed and developed in 2022-2024 by William Walker (wnwalker@ucsc.edu)
+ *     Copyright 2022-2024 The Regents of the University of California. All rights reserved.
+ *
+ *     This library is free software; you can redistribute it and/or
+ *     modify it under the terms of the GNU Lesser General Public
+ *     License as published by the Free Software Foundation; version 2.1 of the License.
+ *
+ *     This library is distributed in the hope that it will be useful,
+ *     but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ *     Lesser General Public License for more details.
+ *
+ *     You should have received a copy of the GNU Lesser General Public
+ *     License along with this library; if not, write to the Free Software
+ *     Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
+ */
+package edu.ucsc.its.temerity.extensions.coroutines
+
+import edu.ucsc.its.temerity.core.Temerity.Companion.DEFAULT_MINIMUM_THREAD_COUNT
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.SupervisorJob
+import kotlin.coroutines.CoroutineContext
+
+internal fun createJobScope(coroutineContext: CoroutineContext, allowIndependentFailure: Boolean = false): CoroutineScope {
+  val parentJob = if (allowIndependentFailure) SupervisorJob() else Job()
+  return CoroutineScope(coroutineContext + parentJob)
+}
+
+// This function is used to create a dispatcher with a limited number of threads from a platform-specific thread pool
+internal fun createDispatcher(dispatcherThreadPool: CoroutineDispatcher, threadCount: Int, dispatcherName: String): CoroutineDispatcher = dispatcherThreadPool.limitedParallelism(threadCount, dispatcherName)
+
+internal fun createLibraryScope(dispatcher: CoroutineDispatcher): CoroutineScope = createJobScope(dispatcher, allowIndependentFailure = true)
+
+private fun availableThreads() = Runtime.getRuntime().availableProcessors().minus(1)
+
+internal fun calculateMaxThreads(defaultMinimumThreadCount: Int) = availableThreads().coerceAtLeast(defaultMinimumThreadCount)
+
+internal fun setThreadCount(maximumThreadCount: Int? = null): Int = when (maximumThreadCount) {
+  null -> calculateMaxThreads(DEFAULT_MINIMUM_THREAD_COUNT)
+  else -> {
+    if (maximumThreadCount > availableThreads()) {
+      // TODO: Log warning that the configured thread count is higher than the number of available threads
+      calculateMaxThreads(DEFAULT_MINIMUM_THREAD_COUNT)
+    }
+    if (maximumThreadCount < DEFAULT_MINIMUM_THREAD_COUNT) {
+      // TODO: Log warning that the configured thread count is lower than the default minimum thread pool size
+      calculateMaxThreads(DEFAULT_MINIMUM_THREAD_COUNT)
+    }
+    maxOf(maximumThreadCount, calculateMaxThreads(DEFAULT_MINIMUM_THREAD_COUNT))
+  }
+}
diff --git a/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/extensions/datetime/DateTimeExt.kt b/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/extensions/datetime/DateTimeExt.kt
new file mode 100644
index 0000000000000000000000000000000000000000..04c295361d81e270b0325b9d5ddfbbc9afd02286
--- /dev/null
+++ b/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/extensions/datetime/DateTimeExt.kt
@@ -0,0 +1,35 @@
+/*
+ *     Designed and developed in 2022-2024 by William Walker (wnwalker@ucsc.edu)
+ *     Copyright 2022-2024 The Regents of the University of California. All rights reserved.
+ *
+ *     This library is free software; you can redistribute it and/or
+ *     modify it under the terms of the GNU Lesser General Public
+ *     License as published by the Free Software Foundation; version 2.1 of the License.
+ *
+ *     This library is distributed in the hope that it will be useful,
+ *     but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ *     Lesser General Public License for more details.
+ *
+ *     You should have received a copy of the GNU Lesser General Public
+ *     License along with this library; if not, write to the Free Software
+ *     Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
+ */
+package edu.ucsc.its.temerity.extensions.datetime
+
+import kotlinx.datetime.Clock
+import kotlinx.datetime.LocalDate
+import kotlinx.datetime.LocalTime
+import kotlinx.datetime.TimeZone
+import kotlinx.datetime.toLocalDateTime
+import kotlinx.datetime.todayIn
+
+internal object DateTimeExt {
+  internal fun currentTz(): TimeZone = TimeZone.currentSystemDefault()
+
+  internal fun thisInstant() = Clock.System.now()
+
+  internal fun currentDate(): LocalDate = Clock.System.todayIn(currentTz())
+
+  internal fun currentTime(): LocalTime = thisInstant().toLocalDateTime(currentTz()).time
+}
diff --git a/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/extensions/log/FilesystemLogWriter.kt b/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/extensions/log/FilesystemLogWriter.kt
index 9c4f5388dded3be5f714a86c4635ab43c3e48a80..baf4c7d887f311954ef2b7cd7fe29e6472e57d47 100644
--- a/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/extensions/log/FilesystemLogWriter.kt
+++ b/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/extensions/log/FilesystemLogWriter.kt
@@ -1,3 +1,20 @@
+/*
+ *     Designed and developed in 2022-2024 by William Walker (wnwalker@ucsc.edu)
+ *     Copyright 2022-2024 The Regents of the University of California. All rights reserved.
+ *
+ *     This library is free software; you can redistribute it and/or
+ *     modify it under the terms of the GNU Lesser General Public
+ *     License as published by the Free Software Foundation; version 2.1 of the License.
+ *
+ *     This library is distributed in the hope that it will be useful,
+ *     but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ *     Lesser General Public License for more details.
+ *
+ *     You should have received a copy of the GNU Lesser General Public
+ *     License along with this library; if not, write to the Free Software
+ *     Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
+ */
 package edu.ucsc.its.temerity.extensions.log
 
 import co.touchlab.kermit.DefaultFormatter
@@ -14,67 +31,67 @@ import kotlinx.io.writeString
 internal expect fun fileSystem(): FileSystem
 
 internal class FilesystemLogWriter internal constructor(
-    private val logPath: String,
-    private val logRoller: LogRoller? = null,
-    private val formatter: MessageStringFormatter = DefaultFormatter
+  private val logPath: String,
+  private val logRoller: LogRoller? = null,
+  private val formatter: MessageStringFormatter = DefaultFormatter,
 ) : LogWriter() {
 
-    // Not called since we are "Context Aware"
-    override fun log(severity: Severity, message: String, tag: String, throwable: Throwable?) {
-        val fileSystem = fileSystem()
-        val kotlinxIoPath = Path(logPath)
+  // Not called since we are "Context Aware"
+  override fun log(severity: Severity, message: String, tag: String, throwable: Throwable?) {
+    val fileSystem = fileSystem()
+    val kotlinxIoPath = Path(logPath)
 
-        logRoller?.rollLogs(kotlinxIoPath, fileSystem)
+    logRoller?.rollLogs(kotlinxIoPath, fileSystem)
 
-        val sink = fileSystem.sink(kotlinxIoPath, append = true).buffered()
+    val sink = fileSystem.sink(kotlinxIoPath, append = true).buffered()
 
-        with(sink) {
-            writeString(formatter.formatMessage(severity, Tag(tag), Message(message)))
-            writeString("\n")
-            throwable?.let {
-                writeString(it.stackTraceToString())
-            }
-            flush()
-        }
+    with(sink) {
+      writeString(formatter.formatMessage(severity, Tag(tag), Message(message)))
+      writeString("\n")
+      throwable?.let {
+        writeString(it.stackTraceToString())
+      }
+      flush()
     }
+  }
 
-    companion object {
-        operator fun invoke(block: Builder.() -> Unit) =
-            with(FilesystemLogWriterBuilder()) {
-                block(this)
-                build()
-            }
-    }
+  companion object {
+    operator fun invoke(block: Builder.() -> Unit) =
+      with(FilesystemLogWriterBuilder()) {
+        block(this)
+        build()
+      }
+  }
 
-    interface Builder {
-        fun rollLogAtSize(size: Long): Builder
-        fun logPath(path: String): Builder
-        fun build(): FilesystemLogWriter
-    }
+  interface Builder {
+    fun rollLogAtSize(size: Long): Builder
+    fun logPath(path: String): Builder
+    fun build(): FilesystemLogWriter
+  }
 
-    class FilesystemLogWriterBuilder() : Builder {
-        private var maxFileSize: Long? = null
-        private var logPath: String? = null
+  class FilesystemLogWriterBuilder : Builder {
+    private var maxFileSize: Long? = null
+    private var logPath: String? = null
 
-        override fun rollLogAtSize(size: Long): Builder {
-            maxFileSize = size
-            return this
-        }
+    override fun rollLogAtSize(size: Long): Builder {
+      maxFileSize = size
+      return this
+    }
 
-        override fun logPath(path: String): Builder {
-            logPath = path
-            return this
-        }
+    override fun logPath(path: String): Builder {
+      logPath = path
+      return this
+    }
 
-        override fun build(): FilesystemLogWriter {
-            if (logPath == null) throw NullPointerException("Invalid / missing log path")
+    override fun build(): FilesystemLogWriter {
+      if (logPath == null) throw NullPointerException("Invalid / missing log path")
 
-            // Can you resist the urge to Rick-roll the logs?
-            val rick = maxFileSize?.let {
-                FileSizeLogRoller(logPath!!, maxFileSize!!)
-            }
+      // Can you resist the urge to Rick-roll the logs?
+      val rick = maxFileSize?.let {
+        FileSizeLogRoller(logPath!!, maxFileSize!!)
+      }
 
-            return FilesystemLogWriter(logPath!!, rick)
-        }
+      return FilesystemLogWriter(logPath!!, rick)
     }
+  }
 }
diff --git a/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/extensions/log/LogRoller.kt b/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/extensions/log/LogRoller.kt
index 68b050cc5c46c2f2df730508a6e3265158bc2202..69171fedb96f80f28c3987d4ae125c66336884ab 100644
--- a/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/extensions/log/LogRoller.kt
+++ b/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/extensions/log/LogRoller.kt
@@ -1,29 +1,46 @@
+/*
+ *     Designed and developed in 2022-2024 by William Walker (wnwalker@ucsc.edu)
+ *     Copyright 2022-2024 The Regents of the University of California. All rights reserved.
+ *
+ *     This library is free software; you can redistribute it and/or
+ *     modify it under the terms of the GNU Lesser General Public
+ *     License as published by the Free Software Foundation; version 2.1 of the License.
+ *
+ *     This library is distributed in the hope that it will be useful,
+ *     but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ *     Lesser General Public License for more details.
+ *
+ *     You should have received a copy of the GNU Lesser General Public
+ *     License along with this library; if not, write to the Free Software
+ *     Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
+ */
 package edu.ucsc.its.temerity.extensions.log
 
 import kotlinx.io.files.FileSystem
 import kotlinx.io.files.Path
 
 internal interface LogRoller {
-    fun rollLogs(kotlinxIoPath: Path, fileSystem: FileSystem)
+  fun rollLogs(kotlinxIoPath: Path, fileSystem: FileSystem)
 }
 
 internal class FileSizeLogRoller(
-    private val logPath: String,
-    private val maxFileSize: Long
+  private val logPath: String,
+  private val maxFileSize: Long,
 ) : LogRoller {
-    override fun rollLogs(kotlinxIoPath: Path, fileSystem: FileSystem) {
-        val metadata = fileSystem.metadataOrNull(kotlinxIoPath)
-        metadata?.let {
-            it.size.let { size ->
-                if (size >= maxFileSize) {
-                    val filename = kotlinxIoPath.name
-                    val count = fileSystem.list(kotlinxIoPath.parent!!).count { p: Path ->
-                        p.name.contains(filename)
-                    }
-                    val to = Path("$logPath.$count")
-                    fileSystem.atomicMove(kotlinxIoPath, to)
-                }
-            }
+  override fun rollLogs(kotlinxIoPath: Path, fileSystem: FileSystem) {
+    val metadata = fileSystem.metadataOrNull(kotlinxIoPath)
+    metadata?.let {
+      it.size.let { size ->
+        if (size >= maxFileSize) {
+          val filename = kotlinxIoPath.name
+          val count = fileSystem.list(kotlinxIoPath.parent!!).count { p: Path ->
+            p.name.contains(filename)
+          }
+          val to = Path("$logPath.$count")
+          fileSystem.atomicMove(kotlinxIoPath, to)
         }
+      }
     }
+  }
 }
diff --git a/temerity/src/jvmMain/kotlin/edu/ucsc/its/temerity/core/LoggerFactory.jvm.kt b/temerity/src/jvmMain/kotlin/edu/ucsc/its/temerity/core/LoggerFactory.jvm.kt
new file mode 100644
index 0000000000000000000000000000000000000000..e6e70b71c5cb8e6be0817e8343a88d54e039af68
--- /dev/null
+++ b/temerity/src/jvmMain/kotlin/edu/ucsc/its/temerity/core/LoggerFactory.jvm.kt
@@ -0,0 +1,58 @@
+/*
+ *     Designed and developed in 2022-2024 by William Walker (wnwalker@ucsc.edu)
+ *     Copyright 2022-2024 The Regents of the University of California. All rights reserved.
+ *
+ *     This library is free software; you can redistribute it and/or
+ *     modify it under the terms of the GNU Lesser General Public
+ *     License as published by the Free Software Foundation; version 2.1 of the License.
+ *
+ *     This library is distributed in the hope that it will be useful,
+ *     but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ *     Lesser General Public License for more details.
+ *
+ *     You should have received a copy of the GNU Lesser General Public
+ *     License along with this library; if not, write to the Free Software
+ *     Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
+ */
+package edu.ucsc.its.temerity.core
+
+import co.touchlab.kermit.CommonWriter
+import co.touchlab.kermit.Logger
+import co.touchlab.kermit.NoTagFormatter
+import co.touchlab.kermit.Severity.Debug
+import co.touchlab.kermit.loggerConfigInit
+import co.touchlab.kermit.platformLogWriter
+import edu.ucsc.its.temerity.TemClientConfig
+
+internal actual fun createLoggerCommon(tag: String?, config: TemClientConfig?, supportKtxNotebook: Boolean): Logger = when (config) {
+  null -> {
+    Logger(
+      config = loggerConfigInit(
+        if (supportKtxNotebook) CommonWriter(NoTagFormatter) else platformLogWriter(NoTagFormatter),
+        minSeverity = Debug,
+      ),
+      tag = tag ?: "TemerityLib",
+    )
+  }
+  else -> {
+    when (config.optDebugEnabled) {
+      // If debug logging is enabled, create a logger which color prints to the console
+      true -> {
+        Logger(
+          config = loggerConfigInit(
+            if (supportKtxNotebook) CommonWriter(NoTagFormatter) else platformLogWriter(NoTagFormatter),
+            minSeverity = Debug,
+          ),
+          tag = tag ?: "TemerityLib",
+        )
+      }
+      // If debug logging is disabled, create a logger which prints to a log file
+      false -> {
+        Logger(
+          config = loggerConfigInit(),
+        )
+      }
+    }
+  }
+}
diff --git a/temerity/src/jvmMain/kotlin/edu/ucsc/its/temerity/Actual.kt b/temerity/src/jvmMain/kotlin/edu/ucsc/its/temerity/di/PlatformModule.jvm.kt
similarity index 85%
rename from temerity/src/jvmMain/kotlin/edu/ucsc/its/temerity/Actual.kt
rename to temerity/src/jvmMain/kotlin/edu/ucsc/its/temerity/di/PlatformModule.jvm.kt
index cbc6a9d34e6893c582470916f64043e4006b5910..395475859059a991d03abf620caa4e789158bdba 100644
--- a/temerity/src/jvmMain/kotlin/edu/ucsc/its/temerity/Actual.kt
+++ b/temerity/src/jvmMain/kotlin/edu/ucsc/its/temerity/di/PlatformModule.jvm.kt
@@ -15,16 +15,17 @@
  *     License along with this library; if not, write to the Free Software
  *     Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  */
-package edu.ucsc.its.temerity
+package edu.ucsc.its.temerity.di
 
-import edu.ucsc.its.temerity.core.DispatcherFactory.createDispatcher
+import edu.ucsc.its.temerity.extensions.coroutines.createDispatcher
 import io.ktor.client.engine.java.Java
 import kotlinx.coroutines.CoroutineDispatcher
 import kotlinx.coroutines.Dispatchers
+import org.koin.core.qualifier.named
 import org.koin.dsl.module
 
 internal actual fun platformModule() = module {
-  single { (threadCount: Int, dispatcherName: String) ->
+  single(named("libraryCoroutineDispatcher")) { (threadCount: Int, dispatcherName: String) ->
     createDispatcher(Dispatchers.IO, threadCount, dispatcherName)
   }
   factory { (libraryCoroutineDispatcher: CoroutineDispatcher) ->
diff --git a/temerity/src/jvmMain/kotlin/edu/ucsc/its/temerity/extensions/log/ColorFormatter.kt b/temerity/src/jvmMain/kotlin/edu/ucsc/its/temerity/extensions/log/ColorFormatter.kt
new file mode 100644
index 0000000000000000000000000000000000000000..d11bb8f44f4a9c7949808ddded9343f09f56e290
--- /dev/null
+++ b/temerity/src/jvmMain/kotlin/edu/ucsc/its/temerity/extensions/log/ColorFormatter.kt
@@ -0,0 +1,65 @@
+/*
+ *     Designed and developed in 2022-2024 by William Walker (wnwalker@ucsc.edu)
+ *     Copyright 2022-2024 The Regents of the University of California. All rights reserved.
+ *
+ *     This library is free software; you can redistribute it and/or
+ *     modify it under the terms of the GNU Lesser General Public
+ *     License as published by the Free Software Foundation; version 2.1 of the License.
+ *
+ *     This library is distributed in the hope that it will be useful,
+ *     but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ *     Lesser General Public License for more details.
+ *
+ *     You should have received a copy of the GNU Lesser General Public
+ *     License along with this library; if not, write to the Free Software
+ *     Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
+ */
+@file:Suppress("unused")
+
+package edu.ucsc.its.temerity.extensions.log
+
+import co.touchlab.kermit.DefaultFormatter
+import co.touchlab.kermit.Message
+import co.touchlab.kermit.MessageStringFormatter
+import co.touchlab.kermit.Severity
+import co.touchlab.kermit.Tag
+import org.fusesource.jansi.Ansi
+
+private fun withColor(messageStringFormatter: MessageStringFormatter = DefaultFormatter): MessageStringFormatter = ColorFormatter(messageStringFormatter)
+
+private fun withBrightColor(messageStringFormatter: MessageStringFormatter = DefaultFormatter): MessageStringFormatter = BrightColorFormatter(messageStringFormatter)
+
+internal class ColorFormatter(
+  messageStringFormatter: MessageStringFormatter = DefaultFormatter,
+) : WrappingFormatter(messageStringFormatter) {
+  override fun prefix(severity: Severity?, tag: Tag?, message: Message) =
+    severity?.toAnsiColor() ?: ""
+
+  override fun suffix(severity: Severity?, tag: Tag?, message: Message) =
+    resetColor()
+}
+
+internal class BrightColorFormatter(
+  messageStringFormatter: MessageStringFormatter = DefaultFormatter,
+) : WrappingFormatter(messageStringFormatter) {
+  override fun prefix(severity: Severity?, tag: Tag?, message: Message) =
+    severity?.toBrightAnsiColor() ?: ""
+
+  override fun suffix(severity: Severity?, tag: Tag?, message: Message) = resetColor()
+}
+
+internal fun Severity.toBrightAnsiColor() = "${Ansi.ansi().fgBright(this.asColor())}"
+
+internal fun Severity.toAnsiColor() = "${Ansi.ansi().fg(this.asColor())}"
+
+internal fun resetColor() = "${Ansi.ansi().a(Ansi.Attribute.RESET)}"
+
+internal fun Severity.asColor(): Ansi.Color = when (this) {
+  Severity.Verbose -> Ansi.Color.WHITE
+  Severity.Debug -> Ansi.Color.CYAN
+  Severity.Info -> Ansi.Color.GREEN
+  Severity.Warn -> Ansi.Color.YELLOW
+  Severity.Error -> Ansi.Color.RED
+  Severity.Assert -> Ansi.Color.MAGENTA
+}
diff --git a/temerity/src/jvmMain/kotlin/edu/ucsc/its/temerity/extensions/log/Filesystem.jvm.kt b/temerity/src/jvmMain/kotlin/edu/ucsc/its/temerity/extensions/log/Filesystem.jvm.kt
new file mode 100644
index 0000000000000000000000000000000000000000..094b3d7530deffde589ca3ccadd23ad63130c62f
--- /dev/null
+++ b/temerity/src/jvmMain/kotlin/edu/ucsc/its/temerity/extensions/log/Filesystem.jvm.kt
@@ -0,0 +1,23 @@
+/*
+ *     Designed and developed in 2022-2024 by William Walker (wnwalker@ucsc.edu)
+ *     Copyright 2022-2024 The Regents of the University of California. All rights reserved.
+ *
+ *     This library is free software; you can redistribute it and/or
+ *     modify it under the terms of the GNU Lesser General Public
+ *     License as published by the Free Software Foundation; version 2.1 of the License.
+ *
+ *     This library is distributed in the hope that it will be useful,
+ *     but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ *     Lesser General Public License for more details.
+ *
+ *     You should have received a copy of the GNU Lesser General Public
+ *     License along with this library; if not, write to the Free Software
+ *     Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
+ */
+package edu.ucsc.its.temerity.extensions.log
+
+import kotlinx.io.files.FileSystem
+import kotlinx.io.files.SystemFileSystem
+
+internal actual fun fileSystem(): FileSystem = SystemFileSystem
diff --git a/temerity/src/jvmMain/kotlin/edu/ucsc/its/temerity/extensions/log/KermitServiceProvider.kt b/temerity/src/jvmMain/kotlin/edu/ucsc/its/temerity/extensions/log/KermitServiceProvider.kt
index 0010197ea7ae91755baa602f0d1feb199a88f1ce..2f4e0b8a4687d191296792ed2c4617062eefa755 100644
--- a/temerity/src/jvmMain/kotlin/edu/ucsc/its/temerity/extensions/log/KermitServiceProvider.kt
+++ b/temerity/src/jvmMain/kotlin/edu/ucsc/its/temerity/extensions/log/KermitServiceProvider.kt
@@ -1,3 +1,20 @@
+/*
+ *     Designed and developed in 2022-2024 by William Walker (wnwalker@ucsc.edu)
+ *     Copyright 2022-2024 The Regents of the University of California. All rights reserved.
+ *
+ *     This library is free software; you can redistribute it and/or
+ *     modify it under the terms of the GNU Lesser General Public
+ *     License as published by the Free Software Foundation; version 2.1 of the License.
+ *
+ *     This library is distributed in the hope that it will be useful,
+ *     but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ *     Lesser General Public License for more details.
+ *
+ *     You should have received a copy of the GNU Lesser General Public
+ *     License along with this library; if not, write to the Free Software
+ *     Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
+ */
 @file:Suppress("unused")
 
 package edu.ucsc.its.temerity.extensions.log
@@ -6,50 +23,46 @@ import co.touchlab.kermit.CommonWriter
 import co.touchlab.kermit.LogWriter
 import co.touchlab.kermit.Severity
 import co.touchlab.kermit.StaticConfig
-import kotlinx.io.files.FileSystem
-import kotlinx.io.files.SystemFileSystem
 import org.slf4j.ILoggerFactory
 import org.slf4j.helpers.BasicMarkerFactory
 import org.slf4j.helpers.NOPMDCAdapter
 
-internal actual fun fileSystem() : FileSystem = SystemFileSystem
-
 internal class KermitServiceProvider : org.slf4j.spi.SLF4JServiceProvider {
-    private val markerFactory = BasicMarkerFactory()
-    private val mdcAdapter = NOPMDCAdapter()
-    override fun getLoggerFactory() = ILoggerFactory {
-        Slf4jKermitLogger(it, config)
-    }
+  private val markerFactory = BasicMarkerFactory()
+  private val mdcAdapter = NOPMDCAdapter()
+  override fun getLoggerFactory() = ILoggerFactory {
+    Slf4jKermitLogger(it, config)
+  }
 
-    override fun getMarkerFactory() = markerFactory
+  override fun getMarkerFactory() = markerFactory
 
-    override fun getMDCAdapter() = mdcAdapter
+  override fun getMDCAdapter() = mdcAdapter
 
-    override fun getRequestedApiVersion() = "2.0.99"
+  override fun getRequestedApiVersion() = "2.0.99"
 
-    override fun initialize() = Unit
+  override fun initialize() = Unit
 
-    companion object {
-        private val writers = mutableListOf<LogWriter>().apply {
-            add(CommonWriter())
-        }
-        var config: StaticConfig = StaticConfig(logWriterList = writers)
+  companion object {
+    private val writers = mutableListOf<LogWriter>().apply {
+      add(CommonWriter())
+    }
+    var config: StaticConfig = StaticConfig(logWriterList = writers)
 
-        var minSeverity: Severity
-            get() = config.minSeverity
-            set(value) {
-                config = config.copy(minSeverity = value)
-            }
+    var minSeverity: Severity
+      get() = config.minSeverity
+      set(value) {
+        config = config.copy(minSeverity = value)
+      }
 
-        fun addWriter(writer: LogWriter) {
-            writers.add(writer)
-            config = config.copy(logWriterList = writers)
-        }
+    fun addWriter(writer: LogWriter) {
+      writers.add(writer)
+      config = config.copy(logWriterList = writers)
+    }
 
-        fun setWriters(vararg writer: LogWriter) {
-            writers.clear()
-            writers.addAll(writer)
-            config = config.copy(logWriterList = writers)
-        }
+    fun setWriters(vararg writer: LogWriter) {
+      writers.clear()
+      writers.addAll(writer)
+      config = config.copy(logWriterList = writers)
     }
+  }
 }
diff --git a/temerity/src/jvmMain/kotlin/edu/ucsc/its/temerity/extensions/log/Slf4jKermitLogger.kt b/temerity/src/jvmMain/kotlin/edu/ucsc/its/temerity/extensions/log/Slf4jKermitLogger.kt
index 4ffbc2053e5345de72ee129959198bb3d6c884cd..d5ea3eb24c621c3610145a9519817e34535d526a 100644
--- a/temerity/src/jvmMain/kotlin/edu/ucsc/its/temerity/extensions/log/Slf4jKermitLogger.kt
+++ b/temerity/src/jvmMain/kotlin/edu/ucsc/its/temerity/extensions/log/Slf4jKermitLogger.kt
@@ -1,3 +1,20 @@
+/*
+ *     Designed and developed in 2022-2024 by William Walker (wnwalker@ucsc.edu)
+ *     Copyright 2022-2024 The Regents of the University of California. All rights reserved.
+ *
+ *     This library is free software; you can redistribute it and/or
+ *     modify it under the terms of the GNU Lesser General Public
+ *     License as published by the Free Software Foundation; version 2.1 of the License.
+ *
+ *     This library is distributed in the hope that it will be useful,
+ *     but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ *     Lesser General Public License for more details.
+ *
+ *     You should have received a copy of the GNU Lesser General Public
+ *     License along with this library; if not, write to the Free Software
+ *     Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
+ */
 @file:Suppress("unused")
 
 package edu.ucsc.its.temerity.extensions.log
@@ -7,54 +24,55 @@ import co.touchlab.kermit.LoggerConfig
 import co.touchlab.kermit.Severity
 import org.slf4j.Marker
 import org.slf4j.event.Level
-import org.slf4j.event.Level.*
 import org.slf4j.helpers.AbstractLogger
 
 internal class Slf4jKermitLogger(private val name: String, config: LoggerConfig) : AbstractLogger() {
-    private val logger = BaseLogger(config)
-    override fun getName(): String = "slf4j-over-kermit"
-
-    //region Is Logging enabled at various levels
-    override fun isTraceEnabled() = logger.config.minSeverity <= Severity.Verbose
-    override fun isTraceEnabled(marker: Marker?) = logger.config.minSeverity <= Severity.Verbose
-    override fun isDebugEnabled() = logger.config.minSeverity <= Severity.Debug
-    override fun isDebugEnabled(marker: Marker?) = logger.config.minSeverity <= Severity.Debug
-    override fun isInfoEnabled() = logger.config.minSeverity <= Severity.Info
-    override fun isInfoEnabled(marker: Marker?) = logger.config.minSeverity <= Severity.Info
-    override fun isWarnEnabled() = logger.config.minSeverity <= Severity.Warn
-    override fun isWarnEnabled(marker: Marker?) = logger.config.minSeverity <= Severity.Warn
-    override fun isErrorEnabled() = logger.config.minSeverity <= Severity.Error
-    override fun isErrorEnabled(marker: Marker?) = logger.config.minSeverity <= Severity.Error
-    //endregion
-
-    override fun getFullyQualifiedCallerName(): String? = null
-
-    override fun handleNormalizedLoggingCall(
-        level: Level?,
-        marker: Marker?,
-        messagePattern: String?,
-        arguments: Array<out Any>?,
-        throwable: Throwable?
-    ) {
-        val severity = when (level) {
-            ERROR -> Severity.Error
-            WARN -> Severity.Warn
-            INFO -> Severity.Info
-            DEBUG -> Severity.Debug
-            else -> Severity.Verbose
-        }
-
-        val formatted = if (messagePattern != null && arguments != null) {
-            String.format(messagePattern, *(arguments.toList().toTypedArray()))
-        } else null
-
-        messagePattern.let {
-            logger.log(
-                severity,
-                marker?.toString() ?: name,
-                throwable,
-                formatted ?: (messagePattern ?: "")
-            )
-        }
+  private val logger = BaseLogger(config)
+  override fun getName(): String = "slf4j-over-kermit"
+
+  //region Is Logging enabled at various levels
+  override fun isTraceEnabled() = logger.config.minSeverity <= Severity.Verbose
+  override fun isTraceEnabled(marker: Marker?) = logger.config.minSeverity <= Severity.Verbose
+  override fun isDebugEnabled() = logger.config.minSeverity <= Severity.Debug
+  override fun isDebugEnabled(marker: Marker?) = logger.config.minSeverity <= Severity.Debug
+  override fun isInfoEnabled() = logger.config.minSeverity <= Severity.Info
+  override fun isInfoEnabled(marker: Marker?) = logger.config.minSeverity <= Severity.Info
+  override fun isWarnEnabled() = logger.config.minSeverity <= Severity.Warn
+  override fun isWarnEnabled(marker: Marker?) = logger.config.minSeverity <= Severity.Warn
+  override fun isErrorEnabled() = logger.config.minSeverity <= Severity.Error
+  override fun isErrorEnabled(marker: Marker?) = logger.config.minSeverity <= Severity.Error
+  //endregion
+
+  override fun getFullyQualifiedCallerName(): String? = null
+
+  override fun handleNormalizedLoggingCall(
+    level: Level?,
+    marker: Marker?,
+    messagePattern: String?,
+    arguments: Array<out Any>?,
+    throwable: Throwable?,
+  ) {
+    val severity = when (level) {
+      Level.ERROR -> Severity.Error
+      Level.WARN -> Severity.Warn
+      Level.INFO -> Severity.Info
+      Level.DEBUG -> Severity.Debug
+      else -> Severity.Verbose
+    }
+
+    val formatted = if (messagePattern != null && arguments != null) {
+      String.format(messagePattern, *(arguments.toList().toTypedArray()))
+    } else {
+      null
+    }
+
+    messagePattern.let {
+      logger.log(
+        severity,
+        marker?.toString() ?: name,
+        throwable,
+        formatted ?: (messagePattern ?: ""),
+      )
     }
+  }
 }
diff --git a/temerity/src/jvmMain/kotlin/edu/ucsc/its/temerity/extensions/log/WrappingFormatter.kt b/temerity/src/jvmMain/kotlin/edu/ucsc/its/temerity/extensions/log/WrappingFormatter.kt
new file mode 100644
index 0000000000000000000000000000000000000000..26703f81aca62012f948c33e137c017af0a24728
--- /dev/null
+++ b/temerity/src/jvmMain/kotlin/edu/ucsc/its/temerity/extensions/log/WrappingFormatter.kt
@@ -0,0 +1,37 @@
+/*
+ *     Designed and developed in 2022-2024 by William Walker (wnwalker@ucsc.edu)
+ *     Copyright 2022-2024 The Regents of the University of California. All rights reserved.
+ *
+ *     This library is free software; you can redistribute it and/or
+ *     modify it under the terms of the GNU Lesser General Public
+ *     License as published by the Free Software Foundation; version 2.1 of the License.
+ *
+ *     This library is distributed in the hope that it will be useful,
+ *     but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ *     Lesser General Public License for more details.
+ *
+ *     You should have received a copy of the GNU Lesser General Public
+ *     License along with this library; if not, write to the Free Software
+ *     Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
+ */
+package edu.ucsc.its.temerity.extensions.log
+
+import co.touchlab.kermit.Message
+import co.touchlab.kermit.MessageStringFormatter
+import co.touchlab.kermit.Severity
+import co.touchlab.kermit.Tag
+
+internal abstract class WrappingFormatter(
+  private val messageStringFormatter: MessageStringFormatter,
+) : MessageStringFormatter {
+  override fun formatMessage(severity: Severity?, tag: Tag?, message: Message): String {
+    val prefix = prefix(severity, tag, message)
+    val content = messageStringFormatter.formatMessage(severity, tag, message)
+    val suffix = suffix(severity, tag, message)
+    return "${prefix}$content$suffix"
+  }
+
+  open fun prefix(severity: Severity?, tag: Tag?, message: Message): String = ""
+  open fun suffix(severity: Severity?, tag: Tag?, message: Message): String = ""
+}
diff --git a/temerity/src/jvmTest/kotlin/edu/ucsc/its/temerity/test/DevDeviceApiTests.kt b/temerity/src/jvmTest/kotlin/edu/ucsc/its/temerity/test/DevDeviceApiTests.kt
index a629715af9f553b9f07284e8bcc20299c40b58a0..058672e8279d74875a658994c3975424e65c6dbb 100644
--- a/temerity/src/jvmTest/kotlin/edu/ucsc/its/temerity/test/DevDeviceApiTests.kt
+++ b/temerity/src/jvmTest/kotlin/edu/ucsc/its/temerity/test/DevDeviceApiTests.kt
@@ -20,11 +20,10 @@ package edu.ucsc.its.temerity.test
 import edu.ucsc.its.temerity.core.Temerity
 import io.kotest.core.spec.style.FunSpec
 import kotlinx.coroutines.runBlocking
-import org.dotenv.vault.dotenvVault
 
 class DevDeviceApiTests :
   FunSpec({
-    val dotenv = dotenvVault()
+    val dotenv = dotenvVaultJvm()
 
     lateinit var temerityTest: Temerity
 
diff --git a/temerity/src/jvmTest/kotlin/edu/ucsc/its/temerity/test/DevGroupApiTests.kt b/temerity/src/jvmTest/kotlin/edu/ucsc/its/temerity/test/DevGroupApiTests.kt
index 7457d64fed42fe280fe04e6f2764a30733fee869..3658f2465eaef2f5dc443af31816e9b5143d2296 100644
--- a/temerity/src/jvmTest/kotlin/edu/ucsc/its/temerity/test/DevGroupApiTests.kt
+++ b/temerity/src/jvmTest/kotlin/edu/ucsc/its/temerity/test/DevGroupApiTests.kt
@@ -20,12 +20,11 @@ package edu.ucsc.its.temerity.test
 import edu.ucsc.its.temerity.core.Temerity
 import io.kotest.core.spec.style.FunSpec
 import kotlinx.coroutines.runBlocking
-import org.dotenv.vault.dotenvVault
 
 class DevGroupApiTests :
   FunSpec({
 
-    val dotenv = dotenvVault()
+    val dotenv = dotenvVaultJvm()
 
     lateinit var testTemerity: Temerity
 
diff --git a/temerity/src/jvmTest/kotlin/edu/ucsc/its/temerity/test/DevUserApiTests.kt b/temerity/src/jvmTest/kotlin/edu/ucsc/its/temerity/test/DevUserApiTests.kt
index 6c385a94cadaffe14fc106e14d1bda14d5092a96..e4c44452ea0700c2cddc58f8e5da79fb847bf1c5 100644
--- a/temerity/src/jvmTest/kotlin/edu/ucsc/its/temerity/test/DevUserApiTests.kt
+++ b/temerity/src/jvmTest/kotlin/edu/ucsc/its/temerity/test/DevUserApiTests.kt
@@ -28,12 +28,11 @@ import io.kotest.core.spec.style.FunSpec
 import io.kotest.matchers.shouldBe
 import io.ktor.http.HttpStatusCode
 import kotlinx.coroutines.runBlocking
-import org.dotenv.vault.dotenvVault
 
 class DevUserApiTests :
   FunSpec({
 
-    val dotenv = dotenvVault()
+    val dotenv = dotenvVaultJvm()
     val kermit = createLogger("DevUserApiTests")
 
     lateinit var testTemerity: Temerity
diff --git a/temerity/src/jvmTest/kotlin/edu/ucsc/its/temerity/test/DevUtilityTests.kt b/temerity/src/jvmTest/kotlin/edu/ucsc/its/temerity/test/DevUtilityTests.kt
index 4dbaee47333853244df68ab3a2b2afe5be12fa9d..be3c3b3850f6f14c1ea5ed6342c44ee1b90cb981 100644
--- a/temerity/src/jvmTest/kotlin/edu/ucsc/its/temerity/test/DevUtilityTests.kt
+++ b/temerity/src/jvmTest/kotlin/edu/ucsc/its/temerity/test/DevUtilityTests.kt
@@ -20,7 +20,6 @@ package edu.ucsc.its.temerity.test
 import edu.ucsc.its.temerity.core.Temerity
 import edu.ucsc.its.temerity.core.Temerity.Companion.createLogger
 import io.kotest.core.spec.style.FunSpec
-import org.dotenv.vault.dotenvVault
 import org.koin.test.KoinTest
 import kotlin.time.Duration.Companion.minutes
 
@@ -33,7 +32,7 @@ class DevUtilityTests :
   init {
     coroutineDebugProbes = true
 
-    val dotenv = dotenvVault()
+    val dotenv = dotenvVaultJvm()
 
     lateinit var testTemerity: Temerity
 
diff --git a/temerity/src/jvmTest/kotlin/edu/ucsc/its/temerity/test/ProdReportTests.kt b/temerity/src/jvmTest/kotlin/edu/ucsc/its/temerity/test/ProdReportTests.kt
index 00f46e94e7185ea562e81f324f89c8f33329c901..5eecca73e13bcdac68c5d652fd09ab6cbeff93fd 100644
--- a/temerity/src/jvmTest/kotlin/edu/ucsc/its/temerity/test/ProdReportTests.kt
+++ b/temerity/src/jvmTest/kotlin/edu/ucsc/its/temerity/test/ProdReportTests.kt
@@ -19,7 +19,6 @@ package edu.ucsc.its.temerity.test
 import edu.ucsc.its.temerity.AuditLogSortOrder.NEW_FIRST
 import edu.ucsc.its.temerity.core.Temerity
 import edu.ucsc.its.temerity.core.Temerity.Companion.createLogger
-import edu.ucsc.its.temerity.currentDate
 import edu.ucsc.its.temerity.model.EventType.AUTOMATED_SESSION_FAILED_TO_START
 import edu.ucsc.its.temerity.model.EventType.AUTOMATED_SESSION_MONITOR
 import edu.ucsc.its.temerity.model.EventType.CAPTURE_ERROR
@@ -39,7 +38,6 @@ import kotlinx.datetime.DateTimeUnit
 import kotlinx.datetime.TimeZone
 import kotlinx.datetime.minus
 import kotlinx.datetime.todayIn
-import org.dotenv.vault.dotenvVault
 import org.jetbrains.kotlinx.dataframe.api.toDataFrame
 import org.jetbrains.kotlinx.dataframe.io.writeCSV
 import org.koin.test.KoinTest
@@ -59,10 +57,10 @@ class ProdReportTests :
   init {
     coroutineDebugProbes = true
 
-    val dotenv = dotenvVault()
+    val dotenv = dotenvVaultJvm()
 
     lateinit var testTemerity: Temerity
-    val today = currentDate()
+    val today = Temerity.currentDate()
 
     beforeTest {
       testTemerity = Temerity {
diff --git a/temerity/src/jvmTest/kotlin/edu/ucsc/its/temerity/test/ProdUtilityTests.kt b/temerity/src/jvmTest/kotlin/edu/ucsc/its/temerity/test/ProdUtilityTests.kt
index 222d15af6cb85c07520c1e5e43616b508cf93197..af9e10d6fe64095548c9e886d25a856d2a858e42 100644
--- a/temerity/src/jvmTest/kotlin/edu/ucsc/its/temerity/test/ProdUtilityTests.kt
+++ b/temerity/src/jvmTest/kotlin/edu/ucsc/its/temerity/test/ProdUtilityTests.kt
@@ -19,7 +19,6 @@ package edu.ucsc.its.temerity.test
 
 import edu.ucsc.its.temerity.core.Temerity
 import io.kotest.core.spec.style.FunSpec
-import org.dotenv.vault.dotenvVault
 import org.koin.test.KoinTest
 import kotlin.time.Duration.Companion.minutes
 
@@ -30,7 +29,7 @@ class ProdUtilityTests :
   init {
     coroutineDebugProbes = true
 
-    val dotenv = dotenvVault()
+    val dotenv = dotenvVaultJvm()
 
     lateinit var testTemerity: Temerity
 
diff --git a/temerity/src/jvmTest/kotlin/edu/ucsc/its/temerity/test/TemerityDevTest.kt b/temerity/src/jvmTest/kotlin/edu/ucsc/its/temerity/test/TemerityDevTest.kt
index b98aea8a3321c7370ca9f24a920123efbeab46bd..51fd825ba227df0925ac95ba04aa83696f9dbf26 100644
--- a/temerity/src/jvmTest/kotlin/edu/ucsc/its/temerity/test/TemerityDevTest.kt
+++ b/temerity/src/jvmTest/kotlin/edu/ucsc/its/temerity/test/TemerityDevTest.kt
@@ -22,7 +22,7 @@ import com.skydoves.sandwich.StatusCode
 import com.skydoves.sandwich.ktor.getStatusCode
 import edu.ucsc.its.temerity.AuditLogSortOrder.NEW_FIRST
 import edu.ucsc.its.temerity.core.Temerity
-import edu.ucsc.its.temerity.currentDate
+import edu.ucsc.its.temerity.core.Temerity.Companion.currentDate
 import edu.ucsc.its.temerity.model.EventType.NEW_LOG_IN
 import edu.ucsc.its.temerity.model.NewUser
 import io.github.z4kn4fein.semver.Version
@@ -34,7 +34,6 @@ import kotlinx.coroutines.runBlocking
 import kotlinx.datetime.DateTimeUnit
 import kotlinx.datetime.minus
 import kotlinx.datetime.plus
-import org.dotenv.vault.dotenvVault
 import org.jetbrains.kotlinx.dataframe.api.toDataFrame
 import org.jetbrains.kotlinx.dataframe.io.writeCSV
 import java.io.File
@@ -48,7 +47,7 @@ class TemerityDevTest : FunSpec() {
   private lateinit var kermit: Logger
 
   init {
-    val dotenv = dotenvVault()
+    val dotenv = dotenvVaultJvm()
 
     lateinit var testTemerity: Temerity
 
@@ -60,9 +59,9 @@ class TemerityDevTest : FunSpec() {
     }
 
     test("Temerity client returns a correctly-formatted version String") {
-      val returnedVersion = testTemerity.VERSION.toVersion()
+      val returnedVersion = testTemerity.version.toVersion()
       kermit.d { "Returned client version: $returnedVersion" }
-      assert(testTemerity.VERSION.isNotEmpty())
+      assert(testTemerity.version.isNotEmpty())
       assert(returnedVersion < Version(0, 1, 0))
       assert(returnedVersion.isPreRelease)
     }
diff --git a/temerity/src/jvmTest/kotlin/edu/ucsc/its/temerity/test/Util.kt b/temerity/src/jvmTest/kotlin/edu/ucsc/its/temerity/test/Util.kt
index 5952412206b4a0b89719df0efa9dd5ac1c40d52a..1f0e87313894ddac9d6c2ceb8b4a9bad9e8166cc 100644
--- a/temerity/src/jvmTest/kotlin/edu/ucsc/its/temerity/test/Util.kt
+++ b/temerity/src/jvmTest/kotlin/edu/ucsc/its/temerity/test/Util.kt
@@ -18,7 +18,9 @@
 package edu.ucsc.its.temerity.test
 
 import edu.ucsc.its.temerity.TemClientConfig
+import io.github.cdimascio.dotenv.Configuration
 import io.github.cdimascio.dotenv.Dotenv
+import io.github.cdimascio.dotenv.dotenv
 import io.kotest.core.extensions.TestCaseExtension
 import io.kotest.core.test.TestCase
 import io.kotest.core.test.TestResult
@@ -85,3 +87,10 @@ internal fun TemClientConfig.configureProdEnvironment(dotenv: Dotenv) {
   serviceToken = dotenv["YUJAPROD_TOKEN"]
   optDebugEnabled = true
 }
+
+fun dotenvVaultJvm(block: (Configuration.() -> Unit)? = null): Dotenv = when (block) {
+  null -> dotenv()
+  else -> dotenv {
+    block()
+  }
+}