From 7ca0cf69820f246cb0f3104049c41bf3f1153d74 Mon Sep 17 00:00:00 2001
From: Leon Mika <lmika@lmika.org>
Date: Fri, 6 Oct 2023 15:27:06 +1100
Subject: [PATCH] Converted scripting language Tamarin to Risor (#55)

- Converted Tamarin script language to Risor
- Added a "find" and "merge" method to the result set script type.
- Added the ability to copy the table of results to the pasteboard by pressing C
- Added the -q flag, which will run a query and display the results as a CSV file on the command line
- Upgraded Go to 1.21 in Github actions
- Fix issue with missing limits
- Added the '-where' switch to the mark
- Added the 'marked' function to the query expression.
- Added a sampled time and count on the right-side of the mode line
- Added the 'M' key binding to toggle the marked items
- Started working on tab completion for 'sa' and 'da' commands
- Added count and sample time to the right-side of the mode line
- Added Ctrl+V to the prompt to paste the text of the pasteboard with all whitespace characters trimmed
- Fixed failing unit tests
---
 .github/workflows/ci.yaml                     |   2 +-
 .github/workflows/release.yaml                |   2 +-
 cmd/dynamo-browse/main.go                     |  47 +++++-
 go.mod                                        |  80 +++++++---
 go.sum                                        | 149 ++++++++++++++++++
 internal/common/ui/commandctrl/commandctrl.go |  29 +++-
 internal/common/ui/commandctrl/types.go       |   4 +
 internal/common/ui/events/commands.go         |   5 +
 internal/common/ui/events/errors.go           |   9 +-
 internal/dynamo-browse/controllers/columns.go |  11 ++
 internal/dynamo-browse/controllers/events.go  |  20 +++
 internal/dynamo-browse/controllers/export.go  |  75 ++++++++-
 .../dynamo-browse/controllers/scripts_test.go |  50 +++---
 internal/dynamo-browse/controllers/state.go   |   3 +-
 .../dynamo-browse/controllers/tableread.go    |  82 ++++++----
 .../dynamo-browse/controllers/tablewrite.go   |  38 +++++
 .../controllers/tablewrite_test.go            |  15 +-
 .../dynamo-browse/models/attrutils/truthy.go  |  32 ++++
 internal/dynamo-browse/models/items.go        |  10 +-
 internal/dynamo-browse/models/models.go       |   2 +
 .../models/queryexpr/builtins.go              |  37 +++++
 .../dynamo-browse/models/queryexpr/context.go |  13 ++
 .../dynamo-browse/models/queryexpr/dot.go     |   2 +-
 .../models/queryexpr/equality.go              |   4 +-
 .../dynamo-browse/models/queryexpr/errors.go  |   4 +
 .../dynamo-browse/models/queryexpr/expr.go    |  53 ++++---
 .../models/queryexpr/expr_test.go             |  32 +++-
 .../dynamo-browse/models/queryexpr/fncall.go  |   4 +-
 .../models/queryexpr/helpers_test.go          |   1 +
 internal/dynamo-browse/models/queryexpr/is.go |   2 +-
 .../dynamo-browse/models/queryexpr/types.go   |  14 ++
 .../pasteboardprovider/nilprovider.go         |  11 ++
 .../providers/pasteboardprovider/providers.go |  55 +++++++
 .../providers/settingstore/settingstore.go    |   2 +-
 .../services/pasteboardprovider.go            |   6 +
 .../services/scriptmanager/builtins.go        |  19 ++-
 .../services/scriptmanager/modext.go          |  33 ++--
 .../services/scriptmanager/modos.go           |  27 ++--
 .../services/scriptmanager/modos_test.go      |  17 +-
 .../services/scriptmanager/modsession.go      |  35 ++--
 .../services/scriptmanager/modsession_test.go |  19 +--
 .../services/scriptmanager/modui.go           |  24 ++-
 .../services/scriptmanager/opts.go            |   6 +-
 .../services/scriptmanager/resultsetproxy.go  | 114 +++++++++++++-
 .../scriptmanager/resultsetproxy_test.go      | 128 ++++++++++++++-
 .../services/scriptmanager/service.go         |  77 +++++----
 .../services/scriptmanager/tableproxy.go      |  28 +++-
 .../services/scriptmanager/typemapping.go     |   2 +-
 .../dynamo-browse/services/tables/service.go  |   2 +
 .../dynamo-browse/ui/keybindings/defaults.go  |   1 +
 .../ui/keybindings/keybindings.go             |   2 +
 internal/dynamo-browse/ui/model.go            |  18 ++-
 .../ui/teamodels/statusandprompt/model.go     |  47 +++++-
 .../ui/teamodels/statusandprompt/types.go     |   4 +
 54 files changed, 1227 insertions(+), 281 deletions(-)
 create mode 100644 internal/dynamo-browse/models/attrutils/truthy.go
 create mode 100644 internal/dynamo-browse/providers/pasteboardprovider/nilprovider.go
 create mode 100644 internal/dynamo-browse/providers/pasteboardprovider/providers.go
 create mode 100644 internal/dynamo-browse/services/pasteboardprovider.go

diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml
index 42d0051..de99ca1 100644
--- a/.github/workflows/ci.yaml
+++ b/.github/workflows/ci.yaml
@@ -24,7 +24,7 @@ jobs:
       - name: Setup Go
         uses: actions/setup-go@v3
         with:
-          go-version: 1.18
+          go-version: 1.21
       - name: Configure
         run: |
           git config --global url."https://${{ secrets.GO_MODULES_TOKEN }}:x-oauth-basic@github.com/lmika".insteadOf "https://github.com/lmika"
diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml
index 3f86c5a..b880db3 100644
--- a/.github/workflows/release.yaml
+++ b/.github/workflows/release.yaml
@@ -20,7 +20,7 @@ jobs:
       - name: Setup Go
         uses: actions/setup-go@v3
         with:
-          go-version: 1.18
+          go-version: 1.21
       - name: Configure
         run: |
           git config --global url."https://${{ secrets.GO_MODULES_TOKEN }}:x-oauth-basic@github.com/lmika".insteadOf "https://github.com/lmika"
diff --git a/cmd/dynamo-browse/main.go b/cmd/dynamo-browse/main.go
index 170a2dc..c059b3a 100644
--- a/cmd/dynamo-browse/main.go
+++ b/cmd/dynamo-browse/main.go
@@ -12,8 +12,10 @@ import (
 	"github.com/lmika/dynamo-browse/internal/common/ui/osstyle"
 	"github.com/lmika/dynamo-browse/internal/common/workspaces"
 	"github.com/lmika/dynamo-browse/internal/dynamo-browse/controllers"
+	"github.com/lmika/dynamo-browse/internal/dynamo-browse/models/queryexpr"
 	"github.com/lmika/dynamo-browse/internal/dynamo-browse/providers/dynamo"
 	"github.com/lmika/dynamo-browse/internal/dynamo-browse/providers/inputhistorystore"
+	"github.com/lmika/dynamo-browse/internal/dynamo-browse/providers/pasteboardprovider"
 	"github.com/lmika/dynamo-browse/internal/dynamo-browse/providers/settingstore"
 	"github.com/lmika/dynamo-browse/internal/dynamo-browse/providers/workspacestore"
 	"github.com/lmika/dynamo-browse/internal/dynamo-browse/services/inputhistory"
@@ -40,6 +42,7 @@ func main() {
 	var flagRO = flag.Bool("ro", false, "enable readonly mode")
 	var flagDefaultLimit = flag.Int("default-limit", 0, "default limit for queries and scans")
 	var flagWorkspace = flag.String("w", "", "workspace file")
+	var flagQuery = flag.String("q", "", "run query")
 	flag.Parse()
 
 	ctx := context.Background()
@@ -84,6 +87,7 @@ func main() {
 	resultSetSnapshotStore := workspacestore.NewResultSetSnapshotStore(ws)
 	settingStore := settingstore.New(ws)
 	inputHistoryStore := inputhistorystore.NewInputHistoryStore(ws)
+	pasteboardProvider := pasteboardprovider.New()
 
 	if *flagRO {
 		if err := settingStore.SetReadOnly(*flagRO); err != nil {
@@ -105,19 +109,57 @@ func main() {
 
 	state := controllers.NewState()
 	jobsController := controllers.NewJobsController(jobsService, eventBus, false)
-	tableReadController := controllers.NewTableReadController(state, tableService, workspaceService, itemRendererService, jobsController, inputHistoryService, eventBus, *flagTable)
+	tableReadController := controllers.NewTableReadController(
+		state,
+		tableService,
+		workspaceService,
+		itemRendererService,
+		jobsController,
+		inputHistoryService,
+		eventBus,
+		pasteboardProvider,
+		*flagTable,
+	)
 	tableWriteController := controllers.NewTableWriteController(state, tableService, jobsController, tableReadController, settingStore)
 	columnsController := controllers.NewColumnsController(eventBus)
-	exportController := controllers.NewExportController(state, tableService, jobsController, columnsController)
+	exportController := controllers.NewExportController(state, tableService, jobsController, columnsController, pasteboardProvider)
 	settingsController := controllers.NewSettingsController(settingStore, eventBus)
 	keyBindings := keybindings.Default()
 	scriptController := controllers.NewScriptController(scriptManagerService, tableReadController, settingsController, eventBus)
 
+	if *flagQuery != "" {
+		if *flagTable == "" {
+			cli.Fatalf("-t will need to be set for -q")
+		}
+
+		ctx := context.Background()
+
+		query, err := queryexpr.Parse(*flagQuery)
+		if err != nil {
+			cli.Fatalf("query: %v", err)
+		}
+
+		ti, err := tableService.Describe(ctx, *flagTable)
+		if err != nil {
+			cli.Fatalf("cannot describe table: %v", err)
+		}
+
+		rs, err := tableService.ScanOrQuery(ctx, ti, query, nil)
+		if err != nil {
+			cli.Fatalf("cannot execute query: %v", err)
+		}
+		if err := exportController.ExportToWriter(os.Stdout, rs); err != nil {
+			cli.Fatalf("cannot export results of query: %v", err)
+		}
+		return
+	}
+
 	keyBindingService := keybindings_service.NewService(keyBindings)
 	keyBindingController := controllers.NewKeyBindingController(keyBindingService, scriptController)
 
 	commandController := commandctrl.NewCommandController(inputHistoryService)
 	commandController.AddCommandLookupExtension(scriptController)
+	commandController.SetCommandCompletionProvider(columnsController)
 
 	model := ui.NewModel(
 		tableReadController,
@@ -131,6 +173,7 @@ func main() {
 		scriptController,
 		eventBus,
 		keyBindingController,
+		pasteboardProvider,
 		keyBindings,
 	)
 
diff --git a/go.mod b/go.mod
index a165db4..728a385 100644
--- a/go.mod
+++ b/go.mod
@@ -1,17 +1,19 @@
 module github.com/lmika/dynamo-browse
 
-go 1.18
+go 1.21
+
+toolchain go1.21.1
 
 require (
 	github.com/alecthomas/participle/v2 v2.0.0-beta.5
 	github.com/asdine/storm v2.1.2+incompatible
-	github.com/aws/aws-sdk-go-v2 v1.17.4
-	github.com/aws/aws-sdk-go-v2/config v1.13.1
-	github.com/aws/aws-sdk-go-v2/credentials v1.8.0
+	github.com/aws/aws-sdk-go-v2 v1.18.1
+	github.com/aws/aws-sdk-go-v2/config v1.18.27
+	github.com/aws/aws-sdk-go-v2/credentials v1.13.26
 	github.com/aws/aws-sdk-go-v2/feature/dynamodb/attributevalue v1.10.12
 	github.com/aws/aws-sdk-go-v2/feature/dynamodb/expression v1.4.39
-	github.com/aws/aws-sdk-go-v2/service/dynamodb v1.18.3
-	github.com/aws/aws-sdk-go-v2/service/sqs v1.16.0
+	github.com/aws/aws-sdk-go-v2/service/dynamodb v1.19.11
+	github.com/aws/aws-sdk-go-v2/service/sqs v1.23.2
 	github.com/aws/aws-sdk-go-v2/service/ssm v1.24.0
 	github.com/brianvoe/gofakeit/v6 v6.15.0
 	github.com/calyptia/go-bubble-table v0.2.1
@@ -27,7 +29,7 @@ require (
 	github.com/muesli/ansi v0.0.0-20211031195517-c9f0611b6c70
 	github.com/muesli/reflow v0.3.0
 	github.com/pkg/errors v0.9.1
-	github.com/stretchr/testify v1.8.1
+	github.com/stretchr/testify v1.8.4
 	golang.design/x/clipboard v0.6.2
 	golang.org/x/exp v0.0.0-20230108222341-4b8118a2686a
 )
@@ -35,38 +37,68 @@ require (
 require (
 	github.com/DataDog/zstd v1.5.2 // indirect
 	github.com/Sereal/Sereal v0.0.0-20220220040404-e0d1e550e879 // indirect
+	github.com/anthonynsimon/bild v0.13.0 // indirect
 	github.com/atotto/clipboard v0.1.4 // indirect
-	github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.10.0 // indirect
-	github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.28 // indirect
-	github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.22 // indirect
-	github.com/aws/aws-sdk-go-v2/internal/ini v1.3.5 // indirect
+	github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.4.10 // indirect
+	github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.13.4 // indirect
+	github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.34 // indirect
+	github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.28 // indirect
+	github.com/aws/aws-sdk-go-v2/internal/ini v1.3.35 // indirect
+	github.com/aws/aws-sdk-go-v2/internal/v4a v1.0.26 // indirect
+	github.com/aws/aws-sdk-go-v2/service/cloudformation v1.30.0 // indirect
+	github.com/aws/aws-sdk-go-v2/service/cloudfront v1.26.8 // indirect
+	github.com/aws/aws-sdk-go-v2/service/cloudtrail v1.27.1 // indirect
+	github.com/aws/aws-sdk-go-v2/service/cloudwatch v1.26.2 // indirect
 	github.com/aws/aws-sdk-go-v2/service/dynamodbstreams v1.14.3 // indirect
+	github.com/aws/aws-sdk-go-v2/service/ebs v1.16.14 // indirect
+	github.com/aws/aws-sdk-go-v2/service/ec2 v1.102.0 // indirect
+	github.com/aws/aws-sdk-go-v2/service/ecr v1.18.13 // indirect
+	github.com/aws/aws-sdk-go-v2/service/ecs v1.27.4 // indirect
+	github.com/aws/aws-sdk-go-v2/service/eks v1.27.14 // indirect
+	github.com/aws/aws-sdk-go-v2/service/elasticache v1.27.2 // indirect
+	github.com/aws/aws-sdk-go-v2/service/elasticsearchservice v1.19.2 // indirect
+	github.com/aws/aws-sdk-go-v2/service/glue v1.52.0 // indirect
+	github.com/aws/aws-sdk-go-v2/service/iam v1.21.0 // indirect
 	github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.9.11 // indirect
-	github.com/aws/aws-sdk-go-v2/service/internal/endpoint-discovery v1.7.22 // indirect
-	github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.7.0 // indirect
-	github.com/aws/aws-sdk-go-v2/service/sso v1.9.0 // indirect
-	github.com/aws/aws-sdk-go-v2/service/sts v1.14.0 // indirect
+	github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.1.29 // indirect
+	github.com/aws/aws-sdk-go-v2/service/internal/endpoint-discovery v1.7.28 // indirect
+	github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.28 // indirect
+	github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.14.3 // indirect
+	github.com/aws/aws-sdk-go-v2/service/kinesis v1.17.14 // indirect
+	github.com/aws/aws-sdk-go-v2/service/kms v1.22.2 // indirect
+	github.com/aws/aws-sdk-go-v2/service/lambda v1.37.0 // indirect
+	github.com/aws/aws-sdk-go-v2/service/rds v1.46.0 // indirect
+	github.com/aws/aws-sdk-go-v2/service/redshift v1.28.0 // indirect
+	github.com/aws/aws-sdk-go-v2/service/route53 v1.28.3 // indirect
+	github.com/aws/aws-sdk-go-v2/service/s3 v1.36.0 // indirect
+	github.com/aws/aws-sdk-go-v2/service/sfn v1.18.0 // indirect
+	github.com/aws/aws-sdk-go-v2/service/sns v1.20.13 // indirect
+	github.com/aws/aws-sdk-go-v2/service/sso v1.12.12 // indirect
+	github.com/aws/aws-sdk-go-v2/service/ssooidc v1.14.12 // indirect
+	github.com/aws/aws-sdk-go-v2/service/sts v1.19.2 // indirect
+	github.com/aws/aws-sdk-go-v2/service/wafv2 v1.35.1 // indirect
 	github.com/aws/smithy-go v1.13.5 // indirect
 	github.com/aymanbagabas/go-osc52 v1.0.3 // indirect
 	github.com/containerd/console v1.0.3 // indirect
 	github.com/davecgh/go-spew v1.1.1 // indirect
-	github.com/gofrs/uuid v4.3.1+incompatible // indirect
+	github.com/gofrs/uuid v4.4.0+incompatible // indirect
 	github.com/golang/protobuf v1.5.2 // indirect
 	github.com/golang/snappy v0.0.4 // indirect
 	github.com/hashicorp/errwrap v1.0.0 // indirect
 	github.com/hashicorp/go-multierror v1.1.1 // indirect
 	github.com/jackc/pgpassfile v1.0.0 // indirect
-	github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b // indirect
-	github.com/jackc/pgx/v5 v5.0.4 // indirect
+	github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect
+	github.com/jackc/pgx/v5 v5.4.1 // indirect
 	github.com/jmespath/go-jmespath v0.4.0 // indirect
 	github.com/juju/ansiterm v0.0.0-20210929141451-8b71cc96ebdc // indirect
 	github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
 	github.com/lunixbochs/vtclean v1.0.0 // indirect
-	github.com/mattn/go-isatty v0.0.16 // indirect
+	github.com/mattn/go-isatty v0.0.17 // indirect
 	github.com/mattn/go-localereader v0.0.1 // indirect
 	github.com/muesli/cancelreader v0.2.2 // indirect
 	github.com/muesli/termenv v0.13.0 // indirect
 	github.com/pmezard/go-difflib v1.0.0 // indirect
+	github.com/risor-io/risor v1.1.1 // indirect
 	github.com/rivo/uniseg v0.4.2 // indirect
 	github.com/sahilm/fuzzy v0.1.0 // indirect
 	github.com/stretchr/objx v0.5.0 // indirect
@@ -76,13 +108,13 @@ require (
 	github.com/vmihailenco/msgpack v4.0.4+incompatible // indirect
 	github.com/wI2L/jsondiff v0.3.0 // indirect
 	go.etcd.io/bbolt v1.3.6 // indirect
-	golang.org/x/crypto v0.0.0-20220829220503-c86fa9a7ed90 // indirect
+	golang.org/x/crypto v0.9.0 // indirect
 	golang.org/x/exp/shiny v0.0.0-20230213192124-5e25df0256eb // indirect
-	golang.org/x/image v0.0.0-20211028202545-6944b10bf410 // indirect
+	golang.org/x/image v0.5.0 // indirect
 	golang.org/x/mobile v0.0.0-20210716004757-34ab1303b554 // indirect
-	golang.org/x/sys v0.1.0 // indirect
-	golang.org/x/term v0.0.0-20220919170432-7a66f970e087 // indirect
-	golang.org/x/text v0.3.8 // indirect
+	golang.org/x/sys v0.8.0 // indirect
+	golang.org/x/term v0.8.0 // indirect
+	golang.org/x/text v0.9.0 // indirect
 	google.golang.org/appengine v1.6.7 // indirect
 	gopkg.in/yaml.v3 v3.0.1 // indirect
 )
diff --git a/go.sum b/go.sum
index ce47c1b..b97ea59 100644
--- a/go.sum
+++ b/go.sum
@@ -1,3 +1,4 @@
+github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
 github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
 github.com/DataDog/zstd v1.5.2 h1:vUG4lAyuPCXO0TLbXvPv7EB7cNK1QV/luu55UHLrrn8=
 github.com/DataDog/zstd v1.5.2/go.mod h1:g4AWEaM3yOg3HYfnJ3YIawPnVdXJh9QME85blwSAmyw=
@@ -7,6 +8,9 @@ github.com/alecthomas/assert/v2 v2.0.3 h1:WKqJODfOiQG0nEJKFKzDIG3E29CN2/4zR9XGJz
 github.com/alecthomas/participle/v2 v2.0.0-beta.5 h1:y6dsSYVb1G5eK6mgmy+BgI3Mw35a3WghArZ/Hbebrjo=
 github.com/alecthomas/participle/v2 v2.0.0-beta.5/go.mod h1:RC764t6n4L8D8ITAJv0qdokritYSNR3wV5cVwmIEaMM=
 github.com/alecthomas/repr v0.1.0 h1:ENn2e1+J3k09gyj2shc0dHr/yjaWSHRlrJ4DPMevDqE=
+github.com/anthonynsimon/bild v0.13.0 h1:mN3tMaNds1wBWi1BrJq0ipDBhpkooYfu7ZFSMhXt1C8=
+github.com/anthonynsimon/bild v0.13.0/go.mod h1:tpzzp0aYkAsMi1zmfhimaDyX1xjn2OUc1AJZK/TF0AE=
+github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
 github.com/asdine/storm v2.1.2+incompatible h1:dczuIkyqwY2LrtXPz8ixMrU/OFgZp71kbKTHGrXYt/Q=
 github.com/asdine/storm v2.1.2+incompatible/go.mod h1:RarYDc9hq1UPLImuiXK3BIWPJLdIygvV3PsInK0FbVQ=
 github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4=
@@ -15,44 +19,126 @@ github.com/aws/aws-sdk-go-v2 v1.13.0/go.mod h1:L6+ZpqHaLbAaxsqV0L4cvxZY7QupWJB4f
 github.com/aws/aws-sdk-go-v2 v1.16.1/go.mod h1:ytwTPBG6fXTZLxxeeCCWj2/EMYp/xDUgX+OET6TLNNU=
 github.com/aws/aws-sdk-go-v2 v1.17.4 h1:wyC6p9Yfq6V2y98wfDsj6OnNQa4w2BLGCLIxzNhwOGY=
 github.com/aws/aws-sdk-go-v2 v1.17.4/go.mod h1:uzbQtefpm44goOPmdKyAlXSNcwlRgF3ePWVW6EtJvvw=
+github.com/aws/aws-sdk-go-v2 v1.18.1 h1:+tefE750oAb7ZQGzla6bLkOwfcQCEtC5y2RqoqCeqKo=
+github.com/aws/aws-sdk-go-v2 v1.18.1/go.mod h1:uzbQtefpm44goOPmdKyAlXSNcwlRgF3ePWVW6EtJvvw=
+github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.4.10 h1:dK82zF6kkPeCo8J1e+tGx4JdvDIQzj7ygIoLg8WMuGs=
+github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.4.10/go.mod h1:VeTZetY5KRJLuD/7fkQXMU6Mw7H5m/KP2J5Iy9osMno=
 github.com/aws/aws-sdk-go-v2/config v1.13.1 h1:yLv8bfNoT4r+UvUKQKqRtdnvuWGMK5a82l4ru9Jvnuo=
 github.com/aws/aws-sdk-go-v2/config v1.13.1/go.mod h1:Ba5Z4yL/UGbjQUzsiaN378YobhFo0MLfueXGiOsYtEs=
+github.com/aws/aws-sdk-go-v2/config v1.18.27 h1:Az9uLwmssTE6OGTpsFqOnaGpLnKDqNYOJzWuC6UAYzA=
+github.com/aws/aws-sdk-go-v2/config v1.18.27/go.mod h1:0My+YgmkGxeqjXZb5BYme5pc4drjTnM+x1GJ3zv42Nw=
 github.com/aws/aws-sdk-go-v2/credentials v1.8.0 h1:8Ow0WcyDesGNL0No11jcgb1JAtE+WtubqXjgxau+S0o=
 github.com/aws/aws-sdk-go-v2/credentials v1.8.0/go.mod h1:gnMo58Vwx3Mu7hj1wpcG8DI0s57c9o42UQ6wgTQT5to=
+github.com/aws/aws-sdk-go-v2/credentials v1.13.26 h1:qmU+yhKmOCyujmuPY7tf5MxR/RKyZrOPO3V4DobiTUk=
+github.com/aws/aws-sdk-go-v2/credentials v1.13.26/go.mod h1:GoXt2YC8jHUBbA4jr+W3JiemnIbkXOfxSXcisUsZ3os=
 github.com/aws/aws-sdk-go-v2/feature/dynamodb/attributevalue v1.10.12 h1:ama2cD4WaH6+8Gq/M/g+ZumPmmqCyanr+6Sm+iJVxfA=
 github.com/aws/aws-sdk-go-v2/feature/dynamodb/attributevalue v1.10.12/go.mod h1:tPnUO5mS3JThpwfq4Q8iPd745s7yh6fGPqDUEBw+Wv4=
 github.com/aws/aws-sdk-go-v2/feature/dynamodb/expression v1.4.39 h1:PhgfvgqwMFQKwOcxLV7V3lNDVnR3ZUWzoB6T9oCFpR4=
 github.com/aws/aws-sdk-go-v2/feature/dynamodb/expression v1.4.39/go.mod h1:/GkvC7uHpK50ilKkKx9I2gZiI/ieZbKjS2aah1rT9uE=
 github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.10.0 h1:NITDuUZO34mqtOwFWZiXo7yAHj7kf+XPE+EiKuCBNUI=
 github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.10.0/go.mod h1:I6/fHT/fH460v09eg2gVrd8B/IqskhNdpcLH0WNO3QI=
+github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.13.4 h1:LxK/bitrAr4lnh9LnIS6i7zWbCOdMsfzKFBI6LUCS0I=
+github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.13.4/go.mod h1:E1hLXN/BL2e6YizK1zFlYd8vsfi2GTjbjBazinMmeaM=
 github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.4/go.mod h1:XHgQ7Hz2WY2GAn//UXHofLfPXWh+s62MbMOijrg12Lw=
 github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.8/go.mod h1:LnTQMTqbKsbtt+UI5+wPsB7jedW+2ZgozoPG8k6cMxg=
 github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.28 h1:r+XwaCLpIvCKjBIYy/HVZujQS9tsz5ohHG3ZIe0wKoE=
 github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.28/go.mod h1:3lwChorpIM/BhImY/hy+Z6jekmN92cXGPI1QJasVPYY=
+github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.34 h1:A5UqQEmPaCFpedKouS4v+dHCTUo2sKqhoKO9U5kxyWo=
+github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.34/go.mod h1:wZpTEecJe0Btj3IYnDx/VlUzor9wm3fJHyvLpQF0VwY=
 github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.2.0/go.mod h1:BsCSJHx5DnDXIrOcqB8KN1/B+hXLG/bi4Y6Vjcx/x9E=
 github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.2/go.mod h1:1x4ZP3Z8odssdhuLI+/1Tqw6Pt/VAaP4Tr8EUxHvPXE=
 github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.22 h1:7AwGYXDdqRQYsluvKFmWoqpcOQJ4bH634SkYf3FNj/A=
 github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.22/go.mod h1:EqK7gVrIGAHyZItrD1D8B0ilgwMD1GiWAmbU4u/JHNk=
+github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.28 h1:srIVS45eQuewqz6fKKu6ZGXaq6FuFg5NzgQBAM6g8Y4=
+github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.28/go.mod h1:7VRpKQQedkfIEXb4k52I7swUnZP0wohVajJMRn3vsUw=
 github.com/aws/aws-sdk-go-v2/internal/ini v1.3.5 h1:ixotxbfTCFpqbuwFv/RcZwyzhkxPSYDYEMcj4niB5Uk=
 github.com/aws/aws-sdk-go-v2/internal/ini v1.3.5/go.mod h1:R3sWUqPcfXSiF/LSFJhjyJmpg9uV6yP2yv3YZZjldVI=
+github.com/aws/aws-sdk-go-v2/internal/ini v1.3.35 h1:LWA+3kDM8ly001vJ1X1waCuLJdtTl48gwkPKWy9sosI=
+github.com/aws/aws-sdk-go-v2/internal/ini v1.3.35/go.mod h1:0Eg1YjxE0Bhn56lx+SHJwCzhW+2JGtizsrx+lCqrfm0=
+github.com/aws/aws-sdk-go-v2/internal/v4a v1.0.26 h1:wscW+pnn3J1OYnanMnza5ZVYXLX4cKk5rAvUAl4Qu+c=
+github.com/aws/aws-sdk-go-v2/internal/v4a v1.0.26/go.mod h1:MtYiox5gvyB+OyP0Mr0Sm/yzbEAIPL9eijj/ouHAPw0=
+github.com/aws/aws-sdk-go-v2/service/cloudformation v1.30.0 h1:XbDkc4FLeg1RfnqeblfbJvaEabqq9ByZl4zqyPFkfSc=
+github.com/aws/aws-sdk-go-v2/service/cloudformation v1.30.0/go.mod h1:SwQFcCs9Rog8hSHm+81KBkAK+UKLXErA/1ChaEI8mLE=
+github.com/aws/aws-sdk-go-v2/service/cloudfront v1.26.8 h1:loRDtQ0vT0+JCB0hQBCfv95tttEzJ1rqSaTDy5cpy0A=
+github.com/aws/aws-sdk-go-v2/service/cloudfront v1.26.8/go.mod h1:YTd4wGn2beCF9wkSTpEcupk79zDFYJk2Ca76B8YyvJg=
+github.com/aws/aws-sdk-go-v2/service/cloudtrail v1.27.1 h1:Qw1G/M7eanpm6s/URkG1UuRLKEnRnpUvkUb7NMVvWb8=
+github.com/aws/aws-sdk-go-v2/service/cloudtrail v1.27.1/go.mod h1:oKRYqorIUkfAVmX03+lpv3tW5WelDpaliqzTwmCj/k8=
+github.com/aws/aws-sdk-go-v2/service/cloudwatch v1.26.2 h1:PWGu2JhCb/XJlJ7SSFJq76pxk4xWsN76nZxh7TzMHx0=
+github.com/aws/aws-sdk-go-v2/service/cloudwatch v1.26.2/go.mod h1:2KOZkkzMDZCo/aLzPhys06mHNkiU74u85aMJA3PLRvg=
 github.com/aws/aws-sdk-go-v2/service/dynamodb v1.18.3 h1:MxOpCZ+o9+AIeQHi2ocW7H4D7p0LhEkmetETVvDnkvg=
 github.com/aws/aws-sdk-go-v2/service/dynamodb v1.18.3/go.mod h1:nkpC9xkh+3vdxmhqN8Ac10pgV14DsJDLzUsV2CcS+44=
+github.com/aws/aws-sdk-go-v2/service/dynamodb v1.19.11 h1:tLTGNAsazbfjfjW1k/i43kyCcyTTTTFaD93H7JbSbbs=
+github.com/aws/aws-sdk-go-v2/service/dynamodb v1.19.11/go.mod h1:W1oiFegjVosgjIwb2Vv45jiCQT1ee8x85u8EyZRYLes=
 github.com/aws/aws-sdk-go-v2/service/dynamodbstreams v1.14.3 h1:B+bkmCnNJi194pu9aTtYUe8f4EPXafC+xfU+zciVxdg=
 github.com/aws/aws-sdk-go-v2/service/dynamodbstreams v1.14.3/go.mod h1:bRphLmXQD9Ux4jLcFEwyrWdmuPTj2Lh8VGl9wILuJII=
+github.com/aws/aws-sdk-go-v2/service/ebs v1.16.14 h1:DosI4CvEUo6/V21pDspzYkOa2X3Zwy5XS/cbPFiqDv0=
+github.com/aws/aws-sdk-go-v2/service/ebs v1.16.14/go.mod h1:yVTqVHjnrbAj6FvhTQfjNgwQbjPbDUUvA1x4IpXFmrE=
+github.com/aws/aws-sdk-go-v2/service/ec2 v1.102.0 h1:P4dyjm49F2kKws0FpouBC6fjVImACXKt752+CWa01lM=
+github.com/aws/aws-sdk-go-v2/service/ec2 v1.102.0/go.mod h1:tIctCeX9IbzsUTKHt53SVEcgyfxV2ElxJeEB+QUbc4M=
+github.com/aws/aws-sdk-go-v2/service/ecr v1.18.13 h1:hF7MUVNjubetjggZDtn3AmqCJzD7EUi//tSdxMYPm7U=
+github.com/aws/aws-sdk-go-v2/service/ecr v1.18.13/go.mod h1:XwEFO35g0uN/SftK0asWxh8Rk6DOx37R83TmWe2tzEE=
+github.com/aws/aws-sdk-go-v2/service/ecs v1.27.4 h1:F1N0Eh5EGRRY9QpF+tMTkx8Wb59DkQWE91Xza/9dk1c=
+github.com/aws/aws-sdk-go-v2/service/ecs v1.27.4/go.mod h1:0irnFofeEZwT7uTjSkNVcSQJbWRqZ9BRoxhKjt1BObM=
+github.com/aws/aws-sdk-go-v2/service/eks v1.27.14 h1:47HQVuJXgwvuoc4AT3rVdm77H0qGFbFnsuE4PRT+xX0=
+github.com/aws/aws-sdk-go-v2/service/eks v1.27.14/go.mod h1:QxuWcm9rlLkW3aEV8tiDzqZewnNSNUZfnqJvo1Nv9A0=
+github.com/aws/aws-sdk-go-v2/service/elasticache v1.27.2 h1:IC9XLGcT3yEkziTlX7PX54km7cHJnltlV7Ppwq2+7ik=
+github.com/aws/aws-sdk-go-v2/service/elasticache v1.27.2/go.mod h1:+oJhn/SIud310/2LLSVmlNZmExmlYPaGCLmUsnq5JZc=
+github.com/aws/aws-sdk-go-v2/service/elasticsearchservice v1.19.2 h1:Zam6yofBgdtP13laNoeA+DA9wlKJNooU8p3CWw6xLaI=
+github.com/aws/aws-sdk-go-v2/service/elasticsearchservice v1.19.2/go.mod h1:dehjpZ00q0RJcBUOUEysaj7zHK2rHSS4ePp89MsFiaI=
+github.com/aws/aws-sdk-go-v2/service/glue v1.52.0 h1:ukSf8ZdoZ6AygsUWIjj177wLOXljxBspBaNMgvx6fRA=
+github.com/aws/aws-sdk-go-v2/service/glue v1.52.0/go.mod h1:wMCE0B6l8eHb57l2DMYCGxt0rHIfcu3RvIY7SAfc+Fs=
+github.com/aws/aws-sdk-go-v2/service/iam v1.21.0 h1:8hEpu60CWlrp7iEBUFRZhgPoX6+gadaGL1sD4LoRYS0=
+github.com/aws/aws-sdk-go-v2/service/iam v1.21.0/go.mod h1:aQZ8BI+reeaY7RI/QQp7TKCSUHOesTdrzzylp3CW85c=
 github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.9.11 h1:y2+VQzC6Zh2ojtV2LoC0MNwHWc6qXv/j2vrQtlftkdA=
 github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.9.11/go.mod h1:iV4q2hsqtNECrfmlXyord9u4zyuFEJX9eLgLpSPzWA8=
+github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.1.29 h1:zZSLP3v3riMOP14H7b4XP0uyfREDQOYv2cqIrvTXDNQ=
+github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.1.29/go.mod h1:z7EjRjVwZ6pWcWdI2H64dKttvzaP99jRIj5hphW0M5U=
 github.com/aws/aws-sdk-go-v2/service/internal/endpoint-discovery v1.7.22 h1:6zEryIiJOSk5/OcVHzkPDwzNBQ2atYCTShyA7TqkuxA=
 github.com/aws/aws-sdk-go-v2/service/internal/endpoint-discovery v1.7.22/go.mod h1:moeOz5SKfY0p6pNIChdPIQdfaUfWI67+OVe0/r6+aGY=
+github.com/aws/aws-sdk-go-v2/service/internal/endpoint-discovery v1.7.28 h1:/D994rtMQd1jQ2OY+7tvUlMlrv1L1c7Xtma/FhkbVtY=
+github.com/aws/aws-sdk-go-v2/service/internal/endpoint-discovery v1.7.28/go.mod h1:3bJI2pLY3ilrqO5EclusI1GbjFJh1iXYrhOItf2sjKw=
 github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.7.0 h1:4QAOB3KrvI1ApJK14sliGr3Ie2pjyvNypn/lfzDHfUw=
 github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.7.0/go.mod h1:K/qPe6AP2TGYv4l6n7c88zh9jWBDf6nHhvg1fx/EWfU=
+github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.28 h1:bkRyG4a929RCnpVSTvLM2j/T4ls015ZhhYApbmYs15s=
+github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.28/go.mod h1:jj7znCIg05jXlaGBlFMGP8+7UN3VtCkRBG2spnmRQkU=
+github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.14.3 h1:dBL3StFxHtpBzJJ/mNEsjXVgfO+7jR0dAIEwLqMapEA=
+github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.14.3/go.mod h1:f1QyiAsvIv4B49DmCqrhlXqyaR+0IxMmyX+1P+AnzOM=
+github.com/aws/aws-sdk-go-v2/service/kinesis v1.17.14 h1:oSw0SQN9cKeYvCUYfPul7bH11b8E9I9BnoVUme3iSaU=
+github.com/aws/aws-sdk-go-v2/service/kinesis v1.17.14/go.mod h1:omXkSCk1T1difhE8wVaecXNeerY6jmpFFu49ngjEDQk=
+github.com/aws/aws-sdk-go-v2/service/kms v1.22.2 h1:jwmtdM1/l1DRNy5jQrrYpsQm8zwetkgeqhAqefDr1yI=
+github.com/aws/aws-sdk-go-v2/service/kms v1.22.2/go.mod h1:aNfh11Smy55o65PB3MyKbkM8BFyFUcZmj1k+4g8eNfg=
+github.com/aws/aws-sdk-go-v2/service/lambda v1.37.0 h1:xzyM5ZR9kZW0/Bkw5EiihOy6B+BYclp5K+yb6OHjc7s=
+github.com/aws/aws-sdk-go-v2/service/lambda v1.37.0/go.mod h1:Q8zQi5nZpjUF/H55dKEpKfEvFWJkgZzjjqvDb2AR5b4=
+github.com/aws/aws-sdk-go-v2/service/rds v1.46.0 h1:uv2LAciZRd5lEXzJo2u92tdZh/JxcVL7YLC51D4NLG4=
+github.com/aws/aws-sdk-go-v2/service/rds v1.46.0/go.mod h1:goBDR4OPrsnKpYyU0GHGcEnlTmL8O+eKGsWeyOAFJ5M=
+github.com/aws/aws-sdk-go-v2/service/redshift v1.28.0 h1:tmhg03t7nNVSFqhxb8YpHqq8H1wwwrfEQv/rL7NkTAE=
+github.com/aws/aws-sdk-go-v2/service/redshift v1.28.0/go.mod h1:x9am33DT5lVKUb0DH1UVbX+iFfpIqAKx6DAqB5Qu6jU=
+github.com/aws/aws-sdk-go-v2/service/route53 v1.28.3 h1:nJbE4+tHd8xpM1RB1ZF0/xTJnFd/ATz42ZC35lwXx0w=
+github.com/aws/aws-sdk-go-v2/service/route53 v1.28.3/go.mod h1:Cd4MnFoV+6fELBrgWXJ4Y09FrSkn/VjNPkOr1Yr1muU=
+github.com/aws/aws-sdk-go-v2/service/s3 v1.36.0 h1:lEmQ1XSD9qLk+NZXbgvLJI/IiTz7OIR2TYUTFH25EI4=
+github.com/aws/aws-sdk-go-v2/service/s3 v1.36.0/go.mod h1:aVbf0sko/TsLWHx30c/uVu7c62+0EAJ3vbxaJga0xCw=
+github.com/aws/aws-sdk-go-v2/service/sfn v1.18.0 h1:1AIwJvCywFO4nGtHj7ZtKb9mhLpB5hToyjtE5OO6o/I=
+github.com/aws/aws-sdk-go-v2/service/sfn v1.18.0/go.mod h1:41VgIwo6R/QE8DnFZ4RrP+f2w9xTzB77h3NRu/BzXyE=
+github.com/aws/aws-sdk-go-v2/service/sns v1.20.13 h1:+ADGcDhddHTKyu6Qp3oZKootryteS7D3ODo2ZPDBgjQ=
+github.com/aws/aws-sdk-go-v2/service/sns v1.20.13/go.mod h1:rWrvp9i8y/lX94lS7Kn/0iu9RY6vXzeKRqS/knVX8/c=
 github.com/aws/aws-sdk-go-v2/service/sqs v1.16.0 h1:dzWS4r8E9bA0TesHM40FSAtedwpTVCuTsLI8EziSqyk=
 github.com/aws/aws-sdk-go-v2/service/sqs v1.16.0/go.mod h1:IBTQMG8mtyj37OWg7vIXcg714Ntcb/LlYou/rZpvV1k=
+github.com/aws/aws-sdk-go-v2/service/sqs v1.23.2 h1:Y2vfLiY3HmaMisuwx6fS2kMRYbajRXXB+9vesGVPseY=
+github.com/aws/aws-sdk-go-v2/service/sqs v1.23.2/go.mod h1:TaV67b6JMD1988x/uMDop/JnMFK6v5d4Ru+sDmFg+ww=
 github.com/aws/aws-sdk-go-v2/service/ssm v1.24.0 h1:p22U2yL/AeRToERGcZv1R26Yci5VQnWIrpzcZdG54cg=
 github.com/aws/aws-sdk-go-v2/service/ssm v1.24.0/go.mod h1:chcyLYBEVRac/7rWJsD6cUHUR2osROwavvNqCplfwog=
 github.com/aws/aws-sdk-go-v2/service/sso v1.9.0 h1:1qLJeQGBmNQW3mBNzK2CFmrQNmoXWrscPqsrAaU1aTA=
 github.com/aws/aws-sdk-go-v2/service/sso v1.9.0/go.mod h1:vCV4glupK3tR7pw7ks7Y4jYRL86VvxS+g5qk04YeWrU=
+github.com/aws/aws-sdk-go-v2/service/sso v1.12.12 h1:nneMBM2p79PGWBQovYO/6Xnc2ryRMw3InnDJq1FHkSY=
+github.com/aws/aws-sdk-go-v2/service/sso v1.12.12/go.mod h1:HuCOxYsF21eKrerARYO6HapNeh9GBNq7fius2AcwodY=
+github.com/aws/aws-sdk-go-v2/service/ssooidc v1.14.12 h1:2qTR7IFk7/0IN/adSFhYu9Xthr0zVFTgBrmPldILn80=
+github.com/aws/aws-sdk-go-v2/service/ssooidc v1.14.12/go.mod h1:E4VrHCPzmVB/KFXtqBGKb3c8zpbNBgKe3fisDNLAW5w=
 github.com/aws/aws-sdk-go-v2/service/sts v1.14.0 h1:ksiDXhvNYg0D2/UFkLejsaz3LqpW5yjNQ8Nx9Sn2c0E=
 github.com/aws/aws-sdk-go-v2/service/sts v1.14.0/go.mod h1:u0xMJKDvvfocRjiozsoZglVNXRG19043xzp3r2ivLIk=
+github.com/aws/aws-sdk-go-v2/service/sts v1.19.2 h1:XFJ2Z6sNUUcAz9poj+245DMkrHE4h2j5I9/xD50RHfE=
+github.com/aws/aws-sdk-go-v2/service/sts v1.19.2/go.mod h1:dp0yLPsLBOi++WTxzCjA/oZqi6NPIhoR+uF7GeMU9eg=
+github.com/aws/aws-sdk-go-v2/service/wafv2 v1.35.1 h1:FtzLuTf9HPECIcKdBMtA16ZwZWOIj/r57Z3QuWuYfqc=
+github.com/aws/aws-sdk-go-v2/service/wafv2 v1.35.1/go.mod h1:RBpb9oTsEgAUfyaTAT2hFC83DxtLxj+SQpcbhaXiHnU=
 github.com/aws/smithy-go v1.10.0/go.mod h1:SObp3lf9smib00L/v3U2eAKG8FyQ7iLrJnQiAmR5n+E=
 github.com/aws/smithy-go v1.11.2/go.mod h1:3xHYmszWVx2c0kIwQeEVf9uSm4fYZt67FBJnwub1bgM=
 github.com/aws/smithy-go v1.13.5 h1:hgz0X/DX0dGqTYpGALqXJoRKRj5oQ7150i5FdTePzO8=
@@ -76,11 +162,18 @@ github.com/cloudcmds/tamarin v1.0.0 h1:PhrJ74FCUJo24/nIPXnQe9E3WVEIYo4aG58pICOMD
 github.com/cloudcmds/tamarin v1.0.0/go.mod h1:U1aHBoAFtJbI9jzgaj8TUo9C6vfzUKzn1OhWKIdigVM=
 github.com/containerd/console v1.0.3 h1:lIr7SlA5PxZyMV30bDW0MGbiOPXwc63yRuCP0ARubLw=
 github.com/containerd/console v1.0.3/go.mod h1:7LqA/THxQ86k76b8c/EMSiaJ3h1eZkMkXar0TQ1gf3U=
+github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
+github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk=
+github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
+github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE=
 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/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
 github.com/gofrs/uuid v4.3.1+incompatible h1:0/KbAdpx3UXAx1kEOWHJeOkpbgRFGHVgv+CFIY7dBJI=
 github.com/gofrs/uuid v4.3.1+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
+github.com/gofrs/uuid v4.4.0+incompatible h1:3qXRTX8/NbyulANqlc0lchS1gqAVxRgsuW1YrTJupqA=
+github.com/gofrs/uuid v4.4.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
 github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
 github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
 github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw=
@@ -97,13 +190,19 @@ github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/U
 github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
 github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=
 github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
+github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
 github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM=
+github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
 github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
 github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
 github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b h1:C8S2+VttkHFdOOCXJe+YGfa4vHYwlt4Zx+IVXQ97jYg=
 github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b/go.mod h1:vsD4gTJCa9TptPL8sPkXrLZ+hDuNrZCnj29CQpr4X1E=
+github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk=
+github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
 github.com/jackc/pgx/v5 v5.0.4 h1:r5O6y84qHX/z/HZV40JBdx2obsHz7/uRj5b+CcYEdeY=
 github.com/jackc/pgx/v5 v5.0.4/go.mod h1:U0ynklHtgg43fue9Ly30w3OCSTDPlXjig9ghrNGaguQ=
+github.com/jackc/pgx/v5 v5.4.1 h1:oKfB/FhuVtit1bBM3zNRRsZ925ZkMN3HXL+LgLUM9lE=
+github.com/jackc/pgx/v5 v5.4.1/go.mod h1:q6iHT8uDNXWiFNOlRqJzBTaSH3+2xCXkokxHZC5qWFY=
 github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg=
 github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo=
 github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8=
@@ -112,6 +211,7 @@ github.com/juju/ansiterm v0.0.0-20210929141451-8b71cc96ebdc h1:ZQrgZFsLzkw7o3CoD
 github.com/juju/ansiterm v0.0.0-20210929141451-8b71cc96ebdc/go.mod h1:PyXUpnI3olx3bsPcHt98FGPX/KCFZ1Fi+hw1XLI6384=
 github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
 github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
+github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
 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=
@@ -129,11 +229,14 @@ github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69
 github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
 github.com/lunixbochs/vtclean v1.0.0 h1:xu2sLAri4lGiovBDQKxl5mrXyESr3gUr5m5SM5+LVb8=
 github.com/lunixbochs/vtclean v1.0.0/go.mod h1:pHhQNgMf3btfWnGBVipUOjRYhoOsdGqdm/+2c2E2WMI=
+github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
 github.com/mattn/go-colorable v0.1.10/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
 github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
 github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
 github.com/mattn/go-isatty v0.0.16 h1:bq3VjFmv/sOjHtdEhmkEV4x1AJtvUvOJ2PFAZ5+peKQ=
 github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
+github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng=
+github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
 github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4=
 github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88=
 github.com/mattn/go-runewidth v0.0.10/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
@@ -141,6 +244,8 @@ github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRC
 github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
 github.com/mattn/go-runewidth v0.0.14 h1:+xnbZSEeDbOIg5/mE6JF0w6n9duR1l3/WmbinWVwUuU=
 github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
+github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
+github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
 github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b/go.mod h1:fQuZ0gauxyBcmsdE3ZT4NasjaRdxmbCS0jRHsrWu3Ho=
 github.com/muesli/ansi v0.0.0-20211031195517-c9f0611b6c70 h1:kMlmsLSbjkikxQJ1IPwaM+7LJ9ltFu/fi8CRzvSnQmA=
 github.com/muesli/ansi v0.0.0-20211031195517-c9f0611b6c70/go.mod h1:fQuZ0gauxyBcmsdE3ZT4NasjaRdxmbCS0jRHsrWu3Ho=
@@ -154,27 +259,45 @@ github.com/muesli/termenv v0.11.1-0.20220204035834-5ac8409525e0/go.mod h1:Bd5NYQ
 github.com/muesli/termenv v0.11.1-0.20220212125758-44cd13922739/go.mod h1:Bd5NYQ7pd+SrtBSrSNoBBmXlcY8+Xj4BMJgh8qcZrvs=
 github.com/muesli/termenv v0.13.0 h1:wK20DRpJdDX8b7Ek2QfhvqhRQFZ237RGRO0RQ/Iqdy0=
 github.com/muesli/termenv v0.13.0/go.mod h1:sP1+uffeLaEYpyOTb8pLCUctGcGLnoFjSn4YJK5e2bc=
+github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
 github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
 github.com/pkg/errors v0.9.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/risor-io/risor v0.8.0 h1:G0fpHMGztvocKVu8egkKNbvLy4Rsjkuk+0zReu2JSn8=
+github.com/risor-io/risor v0.8.0/go.mod h1:lvatEIYxs6HL+X/Bm0R+Mq4Z9a5Y036mniw6DwUnqs0=
+github.com/risor-io/risor v1.1.1 h1:J8rIZX/0HXhg/t2+QygksvP65XCWhg5QxRZrwZabhxE=
+github.com/risor-io/risor v1.1.1/go.mod h1:0UMw7ZMbUKSPFgQyuHCFe7UuBUewBKX4K3By4ba1CBA=
 github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
 github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
 github.com/rivo/uniseg v0.4.2 h1:YwD0ulJSJytLpiaWua0sBDusfsCZohxjxzVTYjwxfV8=
 github.com/rivo/uniseg v0.4.2/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
 github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
+github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
 github.com/sahilm/fuzzy v0.1.0 h1:FzWGaw2Opqyu+794ZQ9SYifWv2EIXpwP4q8dY1kDAwI=
 github.com/sahilm/fuzzy v0.1.0/go.mod h1:VFvziUEIMCrT6A6tw2RFIXPXXmzXbOsSHF0DOI8ZK9Y=
+github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
+github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
+github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU=
+github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
+github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
+github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s=
 github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
 github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
 github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c=
 github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
+github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
 github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
 github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
+github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
 github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
 github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
 github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
 github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
+github.com/stretchr/testify v1.8.3 h1:RP3t2pwF7cMEbC1dqtB6poj3niw/9gnV4Cjg5oW5gtY=
+github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
+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/tidwall/gjson v1.14.3 h1:9jvXn7olKEHU1S9vwoMGliaT8jq1vJ7IH/n9zD9Dnlw=
 github.com/tidwall/gjson v1.14.3/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
 github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
@@ -182,42 +305,58 @@ github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JT
 github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
 github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4=
 github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
+github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0=
 github.com/vmihailenco/msgpack v4.0.4+incompatible h1:dSLoQfGFAo3F6OoNhwUmLwVgaUXK79GlxNBwueZn0xI=
 github.com/vmihailenco/msgpack v4.0.4+incompatible/go.mod h1:fy3FlTQTDXWkZ7Bh6AcGMlsjHatGryHQYUTf1ShIgkk=
 github.com/wI2L/jsondiff v0.3.0 h1:iTzQ9u/d86GE9RsBzVHX88f2EA1vQUboHwLhSQFc1s4=
 github.com/wI2L/jsondiff v0.3.0/go.mod h1:y1IMzNNjlSsk3IUoJdRJO7VRBtzMvRgyo4Vu0LdHpTc=
+github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
 github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
+github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
 go.etcd.io/bbolt v1.3.6 h1:/ecaJf0sk1l4l6V4awd65v2C3ILy7MSj+s/x1ADCIMU=
 go.etcd.io/bbolt v1.3.6/go.mod h1:qXsaaIqmgQH0T+OPdb99Bf+PKfBBQVAdyD6TY9G8XM4=
 golang.design/x/clipboard v0.6.2 h1:a3Np4qfKnLWwfFJQhUWU3IDeRfmVuqWl+QPtP4CSYGw=
 golang.design/x/clipboard v0.6.2/go.mod h1:kqBSweBP0/im4SZGGjLrppH0D400Hnfo5WbFKSNK8N4=
+golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
 golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
 golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
 golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
+golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
 golang.org/x/crypto v0.0.0-20220829220503-c86fa9a7ed90 h1:Y/gsMcFOcR+6S6f3YeMKl5g+dZMEWqcz5Czj/GWYbkM=
 golang.org/x/crypto v0.0.0-20220829220503-c86fa9a7ed90/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
+golang.org/x/crypto v0.9.0 h1:LF6fAI+IutBocDJ2OT0Q1g8plpYljMZ4+lty+dsqw3g=
+golang.org/x/crypto v0.9.0/go.mod h1:yrmDGqONDYtNj3tH8X9dzUun2m2lzPa9ngI6/RUPGR0=
 golang.org/x/exp v0.0.0-20190731235908-ec7cb31e5a56/go.mod h1:JhuoJpWY28nO4Vef9tZUw9qufEGTyX1+7lmHxV5q5G4=
 golang.org/x/exp v0.0.0-20230108222341-4b8118a2686a h1:tlXy25amD5A7gOfbXdqCGN5k8ESEed/Ee1E5RcrYnqU=
 golang.org/x/exp v0.0.0-20230108222341-4b8118a2686a/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc=
 golang.org/x/exp/shiny v0.0.0-20230213192124-5e25df0256eb h1:gdeQX7xJSkTNF+Sw7++XNIOo4pGL0CjQv3N2Vm1Erxk=
 golang.org/x/exp/shiny v0.0.0-20230213192124-5e25df0256eb/go.mod h1:UH99kUObWAZkDnWqppdQe5ZhPYESUw8I0zVV1uWBR+0=
 golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
+golang.org/x/image v0.0.0-20190703141733-d6a02ce849c9/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
 golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
 golang.org/x/image v0.0.0-20211028202545-6944b10bf410 h1:hTftEOvwiOq2+O8k2D5/Q7COC7k5Qcrgc2TFURJYnvQ=
 golang.org/x/image v0.0.0-20211028202545-6944b10bf410/go.mod h1:023OzeP/+EPmXeapQh35lcL3II3LrY8Ic+EFFKVhULM=
+golang.org/x/image v0.5.0 h1:5JMiNunQeQw++mMOz48/ISeNu3Iweh/JaZU8ZLqHRrI=
+golang.org/x/image v0.5.0/go.mod h1:FVC7BI/5Ym8R25iw5OLsgshdUBbT1h5jZTpA+mvAdZ4=
 golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
 golang.org/x/mobile v0.0.0-20210716004757-34ab1303b554 h1:3In5TnfvnuXTF/uflgpYxSCEGP2NdYT37KsPh3VjZYU=
 golang.org/x/mobile v0.0.0-20210716004757-34ab1303b554/go.mod h1:jFTmtFYCV0MFtXBU+J5V/+5AUeVS0ON/0WkE/KSrl6E=
 golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
 golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
+golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
 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-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
 golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
 golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
 golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2 h1:CIJ76btIcR3eFI5EgSo6k1qKw9KJexJuRLI9G7Hp5wE=
+golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
+golang.org/x/net v0.10.0 h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M=
 golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 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-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -231,13 +370,19 @@ golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBc
 golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20220204135822-1c1b9b1eba6a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20220209214540-3681064d5158/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.1.0 h1:kunALQeHf1/185U1i0GOB/fy1IPRDDpuoOOqRReG57U=
 golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU=
+golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
 golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
 golang.org/x/term v0.0.0-20220919170432-7a66f970e087 h1:tPwmk4vmvVCMdr98VgL4JH+qZxPL8fqlUOHnyOM8N3w=
 golang.org/x/term v0.0.0-20220919170432-7a66f970e087/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
+golang.org/x/term v0.8.0 h1:n5xxQn2i3PC0yLAbjTpNT85q/Kgzcr2gIoX9OrJUols=
+golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
 golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
 golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
@@ -245,10 +390,14 @@ golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
 golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
 golang.org/x/text v0.3.8 h1:nAL+RVCQ9uMn3vJZbV+MRnydTJFPf8qqY42YiA6MrqY=
 golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
+golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
+golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE=
+golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
 golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
 golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
 golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
 golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
+golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
 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-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
diff --git a/internal/common/ui/commandctrl/commandctrl.go b/internal/common/ui/commandctrl/commandctrl.go
index b3e831f..c0d857f 100644
--- a/internal/common/ui/commandctrl/commandctrl.go
+++ b/internal/common/ui/commandctrl/commandctrl.go
@@ -18,9 +18,10 @@ import (
 const commandsCategory = "commands"
 
 type CommandController struct {
-	historyProvider  IterProvider
-	commandList      *CommandList
-	lookupExtensions []CommandLookupExtension
+	historyProvider    IterProvider
+	commandList        *CommandList
+	lookupExtensions   []CommandLookupExtension
+	completionProvider CommandCompletionProvider
 }
 
 func NewCommandController(historyProvider IterProvider) *CommandController {
@@ -40,6 +41,10 @@ func (c *CommandController) AddCommandLookupExtension(ext CommandLookupExtension
 	c.lookupExtensions = append(c.lookupExtensions, ext)
 }
 
+func (c *CommandController) SetCommandCompletionProvider(provider CommandCompletionProvider) {
+	c.completionProvider = provider
+}
+
 func (c *CommandController) Prompt() tea.Msg {
 	return events.PromptForInputMsg{
 		Prompt:  ":",
@@ -47,6 +52,24 @@ func (c *CommandController) Prompt() tea.Msg {
 		OnDone: func(value string) tea.Msg {
 			return c.Execute(value)
 		},
+		// TEMP
+		OnTabComplete: func(value string) (string, bool) {
+			if c.completionProvider == nil {
+				return "", false
+			}
+
+			if strings.HasPrefix(value, "sa ") || strings.HasPrefix(value, "da ") {
+				tokens := shellwords.Split(strings.TrimSpace(value))
+				lastToken := tokens[len(tokens)-1]
+
+				options := c.completionProvider.AttributesWithPrefix(lastToken)
+				if len(options) == 1 {
+					return value[:len(value)-len(lastToken)] + options[0], true
+				}
+			}
+			return "", false
+		},
+		// END TEMP
 	}
 }
 
diff --git a/internal/common/ui/commandctrl/types.go b/internal/common/ui/commandctrl/types.go
index c8a9058..7861e09 100644
--- a/internal/common/ui/commandctrl/types.go
+++ b/internal/common/ui/commandctrl/types.go
@@ -19,3 +19,7 @@ type CommandList struct {
 type CommandLookupExtension interface {
 	LookupCommand(name string) Command
 }
+
+type CommandCompletionProvider interface {
+	AttributesWithPrefix(prefix string) []string
+}
diff --git a/internal/common/ui/events/commands.go b/internal/common/ui/events/commands.go
index 183c9c6..68fdd5d 100644
--- a/internal/common/ui/events/commands.go
+++ b/internal/common/ui/events/commands.go
@@ -54,3 +54,8 @@ type MessageWithMode interface {
 	MessageWithStatus
 	ModeMessage() string
 }
+
+type MessageWithRightMode interface {
+	MessageWithStatus
+	RightModeMessage() string
+}
diff --git a/internal/common/ui/events/errors.go b/internal/common/ui/events/errors.go
index 08eda79..127c3b3 100644
--- a/internal/common/ui/events/errors.go
+++ b/internal/common/ui/events/errors.go
@@ -21,8 +21,9 @@ type ModeMessage string
 
 // PromptForInput indicates that the context is requesting a line of input
 type PromptForInputMsg struct {
-	Prompt   string
-	History  services.HistoryProvider
-	OnDone   func(value string) tea.Msg
-	OnCancel func() tea.Msg
+	Prompt        string
+	History       services.HistoryProvider
+	OnDone        func(value string) tea.Msg
+	OnCancel      func() tea.Msg
+	OnTabComplete func(value string) (string, bool)
 }
diff --git a/internal/dynamo-browse/controllers/columns.go b/internal/dynamo-browse/controllers/columns.go
index b05f999..8080477 100644
--- a/internal/dynamo-browse/controllers/columns.go
+++ b/internal/dynamo-browse/controllers/columns.go
@@ -7,6 +7,7 @@ import (
 	"github.com/lmika/dynamo-browse/internal/dynamo-browse/models/columns"
 	"github.com/lmika/dynamo-browse/internal/dynamo-browse/models/queryexpr"
 	bus "github.com/lmika/events"
+	"strings"
 )
 
 type ColumnsController struct {
@@ -115,3 +116,13 @@ func (cc *ColumnsController) DeleteColumn(afterIndex int) tea.Msg {
 
 	return ColumnsUpdated{}
 }
+
+func (c *ColumnsController) AttributesWithPrefix(prefix string) []string {
+	options := make([]string, 0)
+	for _, col := range c.resultSet.Columns() {
+		if strings.HasPrefix(col, prefix) {
+			options = append(options, col)
+		}
+	}
+	return options
+}
diff --git a/internal/dynamo-browse/controllers/events.go b/internal/dynamo-browse/controllers/events.go
index b2ef4cc..3d0821f 100644
--- a/internal/dynamo-browse/controllers/events.go
+++ b/internal/dynamo-browse/controllers/events.go
@@ -4,6 +4,8 @@ import (
 	"fmt"
 	tea "github.com/charmbracelet/bubbletea"
 	"github.com/lmika/dynamo-browse/internal/dynamo-browse/models"
+	"strings"
+	"time"
 )
 
 type SetTableItemView struct {
@@ -42,6 +44,24 @@ func (rs NewResultSet) ModeMessage() string {
 	return modeLine
 }
 
+func (rs NewResultSet) RightModeMessage() string {
+	var sb strings.Builder
+
+	itemCountStr := applyToN("", len(rs.ResultSet.Items()), "item", "items", "")
+	if rs.currentFilter != "" {
+		sb.WriteString(fmt.Sprintf("%d of %v", rs.filteredCount, itemCountStr))
+	} else {
+		sb.WriteString(itemCountStr)
+	}
+
+	if !rs.ResultSet.Created.IsZero() {
+		sb.WriteString(" • ")
+		sb.WriteString(rs.ResultSet.Created.Format(time.Kitchen))
+	}
+
+	return sb.String()
+}
+
 func (rs NewResultSet) StatusMessage() string {
 	if rs.statusMessage != "" {
 		return rs.statusMessage
diff --git a/internal/dynamo-browse/controllers/export.go b/internal/dynamo-browse/controllers/export.go
index 15a86c6..7f4ce8e 100644
--- a/internal/dynamo-browse/controllers/export.go
+++ b/internal/dynamo-browse/controllers/export.go
@@ -1,26 +1,39 @@
 package controllers
 
 import (
+	"bytes"
 	"context"
 	"encoding/csv"
 	"fmt"
+	"io"
+	"os"
+
 	tea "github.com/charmbracelet/bubbletea"
 	"github.com/lmika/dynamo-browse/internal/common/ui/events"
+	"github.com/lmika/dynamo-browse/internal/dynamo-browse/models"
 	"github.com/lmika/dynamo-browse/internal/dynamo-browse/models/attrutils"
+	"github.com/lmika/dynamo-browse/internal/dynamo-browse/models/columns"
+	"github.com/lmika/dynamo-browse/internal/dynamo-browse/services"
 	"github.com/lmika/dynamo-browse/internal/dynamo-browse/services/jobs"
 	"github.com/pkg/errors"
-	"os"
 )
 
 type ExportController struct {
-	state         *State
-	tableService  TableReadService
-	jobController *JobsController
-	columns       *ColumnsController
+	state              *State
+	tableService       TableReadService
+	jobController      *JobsController
+	columns            *ColumnsController
+	pasteboardProvider services.PasteboardProvider
 }
 
-func NewExportController(state *State, tableService TableReadService, jobsController *JobsController, columns *ColumnsController) *ExportController {
-	return &ExportController{state, tableService, jobsController, columns}
+func NewExportController(
+	state *State,
+	tableService TableReadService,
+	jobsController *JobsController,
+	columns *ColumnsController,
+	pasteboardProvider services.PasteboardProvider,
+) *ExportController {
+	return &ExportController{state, tableService, jobsController, columns, pasteboardProvider}
 }
 
 func (c *ExportController) ExportCSV(filename string, opts ExportOptions) tea.Msg {
@@ -79,6 +92,54 @@ func (c *ExportController) ExportCSV(filename string, opts ExportOptions) tea.Ms
 	}).Submit()
 }
 
+func (c *ExportController) ExportCSVToClipboard() tea.Msg {
+	var bts bytes.Buffer
+
+	resultSet := c.state.ResultSet()
+	if resultSet == nil {
+		return errors.New("no result set")
+	}
+
+	if err := c.exportCSV(&bts, c.columns.Columns().VisibleColumns(), resultSet); err != nil {
+		return events.Error(err)
+	}
+
+	if err := c.pasteboardProvider.WriteText(bts.Bytes()); err != nil {
+		return events.Error(err)
+	}
+	return nil
+}
+
+// TODO: this really needs to be a service!
+func (c *ExportController) ExportToWriter(w io.Writer, resultSet *models.ResultSet) error {
+	return c.exportCSV(w, columns.NewColumnsFromResultSet(resultSet).Columns, resultSet)
+}
+
+func (c *ExportController) exportCSV(w io.Writer, cols []columns.Column, resultSet *models.ResultSet) error {
+	cw := csv.NewWriter(w)
+	defer cw.Flush()
+
+	colNames := make([]string, len(cols))
+	for i, c := range cols {
+		colNames[i] = c.Name
+	}
+	if err := cw.Write(colNames); err != nil {
+		return errors.Wrap(err, "cannot export to clipboard")
+	}
+
+	row := make([]string, len(cols))
+	for _, item := range resultSet.Items() {
+		for i, col := range cols {
+			row[i], _ = attrutils.AttributeToString(col.Evaluator.EvaluateForItem(item))
+		}
+		if err := cw.Write(row); err != nil {
+			return errors.Wrap(err, "cannot export to clipboard")
+		}
+	}
+
+	return nil
+}
+
 type ExportOptions struct {
 	// AllResults returns all results from the table
 	AllResults bool
diff --git a/internal/dynamo-browse/controllers/scripts_test.go b/internal/dynamo-browse/controllers/scripts_test.go
index a99e820..ffcb8a8 100644
--- a/internal/dynamo-browse/controllers/scripts_test.go
+++ b/internal/dynamo-browse/controllers/scripts_test.go
@@ -1,12 +1,13 @@
 package controllers_test
 
 import (
+	"testing"
+	"time"
+
 	"github.com/aws/aws-sdk-go-v2/service/dynamodb/types"
 	"github.com/lmika/dynamo-browse/internal/common/ui/events"
 	"github.com/lmika/dynamo-browse/internal/dynamo-browse/controllers"
 	"github.com/stretchr/testify/assert"
-	"testing"
-	"time"
 )
 
 func TestScriptController_RunScript(t *testing.T) {
@@ -53,7 +54,7 @@ func TestScriptController_RunScript(t *testing.T) {
 			srv := newService(t, serviceConfig{
 				tableName: "alpha-table",
 				scriptFS: testScriptFile(t, "test.tm", `
-					rs := session.query('pk="abc"').unwrap()
+					rs := session.query('pk="abc"')
 					ui.print(rs.length)
 				`),
 			})
@@ -72,7 +73,7 @@ func TestScriptController_RunScript(t *testing.T) {
 			srv := newService(t, serviceConfig{
 				tableName: "alpha-table",
 				scriptFS: testScriptFile(t, "test.tm", `
-					rs := session.query('pk!="abc"', { table: "count-to-30" }).unwrap()
+					rs := session.query('pk!="abc"', { table: "count-to-30" })
 					ui.print(rs.length)
 				`),
 			})
@@ -93,7 +94,7 @@ func TestScriptController_RunScript(t *testing.T) {
 			srv := newService(t, serviceConfig{
 				tableName: "alpha-table",
 				scriptFS: testScriptFile(t, "test.tm", `
-					rs := session.query('pk="abc"').unwrap()
+					rs := session.query('pk="abc"')
 					session.set_result_set(rs)
 				`),
 			})
@@ -112,7 +113,7 @@ func TestScriptController_RunScript(t *testing.T) {
 			srv := newService(t, serviceConfig{
 				tableName: "alpha-table",
 				scriptFS: testScriptFile(t, "test.tm", `
-						rs := session.query('pk="abc"').unwrap()
+						rs := session.query('pk="abc"')
 						rs[0].set_attr("pk", "131")
 						session.set_result_set(rs)
 					`),
@@ -135,22 +136,35 @@ func TestScriptController_RunScript(t *testing.T) {
 
 func TestScriptController_LookupCommand(t *testing.T) {
 	t.Run("should schedule the script on a separate go-routine", func(t *testing.T) {
-		srv := newService(t, serviceConfig{
-			tableName: "alpha-table",
-			scriptFS: testScriptFile(t, "test.tm", `
-				ext.command("mycommand", func(name) {
-					ui.print("Hello, ", name)
+		scenarios := []struct {
+			descr          string
+			command        string
+			expectedOutput string
+		}{
+			{descr: "command with arg", command: "mycommand \"test name\"", expectedOutput: "Hello, test name"},
+			{descr: "command no arg", command: "mycommand", expectedOutput: "Hello, nil value"},
+		}
+
+		for _, scenario := range scenarios {
+			t.Run(scenario.descr, func(t *testing.T) {
+				srv := newService(t, serviceConfig{
+					tableName: "alpha-table",
+					scriptFS: testScriptFile(t, "test.tm", `
+						ext.command("mycommand", func(name = "nil value") {
+							ui.print(sprintf("Hello, %v", name))
+						})
+					`),
 				})
-			`),
-		})
 
-		invokeCommand(t, srv.scriptController.LoadScript("test.tm"))
-		invokeCommand(t, srv.commandController.Execute(`mycommand "test name"`))
+				invokeCommand(t, srv.scriptController.LoadScript("test.tm"))
+				invokeCommand(t, srv.commandController.Execute(scenario.command))
 
-		srv.msgSender.waitForAtLeastOneMessages(t, 5*time.Second)
+				srv.msgSender.waitForAtLeastOneMessages(t, 5*time.Second)
 
-		assert.Len(t, srv.msgSender.msgs, 1)
-		assert.Equal(t, events.StatusMsg("Hello, test name"), srv.msgSender.msgs[0])
+				assert.Len(t, srv.msgSender.msgs, 1)
+				assert.Equal(t, events.StatusMsg(scenario.expectedOutput), srv.msgSender.msgs[0])
+			})
+		}
 	})
 
 	t.Run("should only allow one script to run at a time", func(t *testing.T) {
diff --git a/internal/dynamo-browse/controllers/state.go b/internal/dynamo-browse/controllers/state.go
index 2a75518..6a886d2 100644
--- a/internal/dynamo-browse/controllers/state.go
+++ b/internal/dynamo-browse/controllers/state.go
@@ -1,9 +1,8 @@
 package controllers
 
 import (
-	"sync"
-
 	"github.com/lmika/dynamo-browse/internal/dynamo-browse/models"
+	"sync"
 )
 
 type State struct {
diff --git a/internal/dynamo-browse/controllers/tableread.go b/internal/dynamo-browse/controllers/tableread.go
index 5297d1a..f055e7d 100644
--- a/internal/dynamo-browse/controllers/tableread.go
+++ b/internal/dynamo-browse/controllers/tableread.go
@@ -4,22 +4,24 @@ import (
 	"bytes"
 	"context"
 	"fmt"
+	"log"
+	"strings"
+	"sync"
+
 	"github.com/aws/aws-sdk-go-v2/service/dynamodb/types"
 	tea "github.com/charmbracelet/bubbletea"
 	"github.com/lmika/dynamo-browse/internal/common/ui/events"
 	"github.com/lmika/dynamo-browse/internal/dynamo-browse/models"
 	"github.com/lmika/dynamo-browse/internal/dynamo-browse/models/attrcodec"
+	"github.com/lmika/dynamo-browse/internal/dynamo-browse/models/attrutils"
 	"github.com/lmika/dynamo-browse/internal/dynamo-browse/models/queryexpr"
 	"github.com/lmika/dynamo-browse/internal/dynamo-browse/models/serialisable"
+	"github.com/lmika/dynamo-browse/internal/dynamo-browse/services"
 	"github.com/lmika/dynamo-browse/internal/dynamo-browse/services/inputhistory"
 	"github.com/lmika/dynamo-browse/internal/dynamo-browse/services/itemrenderer"
 	"github.com/lmika/dynamo-browse/internal/dynamo-browse/services/viewsnapshot"
 	bus "github.com/lmika/events"
 	"github.com/pkg/errors"
-	"golang.design/x/clipboard"
-	"log"
-	"strings"
-	"sync"
 )
 
 type resultSetUpdateOp int
@@ -57,11 +59,11 @@ type TableReadController struct {
 	eventBus            *bus.Bus
 	tableName           string
 	loadFromLastView    bool
+	pasteboardProvider  services.PasteboardProvider
 
 	// state
-	mutex         *sync.Mutex
-	state         *State
-	clipboardInit bool
+	mutex *sync.Mutex
+	state *State
 }
 
 func NewTableReadController(
@@ -72,6 +74,7 @@ func NewTableReadController(
 	jobController *JobsController,
 	inputHistoryService *inputhistory.Service,
 	eventBus *bus.Bus,
+	pasteboardProvider services.PasteboardProvider,
 	tableName string,
 ) *TableReadController {
 	return &TableReadController{
@@ -83,6 +86,7 @@ func NewTableReadController(
 		inputHistoryService: inputHistoryService,
 		eventBus:            eventBus,
 		tableName:           tableName,
+		pasteboardProvider:  pasteboardProvider,
 		mutex:               new(sync.Mutex),
 	}
 }
@@ -276,13 +280,35 @@ func (c *TableReadController) setResultSetAndFilter(resultSet *models.ResultSet,
 	return c.state.buildNewResultSetMessage("")
 }
 
-func (c *TableReadController) Mark(op MarkOp) tea.Msg {
-	c.state.withResultSet(func(resultSet *models.ResultSet) {
-		for i := range resultSet.Items() {
+func (c *TableReadController) Mark(op MarkOp, where string) tea.Msg {
+	var (
+		whereExpr *queryexpr.QueryExpr
+		err       error
+	)
+
+	if where != "" {
+		whereExpr, err = queryexpr.Parse(where)
+		if err != nil {
+			return events.Error(err)
+		}
+	}
+
+	if err := c.state.withResultSetReturningError(func(resultSet *models.ResultSet) error {
+		for i, item := range resultSet.Items() {
 			if resultSet.Hidden(i) {
 				continue
 			}
 
+			if whereExpr != nil {
+				res, err := whereExpr.EvalItem(item)
+				if err != nil {
+					return errors.Wrapf(err, "item %d", i)
+				}
+				if !attrutils.Truthy(res) {
+					continue
+				}
+			}
+
 			switch op {
 			case MarkOpMark:
 				resultSet.SetMark(i, true)
@@ -292,7 +318,10 @@ func (c *TableReadController) Mark(op MarkOp) tea.Msg {
 				resultSet.SetMark(i, !resultSet.Marked(i))
 			}
 		}
-	})
+		return nil
+	}); err != nil {
+		return events.Error(err)
+	}
 	return ResultSetUpdated{}
 }
 
@@ -446,12 +475,8 @@ func (c *TableReadController) updateViewToSnapshot(viewSnapshot *serialisable.Vi
 }
 
 func (c *TableReadController) CopyItemToClipboard(idx int) tea.Msg {
-	if err := c.initClipboard(); err != nil {
-		return events.Error(err)
-	}
-
 	itemCount := 0
-	c.state.withResultSet(func(resultSet *models.ResultSet) {
+	if err := c.state.withResultSetReturningError(func(resultSet *models.ResultSet) error {
 		sb := new(strings.Builder)
 		_ = applyToMarkedItems(resultSet, idx, func(idx int, item models.Item) error {
 			if sb.Len() > 0 {
@@ -461,23 +486,14 @@ func (c *TableReadController) CopyItemToClipboard(idx int) tea.Msg {
 			itemCount += 1
 			return nil
 		})
-		clipboard.Write(clipboard.FmtText, []byte(sb.String()))
-	})
+
+		if err := c.pasteboardProvider.WriteText([]byte(sb.String())); err != nil {
+			return err
+		}
+		return nil
+	}); err != nil {
+		return events.Error(err)
+	}
 
 	return events.StatusMsg(applyToN("", itemCount, "item", "items", " copied to clipboard"))
 }
-
-func (c *TableReadController) initClipboard() error {
-	c.mutex.Lock()
-	defer c.mutex.Unlock()
-
-	if c.clipboardInit {
-		return nil
-	}
-
-	if err := clipboard.Init(); err != nil {
-		return errors.Wrap(err, "unable to enable clipboard")
-	}
-	c.clipboardInit = true
-	return nil
-}
diff --git a/internal/dynamo-browse/controllers/tablewrite.go b/internal/dynamo-browse/controllers/tablewrite.go
index a92319b..033a0cb 100644
--- a/internal/dynamo-browse/controllers/tablewrite.go
+++ b/internal/dynamo-browse/controllers/tablewrite.go
@@ -458,6 +458,44 @@ func (twc *TableWriteController) assertReadWrite() error {
 	return nil
 }
 
+func (twc *TableWriteController) CloneItem(idx int) tea.Msg {
+	if err := twc.assertReadWrite(); err != nil {
+		return events.Error(err)
+	}
+
+	// Work out which keys we need to prompt for
+	rs := twc.state.ResultSet()
+
+	keyPrompts := &promptSequence{
+		prompts: []string{rs.TableInfo.Keys.PartitionKey + ": "},
+	}
+	if rs.TableInfo.Keys.SortKey != "" {
+		keyPrompts.prompts = append(keyPrompts.prompts, rs.TableInfo.Keys.SortKey+": ")
+	}
+	keyPrompts.onAllDone = func(values []string) tea.Msg {
+		twc.state.withResultSet(func(set *models.ResultSet) {
+			applyToMarkedItems(set, idx, func(idx int, item models.Item) error {
+				// TODO: should be a deep clone
+				clonedItem := item.Clone()
+
+				clonedItem[rs.TableInfo.Keys.PartitionKey] = &types.AttributeValueMemberS{Value: values[0]}
+				if len(values) == 2 {
+					clonedItem[rs.TableInfo.Keys.SortKey] = &types.AttributeValueMemberS{Value: values[1]}
+				}
+
+				set.AddNewItem(clonedItem, models.ItemAttribute{
+					New:   true,
+					Dirty: true,
+				})
+				return nil
+			})
+		})
+		return twc.state.buildNewResultSetMessage("New item cloned")
+	}
+
+	return keyPrompts.next()
+}
+
 func applyToN(prefix string, n int, singular, plural, suffix string) string {
 	if n == 1 {
 		return fmt.Sprintf("%v%v %v%v", prefix, n, singular, suffix)
diff --git a/internal/dynamo-browse/controllers/tablewrite_test.go b/internal/dynamo-browse/controllers/tablewrite_test.go
index a4f25ec..493b5c7 100644
--- a/internal/dynamo-browse/controllers/tablewrite_test.go
+++ b/internal/dynamo-browse/controllers/tablewrite_test.go
@@ -9,6 +9,7 @@ import (
 	"github.com/lmika/dynamo-browse/internal/dynamo-browse/models"
 	"github.com/lmika/dynamo-browse/internal/dynamo-browse/providers/dynamo"
 	"github.com/lmika/dynamo-browse/internal/dynamo-browse/providers/inputhistorystore"
+	"github.com/lmika/dynamo-browse/internal/dynamo-browse/providers/pasteboardprovider"
 	"github.com/lmika/dynamo-browse/internal/dynamo-browse/providers/settingstore"
 	"github.com/lmika/dynamo-browse/internal/dynamo-browse/providers/workspacestore"
 	"github.com/lmika/dynamo-browse/internal/dynamo-browse/services/inputhistory"
@@ -617,11 +618,21 @@ func newService(t *testing.T, cfg serviceConfig) *services {
 
 	state := controllers.NewState()
 	jobsController := controllers.NewJobsController(jobs.NewService(eventBus), eventBus, true)
-	readController := controllers.NewTableReadController(state, service, workspaceService, itemRendererService, jobsController, inputHistoryService, eventBus, cfg.tableName)
+	readController := controllers.NewTableReadController(
+		state,
+		service,
+		workspaceService,
+		itemRendererService,
+		jobsController,
+		inputHistoryService,
+		eventBus,
+		pasteboardprovider.NilProvider{},
+		cfg.tableName,
+	)
 	writeController := controllers.NewTableWriteController(state, service, jobsController, readController, settingStore)
 	settingsController := controllers.NewSettingsController(settingStore, eventBus)
 	columnsController := controllers.NewColumnsController(eventBus)
-	exportController := controllers.NewExportController(state, service, jobsController, columnsController)
+	exportController := controllers.NewExportController(state, service, jobsController, columnsController, pasteboardprovider.NilProvider{})
 	scriptController := controllers.NewScriptController(scriptService, readController, settingsController, eventBus)
 
 	commandController := commandctrl.NewCommandController(inputHistoryService)
diff --git a/internal/dynamo-browse/models/attrutils/truthy.go b/internal/dynamo-browse/models/attrutils/truthy.go
new file mode 100644
index 0000000..91bce48
--- /dev/null
+++ b/internal/dynamo-browse/models/attrutils/truthy.go
@@ -0,0 +1,32 @@
+package attrutils
+
+import (
+	"github.com/aws/aws-sdk-go-v2/service/dynamodb/types"
+)
+
+func Truthy(x types.AttributeValue) bool {
+	switch xVal := x.(type) {
+	case *types.AttributeValueMemberS:
+		return len(xVal.Value) > 0
+	case *types.AttributeValueMemberN:
+		return len(xVal.Value) > 0 && xVal.Value != "0"
+	case *types.AttributeValueMemberBOOL:
+		return xVal.Value
+	case *types.AttributeValueMemberB:
+		return len(xVal.Value) > 0
+	case *types.AttributeValueMemberNULL:
+		return !xVal.Value
+	case *types.AttributeValueMemberL:
+		return len(xVal.Value) > 0
+	case *types.AttributeValueMemberM:
+		return len(xVal.Value) > 0
+	case *types.AttributeValueMemberBS:
+		return len(xVal.Value) > 0
+	case *types.AttributeValueMemberNS:
+		return len(xVal.Value) > 0
+	case *types.AttributeValueMemberSS:
+		return len(xVal.Value) > 0
+	}
+
+	return false
+}
diff --git a/internal/dynamo-browse/models/items.go b/internal/dynamo-browse/models/items.go
index 0df6bf8..1319bee 100644
--- a/internal/dynamo-browse/models/items.go
+++ b/internal/dynamo-browse/models/items.go
@@ -16,7 +16,7 @@ type Item map[string]types.AttributeValue
 func (i Item) Clone() Item {
 	newItem := Item{}
 
-	// TODO: should be a deep clone?
+	// TODO: should be a deep clone? YES!!
 	for k, v := range i {
 		newItem[k] = v
 	}
@@ -33,6 +33,14 @@ func (i Item) KeyValue(info *TableInfo) map[string]types.AttributeValue {
 	return itemKey
 }
 
+func (i Item) PKSK(info *TableInfo) (pk types.AttributeValue, sk types.AttributeValue) {
+	pk = i[info.Keys.PartitionKey]
+	if info.Keys.SortKey != "" {
+		sk = i[info.Keys.SortKey]
+	}
+	return pk, sk
+}
+
 func (i Item) AttributeValueAsString(key string) (string, bool) {
 	return attrutils.AttributeToString(i[key])
 }
diff --git a/internal/dynamo-browse/models/models.go b/internal/dynamo-browse/models/models.go
index f94aa72..0aed4f1 100644
--- a/internal/dynamo-browse/models/models.go
+++ b/internal/dynamo-browse/models/models.go
@@ -3,12 +3,14 @@ package models
 import (
 	"github.com/aws/aws-sdk-go-v2/service/dynamodb/types"
 	"sort"
+	"time"
 )
 
 type ResultSet struct {
 	// Query information
 	TableInfo         *TableInfo
 	Query             Queryable
+	Created           time.Time
 	ExclusiveStartKey map[string]types.AttributeValue
 
 	// Result information
diff --git a/internal/dynamo-browse/models/queryexpr/builtins.go b/internal/dynamo-browse/models/queryexpr/builtins.go
index a3e9322..d1a072a 100644
--- a/internal/dynamo-browse/models/queryexpr/builtins.go
+++ b/internal/dynamo-browse/models/queryexpr/builtins.go
@@ -2,6 +2,7 @@ package queryexpr
 
 import (
 	"context"
+
 	"github.com/pkg/errors"
 )
 
@@ -50,6 +51,42 @@ var nativeFuncs = map[string]nativeFunc{
 		return listExprValue(xs), nil
 	},
 
+	"marked": func(ctx context.Context, args []exprValue) (exprValue, error) {
+		if len(args) != 1 {
+			return nil, InvalidArgumentNumberError{Name: "marked", Expected: 1, Actual: len(args)}
+		}
+
+		fieldName, ok := args[0].(stringableExprValue)
+		if !ok {
+			return nil, InvalidArgumentTypeError{Name: "marked", ArgIndex: 0, Expected: "S"}
+		}
+
+		rs := currentResultSetFromContext(ctx)
+		if rs == nil {
+			return listExprValue{}, nil
+		}
+
+		var items = []exprValue{}
+		for i, itm := range rs.Items() {
+			if !rs.Marked(i) {
+				continue
+			}
+
+			attr, hasAttr := itm[fieldName.asString()]
+			if !hasAttr {
+				continue
+			}
+
+			exprAttrValue, err := newExprValueFromAttributeValue(attr)
+			if err != nil {
+				return nil, errors.Wrapf(err, "marked(): item %d, attr %v", i, fieldName.asString())
+			}
+
+			items = append(items, exprAttrValue)
+		}
+		return listExprValue(items), nil
+	},
+
 	"_x_now": func(ctx context.Context, args []exprValue) (exprValue, error) {
 		now := timeSourceFromContext(ctx).now().Unix()
 		return int64ExprValue(now), nil
diff --git a/internal/dynamo-browse/models/queryexpr/context.go b/internal/dynamo-browse/models/queryexpr/context.go
index 18c56ef..08bf1e1 100644
--- a/internal/dynamo-browse/models/queryexpr/context.go
+++ b/internal/dynamo-browse/models/queryexpr/context.go
@@ -3,6 +3,8 @@ package queryexpr
 import (
 	"context"
 	"time"
+
+	"github.com/lmika/dynamo-browse/internal/dynamo-browse/models"
 )
 
 type timeSource interface {
@@ -25,3 +27,14 @@ func timeSourceFromContext(ctx context.Context) timeSource {
 	}
 	return defaultTimeSource{}
 }
+
+type currentResultSetContextKeyType struct{}
+
+var currentResultSetContextKey = currentResultSetContextKeyType{}
+
+func currentResultSetFromContext(ctx context.Context) *models.ResultSet {
+	if crs, ok := ctx.Value(currentResultSetContextKey).(*models.ResultSet); ok {
+		return crs
+	}
+	return nil
+}
diff --git a/internal/dynamo-browse/models/queryexpr/dot.go b/internal/dynamo-browse/models/queryexpr/dot.go
index 8981f5a..cdb9412 100644
--- a/internal/dynamo-browse/models/queryexpr/dot.go
+++ b/internal/dynamo-browse/models/queryexpr/dot.go
@@ -18,7 +18,7 @@ func (dt *astRef) unqualifiedName() (string, bool) {
 func (dt *astRef) evalItem(ctx *evalContext, item models.Item) (exprValue, error) {
 	res, hasV := item[dt.Name]
 	if !hasV {
-		return nil, nil
+		return undefinedExprValue{}, nil
 	}
 
 	return newExprValueFromAttributeValue(res)
diff --git a/internal/dynamo-browse/models/queryexpr/equality.go b/internal/dynamo-browse/models/queryexpr/equality.go
index 8d317e5..f67803b 100644
--- a/internal/dynamo-browse/models/queryexpr/equality.go
+++ b/internal/dynamo-browse/models/queryexpr/equality.go
@@ -73,8 +73,6 @@ func (a *astEqualityOp) evalItem(ctx *evalContext, item models.Item) (exprValue,
 		return nil, err
 	}
 
-	// TODO: use expr values here
-
 	switch a.Op {
 	case "=":
 		cmp, isComparable := attrutils.CompareScalarAttributes(left.asAttributeValue(), right.asAttributeValue())
@@ -96,7 +94,7 @@ func (a *astEqualityOp) evalItem(ctx *evalContext, item models.Item) (exprValue,
 
 		leftAsStr, canBeString := left.(stringableExprValue)
 		if !canBeString {
-			return nil, ValueNotConvertableToString{Val: leftAsStr.asAttributeValue()}
+			return nil, ValueNotConvertableToString{Val: left.asAttributeValue()}
 		}
 		return boolExprValue(strings.HasPrefix(leftAsStr.asString(), strValue.asString())), nil
 	}
diff --git a/internal/dynamo-browse/models/queryexpr/errors.go b/internal/dynamo-browse/models/queryexpr/errors.go
index 172b2fa..852aaef 100644
--- a/internal/dynamo-browse/models/queryexpr/errors.go
+++ b/internal/dynamo-browse/models/queryexpr/errors.go
@@ -69,6 +69,10 @@ type ValueNotConvertableToString struct {
 
 func (n ValueNotConvertableToString) Error() string {
 	render := itemrender.ToRenderer(n.Val)
+	if render == nil {
+		return "nil value is not convertable to string"
+	}
+
 	return fmt.Sprintf("values '%v', type %v, is not convertable to string", render.StringValue(), render.TypeName())
 }
 
diff --git a/internal/dynamo-browse/models/queryexpr/expr.go b/internal/dynamo-browse/models/queryexpr/expr.go
index 2d254e7..5a263cc 100644
--- a/internal/dynamo-browse/models/queryexpr/expr.go
+++ b/internal/dynamo-browse/models/queryexpr/expr.go
@@ -3,6 +3,9 @@ package queryexpr
 import (
 	"bytes"
 	"encoding/gob"
+	"hash/fnv"
+	"io"
+
 	"github.com/aws/aws-sdk-go-v2/service/dynamodb/types"
 	"github.com/lmika/dynamo-browse/internal/dynamo-browse/models"
 	"github.com/lmika/dynamo-browse/internal/dynamo-browse/models/attrcodec"
@@ -10,15 +13,14 @@ import (
 	"github.com/pkg/errors"
 	"golang.org/x/exp/maps"
 	"golang.org/x/exp/slices"
-	"hash/fnv"
-	"io"
 )
 
 type QueryExpr struct {
-	ast    *astExpr
-	index  string
-	names  map[string]string
-	values map[string]types.AttributeValue
+	ast              *astExpr
+	index            string
+	names            map[string]string
+	values           map[string]types.AttributeValue
+	currentResultSet *models.ResultSet
 
 	// tests fields only
 	timeSource timeSource
@@ -141,10 +143,11 @@ func (md *QueryExpr) HashCode() uint64 {
 
 func (md *QueryExpr) WithNameParams(value map[string]string) *QueryExpr {
 	return &QueryExpr{
-		ast:    md.ast,
-		index:  md.index,
-		names:  value,
-		values: md.values,
+		ast:              md.ast,
+		index:            md.index,
+		names:            value,
+		values:           md.values,
+		currentResultSet: md.currentResultSet,
 	}
 }
 
@@ -166,19 +169,31 @@ func (md *QueryExpr) ValueParamOrNil(name string) types.AttributeValue {
 
 func (md *QueryExpr) WithValueParams(value map[string]types.AttributeValue) *QueryExpr {
 	return &QueryExpr{
-		ast:    md.ast,
-		index:  md.index,
-		names:  md.names,
-		values: value,
+		ast:              md.ast,
+		index:            md.index,
+		names:            md.names,
+		values:           value,
+		currentResultSet: md.currentResultSet,
 	}
 }
 
 func (md *QueryExpr) WithIndex(index string) *QueryExpr {
 	return &QueryExpr{
-		ast:    md.ast,
-		index:  index,
-		names:  md.names,
-		values: md.values,
+		ast:              md.ast,
+		index:            index,
+		names:            md.names,
+		values:           md.values,
+		currentResultSet: md.currentResultSet,
+	}
+}
+
+func (md *QueryExpr) WithCurrentResultSet(currentResultSet *models.ResultSet) *QueryExpr {
+	return &QueryExpr{
+		ast:              md.ast,
+		index:            md.index,
+		names:            md.names,
+		values:           md.values,
+		currentResultSet: currentResultSet,
 	}
 }
 
@@ -217,6 +232,7 @@ func (md *QueryExpr) evalContext() *evalContext {
 	return &evalContext{
 		namePlaceholders:  md.names,
 		valuePlaceholders: md.values,
+		ctxResultSet:      md.currentResultSet,
 	}
 }
 
@@ -268,6 +284,7 @@ type evalContext struct {
 	valuePlaceholders map[string]types.AttributeValue
 	valueLookup       func(string) (types.AttributeValue, bool)
 	timeSource        timeSource
+	ctxResultSet      *models.ResultSet
 }
 
 func (ec *evalContext) lookupName(name string) (string, bool) {
diff --git a/internal/dynamo-browse/models/queryexpr/expr_test.go b/internal/dynamo-browse/models/queryexpr/expr_test.go
index b8c7dce..76e7a02 100644
--- a/internal/dynamo-browse/models/queryexpr/expr_test.go
+++ b/internal/dynamo-browse/models/queryexpr/expr_test.go
@@ -3,11 +3,12 @@ package queryexpr_test
 import (
 	"bytes"
 	"fmt"
+	"testing"
+	"time"
+
 	"github.com/aws/aws-sdk-go-v2/aws"
 	"github.com/aws/aws-sdk-go-v2/service/dynamodb/types"
 	"github.com/lmika/dynamo-browse/internal/dynamo-browse/models/queryexpr"
-	"testing"
-	"time"
 
 	"github.com/lmika/dynamo-browse/internal/dynamo-browse/models"
 	"github.com/stretchr/testify/assert"
@@ -603,19 +604,40 @@ func TestQueryExpr_EvalItem(t *testing.T) {
 	t.Run("functions", func(t *testing.T) {
 		timeNow := time.Now()
 
+		contextResultSet := models.ResultSet{}
+		contextResultSet.SetItems([]models.Item{
+			{"pk": &types.AttributeValueMemberS{Value: "1"}, "num": &types.AttributeValueMemberN{Value: "1"}},
+			{"pk": &types.AttributeValueMemberS{Value: "2"}, "num": &types.AttributeValueMemberN{Value: "2"}},
+			{"pk": &types.AttributeValueMemberS{Value: "3"}, "num": &types.AttributeValueMemberN{Value: "3"}},
+			{"pk": &types.AttributeValueMemberS{Value: "4"}, "num": &types.AttributeValueMemberN{Value: "4"}},
+		})
+		contextResultSet.SetMark(0, true)
+		contextResultSet.SetMark(1, true)
+
 		scenarios := []struct {
 			expr     string
 			expected types.AttributeValue
 		}{
 			// _x_now() -- unreleased version of now
 			{expr: `_x_now()`, expected: &types.AttributeValueMemberN{Value: fmt.Sprint(timeNow.Unix())}},
+
+			// Marked
+			{expr: `marked("num")`, expected: &types.AttributeValueMemberL{Value: []types.AttributeValue{
+				&types.AttributeValueMemberN{Value: "1"},
+				&types.AttributeValueMemberN{Value: "2"},
+			}}},
+			{expr: `one in marked("num")`, expected: &types.AttributeValueMemberBOOL{Value: true}},
+			{expr: `three in marked("num")`, expected: &types.AttributeValueMemberBOOL{Value: false}},
 		}
 		for _, scenario := range scenarios {
 			t.Run(scenario.expr, func(t *testing.T) {
 				modExpr, err := queryexpr.Parse(scenario.expr)
 				assert.NoError(t, err)
 
-				res, err := modExpr.WithTestTimeSource(timeNow).EvalItem(item)
+				res, err := modExpr.
+					WithTestTimeSource(timeNow).
+					WithCurrentResultSet(&contextResultSet).
+					EvalItem(item)
 				assert.NoError(t, err)
 
 				assert.Equal(t, scenario.expected, res)
@@ -646,6 +668,10 @@ func TestQueryExpr_EvalItem(t *testing.T) {
 		}{
 			{expr: `alpha.bravo`, expectedError: queryexpr.ValueNotAMapError([]string{"alpha", "bravo"})},
 			{expr: `charlie.tree.bla`, expectedError: queryexpr.ValueNotAMapError([]string{"charlie", "tree", "bla"})},
+
+			{expr: `missing="no"`, expectedError: queryexpr.ValuesNotComparable{Right: &types.AttributeValueMemberS{Value: "no"}}},
+			{expr: `missing!="no"`, expectedError: queryexpr.ValuesNotComparable{Right: &types.AttributeValueMemberS{Value: "no"}}},
+			{expr: `missing^="no"`, expectedError: queryexpr.ValueNotConvertableToString{nil}},
 		}
 
 		for _, scenario := range scenarios {
diff --git a/internal/dynamo-browse/models/queryexpr/fncall.go b/internal/dynamo-browse/models/queryexpr/fncall.go
index 69d6790..5a092af 100644
--- a/internal/dynamo-browse/models/queryexpr/fncall.go
+++ b/internal/dynamo-browse/models/queryexpr/fncall.go
@@ -2,11 +2,12 @@ package queryexpr
 
 import (
 	"context"
+	"strings"
+
 	"github.com/aws/aws-sdk-go-v2/feature/dynamodb/expression"
 	"github.com/lmika/dynamo-browse/internal/common/sliceutils"
 	"github.com/lmika/dynamo-browse/internal/dynamo-browse/models"
 	"github.com/pkg/errors"
-	"strings"
 )
 
 func (a *astFunctionCall) evalToIR(ctx *evalContext, info *models.TableInfo) (irAtom, error) {
@@ -88,6 +89,7 @@ func (a *astFunctionCall) evalItem(ctx *evalContext, item models.Item) (exprValu
 	}
 
 	cCtx := context.WithValue(context.Background(), timeSourceContextKey, ctx.timeSource)
+	cCtx = context.WithValue(cCtx, currentResultSetContextKey, ctx.ctxResultSet)
 	return fn(cCtx, args)
 }
 
diff --git a/internal/dynamo-browse/models/queryexpr/helpers_test.go b/internal/dynamo-browse/models/queryexpr/helpers_test.go
index e856529..fe495da 100644
--- a/internal/dynamo-browse/models/queryexpr/helpers_test.go
+++ b/internal/dynamo-browse/models/queryexpr/helpers_test.go
@@ -14,3 +14,4 @@ func (a *QueryExpr) WithTestTimeSource(timeNow time.Time) *QueryExpr {
 	a.timeSource = testTimeSource(timeNow)
 	return a
 }
+
diff --git a/internal/dynamo-browse/models/queryexpr/is.go b/internal/dynamo-browse/models/queryexpr/is.go
index c7cd392..cc2bc88 100644
--- a/internal/dynamo-browse/models/queryexpr/is.go
+++ b/internal/dynamo-browse/models/queryexpr/is.go
@@ -127,7 +127,7 @@ func (a *astIsOp) evalItem(ctx *evalContext, item models.Item) (exprValue, error
 
 	var resultOfIs bool
 	if typeInfo.isAny {
-		resultOfIs = ref != nil
+		resultOfIs = ref != undefinedExprValue{}
 	} else {
 		refType := reflect.TypeOf(ref)
 
diff --git a/internal/dynamo-browse/models/queryexpr/types.go b/internal/dynamo-browse/models/queryexpr/types.go
index 32e39ae..2e55c7a 100644
--- a/internal/dynamo-browse/models/queryexpr/types.go
+++ b/internal/dynamo-browse/models/queryexpr/types.go
@@ -83,6 +83,20 @@ func newExprValueFromAttributeValue(ev types.AttributeValue) (exprValue, error)
 	return nil, errors.New("cannot convert to expr value")
 }
 
+type undefinedExprValue struct{}
+
+func (b undefinedExprValue) asGoValue() any {
+	return nil
+}
+
+func (b undefinedExprValue) asAttributeValue() types.AttributeValue {
+	return nil
+}
+
+func (s undefinedExprValue) typeName() string {
+	return "UNDEFINED"
+}
+
 type stringExprValue string
 
 func (s stringExprValue) asGoValue() any {
diff --git a/internal/dynamo-browse/providers/pasteboardprovider/nilprovider.go b/internal/dynamo-browse/providers/pasteboardprovider/nilprovider.go
new file mode 100644
index 0000000..7285aff
--- /dev/null
+++ b/internal/dynamo-browse/providers/pasteboardprovider/nilprovider.go
@@ -0,0 +1,11 @@
+package pasteboardprovider
+
+type NilProvider struct{}
+
+func (NilProvider) ReadText() (string, bool) {
+	return "", false
+}
+
+func (n NilProvider) WriteText(bts []byte) error {
+	return nil
+}
diff --git a/internal/dynamo-browse/providers/pasteboardprovider/providers.go b/internal/dynamo-browse/providers/pasteboardprovider/providers.go
new file mode 100644
index 0000000..fb53ac2
--- /dev/null
+++ b/internal/dynamo-browse/providers/pasteboardprovider/providers.go
@@ -0,0 +1,55 @@
+package pasteboardprovider
+
+import (
+	"github.com/pkg/errors"
+	"golang.design/x/clipboard"
+	"sync"
+)
+
+type Provider struct {
+	mutex         *sync.Mutex
+	clipboardInit bool
+}
+
+func New() *Provider {
+	return &Provider{
+		mutex: new(sync.Mutex),
+	}
+}
+
+func (c *Provider) initClipboard() error {
+	c.mutex.Lock()
+	defer c.mutex.Unlock()
+
+	if c.clipboardInit {
+		return nil
+	}
+
+	if err := clipboard.Init(); err != nil {
+		return errors.Wrap(err, "unable to enable clipboard")
+	}
+	c.clipboardInit = true
+	return nil
+}
+
+func (c *Provider) WriteText(bts []byte) error {
+	if err := c.initClipboard(); err != nil {
+		return err
+	}
+
+	clipboard.Write(clipboard.FmtText, bts)
+	return nil
+}
+
+func (c *Provider) ReadText() (string, bool) {
+	if err := c.initClipboard(); err != nil {
+		return "", false
+	}
+
+	content := clipboard.Read(clipboard.FmtText)
+	if content == nil {
+		return "", false
+	}
+
+	return string(content), true
+}
diff --git a/internal/dynamo-browse/providers/settingstore/settingstore.go b/internal/dynamo-browse/providers/settingstore/settingstore.go
index bcdba19..b22b596 100644
--- a/internal/dynamo-browse/providers/settingstore/settingstore.go
+++ b/internal/dynamo-browse/providers/settingstore/settingstore.go
@@ -113,7 +113,7 @@ func (c *SettingStore) SetDefaultLimit(limit int) error {
 
 func (c *SettingStore) getStringValue(key string, def string) (string, error) {
 	var val string
-	if err := c.ws.Get(settingBucket, keyTableReadOnly, &val); err != nil {
+	if err := c.ws.Get(settingBucket, key, &val); err != nil {
 		if errors.Is(err, storm.ErrNotFound) {
 			return def, nil
 		}
diff --git a/internal/dynamo-browse/services/pasteboardprovider.go b/internal/dynamo-browse/services/pasteboardprovider.go
new file mode 100644
index 0000000..18b3549
--- /dev/null
+++ b/internal/dynamo-browse/services/pasteboardprovider.go
@@ -0,0 +1,6 @@
+package services
+
+type PasteboardProvider interface {
+	ReadText() (string, bool)
+	WriteText(bts []byte) error
+}
diff --git a/internal/dynamo-browse/services/scriptmanager/builtins.go b/internal/dynamo-browse/services/scriptmanager/builtins.go
index 9674c99..87e2852 100644
--- a/internal/dynamo-browse/services/scriptmanager/builtins.go
+++ b/internal/dynamo-browse/services/scriptmanager/builtins.go
@@ -7,7 +7,8 @@ package scriptmanager
 
 import (
 	"context"
-	"github.com/cloudcmds/tamarin/object"
+	"fmt"
+	"github.com/risor-io/risor/object"
 	"log"
 )
 
@@ -53,3 +54,19 @@ func printfBuiltin(ctx context.Context, args ...object.Object) object.Object {
 	log.Printf("%s "+format, values...)
 	return object.Nil
 }
+
+// This is taken from the args package
+func require(funcName string, count int, args []object.Object) *object.Error {
+	nArgs := len(args)
+	if nArgs != count {
+		if count == 1 {
+			return object.Errorf(
+				fmt.Sprintf("type error: %s() takes exactly 1 argument (%d given)",
+					funcName, nArgs))
+		}
+		return object.Errorf(
+			fmt.Sprintf("type error: %s() takes exactly %d arguments (%d given)",
+				funcName, count, nArgs))
+	}
+	return nil
+}
diff --git a/internal/dynamo-browse/services/scriptmanager/modext.go b/internal/dynamo-browse/services/scriptmanager/modext.go
index c581d17..f08051a 100644
--- a/internal/dynamo-browse/services/scriptmanager/modext.go
+++ b/internal/dynamo-browse/services/scriptmanager/modext.go
@@ -3,10 +3,8 @@ package scriptmanager
 import (
 	"context"
 	"fmt"
-	"github.com/cloudcmds/tamarin/arg"
-	"github.com/cloudcmds/tamarin/object"
-	"github.com/cloudcmds/tamarin/scope"
 	"github.com/pkg/errors"
+	"github.com/risor-io/risor/object"
 	"regexp"
 )
 
@@ -18,22 +16,17 @@ type extModule struct {
 	scriptPlugin *ScriptPlugin
 }
 
-func (m *extModule) register(scp *scope.Scope) {
-	modScope := scope.New(scope.Opts{})
-	mod := object.NewModule("ext", modScope)
-
-	modScope.AddBuiltins([]*object.Builtin{
-		object.NewBuiltin("command", m.command, mod),
-		object.NewBuiltin("key_binding", m.keyBinding, mod),
+func (m *extModule) register() *object.Module {
+	return object.NewBuiltinsModule("ext", map[string]object.Object{
+		"command":     object.NewBuiltin("command", m.command),
+		"key_binding": object.NewBuiltin("key_binding", m.keyBinding),
 	})
-
-	scp.Declare("ext", mod, true)
 }
 
 func (m *extModule) command(ctx context.Context, args ...object.Object) object.Object {
 	thisEnv := scriptEnvFromCtx(ctx)
 
-	if err := arg.Require("ext.command", 2, args); err != nil {
+	if err := require("ext.command", 2, args); err != nil {
 		return err
 	}
 
@@ -62,8 +55,10 @@ func (m *extModule) command(ctx context.Context, args ...object.Object) object.O
 		newEnv.options = m.scriptPlugin.scriptService.options
 		ctx = ctxWithScriptEnv(ctx, newEnv)
 
-		res := callFn(ctx, fnRes.Scope(), fnRes, objArgs)
-		if object.IsError(res) {
+		res, err := callFn(ctx, fnRes, objArgs)
+		if err != nil {
+			return errors.Errorf("command error '%v':%v - %v", m.scriptPlugin.name, cmdName, err)
+		} else if object.IsError(res) {
 			errObj := res.(*object.Error)
 			return errors.Errorf("command error '%v':%v - %v", m.scriptPlugin.name, cmdName, errObj.Inspect())
 		}
@@ -80,7 +75,7 @@ func (m *extModule) command(ctx context.Context, args ...object.Object) object.O
 func (m *extModule) keyBinding(ctx context.Context, args ...object.Object) object.Object {
 	thisEnv := scriptEnvFromCtx(ctx)
 
-	if err := arg.Require("ext.key_binding", 3, args); err != nil {
+	if err := require("ext.key_binding", 3, args); err != nil {
 		return err
 	}
 
@@ -122,8 +117,10 @@ func (m *extModule) keyBinding(ctx context.Context, args ...object.Object) objec
 		newEnv.options = m.scriptPlugin.scriptService.options
 		ctx = ctxWithScriptEnv(ctx, newEnv)
 
-		res := callFn(ctx, fnRes.Scope(), fnRes, objArgs)
-		if object.IsError(res) {
+		res, err := callFn(ctx, fnRes, objArgs)
+		if err != nil {
+			return errors.Errorf("command error '%v':%v - %v", m.scriptPlugin.name, bindingName, err)
+		} else if object.IsError(res) {
 			errObj := res.(*object.Error)
 			return errors.Errorf("command error '%v':%v - %v", m.scriptPlugin.name, bindingName, errObj.Inspect())
 		}
diff --git a/internal/dynamo-browse/services/scriptmanager/modos.go b/internal/dynamo-browse/services/scriptmanager/modos.go
index b050fc4..a89d130 100644
--- a/internal/dynamo-browse/services/scriptmanager/modos.go
+++ b/internal/dynamo-browse/services/scriptmanager/modos.go
@@ -2,9 +2,7 @@ package scriptmanager
 
 import (
 	"context"
-	"github.com/cloudcmds/tamarin/arg"
-	"github.com/cloudcmds/tamarin/object"
-	"github.com/cloudcmds/tamarin/scope"
+	"github.com/risor-io/risor/object"
 	"os"
 	"os/exec"
 )
@@ -13,7 +11,7 @@ type osModule struct {
 }
 
 func (om *osModule) exec(ctx context.Context, args ...object.Object) object.Object {
-	if err := arg.Require("os.exec", 1, args); err != nil {
+	if err := require("os.exec", 1, args); err != nil {
 		return err
 	}
 
@@ -24,20 +22,20 @@ func (om *osModule) exec(ctx context.Context, args ...object.Object) object.Obje
 
 	opts := scriptEnvFromCtx(ctx).options
 	if !opts.Permissions.AllowShellCommands {
-		return object.NewErrResult(object.Errorf("permission error: no permission to shell out"))
+		return object.Errorf("permission error: no permission to shell out")
 	}
 
 	cmd := exec.Command(opts.OSExecShell, "-c", cmdExec)
 	out, err := cmd.Output()
 	if err != nil {
-		return object.NewErrResult(object.NewError(err))
+		return object.NewError(err)
 	}
 
-	return object.NewOkResult(object.NewString(string(out)))
+	return object.NewString(string(out))
 }
 
 func (om *osModule) env(ctx context.Context, args ...object.Object) object.Object {
-	if err := arg.Require("os.env", 1, args); err != nil {
+	if err := require("os.env", 1, args); err != nil {
 		return err
 	}
 
@@ -58,14 +56,9 @@ func (om *osModule) env(ctx context.Context, args ...object.Object) object.Objec
 	return object.NewString(envVal)
 }
 
-func (om *osModule) register(scp *scope.Scope) {
-	modScope := scope.New(scope.Opts{})
-	mod := object.NewModule("os", modScope)
-
-	modScope.AddBuiltins([]*object.Builtin{
-		object.NewBuiltin("exec", om.exec, mod),
-		object.NewBuiltin("env", om.env, mod),
+func (om *osModule) register() *object.Module {
+	return object.NewBuiltinsModule("os", map[string]object.Object{
+		"exec": object.NewBuiltin("exec", om.exec),
+		"env":  object.NewBuiltin("env", om.env),
 	})
-
-	scp.Declare("os", mod, true)
 }
diff --git a/internal/dynamo-browse/services/scriptmanager/modos_test.go b/internal/dynamo-browse/services/scriptmanager/modos_test.go
index 63dfe32..49d2edf 100644
--- a/internal/dynamo-browse/services/scriptmanager/modos_test.go
+++ b/internal/dynamo-browse/services/scriptmanager/modos_test.go
@@ -68,13 +68,11 @@ func TestOSModule_Env(t *testing.T) {
 func TestOSModule_Exec(t *testing.T) {
 	t.Run("should run command and return stdout", func(t *testing.T) {
 		mockedUIService := mocks.NewUIService(t)
-		mockedUIService.EXPECT().PrintMessage(mock.Anything, "false")
 		mockedUIService.EXPECT().PrintMessage(mock.Anything, "hello world\n")
 
 		testFS := testScriptFile(t, "test.tm", `
 			res := os.exec('echo "hello world"')
-			ui.print(res.is_err())
-			ui.print(res.unwrap())
+			ui.print(res)
 		`)
 
 		srv := scriptmanager.New(scriptmanager.WithFS(testFS))
@@ -97,11 +95,11 @@ func TestOSModule_Exec(t *testing.T) {
 
 	t.Run("should refuse to execute command if do not have permissions", func(t *testing.T) {
 		mockedUIService := mocks.NewUIService(t)
-		mockedUIService.EXPECT().PrintMessage(mock.Anything, "true")
+		mockedUIService.EXPECT().PrintMessage(mock.Anything, "failed")
 
 		testFS := testScriptFile(t, "test.tm", `
-			res := os.exec('echo "hello world"')
-			ui.print(res.is_err())
+			res := try(func() { return os.exec('echo "hello world"') }, "failed")
+			ui.print(res)
 		`)
 
 		srv := scriptmanager.New(scriptmanager.WithFS(testFS))
@@ -125,14 +123,13 @@ func TestOSModule_Exec(t *testing.T) {
 	t.Run("should be able to change permissions which will affect plugins", func(t *testing.T) {
 		mockedUIService := mocks.NewUIService(t)
 		mockedUIService.EXPECT().PrintMessage(mock.Anything, "Loaded the plugin\n")
-		mockedUIService.EXPECT().PrintMessage(mock.Anything, "true")
 
 		testFS := testScriptFile(t, "test.tm", `
 			ext.command("mycommand", func() {
-				ui.print(os.exec('echo "this cannot run"').is_err())
+				ui.print(os.exec('echo "this cannot run"'))
 			})
 
-			ui.print(os.exec('echo "Loaded the plugin"').unwrap())
+			ui.print(os.exec('echo "Loaded the plugin"'))
 		`)
 
 		srv := scriptmanager.New(scriptmanager.WithFS(testFS))
@@ -159,7 +156,7 @@ func TestOSModule_Exec(t *testing.T) {
 
 		errChan := make(chan error)
 		assert.NoError(t, srv.LookupCommand("mycommand").Invoke(ctx, []string{}, errChan))
-		assert.NoError(t, waitForErr(t, errChan))
+		assert.Error(t, waitForErr(t, errChan))
 
 		mockedUIService.AssertExpectations(t)
 	})
diff --git a/internal/dynamo-browse/services/scriptmanager/modsession.go b/internal/dynamo-browse/services/scriptmanager/modsession.go
index d8e65b6..95c16c7 100644
--- a/internal/dynamo-browse/services/scriptmanager/modsession.go
+++ b/internal/dynamo-browse/services/scriptmanager/modsession.go
@@ -4,10 +4,8 @@ import (
 	"context"
 	"fmt"
 	"github.com/aws/aws-sdk-go-v2/service/dynamodb/types"
-	"github.com/cloudcmds/tamarin/arg"
-	"github.com/cloudcmds/tamarin/object"
-	"github.com/cloudcmds/tamarin/scope"
 	"github.com/pkg/errors"
+	"github.com/risor-io/risor/object"
 )
 
 type sessionModule struct {
@@ -77,13 +75,13 @@ func (um *sessionModule) query(ctx context.Context, args ...object.Object) objec
 	resp, err := um.sessionService.Query(ctx, expr, options)
 
 	if err != nil {
-		return object.NewErrResult(object.NewError(err))
+		return object.NewError(err)
 	}
-	return object.NewOkResult(&resultSetProxy{resultSet: resp})
+	return &resultSetProxy{resultSet: resp}
 }
 
 func (um *sessionModule) resultSet(ctx context.Context, args ...object.Object) object.Object {
-	if err := arg.Require("session.result_set", 0, args); err != nil {
+	if err := require("session.result_set", 0, args); err != nil {
 		return err
 	}
 
@@ -95,7 +93,7 @@ func (um *sessionModule) resultSet(ctx context.Context, args ...object.Object) o
 }
 
 func (um *sessionModule) selectedItem(ctx context.Context, args ...object.Object) object.Object {
-	if err := arg.Require("session.result_set", 0, args); err != nil {
+	if err := require("session.result_set", 0, args); err != nil {
 		return err
 	}
 
@@ -110,7 +108,7 @@ func (um *sessionModule) selectedItem(ctx context.Context, args ...object.Object
 }
 
 func (um *sessionModule) setResultSet(ctx context.Context, args ...object.Object) object.Object {
-	if err := arg.Require("session.set_result_set", 1, args); err != nil {
+	if err := require("session.set_result_set", 1, args); err != nil {
 		return err
 	}
 
@@ -124,7 +122,7 @@ func (um *sessionModule) setResultSet(ctx context.Context, args ...object.Object
 }
 
 func (um *sessionModule) currentTable(ctx context.Context, args ...object.Object) object.Object {
-	if err := arg.Require("session.current_table", 0, args); err != nil {
+	if err := require("session.current_table", 0, args); err != nil {
 		return err
 	}
 
@@ -136,17 +134,12 @@ func (um *sessionModule) currentTable(ctx context.Context, args ...object.Object
 	return &tableProxy{table: rs.TableInfo}
 }
 
-func (um *sessionModule) register(scp *scope.Scope) {
-	modScope := scope.New(scope.Opts{})
-	mod := object.NewModule("session", modScope)
-
-	modScope.AddBuiltins([]*object.Builtin{
-		object.NewBuiltin("query", um.query, mod),
-		object.NewBuiltin("current_table", um.currentTable, mod),
-		object.NewBuiltin("result_set", um.resultSet, mod),
-		object.NewBuiltin("selected_item", um.selectedItem, mod),
-		object.NewBuiltin("set_result_set", um.setResultSet, mod),
+func (um *sessionModule) register() *object.Module {
+	return object.NewBuiltinsModule("session", map[string]object.Object{
+		"query":          object.NewBuiltin("query", um.query),
+		"current_table":  object.NewBuiltin("current_table", um.currentTable),
+		"result_set":     object.NewBuiltin("result_set", um.resultSet),
+		"selected_item":  object.NewBuiltin("selected_item", um.selectedItem),
+		"set_result_set": object.NewBuiltin("set_result_set", um.setResultSet),
 	})
-
-	scp.Declare("session", mod, true)
 }
diff --git a/internal/dynamo-browse/services/scriptmanager/modsession_test.go b/internal/dynamo-browse/services/scriptmanager/modsession_test.go
index 947efe0..8712cf5 100644
--- a/internal/dynamo-browse/services/scriptmanager/modsession_test.go
+++ b/internal/dynamo-browse/services/scriptmanager/modsession_test.go
@@ -102,7 +102,7 @@ func TestModSession_Query(t *testing.T) {
 		mockedUIService.EXPECT().PrintMessage(mock.Anything, "res[1].attr('size(pk)') = 4")
 
 		testFS := testScriptFile(t, "test.tm", `
-			res := session.query("some expr").unwrap()
+			res := session.query("some expr")
 			ui.print(res.length)
 			ui.print("res[0]['pk'].S = ", res[0].attr("pk"))
 			ui.print("res[1]['pk'].S = ", res[1].attr("pk"))
@@ -128,13 +128,9 @@ func TestModSession_Query(t *testing.T) {
 		mockedSessionService.EXPECT().Query(mock.Anything, "some expr", scriptmanager.QueryOptions{}).Return(nil, errors.New("bang"))
 
 		mockedUIService := mocks.NewUIService(t)
-		mockedUIService.EXPECT().PrintMessage(mock.Anything, "true")
-		mockedUIService.EXPECT().PrintMessage(mock.Anything, "err(\"bang\")")
 
 		testFS := testScriptFile(t, "test.tm", `
 			res := session.query("some expr")
-			ui.print(res.is_err())
-			ui.print(res)
 		`)
 
 		srv := scriptmanager.New(scriptmanager.WithFS(testFS))
@@ -145,7 +141,7 @@ func TestModSession_Query(t *testing.T) {
 
 		ctx := context.Background()
 		err := <-srv.RunAdHocScript(ctx, "test.tm")
-		assert.NoError(t, err)
+		assert.Error(t, err)
 
 		mockedUIService.AssertExpectations(t)
 		mockedSessionService.AssertExpectations(t)
@@ -165,7 +161,7 @@ func TestModSession_Query(t *testing.T) {
 			res := session.query("some expr", {
 				table: "some-table",
 			})
-			assert(!res.is_err())
+			assert(res)
 		`)
 
 		srv := scriptmanager.New(scriptmanager.WithFS(testFS))
@@ -201,7 +197,7 @@ func TestModSession_Query(t *testing.T) {
 			res := session.query("some expr", {
 				table: session.result_set().table,
 			})
-			assert(!res.is_err())
+			assert(res)
 		`)
 
 		srv := scriptmanager.New(scriptmanager.WithFS(testFS))
@@ -242,7 +238,7 @@ func TestModSession_Query(t *testing.T) {
 					value: "world",
 				},
 			})
-			assert(!res.is_err())
+			assert(res)
 		`)
 
 		srv := scriptmanager.New(scriptmanager.WithFS(testFS))
@@ -288,7 +284,7 @@ func TestModSession_Query(t *testing.T) {
 					"nil": nil,
 				},
 			})
-			assert(!res.is_err())
+			assert(res)
 		`)
 
 		srv := scriptmanager.New(scriptmanager.WithFS(testFS))
@@ -315,7 +311,6 @@ func TestModSession_Query(t *testing.T) {
 					"bad": func() { },
 				},
 			})
-			assert(res.is_err())
 		`)
 
 		srv := scriptmanager.New(scriptmanager.WithFS(testFS))
@@ -411,7 +406,7 @@ func TestModSession_SetResultSet(t *testing.T) {
 		mockedUIService := mocks.NewUIService(t)
 
 		testFS := testScriptFile(t, "test.tm", `
-			res := session.query("some expr").unwrap()
+			res := session.query("some expr")
 			session.set_result_set(res)
 		`)
 
diff --git a/internal/dynamo-browse/services/scriptmanager/modui.go b/internal/dynamo-browse/services/scriptmanager/modui.go
index 081eaa5..d53b2e4 100644
--- a/internal/dynamo-browse/services/scriptmanager/modui.go
+++ b/internal/dynamo-browse/services/scriptmanager/modui.go
@@ -2,10 +2,9 @@ package scriptmanager
 
 import (
 	"context"
-	"github.com/cloudcmds/tamarin/arg"
-	"github.com/cloudcmds/tamarin/object"
-	"github.com/cloudcmds/tamarin/scope"
 	"strings"
+
+	"github.com/risor-io/risor/object"
 )
 
 type uiModule struct {
@@ -15,6 +14,10 @@ type uiModule struct {
 func (um *uiModule) print(ctx context.Context, args ...object.Object) object.Object {
 	var msg strings.Builder
 	for _, arg := range args {
+		if arg == nil {
+			continue
+		}
+
 		switch a := arg.(type) {
 		case *object.String:
 			msg.WriteString(a.Value())
@@ -28,7 +31,7 @@ func (um *uiModule) print(ctx context.Context, args ...object.Object) object.Obj
 }
 
 func (um *uiModule) prompt(ctx context.Context, args ...object.Object) object.Object {
-	if err := arg.Require("ui.prompt", 1, args); err != nil {
+	if err := require("ui.prompt", 1, args); err != nil {
 		return err
 	}
 
@@ -47,14 +50,9 @@ func (um *uiModule) prompt(ctx context.Context, args ...object.Object) object.Ob
 	}
 }
 
-func (um *uiModule) register(scp *scope.Scope) {
-	modScope := scope.New(scope.Opts{})
-	mod := object.NewModule("ui", modScope)
-
-	modScope.AddBuiltins([]*object.Builtin{
-		object.NewBuiltin("print", um.print, mod),
-		object.NewBuiltin("prompt", um.prompt, mod),
+func (um *uiModule) register() *object.Module {
+	return object.NewBuiltinsModule("ui", map[string]object.Object{
+		"print":  object.NewBuiltin("print", um.print),
+		"prompt": object.NewBuiltin("prompt", um.prompt),
 	})
-
-	scp.Declare("ui", mod, true)
 }
diff --git a/internal/dynamo-browse/services/scriptmanager/opts.go b/internal/dynamo-browse/services/scriptmanager/opts.go
index 9d8e489..9ef4408 100644
--- a/internal/dynamo-browse/services/scriptmanager/opts.go
+++ b/internal/dynamo-browse/services/scriptmanager/opts.go
@@ -3,6 +3,8 @@ package scriptmanager
 import (
 	"context"
 	"os"
+
+	"github.com/risor-io/risor/limits"
 )
 
 type Options struct {
@@ -50,5 +52,7 @@ func scriptEnvFromCtx(ctx context.Context) scriptEnv {
 }
 
 func ctxWithScriptEnv(ctx context.Context, perms scriptEnv) context.Context {
-	return context.WithValue(ctx, scriptEnvKey, perms)
+	newCtx := context.WithValue(ctx, scriptEnvKey, perms)
+	newCtx = limits.WithLimits(newCtx, limits.New())
+	return newCtx
 }
diff --git a/internal/dynamo-browse/services/scriptmanager/resultsetproxy.go b/internal/dynamo-browse/services/scriptmanager/resultsetproxy.go
index d3ee331..0d6be7d 100644
--- a/internal/dynamo-browse/services/scriptmanager/resultsetproxy.go
+++ b/internal/dynamo-browse/services/scriptmanager/resultsetproxy.go
@@ -2,17 +2,33 @@ package scriptmanager
 
 import (
 	"context"
-	"github.com/cloudcmds/tamarin/arg"
-	"github.com/cloudcmds/tamarin/object"
+	"time"
+
+	"github.com/aws/aws-sdk-go-v2/service/dynamodb/types"
 	"github.com/lmika/dynamo-browse/internal/dynamo-browse/models"
+	"github.com/lmika/dynamo-browse/internal/dynamo-browse/models/attrutils"
 	"github.com/lmika/dynamo-browse/internal/dynamo-browse/models/queryexpr"
 	"github.com/pkg/errors"
+	"github.com/risor-io/risor/object"
+	"github.com/risor-io/risor/op"
 )
 
 type resultSetProxy struct {
 	resultSet *models.ResultSet
 }
 
+func (r *resultSetProxy) SetAttr(name string, value object.Object) error {
+	return errors.Errorf("attribute error: %v", name)
+}
+
+func (r *resultSetProxy) RunOperation(opType op.BinaryOpType, right object.Object) object.Object {
+	return object.Errorf("op error: unsupported %v", opType)
+}
+
+func (r *resultSetProxy) Cost() int {
+	return len(r.resultSet.Items())
+}
+
 func (r *resultSetProxy) Interface() interface{} {
 	return r.resultSet
 }
@@ -95,17 +111,105 @@ func (r *resultSetProxy) GetAttr(name string) (object.Object, bool) {
 		return &tableProxy{table: r.resultSet.TableInfo}, true
 	case "length":
 		return object.NewInt(int64(len(r.resultSet.Items()))), true
+	case "find":
+		return object.NewBuiltin("find", r.find), true
+	case "merge":
+		return object.NewBuiltin("merge", r.merge), true
 	}
 
 	return nil, false
 }
 
+func (i *resultSetProxy) find(ctx context.Context, args ...object.Object) object.Object {
+	if objErr := require("resultset.find", 1, args); objErr != nil {
+		return objErr
+	}
+
+	str, objErr := object.AsString(args[0])
+	if objErr != nil {
+		return objErr
+	}
+
+	modExpr, err := queryexpr.Parse(str)
+	if err != nil {
+		return object.Errorf("arg error: invalid path expression: %v", err)
+	}
+
+	for idx, item := range i.resultSet.Items() {
+		rs, err := modExpr.EvalItem(item)
+		if err != nil {
+			continue
+		}
+
+		if attrutils.Truthy(rs) {
+			return newItemProxy(i, idx)
+		}
+	}
+
+	return object.Nil
+}
+
+func (i *resultSetProxy) merge(ctx context.Context, args ...object.Object) object.Object {
+	type pksk struct {
+		pk types.AttributeValue
+		sk types.AttributeValue
+	}
+
+	if objErr := require("resultset.merge", 1, args); objErr != nil {
+		return objErr
+	}
+
+	otherRS, isRS := args[0].(*resultSetProxy)
+	if !isRS {
+		return object.NewError(errors.Errorf("type error: expected a resultset (got %v)", args[0].Type()))
+	}
+
+	if !i.resultSet.TableInfo.Equal(otherRS.resultSet.TableInfo) {
+		return object.Nil
+	}
+
+	itemsInI := make(map[pksk]models.Item)
+	newItems := make([]models.Item, 0, len(i.resultSet.Items())+len(otherRS.resultSet.Items()))
+	for _, item := range i.resultSet.Items() {
+		pk, sk := item.PKSK(i.resultSet.TableInfo)
+		itemsInI[pksk{pk, sk}] = item
+		newItems = append(newItems, item)
+	}
+
+	for _, item := range otherRS.resultSet.Items() {
+		pk, sk := item.PKSK(i.resultSet.TableInfo)
+		if _, hasItem := itemsInI[pksk{pk, sk}]; !hasItem {
+			newItems = append(newItems, item)
+		}
+	}
+
+	newResultSet := &models.ResultSet{
+		Created:   time.Now(),
+		TableInfo: i.resultSet.TableInfo,
+	}
+	newResultSet.SetItems(newItems)
+
+	return &resultSetProxy{resultSet: newResultSet}
+}
+
 type itemProxy struct {
 	resultSetProxy *resultSetProxy
 	itemIndex      int
 	item           models.Item
 }
 
+func (i *itemProxy) SetAttr(name string, value object.Object) error {
+	return errors.Errorf("attribute error: %v", name)
+}
+
+func (i *itemProxy) RunOperation(opType op.BinaryOpType, right object.Object) object.Object {
+	return object.Errorf("op error: unsupported %v", opType)
+}
+
+func (i *itemProxy) Cost() int {
+	return len(i.item)
+}
+
 func newItemProxy(rs *resultSetProxy, itemIndex int) *itemProxy {
 	return &itemProxy{
 		resultSetProxy: rs,
@@ -154,7 +258,7 @@ func (i *itemProxy) GetAttr(name string) (object.Object, bool) {
 }
 
 func (i *itemProxy) value(ctx context.Context, args ...object.Object) object.Object {
-	if objErr := arg.Require("item.attr", 1, args); objErr != nil {
+	if objErr := require("item.attr", 1, args); objErr != nil {
 		return objErr
 	}
 
@@ -180,7 +284,7 @@ func (i *itemProxy) value(ctx context.Context, args ...object.Object) object.Obj
 }
 
 func (i *itemProxy) setValue(ctx context.Context, args ...object.Object) object.Object {
-	if objErr := arg.Require("item.set_attr", 2, args); objErr != nil {
+	if objErr := require("item.set_attr", 2, args); objErr != nil {
 		return objErr
 	}
 
@@ -207,7 +311,7 @@ func (i *itemProxy) setValue(ctx context.Context, args ...object.Object) object.
 }
 
 func (i *itemProxy) deleteAttr(ctx context.Context, args ...object.Object) object.Object {
-	if objErr := arg.Require("item.delete_attr", 1, args); objErr != nil {
+	if objErr := require("item.delete_attr", 1, args); objErr != nil {
 		return objErr
 	}
 
diff --git a/internal/dynamo-browse/services/scriptmanager/resultsetproxy_test.go b/internal/dynamo-browse/services/scriptmanager/resultsetproxy_test.go
index c52ea3e..3b7f354 100644
--- a/internal/dynamo-browse/services/scriptmanager/resultsetproxy_test.go
+++ b/internal/dynamo-browse/services/scriptmanager/resultsetproxy_test.go
@@ -2,13 +2,14 @@ package scriptmanager_test
 
 import (
 	"context"
+	"testing"
+
 	"github.com/aws/aws-sdk-go-v2/service/dynamodb/types"
 	"github.com/lmika/dynamo-browse/internal/dynamo-browse/models"
 	"github.com/lmika/dynamo-browse/internal/dynamo-browse/services/scriptmanager"
 	"github.com/lmika/dynamo-browse/internal/dynamo-browse/services/scriptmanager/mocks"
 	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/mock"
-	"testing"
 )
 
 func TestResultSetProxy(t *testing.T) {
@@ -29,7 +30,7 @@ func TestResultSetProxy(t *testing.T) {
 		mockedUIService := mocks.NewUIService(t)
 
 		testFS := testScriptFile(t, "test.tm", `
-			res := session.query("some expr").unwrap()
+			res := session.query("some expr")
 
 			// Test properties of the result set
 			assert(res.table.name, "hello")
@@ -60,6 +61,123 @@ func TestResultSetProxy(t *testing.T) {
 	})
 }
 
+func TestResultSetProxy_Find(t *testing.T) {
+	t.Run("should return the first item that matches the given expression", func(t *testing.T) {
+		rs := &models.ResultSet{}
+		rs.SetItems([]models.Item{
+			{"pk": &types.AttributeValueMemberS{Value: "abc"}},
+			{"pk": &types.AttributeValueMemberS{Value: "abc"}, "sk": &types.AttributeValueMemberS{Value: "abc"}, "primary": &types.AttributeValueMemberS{Value: "yes"}},
+			{"pk": &types.AttributeValueMemberS{Value: "1232"}, "findMe": &types.AttributeValueMemberS{Value: "yes"}},
+			{"pk": &types.AttributeValueMemberS{Value: "2345"}, "findMe": &types.AttributeValueMemberS{Value: "second"}},
+		})
+
+		mockedSessionService := mocks.NewSessionService(t)
+		mockedSessionService.EXPECT().Query(mock.Anything, "some expr", scriptmanager.QueryOptions{}).Return(rs, nil)
+
+		testFS := testScriptFile(t, "test.tm", `
+			res := session.query("some expr")
+
+			assert(res.find('findMe is "any"').attr("pk") == "1232")
+			assert(res.find('findMe = "second"').attr("pk") == "2345")
+			assert(res.find('pk = sk').attr("primary") == "yes")
+
+			assert(res.find('findMe = "missing"') == nil)
+		`)
+
+		srv := scriptmanager.New(scriptmanager.WithFS(testFS))
+		srv.SetIFaces(scriptmanager.Ifaces{
+			Session: mockedSessionService,
+		})
+
+		ctx := context.Background()
+		err := <-srv.RunAdHocScript(ctx, "test.tm")
+		assert.NoError(t, err)
+
+		mockedSessionService.AssertExpectations(t)
+	})
+}
+
+func TestResultSetProxy_Merge(t *testing.T) {
+	t.Run("should return a result set with items from both if both are from the same table", func(t *testing.T) {
+		td := &models.TableInfo{Name: "test", Keys: models.KeyAttribute{PartitionKey: "pk", SortKey: "sk"}}
+
+		rs1 := &models.ResultSet{TableInfo: td}
+		rs1.SetItems([]models.Item{
+			{"pk": &types.AttributeValueMemberS{Value: "abc"}, "sk": &types.AttributeValueMemberS{Value: "123"}},
+		})
+
+		rs2 := &models.ResultSet{TableInfo: td}
+		rs2.SetItems([]models.Item{
+			{"pk": &types.AttributeValueMemberS{Value: "bcd"}, "sk": &types.AttributeValueMemberS{Value: "234"}},
+		})
+
+		mockedSessionService := mocks.NewSessionService(t)
+		mockedSessionService.EXPECT().Query(mock.Anything, "rs1", scriptmanager.QueryOptions{}).Return(rs1, nil)
+		mockedSessionService.EXPECT().Query(mock.Anything, "rs2", scriptmanager.QueryOptions{}).Return(rs2, nil)
+
+		testFS := testScriptFile(t, "test.tm", `
+			r1 := session.query("rs1")
+			r2 := session.query("rs2")
+
+			res := r1.merge(r2)
+
+			assert(res[0].attr("pk") == "abc")
+			assert(res[0].attr("sk") == "123")
+			assert(res[1].attr("pk") == "bcd")
+			assert(res[1].attr("sk") == "234")
+		`)
+
+		srv := scriptmanager.New(scriptmanager.WithFS(testFS))
+		srv.SetIFaces(scriptmanager.Ifaces{
+			Session: mockedSessionService,
+		})
+
+		ctx := context.Background()
+		err := <-srv.RunAdHocScript(ctx, "test.tm")
+		assert.NoError(t, err)
+
+		mockedSessionService.AssertExpectations(t)
+	})
+
+	t.Run("should return nil if result-sets are from different tables", func(t *testing.T) {
+		td1 := &models.TableInfo{Name: "test", Keys: models.KeyAttribute{PartitionKey: "pk", SortKey: "sk"}}
+		rs1 := &models.ResultSet{TableInfo: td1}
+		rs1.SetItems([]models.Item{
+			{"pk": &types.AttributeValueMemberS{Value: "abc"}, "sk": &types.AttributeValueMemberS{Value: "123"}},
+		})
+
+		td2 := &models.TableInfo{Name: "test2", Keys: models.KeyAttribute{PartitionKey: "pk2", SortKey: "sk"}}
+		rs2 := &models.ResultSet{TableInfo: td2}
+		rs2.SetItems([]models.Item{
+			{"pk": &types.AttributeValueMemberS{Value: "bcd"}, "sk": &types.AttributeValueMemberS{Value: "234"}},
+		})
+
+		mockedSessionService := mocks.NewSessionService(t)
+		mockedSessionService.EXPECT().Query(mock.Anything, "rs1", scriptmanager.QueryOptions{}).Return(rs1, nil)
+		mockedSessionService.EXPECT().Query(mock.Anything, "rs2", scriptmanager.QueryOptions{}).Return(rs2, nil)
+
+		testFS := testScriptFile(t, "test.tm", `
+			r1 := session.query("rs1")
+			r2 := session.query("rs2")
+
+			res := r1.merge(r2)
+
+			assert(res == nil)
+		`)
+
+		srv := scriptmanager.New(scriptmanager.WithFS(testFS))
+		srv.SetIFaces(scriptmanager.Ifaces{
+			Session: mockedSessionService,
+		})
+
+		ctx := context.Background()
+		err := <-srv.RunAdHocScript(ctx, "test.tm")
+		assert.NoError(t, err)
+
+		mockedSessionService.AssertExpectations(t)
+	})
+}
+
 func TestResultSetProxy_GetAttr(t *testing.T) {
 	t.Run("should return the value of items within a result set", func(t *testing.T) {
 		rs := &models.ResultSet{}
@@ -87,7 +205,7 @@ func TestResultSetProxy_GetAttr(t *testing.T) {
 		mockedSessionService.EXPECT().Query(mock.Anything, "some expr", scriptmanager.QueryOptions{}).Return(rs, nil)
 
 		testFS := testScriptFile(t, "test.tm", `
-			res := session.query("some expr").unwrap()
+			res := session.query("some expr")
 
 			assert(res[0].attr("pk") == "abc", "str attr")
 			assert(res[0].attr("sk") == 123, "num attr")
@@ -164,7 +282,7 @@ func TestResultSetProxy_SetAttr(t *testing.T) {
 		mockedUIService := mocks.NewUIService(t)
 
 		testFS := testScriptFile(t, "test.tm", `
-			res := session.query("some expr").unwrap()
+			res := session.query("some expr")
 
 			res[0].set_attr("pk", "bla-di-bla")
 			res[0].set_attr("num", 123)
@@ -215,7 +333,7 @@ func TestResultSetProxy_DeleteAttr(t *testing.T) {
 		mockedUIService := mocks.NewUIService(t)
 
 		testFS := testScriptFile(t, "test.tm", `
-			res := session.query("some expr").unwrap()
+			res := session.query("some expr")
 			res[0].delete_attr("deleteMe")
 			session.set_result_set(res)
 		`)
diff --git a/internal/dynamo-browse/services/scriptmanager/service.go b/internal/dynamo-browse/services/scriptmanager/service.go
index ce8ec78..d90629f 100644
--- a/internal/dynamo-browse/services/scriptmanager/service.go
+++ b/internal/dynamo-browse/services/scriptmanager/service.go
@@ -2,16 +2,16 @@ package scriptmanager
 
 import (
 	"context"
-	"github.com/cloudcmds/tamarin/exec"
-	"github.com/cloudcmds/tamarin/object"
-	"github.com/cloudcmds/tamarin/scope"
-	"github.com/lmika/dynamo-browse/internal/dynamo-browse/services/keybindings"
-	"github.com/pkg/errors"
 	"io/fs"
 	"log"
 	"os"
 	"path/filepath"
 	"strings"
+
+	"github.com/lmika/dynamo-browse/internal/dynamo-browse/services/keybindings"
+	"github.com/pkg/errors"
+	"github.com/risor-io/risor"
+	"github.com/risor-io/risor/object"
 )
 
 type Service struct {
@@ -94,19 +94,14 @@ func (s *Service) startAdHocScript(ctx context.Context, filename string, errChan
 		return
 	}
 
-	scp := scope.New(scope.Opts{Parent: s.parentScope()})
-
 	ctx = ctxWithScriptEnv(ctx, scriptEnv{filename: filepath.Base(filename), options: s.options})
 
-	if _, err = exec.Execute(ctx, exec.Opts{
-		Input: string(code),
-		File:  filename,
-		Scope: scp,
-		Builtins: []*object.Builtin{
-			object.NewBuiltin("print", printBuiltin),
-			object.NewBuiltin("printf", printfBuiltin),
-		},
-	}); err != nil {
+	if _, err := risor.Eval(ctx, code,
+		risor.WithGlobals(s.builtins()),
+		// risor.WithDefaultBuiltins(),
+		// risor.WithDefaultModules(),
+		// risor.WithBuiltins(s.builtins()),
+	); err != nil {
 		errChan <- errors.Wrapf(err, "script %v", filename)
 		return
 	}
@@ -131,17 +126,17 @@ func (s *Service) loadScript(ctx context.Context, filename string, resChan chan
 		scriptService: s,
 	}
 
-	scp := scope.New(scope.Opts{Parent: s.parentScope()})
-
-	(&extModule{scriptPlugin: newPlugin}).register(scp)
-
 	ctx = ctxWithScriptEnv(ctx, scriptEnv{filename: filepath.Base(filename), options: s.options})
 
-	if _, err = exec.Execute(ctx, exec.Opts{
-		Input: string(code),
-		File:  filename,
-		Scope: scp,
-	}); err != nil {
+	if _, err := risor.Eval(ctx, code,
+		// risor.WithDefaultBuiltins(),
+		// risor.WithDefaultModules(),
+		// risor.WithBuiltins(s.builtins()),
+		risor.WithGlobals(s.builtins()),
+		risor.WithGlobals(map[string]any{
+			"ext": (&extModule{scriptPlugin: newPlugin}).register(),
+		}),
+	); err != nil {
 		resChan <- loadedScriptResult{err: errors.Wrapf(err, "script %v", filename)}
 		return
 	}
@@ -149,7 +144,7 @@ func (s *Service) loadScript(ctx context.Context, filename string, resChan chan
 	resChan <- loadedScriptResult{scriptPlugin: newPlugin}
 }
 
-func (s *Service) readScript(filename string, allowCwd bool) ([]byte, error) {
+func (s *Service) readScript(filename string, allowCwd bool) (string, error) {
 	if allowCwd {
 		if cwd, err := os.Getwd(); err == nil {
 			fullScriptPath := filepath.Join(cwd, filename)
@@ -157,9 +152,9 @@ func (s *Service) readScript(filename string, allowCwd bool) ([]byte, error) {
 			if stat, err := os.Stat(fullScriptPath); err == nil && !stat.IsDir() {
 				code, err := os.ReadFile(filename)
 				if err != nil {
-					return nil, err
+					return "", err
 				}
-				return code, nil
+				return string(code), nil
 			}
 		} else {
 			log.Printf("warn: cannot get cwd for reading script %v: %v", filename, err)
@@ -169,9 +164,9 @@ func (s *Service) readScript(filename string, allowCwd bool) ([]byte, error) {
 	if strings.HasPrefix(filename, string(filepath.Separator)) {
 		code, err := os.ReadFile(filename)
 		if err != nil {
-			return nil, err
+			return "", err
 		}
-		return code, nil
+		return string(code), nil
 	}
 
 	for _, currFS := range s.lookupPaths {
@@ -181,7 +176,7 @@ func (s *Service) readScript(filename string, allowCwd bool) ([]byte, error) {
 			if errors.Is(err, os.ErrNotExist) {
 				continue
 			} else {
-				return nil, err
+				return "", err
 			}
 		} else if stat.IsDir() {
 			continue
@@ -189,13 +184,13 @@ func (s *Service) readScript(filename string, allowCwd bool) ([]byte, error) {
 
 		code, err := fs.ReadFile(currFS, filename)
 		if err == nil {
-			return code, nil
+			return string(code), nil
 		} else {
-			return nil, err
+			return "", err
 		}
 	}
 
-	return nil, os.ErrNotExist
+	return "", os.ErrNotExist
 }
 
 // LookupCommand looks up a command defined by a script.
@@ -252,10 +247,12 @@ func (s *Service) RebindKeyBinding(keyBinding string, newKey string) error {
 	return keybindings.InvalidBindingError(keyBinding)
 }
 
-func (s *Service) parentScope() *scope.Scope {
-	scp := scope.New(scope.Opts{})
-	(&uiModule{uiService: s.ifaces.UI}).register(scp)
-	(&sessionModule{sessionService: s.ifaces.Session}).register(scp)
-	(&osModule{}).register(scp)
-	return scp
+func (s *Service) builtins() map[string]any {
+	return map[string]any{
+		"ui":      (&uiModule{uiService: s.ifaces.UI}).register(),
+		"session": (&sessionModule{sessionService: s.ifaces.Session}).register(),
+		"os":      (&osModule{}).register(),
+		"print":   object.NewBuiltin("print", printBuiltin),
+		"printf":  object.NewBuiltin("printf", printfBuiltin),
+	}
 }
diff --git a/internal/dynamo-browse/services/scriptmanager/tableproxy.go b/internal/dynamo-browse/services/scriptmanager/tableproxy.go
index c626085..252348f 100644
--- a/internal/dynamo-browse/services/scriptmanager/tableproxy.go
+++ b/internal/dynamo-browse/services/scriptmanager/tableproxy.go
@@ -1,9 +1,11 @@
 package scriptmanager
 
 import (
-	"github.com/cloudcmds/tamarin/object"
 	"github.com/lmika/dynamo-browse/internal/common/sliceutils"
 	"github.com/lmika/dynamo-browse/internal/dynamo-browse/models"
+	"github.com/pkg/errors"
+	"github.com/risor-io/risor/object"
+	"github.com/risor-io/risor/op"
 	"reflect"
 )
 
@@ -16,6 +18,18 @@ type tableProxy struct {
 	table *models.TableInfo
 }
 
+func (t *tableProxy) SetAttr(name string, value object.Object) error {
+	return errors.Errorf("attribute error: %v", name)
+}
+
+func (t *tableProxy) RunOperation(opType op.BinaryOpType, right object.Object) object.Object {
+	return object.Errorf("op error: unsupported %v", opType)
+}
+
+func (t *tableProxy) Cost() int {
+	return 0
+}
+
 func (t *tableProxy) Type() object.Type {
 	return "table"
 }
@@ -68,6 +82,18 @@ type tableIndexProxy struct {
 	gsi models.TableGSI
 }
 
+func (t tableIndexProxy) SetAttr(name string, value object.Object) error {
+	return errors.Errorf("attribute error: %v", name)
+}
+
+func (t tableIndexProxy) RunOperation(opType op.BinaryOpType, right object.Object) object.Object {
+	return object.Errorf("op error: unsupported %v", opType)
+}
+
+func (t tableIndexProxy) Cost() int {
+	return 0
+}
+
 func newTableIndexProxy(gsi models.TableGSI) object.Object {
 	return tableIndexProxy{gsi: gsi}
 }
diff --git a/internal/dynamo-browse/services/scriptmanager/typemapping.go b/internal/dynamo-browse/services/scriptmanager/typemapping.go
index 26cec3c..d7b8a47 100644
--- a/internal/dynamo-browse/services/scriptmanager/typemapping.go
+++ b/internal/dynamo-browse/services/scriptmanager/typemapping.go
@@ -3,10 +3,10 @@ package scriptmanager
 import (
 	"fmt"
 	"github.com/aws/aws-sdk-go-v2/service/dynamodb/types"
-	"github.com/cloudcmds/tamarin/object"
 	"github.com/lmika/dynamo-browse/internal/common/maputils"
 	"github.com/lmika/dynamo-browse/internal/common/sliceutils"
 	"github.com/pkg/errors"
+	"github.com/risor-io/risor/object"
 	"regexp"
 	"strconv"
 )
diff --git a/internal/dynamo-browse/services/tables/service.go b/internal/dynamo-browse/services/tables/service.go
index c49075a..993b4be 100644
--- a/internal/dynamo-browse/services/tables/service.go
+++ b/internal/dynamo-browse/services/tables/service.go
@@ -79,6 +79,7 @@ func (s *Service) doScan(
 	if err != nil && len(results) == 0 {
 		return &models.ResultSet{
 			TableInfo:         tableInfo,
+			Created:           time.Now(),
 			Query:             expr,
 			ExclusiveStartKey: exclusiveStartKey,
 			LastEvaluatedKey:  lastEvalKey,
@@ -89,6 +90,7 @@ func (s *Service) doScan(
 
 	resultSet := &models.ResultSet{
 		TableInfo:         tableInfo,
+		Created:           time.Now(),
 		Query:             expr,
 		ExclusiveStartKey: exclusiveStartKey,
 		LastEvaluatedKey:  lastEvalKey,
diff --git a/internal/dynamo-browse/ui/keybindings/defaults.go b/internal/dynamo-browse/ui/keybindings/defaults.go
index 639a369..87e2757 100644
--- a/internal/dynamo-browse/ui/keybindings/defaults.go
+++ b/internal/dynamo-browse/ui/keybindings/defaults.go
@@ -25,6 +25,7 @@ func Default() *KeyBindings {
 		},
 		View: &ViewKeyBindings{
 			Mark:                 key.NewBinding(key.WithKeys("m"), key.WithHelp("m", "mark")),
+			ToggleMarkedItems:    key.NewBinding(key.WithKeys("M"), key.WithHelp("M", "toggle marged items")),
 			CopyItemToClipboard:  key.NewBinding(key.WithKeys("c"), key.WithHelp("c", "copy item to clipboard")),
 			Rescan:               key.NewBinding(key.WithKeys("R"), key.WithHelp("R", "rescan")),
 			PromptForQuery:       key.NewBinding(key.WithKeys("?"), key.WithHelp("?", "prompt for query")),
diff --git a/internal/dynamo-browse/ui/keybindings/keybindings.go b/internal/dynamo-browse/ui/keybindings/keybindings.go
index 26178fd..d8257de 100644
--- a/internal/dynamo-browse/ui/keybindings/keybindings.go
+++ b/internal/dynamo-browse/ui/keybindings/keybindings.go
@@ -31,7 +31,9 @@ type TableKeyBinding struct {
 
 type ViewKeyBindings struct {
 	Mark                 key.Binding `keymap:"mark"`
+	ToggleMarkedItems    key.Binding `keymap:"toggle-marked-items"`
 	CopyItemToClipboard  key.Binding `keymap:"copy-item-to-clipboard"`
+	CopyTableToClipboard key.Binding `keymap:"copy-table-to-clipboard"`
 	Rescan               key.Binding `keymap:"rescan"`
 	PromptForQuery       key.Binding `keymap:"prompt-for-query"`
 	PromptForFilter      key.Binding `keymap:"prompt-for-filter"`
diff --git a/internal/dynamo-browse/ui/model.go b/internal/dynamo-browse/ui/model.go
index 35aa9cd..c41b572 100644
--- a/internal/dynamo-browse/ui/model.go
+++ b/internal/dynamo-browse/ui/model.go
@@ -7,6 +7,7 @@ import (
 	"github.com/lmika/dynamo-browse/internal/common/ui/events"
 	"github.com/lmika/dynamo-browse/internal/dynamo-browse/controllers"
 	"github.com/lmika/dynamo-browse/internal/dynamo-browse/models"
+	"github.com/lmika/dynamo-browse/internal/dynamo-browse/services"
 	"github.com/lmika/dynamo-browse/internal/dynamo-browse/services/itemrenderer"
 	"github.com/lmika/dynamo-browse/internal/dynamo-browse/ui/keybindings"
 	"github.com/lmika/dynamo-browse/internal/dynamo-browse/ui/teamodels/colselector"
@@ -74,6 +75,7 @@ func NewModel(
 	scriptController *controllers.ScriptController,
 	eventBus *bus.Bus,
 	keyBindingController *controllers.KeyBindingController,
+	pasteboardProvider services.PasteboardProvider,
 	defaultKeyMap *keybindings.KeyBindings,
 ) Model {
 	uiStyles := styles.DefaultStyles
@@ -84,7 +86,7 @@ func NewModel(
 
 	colSelector := colselector.New(mainView, defaultKeyMap, columnsController)
 	itemEdit := dynamoitemedit.NewModel(colSelector)
-	statusAndPrompt := statusandprompt.New(itemEdit, "", uiStyles.StatusAndPrompt)
+	statusAndPrompt := statusandprompt.New(itemEdit, pasteboardProvider, "", uiStyles.StatusAndPrompt)
 	dialogPrompt := dialogprompt.New(statusAndPrompt)
 	tableSelect := tableselect.New(dialogPrompt, uiStyles)
 
@@ -126,7 +128,12 @@ func NewModel(
 					}
 				}
 
-				return rc.Mark(markOp)
+				var whereExpr = ""
+				if len(args) == 3 && args[1] == "-where" {
+					whereExpr = args[2]
+				}
+
+				return rc.Mark(markOp, whereExpr)
 			},
 			"next-page": func(ctx commandctrl.ExecContext, args []string) tea.Msg {
 				return rc.NextPage()
@@ -135,6 +142,9 @@ func NewModel(
 
 			// TEMP
 			"new-item": commandctrl.NoArgCommand(wc.NewItem),
+			"clone": func(ctx commandctrl.ExecContext, args []string) tea.Msg {
+				return wc.CloneItem(dtv.SelectedItemIndex())
+			},
 			"set-attr": func(ctx commandctrl.ExecContext, args []string) tea.Msg {
 				if len(args) == 0 {
 					return events.Error(errors.New("expected field"))
@@ -263,10 +273,14 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 				if idx := m.tableView.SelectedItemIndex(); idx >= 0 {
 					return m, events.SetTeaMessage(m.tableWriteController.ToggleMark(idx))
 				}
+			case key.Matches(msg, m.keyMap.ToggleMarkedItems):
+				return m, events.SetTeaMessage(m.tableReadController.Mark(controllers.MarkOpToggle, ""))
 			case key.Matches(msg, m.keyMap.CopyItemToClipboard):
 				if idx := m.tableView.SelectedItemIndex(); idx >= 0 {
 					return m, events.SetTeaMessage(m.tableReadController.CopyItemToClipboard(idx))
 				}
+			case key.Matches(msg, m.keyMap.CopyTableToClipboard):
+				return m, events.SetTeaMessage(m.exportController.ExportCSVToClipboard())
 			case key.Matches(msg, m.keyMap.Rescan):
 				return m, m.tableReadController.Rescan
 			case key.Matches(msg, m.keyMap.PromptForQuery):
diff --git a/internal/dynamo-browse/ui/teamodels/statusandprompt/model.go b/internal/dynamo-browse/ui/teamodels/statusandprompt/model.go
index 7bdab7c..cb4b838 100644
--- a/internal/dynamo-browse/ui/teamodels/statusandprompt/model.go
+++ b/internal/dynamo-browse/ui/teamodels/statusandprompt/model.go
@@ -9,14 +9,17 @@ import (
 	"github.com/lmika/dynamo-browse/internal/common/ui/events"
 	"github.com/lmika/dynamo-browse/internal/dynamo-browse/ui/teamodels/layout"
 	"github.com/lmika/dynamo-browse/internal/dynamo-browse/ui/teamodels/utils"
+	"strings"
 )
 
 // StatusAndPrompt is a resizing model which displays a submodel and a status bar.  When the start prompt
 // event is received, focus will be torn away and the user will be given a prompt the enter text.
 type StatusAndPrompt struct {
 	model              layout.ResizingModel
+	pasteboardProvider PasteboardProvider
 	style              Style
 	modeLine           string
+	rightModeLine      string
 	statusMessage      string
 	spinner            spinner.Model
 	spinnerVisible     bool
@@ -30,15 +33,17 @@ type Style struct {
 	ModeLine lipgloss.Style
 }
 
-func New(model layout.ResizingModel, initialMsg string, style Style) *StatusAndPrompt {
+func New(model layout.ResizingModel, pasteboardProvider PasteboardProvider, initialMsg string, style Style) *StatusAndPrompt {
 	textInput := textinput.New()
 	return &StatusAndPrompt{
-		model:         model,
-		style:         style,
-		statusMessage: initialMsg,
-		modeLine:      "",
-		spinner:       spinner.New(spinner.WithSpinner(spinner.Line)),
-		textInput:     textInput,
+		model:              model,
+		pasteboardProvider: pasteboardProvider,
+		style:              style,
+		statusMessage:      initialMsg,
+		modeLine:           "",
+		rightModeLine:      "",
+		spinner:            spinner.New(spinner.WithSpinner(spinner.Line)),
+		textInput:          textInput,
 	}
 }
 
@@ -73,6 +78,11 @@ func (s *StatusAndPrompt) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 		if hasModeMessage, ok := msg.(events.MessageWithMode); ok {
 			s.modeLine = hasModeMessage.ModeMessage()
 		}
+		if rightModeMessage, ok := msg.(events.MessageWithRightMode); ok {
+			s.rightModeLine = rightModeMessage.RightModeMessage()
+		} else {
+			s.rightModeLine = ""
+		}
 		s.statusMessage = msg.StatusMessage()
 	case events.PromptForInputMsg:
 		if s.pendingInput != nil {
@@ -96,6 +106,24 @@ func (s *StatusAndPrompt) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 					})
 				}
 				s.pendingInput = nil
+			case tea.KeyCtrlV:
+				if content, ok := s.pasteboardProvider.ReadText(); ok {
+					pasteContent := strings.TrimSpace(content)
+
+					cursorPos := s.textInput.Cursor()
+					beforeValue := s.textInput.Value()[:cursorPos] + pasteContent
+					newValue := beforeValue + s.textInput.Value()[cursorPos:]
+
+					s.textInput.SetValue(newValue)
+					s.textInput.SetCursor(len(beforeValue))
+				}
+			case tea.KeyTab:
+				if tabCompletion := s.pendingInput.originalMsg.OnTabComplete; tabCompletion != nil {
+					if completion, ok := tabCompletion(s.textInput.Value()); ok {
+						s.textInput.SetValue(completion)
+						s.textInput.SetCursor(len(s.textInput.Value()))
+					}
+				}
 			case tea.KeyEnter:
 				pendingInput := s.pendingInput
 				s.pendingInput = nil
@@ -176,7 +204,10 @@ func (s *StatusAndPrompt) Resize(w, h int) layout.ResizingModel {
 }
 
 func (s *StatusAndPrompt) viewStatus() string {
-	modeLine := s.style.ModeLine.Render(lipgloss.PlaceHorizontal(s.width, lipgloss.Left, s.modeLine, lipgloss.WithWhitespaceChars(" ")))
+	rightModeLine := s.style.ModeLine.Render(s.rightModeLine)
+	modeLine := s.style.ModeLine.Render(
+		lipgloss.PlaceHorizontal(s.width-lipgloss.Width(rightModeLine), lipgloss.Left, s.modeLine, lipgloss.WithWhitespaceChars(" ")),
+	) + rightModeLine
 
 	var statusLine string
 	if s.pendingInput != nil {
diff --git a/internal/dynamo-browse/ui/teamodels/statusandprompt/types.go b/internal/dynamo-browse/ui/teamodels/statusandprompt/types.go
index 8f3ccc3..654b303 100644
--- a/internal/dynamo-browse/ui/teamodels/statusandprompt/types.go
+++ b/internal/dynamo-browse/ui/teamodels/statusandprompt/types.go
@@ -10,3 +10,7 @@ type pendingInputState struct {
 func newPendingInputState(msg events.PromptForInputMsg) *pendingInputState {
 	return &pendingInputState{originalMsg: msg, historyIdx: -1}
 }
+
+type PasteboardProvider interface {
+	ReadText() (string, bool)
+}