This is an automated email from the ASF dual-hosted git repository.

msciabarra pushed a commit to branch main
in repository 
https://gitbox.apache.org/repos/asf/openserverless-devcontainer.git

commit 2f7d4d55c1cd6324b8e0bdb5de15cfe11e9f0458
Author: Michele Sciabarra <[email protected]>
AuthorDate: Sun Nov 9 16:34:31 2025 +0000

    initial commit
---
 Dockerfile                                         |   56 +
 olaris-tk/.gitignore                               |   15 +
 olaris-tk/cli/.gitignore                           |    3 +
 olaris-tk/cli/.python-version                      |    1 +
 olaris-tk/cli/README.md                            |    2 +
 olaris-tk/cli/_empty                               |    0
 olaris-tk/cli/main.py                              |    6 +
 olaris-tk/cli/profile_default/ipython_config.py    | 1380 ++++++++++++++++++++
 olaris-tk/cli/profile_default/startup/00-chdir.ipy |    8 +
 olaris-tk/cli/profile_default/startup/01-opsenv.py |   25 +
 olaris-tk/cli/profile_default/startup/README       |   11 +
 olaris-tk/cli/pyproject.toml                       |   10 +
 olaris-tk/cli/uv.lock                              |  223 ++++
 olaris-tk/docopts.md                               |   13 +
 olaris-tk/findimg.js                               |   40 +
 olaris-tk/opsfile.yml                              |  172 +++
 olaris-tk/pg/.python-version                       |    1 +
 olaris-tk/pg/docopts.md                            |   25 +
 olaris-tk/pg/main.py                               |   90 ++
 olaris-tk/pg/opsfile.yml                           |   75 ++
 olaris-tk/pg/pyproject.toml                        |    7 +
 olaris-tk/pg/uv.lock                               |    8 +
 olaris-tk/prereq.yml                               |   88 ++
 olaris-tk/stream.py                                |   30 +
 start-ssh.sh                                       |   10 +
 25 files changed, 2299 insertions(+)

diff --git a/Dockerfile b/Dockerfile
new file mode 100644
index 0000000..e2f213c
--- /dev/null
+++ b/Dockerfile
@@ -0,0 +1,56 @@
+FROM debian:bookworm AS pgloader-builder
+
+ENV DEBIAN_FRONTEND=noninteractive
+
+# Install build dependencies
+RUN apt-get update && apt-get install -y --no-install-recommends \
+    git curl sbcl make unzip ca-certificates \
+    libsqlite3-dev libssl-dev gawk freetds-dev jq \
+    && rm -rf /var/lib/apt/lists/*
+
+# Clone pgloader and build it
+WORKDIR /build
+RUN git clone https://github.com/dimitri/pgloader.git \
+    && cd pgloader \
+    && make
+
+FROM node:22
+# Install basic development tools
+RUN \
+    echo "deb http://apt.postgresql.org/pub/repos/apt bookworm-pgdg main" > 
/etc/apt/sources.list.d/pgdg.list && \
+    wget --quiet -O - https://www.postgresql.org/media/keys/ACCC4CF8.asc |  
apt-key add - && \
+    apt update && \
+    apt install -y less man-db sudo vim jq python-is-python3 
python3-virtualenv \
+    locales postgresql-client-16 openssh-server
+
+RUN \
+    touch /.dockerenv && \
+    echo "en_US.UTF-8 UTF-8" > /etc/locale.gen && \
+    locale-gen en_US.UTF-8 && \
+    update-locale ANG=en_US.UTF-8 LANGUAGE=en_US.UTF-8 LC_ALL=en_US.UTF-8
+
+# Ensure default `node` user has access to `sudo`
+ARG USERNAME=node
+RUN \
+    echo $USERNAME ALL=\(root\) NOPASSWD:ALL > /etc/sudoers.d/$USERNAME \
+    && chmod 0440 /etc/sudoers.d/$USERNAME
+
+# install ops and plugins
+USER node
+RUN \
+    curl -sL 
https://raw.githubusercontent.com/apache/openserverless-cli/refs/heads/main/install.sh
 | bash
+RUN \
+    curl -LsSf https://astral.sh/uv/install.sh | sh
+
+ENV PATH="/home/node/.local/bin:${PATH}"
+
+RUN \
+    ops -update
+RUN \
+    git clone https://github.com/apache/openserverless-devcontainer 
/home/node/.ops/openserverless-devcontainer ;\
+    ln -sf  /home/node/.ops/devcontaine/olaris-tk /home/node/olaris-tk
+
+COPY --from=pgloader-builder /build/pgloader/build/bin/pgloader 
/usr/bin/pgloader
+ADD start-ssh.sh /start-ssh.sh
+
+CMD ["/start-ssh.sh"]
diff --git a/olaris-tk/.gitignore b/olaris-tk/.gitignore
new file mode 100644
index 0000000..fe5da61
--- /dev/null
+++ b/olaris-tk/.gitignore
@@ -0,0 +1,15 @@
+__pycache__
+_svr
+_src
+_sse
+.venv
+cli/profile_default/log
+cli/profile_default/pid
+cli/profile_default/security
+cli/profile_default/history.sqlite
+cli/profile_default/db/
+cli/requirements.txt
+node_modules
+mcp/mcproxy/kubernetes/_*.yaml
+.task/
+_dockerfile
diff --git a/olaris-tk/cli/.gitignore b/olaris-tk/cli/.gitignore
new file mode 100644
index 0000000..af47f1b
--- /dev/null
+++ b/olaris-tk/cli/.gitignore
@@ -0,0 +1,3 @@
+profile_default/log
+profile_default/pid
+profile_default/security
diff --git a/olaris-tk/cli/.python-version b/olaris-tk/cli/.python-version
new file mode 100644
index 0000000..e4fba21
--- /dev/null
+++ b/olaris-tk/cli/.python-version
@@ -0,0 +1 @@
+3.12
diff --git a/olaris-tk/cli/README.md b/olaris-tk/cli/README.md
new file mode 100644
index 0000000..3769bf4
--- /dev/null
+++ b/olaris-tk/cli/README.md
@@ -0,0 +1,2 @@
+starts ipython
+configure it with autoreload of modules
diff --git a/olaris-tk/cli/_empty b/olaris-tk/cli/_empty
new file mode 100644
index 0000000..e69de29
diff --git a/olaris-tk/cli/main.py b/olaris-tk/cli/main.py
new file mode 100644
index 0000000..cfde67e
--- /dev/null
+++ b/olaris-tk/cli/main.py
@@ -0,0 +1,6 @@
+def main():
+    print("Hello from cli!")
+
+
+if __name__ == "__main__":
+    main()
diff --git a/olaris-tk/cli/profile_default/ipython_config.py 
b/olaris-tk/cli/profile_default/ipython_config.py
new file mode 100644
index 0000000..f706139
--- /dev/null
+++ b/olaris-tk/cli/profile_default/ipython_config.py
@@ -0,0 +1,1380 @@
+# Configuration file for ipython.
+
+c = get_config()  #noqa
+
+#------------------------------------------------------------------------------
+# InteractiveShellApp(Configurable) configuration
+#------------------------------------------------------------------------------
+## A Mixin for applications that start InteractiveShell instances.
+#  
+#      Provides configurables for loading extensions and executing files
+#      as part of configuring a Shell environment.
+#  
+#      The following methods should be called by the :meth:`initialize` method
+#      of the subclass:
+#  
+#        - :meth:`init_path`
+#        - :meth:`init_shell` (to be implemented by the subclass)
+#        - :meth:`init_gui_pylab`
+#        - :meth:`init_extensions`
+#        - :meth:`init_code`
+
+## Execute the given command string.
+#  Default: ''
+# c.InteractiveShellApp.code_to_run = ''
+
+## Run the file referenced by the PYTHONSTARTUP environment
+#          variable at IPython startup.
+#  Default: True
+# c.InteractiveShellApp.exec_PYTHONSTARTUP = True
+
+## List of files to run at IPython startup.
+#  Default: []
+# c.InteractiveShellApp.exec_files = []
+
+## lines of code to run at IPython startup.
+#  Default: []
+# c.InteractiveShellApp.exec_lines = []
+
+
+## A list of dotted module names of IPython extensions to load.
+#  Default: []
+# c.InteractiveShellApp.extensions = []
+c.InteractiveShellApp.extensions = ['autoreload']
+
+## Dotted module name(s) of one or more IPython extensions to load.
+#  
+#  For specifying extra extensions to load on the command-line.
+#  
+#  .. versionadded:: 7.10
+#  Default: []
+# c.InteractiveShellApp.extra_extensions = []
+
+## A file to be run
+#  Default: ''
+# c.InteractiveShellApp.file_to_run = ''
+
+## Enable GUI event loop integration with any of ('asyncio', 'glut', 'gtk',
+#  'gtk2', 'gtk3', 'gtk4', 'osx', 'pyglet', 'qt', 'qt5', 'qt6', 'tk', 'wx',
+#  'gtk2', 'qt4').
+#  Choices: any of ['asyncio', 'glut', 'gtk', 'gtk2', 'gtk3', 'gtk4', 'osx', 
'pyglet', 'qt', 'qt5', 'qt6', 'tk', 'wx', 'gtk2', 'qt4'] (case-insensitive) or 
None
+#  Default: None
+# c.InteractiveShellApp.gui = None
+
+## Should variables loaded at startup (by startup files, exec_lines, etc.)
+#          be hidden from tools like %who?
+#  Default: True
+# c.InteractiveShellApp.hide_initial_ns = True
+
+## If True, IPython will not add the current working directory to sys.path.
+#          When False, the current working directory is added to sys.path, 
allowing imports
+#          of modules defined in the current directory.
+#  Default: False
+# c.InteractiveShellApp.ignore_cwd = False
+
+## Configure matplotlib for interactive use with
+#          the default matplotlib backend. The exact options available
+#          depend on what Matplotlib provides at runtime.
+#  Choices: any of ['agg', 'auto', 'gtk', 'gtk3', 'gtk4', 'inline', 'ipympl', 
'nbagg', 'notebook', 'osx', 'pdf', 'ps', 'qt', 'qt4', 'qt5', 'qt6', 'svg', 
'tk', 'webagg', 'widget', 'wx'] (case-insensitive) or None
+#  Default: None
+# c.InteractiveShellApp.matplotlib = None
+
+## Run the module as a script.
+#  Default: ''
+# c.InteractiveShellApp.module_to_run = ''
+
+## Pre-load matplotlib and numpy for interactive use,
+#          selecting a particular matplotlib backend and loop integration.
+#          The exact options available depend on what Matplotlib provides at 
runtime.
+#  Choices: any of ['agg', 'auto', 'gtk', 'gtk3', 'gtk4', 'inline', 'ipympl', 
'nbagg', 'notebook', 'osx', 'pdf', 'ps', 'qt', 'qt4', 'qt5', 'qt6', 'svg', 
'tk', 'webagg', 'widget', 'wx'] (case-insensitive) or None
+#  Default: None
+# c.InteractiveShellApp.pylab = None
+
+## If true, IPython will populate the user namespace with numpy, pylab, etc.
+#          and an ``import *`` is done from numpy and pylab, when using pylab 
mode.
+#  
+#          When False, pylab mode should not import any names into the user
+#  namespace.
+#  Default: True
+# c.InteractiveShellApp.pylab_import_all = True
+
+## Reraise exceptions encountered loading IPython extensions?
+#  Default: False
+# c.InteractiveShellApp.reraise_ipython_extension_failures = False
+
+#------------------------------------------------------------------------------
+# Application(SingletonConfigurable) configuration
+#------------------------------------------------------------------------------
+## This is an application.
+
+## The date format used by logging formatters for %(asctime)s
+#  Default: '%Y-%m-%d %H:%M:%S'
+# c.Application.log_datefmt = '%Y-%m-%d %H:%M:%S'
+
+## The Logging format template
+#  Default: '[%(name)s]%(highlevel)s %(message)s'
+# c.Application.log_format = '[%(name)s]%(highlevel)s %(message)s'
+
+## Set the log level by value or name.
+#  Choices: any of [0, 10, 20, 30, 40, 50, 'DEBUG', 'INFO', 'WARN', 'ERROR', 
'CRITICAL']
+#  Default: 30
+# c.Application.log_level = 30
+
+## Configure additional log handlers.
+#  
+#  The default stderr logs handler is configured by the log_level, log_datefmt
+#  and log_format settings.
+#  
+#  This configuration can be used to configure additional handlers (e.g. to
+#  output the log to a file) or for finer control over the default handlers.
+#  
+#  If provided this should be a logging configuration dictionary, for more
+#  information see:
+#  https://docs.python.org/3/library/logging.config.html#logging-config-
+#  dictschema
+#  
+#  This dictionary is merged with the base logging configuration which defines
+#  the following:
+#  
+#  * A logging formatter intended for interactive use called
+#    ``console``.
+#  * A logging handler that writes to stderr called
+#    ``console`` which uses the formatter ``console``.
+#  * A logger with the name of this application set to ``DEBUG``
+#    level.
+#  
+#  This example adds a new handler that writes to a file:
+#  
+#  .. code-block:: python
+#  
+#     c.Application.logging_config = {
+#         "handlers": {
+#             "file": {
+#                 "class": "logging.FileHandler",
+#                 "level": "DEBUG",
+#                 "filename": "<path/to/file>",
+#             }
+#         },
+#         "loggers": {
+#             "<application-name>": {
+#                 "level": "DEBUG",
+#                 # NOTE: if you don't list the default "console"
+#                 # handler here then it will be disabled
+#                 "handlers": ["console", "file"],
+#             },
+#         },
+#     }
+#  Default: {}
+# c.Application.logging_config = {}
+
+## Instead of starting the Application, dump configuration to stdout
+#  Default: False
+# c.Application.show_config = False
+
+## Instead of starting the Application, dump configuration to stdout (as JSON)
+#  Default: False
+# c.Application.show_config_json = False
+
+#------------------------------------------------------------------------------
+# BaseIPythonApplication(Application) configuration
+#------------------------------------------------------------------------------
+#  Default: False
+# c.BaseIPythonApplication.add_ipython_dir_to_sys_path = False
+
+## Whether to create profile dir if it doesn't exist
+#  Default: False
+# c.BaseIPythonApplication.auto_create = False
+
+## Whether to install the default config files into the profile dir.
+#          If a new profile is being created, and IPython contains config 
files for that
+#          profile, then they will be staged into the new directory.  
Otherwise,
+#          default config files will be automatically generated.
+#  Default: False
+# c.BaseIPythonApplication.copy_config_files = False
+
+## Path to an extra config file to load.
+#  
+#      If specified, load this config file in addition to any other IPython
+#  config.
+#  Default: ''
+# c.BaseIPythonApplication.extra_config_file = ''
+
+## The name of the IPython directory. This directory is used for logging
+#  configuration (through profiles), history storage, etc. The default is 
usually
+#  $HOME/.ipython. This option can also be specified through the environment
+#  variable IPYTHONDIR.
+#  Default: ''
+# c.BaseIPythonApplication.ipython_dir = ''
+
+## The date format used by logging formatters for %(asctime)s
+#  See also: Application.log_datefmt
+# c.BaseIPythonApplication.log_datefmt = '%Y-%m-%d %H:%M:%S'
+
+## The Logging format template
+#  See also: Application.log_format
+# c.BaseIPythonApplication.log_format = '[%(name)s]%(highlevel)s %(message)s'
+
+## Set the log level by value or name.
+#  See also: Application.log_level
+# c.BaseIPythonApplication.log_level = 30
+
+## 
+#  See also: Application.logging_config
+# c.BaseIPythonApplication.logging_config = {}
+
+## Whether to overwrite existing config files when copying
+#  Default: False
+# c.BaseIPythonApplication.overwrite = False
+
+## The IPython profile to use.
+#  Default: 'default'
+# c.BaseIPythonApplication.profile = 'default'
+
+## Instead of starting the Application, dump configuration to stdout
+#  See also: Application.show_config
+# c.BaseIPythonApplication.show_config = False
+
+## Instead of starting the Application, dump configuration to stdout (as JSON)
+#  See also: Application.show_config_json
+# c.BaseIPythonApplication.show_config_json = False
+
+## Create a massive crash report when IPython encounters what may be an
+#          internal error.  The default is to append a short message to the
+#          usual traceback
+#  Default: False
+# c.BaseIPythonApplication.verbose_crash = False
+
+#------------------------------------------------------------------------------
+# TerminalIPythonApp(BaseIPythonApplication, InteractiveShellApp) configuration
+#------------------------------------------------------------------------------
+#  See also: BaseIPythonApplication.add_ipython_dir_to_sys_path
+# c.TerminalIPythonApp.add_ipython_dir_to_sys_path = False
+
+#  Default: True
+# c.TerminalIPythonApp.auto_create = True
+
+## Execute the given command string.
+#  See also: InteractiveShellApp.code_to_run
+# c.TerminalIPythonApp.code_to_run = ''
+
+## Whether to install the default config files into the profile dir.
+#  See also: BaseIPythonApplication.copy_config_files
+# c.TerminalIPythonApp.copy_config_files = False
+
+## Whether to display a banner upon starting IPython.
+#  Default: True
+# c.TerminalIPythonApp.display_banner = True
+
+## Run the file referenced by the PYTHONSTARTUP environment
+#  See also: InteractiveShellApp.exec_PYTHONSTARTUP
+# c.TerminalIPythonApp.exec_PYTHONSTARTUP = True
+
+## List of files to run at IPython startup.
+#  See also: InteractiveShellApp.exec_files
+# c.TerminalIPythonApp.exec_files = []
+
+## lines of code to run at IPython startup.
+#  See also: InteractiveShellApp.exec_lines
+# c.TerminalIPythonApp.exec_lines = []
+c.InteractiveShellApp.exec_lines = ['%autoreload 2']
+
+## A list of dotted module names of IPython extensions to load.
+#  See also: InteractiveShellApp.extensions
+# c.TerminalIPythonApp.extensions = []
+
+## Path to an extra config file to load.
+#  See also: BaseIPythonApplication.extra_config_file
+# c.TerminalIPythonApp.extra_config_file = ''
+
+## 
+#  See also: InteractiveShellApp.extra_extensions
+# c.TerminalIPythonApp.extra_extensions = []
+
+## A file to be run
+#  See also: InteractiveShellApp.file_to_run
+# c.TerminalIPythonApp.file_to_run = ''
+
+## If a command or file is given via the command-line,
+#          e.g. 'ipython foo.py', start an interactive shell after executing 
the
+#          file or command.
+#  Default: False
+# c.TerminalIPythonApp.force_interact = False
+
+## Enable GUI event loop integration with any of ('asyncio', 'glut', 'gtk',
+#  'gtk2', 'gtk3', 'gtk4', 'osx', 'pyglet', 'qt', 'qt5', 'qt6', 'tk', 'wx',
+#  'gtk2', 'qt4').
+#  See also: InteractiveShellApp.gui
+# c.TerminalIPythonApp.gui = None
+
+## Should variables loaded at startup (by startup files, exec_lines, etc.)
+#  See also: InteractiveShellApp.hide_initial_ns
+# c.TerminalIPythonApp.hide_initial_ns = True
+
+## If True, IPython will not add the current working directory to sys.path.
+#  See also: InteractiveShellApp.ignore_cwd
+# c.TerminalIPythonApp.ignore_cwd = False
+
+## Class to use to instantiate the TerminalInteractiveShell object. Useful for
+#  custom Frontends
+#  Default: 'IPython.terminal.interactiveshell.TerminalInteractiveShell'
+# c.TerminalIPythonApp.interactive_shell_class = 
'IPython.terminal.interactiveshell.TerminalInteractiveShell'
+
+## 
+#  See also: BaseIPythonApplication.ipython_dir
+# c.TerminalIPythonApp.ipython_dir = ''
+
+## The date format used by logging formatters for %(asctime)s
+#  See also: Application.log_datefmt
+# c.TerminalIPythonApp.log_datefmt = '%Y-%m-%d %H:%M:%S'
+
+## The Logging format template
+#  See also: Application.log_format
+# c.TerminalIPythonApp.log_format = '[%(name)s]%(highlevel)s %(message)s'
+
+## Set the log level by value or name.
+#  See also: Application.log_level
+# c.TerminalIPythonApp.log_level = 30
+
+## 
+#  See also: Application.logging_config
+# c.TerminalIPythonApp.logging_config = {}
+
+## Configure matplotlib for interactive use with
+#  See also: InteractiveShellApp.matplotlib
+# c.TerminalIPythonApp.matplotlib = None
+
+## Run the module as a script.
+#  See also: InteractiveShellApp.module_to_run
+# c.TerminalIPythonApp.module_to_run = ''
+
+## Whether to overwrite existing config files when copying
+#  See also: BaseIPythonApplication.overwrite
+# c.TerminalIPythonApp.overwrite = False
+
+## The IPython profile to use.
+#  See also: BaseIPythonApplication.profile
+# c.TerminalIPythonApp.profile = 'default'
+
+## Pre-load matplotlib and numpy for interactive use,
+#  See also: InteractiveShellApp.pylab
+# c.TerminalIPythonApp.pylab = None
+
+## If true, IPython will populate the user namespace with numpy, pylab, etc.
+#  See also: InteractiveShellApp.pylab_import_all
+# c.TerminalIPythonApp.pylab_import_all = True
+
+## Start IPython quickly by skipping the loading of config files.
+#  Default: False
+# c.TerminalIPythonApp.quick = False
+
+## Reraise exceptions encountered loading IPython extensions?
+#  See also: InteractiveShellApp.reraise_ipython_extension_failures
+# c.TerminalIPythonApp.reraise_ipython_extension_failures = False
+
+## Instead of starting the Application, dump configuration to stdout
+#  See also: Application.show_config
+# c.TerminalIPythonApp.show_config = False
+
+## Instead of starting the Application, dump configuration to stdout (as JSON)
+#  See also: Application.show_config_json
+# c.TerminalIPythonApp.show_config_json = False
+
+## Create a massive crash report when IPython encounters what may be an
+#  See also: BaseIPythonApplication.verbose_crash
+# c.TerminalIPythonApp.verbose_crash = False
+
+#------------------------------------------------------------------------------
+# InteractiveShell(SingletonConfigurable) configuration
+#------------------------------------------------------------------------------
+## An enhanced, interactive shell for Python.
+
+## 'all', 'last', 'last_expr' or 'none', 'last_expr_or_assign' specifying which
+#  nodes should be run interactively (displaying output from expressions).
+#  Choices: any of ['all', 'last', 'last_expr', 'none', 'last_expr_or_assign']
+#  Default: 'last_expr'
+# c.InteractiveShell.ast_node_interactivity = 'last_expr'
+
+## A list of ast.NodeTransformer subclass instances, which will be applied to
+#  user input before code is run.
+#  Default: []
+# c.InteractiveShell.ast_transformers = []
+
+## Automatically run await statement in the top level repl.
+#  Default: True
+# c.InteractiveShell.autoawait = True
+
+## Make IPython automatically call any callable object even if you didn't type
+#  explicit parentheses. For example, 'str 43' becomes 'str(43)' automatically.
+#  The value can be '0' to disable the feature, '1' for 'smart' autocall, where
+#  it is not applied if there are no more arguments on the line, and '2' for
+#  'full' autocall, where all callable objects are automatically called (even 
if
+#  no arguments are present).
+#  Choices: any of [0, 1, 2]
+#  Default: 0
+# c.InteractiveShell.autocall = 0
+
+## Autoindent IPython code entered interactively.
+#  Default: True
+# c.InteractiveShell.autoindent = True
+
+## Enable magic commands to be called without the leading %.
+#  Default: True
+# c.InteractiveShell.automagic = True
+
+## The part of the banner to be printed before the profile
+#  Default: "Python 3.12.4 | packaged by Anaconda, Inc. | (main, Jun 18 2024, 
10:07:17) [Clang 14.0.6 ]\nType 'copyright', 'credits' or 'license' for more 
information\nIPython 9.1.0 -- An enhanced Interactive Python. Type '?' for 
help.\n"
+# c.InteractiveShell.banner1 = "Python 3.12.4 | packaged by Anaconda, Inc. | 
(main, Jun 18 2024, 10:07:17) [Clang 14.0.6 ]\nType 'copyright', 'credits' or 
'license' for more information\nIPython 9.1.0 -- An enhanced Interactive 
Python. Type '?' for help.\n"
+
+## The part of the banner to be printed after the profile
+#  Default: ''
+# c.InteractiveShell.banner2 = ''
+
+## Set the size of the output cache.  The default is 1000, you can change it
+#  permanently in your config file.  Setting it to 0 completely disables the
+#  caching system, and the minimum value accepted is 3 (if you provide a value
+#  less than 3, it is reset to 0 and a warning is issued).  This limit is 
defined
+#  because otherwise you'll spend more time re-flushing a too small cache than
+#  working
+#  Default: 1000
+# c.InteractiveShell.cache_size = 1000
+
+## Set the color scheme (nocolor, neutral, linux, lightbg).
+#  Default: 'neutral'
+# c.InteractiveShell.colors = 'neutral'
+
+#  Default: False
+# c.InteractiveShell.debug = False
+
+## If True, anything that would be passed to the pager
+#          will be displayed as regular output instead.
+#  Default: False
+# c.InteractiveShell.display_page = False
+
+## (Provisional API) enables html representation in mime bundles sent to 
pagers.
+#  Default: False
+# c.InteractiveShell.enable_html_pager = False
+
+## Set to show a tip when IPython starts.
+#  Default: True
+# c.InteractiveShell.enable_tip = True
+
+## Total length of command history
+#  Default: 10000
+# c.InteractiveShell.history_length = 10000
+
+## The number of saved history entries to be loaded into the history buffer at
+#  startup.
+#  Default: 1000
+# c.InteractiveShell.history_load_length = 1000
+
+## Class to use to instantiate the shell inspector
+#  Default: 'IPython.core.oinspect.Inspector'
+# c.InteractiveShell.inspector_class = 'IPython.core.oinspect.Inspector'
+
+#  Default: ''
+# c.InteractiveShell.ipython_dir = ''
+
+## Start logging to the given file in append mode. Use `logfile` to specify a 
log
+#  file to **overwrite** logs to.
+#  Default: ''
+# c.InteractiveShell.logappend = ''
+
+## The name of the logfile to use.
+#  Default: ''
+# c.InteractiveShell.logfile = ''
+
+## Start logging to the default log file in overwrite mode. Use `logappend` to
+#  specify a log file to **append** logs to.
+#  Default: False
+# c.InteractiveShell.logstart = False
+
+## Select the loop runner that will be used to execute top-level asynchronous
+#  code
+#  Default: 'IPython.core.interactiveshell._asyncio_runner'
+# c.InteractiveShell.loop_runner = 
'IPython.core.interactiveshell._asyncio_runner'
+
+#  Choices: any of [0, 1, 2]
+#  Default: 0
+# c.InteractiveShell.object_info_string_level = 0
+
+## Automatically call the pdb debugger after every exception.
+#  Default: False
+# c.InteractiveShell.pdb = False
+
+#  Default: False
+# c.InteractiveShell.quiet = False
+
+#  Default: '\n'
+# c.InteractiveShell.separate_in = '\n'
+
+#  Default: ''
+# c.InteractiveShell.separate_out = ''
+
+#  Default: ''
+# c.InteractiveShell.separate_out2 = ''
+
+## Show rewritten input, e.g. for autocall.
+#  Default: True
+# c.InteractiveShell.show_rewritten_input = True
+
+## Enables rich html representation of docstrings. (This requires the docrepr
+#  module).
+#  Default: False
+# c.InteractiveShell.sphinxify_docstring = False
+
+## Warn if running in a virtual environment with no IPython installed (so 
IPython
+#  from the global environment is used).
+#  Default: True
+# c.InteractiveShell.warn_venv = True
+
+#  Default: True
+# c.InteractiveShell.wildcards_case_sensitive = True
+
+## Switch modes for the IPython exception handlers.
+#  Choices: any of ['Context', 'Plain', 'Verbose', 'Minimal', 'Docs'] 
(case-insensitive)
+#  Default: 'Context'
+# c.InteractiveShell.xmode = 'Context'
+
+#------------------------------------------------------------------------------
+# TerminalInteractiveShell(InteractiveShell) configuration
+#------------------------------------------------------------------------------
+## 
+#  See also: InteractiveShell.ast_node_interactivity
+# c.TerminalInteractiveShell.ast_node_interactivity = 'last_expr'
+
+## 
+#  See also: InteractiveShell.ast_transformers
+# c.TerminalInteractiveShell.ast_transformers = []
+
+## Automatically add/delete closing bracket or quote when opening bracket or
+#  quote is entered/deleted. Brackets: (), [], {} Quotes: '', ""
+#  Default: False
+# c.TerminalInteractiveShell.auto_match = False
+
+## 
+#  See also: InteractiveShell.autoawait
+# c.TerminalInteractiveShell.autoawait = True
+
+## 
+#  See also: InteractiveShell.autocall
+# c.TerminalInteractiveShell.autocall = 0
+
+## Autoformatter to reformat Terminal code. Can be `'black'`, `'yapf'` or 
`None`
+#  Default: None
+# c.TerminalInteractiveShell.autoformatter = None
+
+## 
+#  See also: InteractiveShell.autoindent
+# c.TerminalInteractiveShell.autoindent = True
+
+## 
+#  See also: InteractiveShell.automagic
+# c.TerminalInteractiveShell.automagic = True
+
+## Specifies from which source automatic suggestions are provided. Can be set 
to
+#  ``'NavigableAutoSuggestFromHistory'`` (:kbd:`up` and :kbd:`down` swap
+#  suggestions), ``'AutoSuggestFromHistory'``,  or ``None`` to disable 
automatic
+#  suggestions. Default is `'NavigableAutoSuggestFromHistory`'.
+#  Default: 'NavigableAutoSuggestFromHistory'
+# c.TerminalInteractiveShell.autosuggestions_provider = 
'NavigableAutoSuggestFromHistory'
+
+## The part of the banner to be printed before the profile
+#  See also: InteractiveShell.banner1
+# c.TerminalInteractiveShell.banner1 = "Python 3.12.4 | packaged by Anaconda, 
Inc. | (main, Jun 18 2024, 10:07:17) [Clang 14.0.6 ]\nType 'copyright', 
'credits' or 'license' for more information\nIPython 9.1.0 -- An enhanced 
Interactive Python. Type '?' for help.\n"
+
+## The part of the banner to be printed after the profile
+#  See also: InteractiveShell.banner2
+# c.TerminalInteractiveShell.banner2 = ''
+
+## 
+#  See also: InteractiveShell.cache_size
+# c.TerminalInteractiveShell.cache_size = 1000
+
+## Set the color scheme (nocolor, neutral, linux, lightbg).
+#  See also: InteractiveShell.colors
+# c.TerminalInteractiveShell.colors = 'neutral'
+
+## Set to confirm when you try to exit IPython with an EOF (Control-D in Unix,
+#  Control-Z/Enter in Windows). By typing 'exit' or 'quit', you can force a
+#  direct exit without any confirmation.
+#  Default: True
+# c.TerminalInteractiveShell.confirm_exit = True
+
+#  See also: InteractiveShell.debug
+# c.TerminalInteractiveShell.debug = False
+
+## File in which to store and read history
+#  Default: '~/.pdbhistory'
+# c.TerminalInteractiveShell.debugger_history_file = '~/.pdbhistory'
+
+## Options for displaying tab completions, 'column', 'multicolumn', and
+#  'readlinelike'. These options are for `prompt_toolkit`, see `prompt_toolkit`
+#  documentation for more information.
+#  Choices: any of ['column', 'multicolumn', 'readlinelike']
+#  Default: 'multicolumn'
+# c.TerminalInteractiveShell.display_completions = 'multicolumn'
+
+## If True, anything that would be passed to the pager
+#  See also: InteractiveShell.display_page
+# c.TerminalInteractiveShell.display_page = False
+
+## Shortcut style to use at the prompt. 'vi' or 'emacs'.
+#  Default: 'emacs'
+# c.TerminalInteractiveShell.editing_mode = 'emacs'
+
+## Set the editor used by IPython (default to $EDITOR/vi/notepad).
+#  Default: 'vi'
+# c.TerminalInteractiveShell.editor = 'vi'
+
+## Add shortcuts from 'emacs' insert mode to 'vi' insert mode.
+#  Default: True
+# c.TerminalInteractiveShell.emacs_bindings_in_vi_insert_mode = True
+
+## Allows to enable/disable the prompt toolkit history search
+#  Default: True
+# c.TerminalInteractiveShell.enable_history_search = True
+
+## 
+#  See also: InteractiveShell.enable_html_pager
+# c.TerminalInteractiveShell.enable_html_pager = False
+
+## 
+#  See also: InteractiveShell.enable_tip
+# c.TerminalInteractiveShell.enable_tip = True
+
+## Enable vi (v) or Emacs (C-X C-E) shortcuts to open an external editor. This 
is
+#  in addition to the F2 binding, which is always enabled.
+#  Default: False
+# c.TerminalInteractiveShell.extra_open_editor_shortcuts = False
+
+## Provide an alternative handler to be called when the user presses Return. 
This
+#  is an advanced option intended for debugging, which may be changed or 
removed
+#  in later releases.
+#  Default: None
+# c.TerminalInteractiveShell.handle_return = None
+
+## Highlight matching brackets.
+#  Default: True
+# c.TerminalInteractiveShell.highlight_matching_brackets = True
+
+## Deprecated, and has not effect, use IPython themes
+#  
+#          The name or class of a Pygments style to use for syntax
+#          highlighting. To see available styles, run `pygmentize -L styles`.
+#  Default: traitlets.Undefined
+# c.TerminalInteractiveShell.highlighting_style = traitlets.Undefined
+
+## Override highlighting format for specific tokens
+#  Default: {}
+# c.TerminalInteractiveShell.highlighting_style_overrides = {}
+
+## Total length of command history
+#  See also: InteractiveShell.history_length
+# c.TerminalInteractiveShell.history_length = 10000
+
+## 
+#  See also: InteractiveShell.history_load_length
+# c.TerminalInteractiveShell.history_load_length = 1000
+
+## Class to use to instantiate the shell inspector
+#  See also: InteractiveShell.inspector_class
+# c.TerminalInteractiveShell.inspector_class = 
'IPython.core.oinspect.Inspector'
+
+#  See also: InteractiveShell.ipython_dir
+# c.TerminalInteractiveShell.ipython_dir = ''
+
+## Extra arguments to pass to `llm_provider_class` constructor.
+#  
+#  This is used to – for example – set the `model_id`
+#  Default: {}
+# c.TerminalInteractiveShell.llm_constructor_kwargs = {}
+
+## Fully Qualifed name of a function that takes an IPython history manager and
+#  return a prefix to pass the llm provider in addition to the current buffer
+#  text.
+#  
+#  You can use:
+#  
+#   - no_prefix
+#   - input_history
+#  
+#  As default value. `input_history` (default),  will use all the input history
+#  of current IPython session
+#  Default: 'input_history'
+# c.TerminalInteractiveShell.llm_prefix_from_history = 'input_history'
+
+## Provisional:
+#      This is a provisinal API in IPython 8.32, before stabilisation
+#      in 9.0, it may change without warnings.
+#  
+#  class to use for the `NavigableAutoSuggestFromHistory` to request 
completions
+#  from a LLM, this should inherit from `jupyter_ai_magics:BaseProvider` and
+#  implement `stream_inline_completions`
+#  Default: None
+# c.TerminalInteractiveShell.llm_provider_class = None
+
+## 
+#  See also: InteractiveShell.logappend
+# c.TerminalInteractiveShell.logappend = ''
+
+## 
+#  See also: InteractiveShell.logfile
+# c.TerminalInteractiveShell.logfile = ''
+
+## 
+#  See also: InteractiveShell.logstart
+# c.TerminalInteractiveShell.logstart = False
+
+## Select the loop runner that will be used to execute top-level asynchronous
+#  code
+#  See also: InteractiveShell.loop_runner
+# c.TerminalInteractiveShell.loop_runner = 
'IPython.core.interactiveshell._asyncio_runner'
+
+#  Default: {}
+# c.TerminalInteractiveShell.mime_renderers = {}
+
+## minimum characters for filling with ellipsis in file completions
+#  Default: 30
+# c.TerminalInteractiveShell.min_elide = 30
+
+## Cursor shape changes depending on vi mode: beam in vi insert mode, block in
+#  nav mode, underscore in replace mode.
+#  Default: True
+# c.TerminalInteractiveShell.modal_cursor = True
+
+## Enable mouse support in the prompt (Note: prevents selecting text with the
+#  mouse)
+#  Default: False
+# c.TerminalInteractiveShell.mouse_support = False
+
+#  See also: InteractiveShell.object_info_string_level
+# c.TerminalInteractiveShell.object_info_string_level = 0
+
+## 
+#  See also: InteractiveShell.pdb
+# c.TerminalInteractiveShell.pdb = False
+
+## Display the current vi mode (when using vi editing mode).
+#  Default: True
+# c.TerminalInteractiveShell.prompt_includes_vi_mode = True
+
+## The format for line numbering, will be passed `line` (int, 1 based) the
+#  current line number and `rel_line` the relative line number. for example to
+#  display both you can use the following template string :
+#  c.TerminalInteractiveShell.prompt_line_number_format='{line:
+#  4d}/{rel_line:+03d} | ' This will display the current line number, with
+#  leading space and a width of at least 4 character, as well as the relative
+#  line number 0 padded and always with a + or - sign. Note that when using 
Emacs
+#  mode the prompt of the first line may not update.
+#  Default: ''
+# c.TerminalInteractiveShell.prompt_line_number_format = ''
+
+## Class used to generate Prompt token for prompt_toolkit
+#  Default: 'IPython.terminal.prompts.Prompts'
+# c.TerminalInteractiveShell.prompts_class = 'IPython.terminal.prompts.Prompts'
+
+#  See also: InteractiveShell.quiet
+# c.TerminalInteractiveShell.quiet = False
+
+#  See also: InteractiveShell.separate_in
+# c.TerminalInteractiveShell.separate_in = '\n'
+
+#  See also: InteractiveShell.separate_out
+# c.TerminalInteractiveShell.separate_out = ''
+
+#  See also: InteractiveShell.separate_out2
+# c.TerminalInteractiveShell.separate_out2 = ''
+
+## Add, disable or modifying shortcuts.
+#  
+#  Each entry on the list should be a dictionary with ``command`` key 
identifying
+#  the target function executed by the shortcut and at least one of the
+#  following:
+#  
+#    - ``match_keys``: list of keys used to match an existing shortcut,
+#    - ``match_filter``: shortcut filter used to match an existing shortcut,
+#    - ``new_keys``: list of keys to set,
+#    - ``new_filter``: a new shortcut filter to set
+#  
+#  The filters have to be composed of pre-defined verbs and joined by one of 
the
+#  following conjunctions: ``&`` (and), ``|`` (or), ``~`` (not). The 
pre-defined
+#  verbs are:
+#  
+#    - ``always``
+#    - ``never``
+#    - ``has_line_below``
+#    - ``has_line_above``
+#    - ``is_cursor_at_the_end_of_line``
+#    - ``has_selection``
+#    - ``has_suggestion``
+#    - ``vi_mode``
+#    - ``vi_insert_mode``
+#    - ``emacs_insert_mode``
+#    - ``emacs_like_insert_mode``
+#    - ``has_completions``
+#    - ``insert_mode``
+#    - ``default_buffer_focused``
+#    - ``search_buffer_focused``
+#    - ``ebivim``
+#    - ``supports_suspend``
+#    - ``is_windows_os``
+#    - ``auto_match``
+#    - ``focused_insert``
+#    - ``not_inside_unclosed_string``
+#    - ``readline_like_completions``
+#    - ``preceded_by_paired_double_quotes``
+#    - ``preceded_by_paired_single_quotes``
+#    - ``preceded_by_raw_str_prefix``
+#    - ``preceded_by_two_double_quotes``
+#    - ``preceded_by_two_single_quotes``
+#    - ``followed_by_closing_paren_or_end``
+#    - ``preceded_by_opening_round_paren``
+#    - ``preceded_by_opening_bracket``
+#    - ``preceded_by_opening_brace``
+#    - ``preceded_by_double_quote``
+#    - ``preceded_by_single_quote``
+#    - ``followed_by_closing_round_paren``
+#    - ``followed_by_closing_bracket``
+#    - ``followed_by_closing_brace``
+#    - ``followed_by_double_quote``
+#    - ``followed_by_single_quote``
+#    - ``navigable_suggestions``
+#    - ``cursor_in_leading_ws``
+#    - ``pass_through``
+#  
+#  To disable a shortcut set ``new_keys`` to an empty list. To add a shortcut 
add
+#  key ``create`` with value ``True``.
+#  
+#  When modifying/disabling shortcuts, ``match_keys``/``match_filter`` can be
+#  omitted if the provided specification uniquely identifies a shortcut to be
+#  modified/disabled. When modifying a shortcut ``new_filter`` or ``new_keys``
+#  can be omitted which will result in reuse of the existing filter/keys.
+#  
+#  Only shortcuts defined in IPython (and not default prompt-toolkit shortcuts)
+#  can be modified or disabled. The full list of shortcuts, command identifiers
+#  and filters is available under :ref:`terminal-shortcuts-list`.
+#  
+#  Here is an example:
+#  
+#  .. code::
+#  
+#      c.TerminalInteractiveShell.shortcuts = [
+#         {
+#             "new_keys": ["c-q"],
+#             "command": "prompt_toolkit:named_commands.capitalize_word",
+#             "create": True,
+#         },
+#         {
+#             "new_keys": ["c-j"],
+#             "command": "prompt_toolkit:named_commands.beginning_of_line",
+#             "create": True,
+#         },
+#      ]
+#  Default: []
+# c.TerminalInteractiveShell.shortcuts = []
+
+## Show rewritten input, e.g. for autocall.
+#  See also: InteractiveShell.show_rewritten_input
+# c.TerminalInteractiveShell.show_rewritten_input = True
+
+## Use `raw_input` for the REPL, without completion and prompt colors.
+#  
+#              Useful when controlling IPython as a subprocess, and piping
+#              STDIN/OUT/ERR. Known usage are: IPython's own testing machinery,
+#              and emacs' inferior-python subprocess (assuming you have set
+#              `python-shell-interpreter` to "ipython") available through the
+#              built-in `M-x run-python` and third party packages such as elpy.
+#  
+#              This mode default to `True` if the `IPY_TEST_SIMPLE_PROMPT`
+#              environment variable is set, or the current terminal is not a 
tty.
+#              Thus the Default value reported in --help-all, or config will 
often
+#              be incorrectly reported.
+#  Default: False
+# c.TerminalInteractiveShell.simple_prompt = False
+
+## Number of line at the bottom of the screen to reserve for the tab completion
+#  menu, search history, ...etc, the height of these menus will at most this
+#  value. Increase it is you prefer long and skinny menus, decrease for short 
and
+#  wide.
+#  Default: 6
+# c.TerminalInteractiveShell.space_for_menu = 6
+
+## 
+#  See also: InteractiveShell.sphinxify_docstring
+# c.TerminalInteractiveShell.sphinxify_docstring = False
+
+## Automatically set the terminal title
+#  Default: True
+# c.TerminalInteractiveShell.term_title = True
+
+## Customize the terminal title format.  This is a python format string.
+#  Available substitutions are: {cwd}.
+#  Default: 'IPython: {cwd}'
+# c.TerminalInteractiveShell.term_title_format = 'IPython: {cwd}'
+
+## The time in milliseconds that is waited for a mapped key
+#         sequence to complete.
+#  Default: 0.5
+# c.TerminalInteractiveShell.timeoutlen = 0.5
+
+## Use 24bit colors instead of 256 colors in prompt highlighting.
+#          If your terminal supports true color, the following command should
+#          print ``TRUECOLOR`` in orange::
+#  
+#              printf "\x1b[38;2;255;100;0mTRUECOLOR\x1b[0m\n"
+#  Default: False
+# c.TerminalInteractiveShell.true_color = False
+
+## The time in milliseconds that is waited for a key code
+#         to complete.
+#  Default: 0.01
+# c.TerminalInteractiveShell.ttimeoutlen = 0.01
+
+## Warn if running in a virtual environment with no IPython installed (so 
IPython
+#  from the global environment is used).
+#  See also: InteractiveShell.warn_venv
+# c.TerminalInteractiveShell.warn_venv = True
+
+#  See also: InteractiveShell.wildcards_case_sensitive
+# c.TerminalInteractiveShell.wildcards_case_sensitive = True
+
+## Switch modes for the IPython exception handlers.
+#  See also: InteractiveShell.xmode
+# c.TerminalInteractiveShell.xmode = 'Context'
+
+#------------------------------------------------------------------------------
+# HistoryAccessor(HistoryAccessorBase) configuration
+#------------------------------------------------------------------------------
+## Access the history database without adding to it.
+#  
+#      This is intended for use by standalone history tools. IPython shells use
+#      HistoryManager, below, which is a subclass of this.
+
+## Options for configuring the SQLite connection
+#  
+#          These options are passed as keyword args to sqlite3.connect
+#          when establishing database connections.
+#  Default: {}
+# c.HistoryAccessor.connection_options = {}
+
+## enable the SQLite history
+#  
+#          set enabled=False to disable the SQLite history,
+#          in which case there will be no stored history, no SQLite connection,
+#          and no background saving thread.  This may be necessary in some
+#          threaded environments where IPython is embedded.
+#  Default: True
+# c.HistoryAccessor.enabled = True
+
+## Path to file to use for SQLite history database.
+#  
+#          By default, IPython will put the history database in the IPython
+#          profile directory.  If you would rather share one history among
+#          profiles, you can set this value in each, so that they are 
consistent.
+#  
+#          Due to an issue with fcntl, SQLite is known to misbehave on some NFS
+#          mounts.  If you see IPython hanging, try setting this to something 
on a
+#          local disk, e.g::
+#  
+#              ipython --HistoryManager.hist_file=/tmp/ipython_hist.sqlite
+#  
+#          you can also use the specific value `:memory:` (including the colon
+#          at both end but not the back ticks), to avoid creating an history 
file.
+#  Default: traitlets.Undefined
+# c.HistoryAccessor.hist_file = traitlets.Undefined
+
+#------------------------------------------------------------------------------
+# HistoryManager(HistoryAccessor) configuration
+#------------------------------------------------------------------------------
+## A class to organize all history-related functionality in one place.
+
+## Options for configuring the SQLite connection
+#  See also: HistoryAccessor.connection_options
+# c.HistoryManager.connection_options = {}
+
+## Write to database every x commands (higher values save disk access & power).
+#  Values of 1 or less effectively disable caching.
+#  Default: 0
+# c.HistoryManager.db_cache_size = 0
+
+## Should the history database include output? (default: no)
+#  Default: False
+# c.HistoryManager.db_log_output = False
+
+## enable the SQLite history
+#  See also: HistoryAccessor.enabled
+# c.HistoryManager.enabled = True
+
+## Path to file to use for SQLite history database.
+#  See also: HistoryAccessor.hist_file
+# c.HistoryManager.hist_file = traitlets.Undefined
+
+#------------------------------------------------------------------------------
+# MagicsManager(Configurable) configuration
+#------------------------------------------------------------------------------
+## Object that handles all magic-related functionality for IPython.
+
+## Automatically call line magics without requiring explicit % prefix
+#  Default: True
+# c.MagicsManager.auto_magic = True
+
+## Mapping from magic names to modules to load.
+#  
+#  This can be used in IPython/IPykernel configuration to declare lazy magics
+#  that will only be imported/registered on first use.
+#  
+#  For example::
+#  
+#      c.MagicsManager.lazy_magics = {
+#        "my_magic": "slow.to.import",
+#        "my_other_magic": "also.slow",
+#      }
+#  
+#  On first invocation of `%my_magic`, `%%my_magic`, `%%my_other_magic` or
+#  `%%my_other_magic`, the corresponding module will be loaded as an ipython
+#  extensions as if you had previously done `%load_ext ipython`.
+#  
+#  Magics names should be without percent(s) as magics can be both cell and 
line
+#  magics.
+#  
+#  Lazy loading happen relatively late in execution process, and complex
+#  extensions that manipulate Python/IPython internal state or global state 
might
+#  not support lazy loading.
+#  Default: {}
+# c.MagicsManager.lazy_magics = {}
+
+#------------------------------------------------------------------------------
+# ProfileDir(LoggingConfigurable) configuration
+#------------------------------------------------------------------------------
+## An object to manage the profile directory and its resources.
+#  
+#      The profile directory is used by all IPython applications, to manage
+#      configuration, logging and security.
+#  
+#      This object knows how to find, create and manage these directories. This
+#      should be used by any code that wants to handle profiles.
+
+## Set the profile location directly. This overrides the logic used by the
+#          `profile` option.
+#  Default: ''
+# c.ProfileDir.location = ''
+
+#------------------------------------------------------------------------------
+# BaseFormatter(Configurable) configuration
+#------------------------------------------------------------------------------
+## A base formatter class that is configurable.
+#  
+#      This formatter should usually be used as the base class of all 
formatters.
+#      It is a traited :class:`Configurable` class and includes an extensible
+#      API for users to determine how their objects are formatted. The 
following
+#      logic is used to find a function to format an given object.
+#  
+#      1. The object is introspected to see if it has a method with the name
+#         :attr:`print_method`. If is does, that object is passed to that 
method
+#         for formatting.
+#      2. If no print method is found, three internal dictionaries are 
consulted
+#         to find print method: :attr:`singleton_printers`, 
:attr:`type_printers`
+#         and :attr:`deferred_printers`.
+#  
+#      Users should use these dictionaries to register functions that will be
+#      used to compute the format data for their objects (if those objects 
don't
+#      have the special print methods). The easiest way of using these
+#      dictionaries is through the :meth:`for_type` and 
:meth:`for_type_by_name`
+#      methods.
+#  
+#      If no function/callable is found to compute the format data, ``None`` is
+#      returned and this format type is not used.
+
+#  Default: {}
+# c.BaseFormatter.deferred_printers = {}
+
+#  Default: True
+# c.BaseFormatter.enabled = True
+
+#  Default: {}
+# c.BaseFormatter.singleton_printers = {}
+
+#  Default: {}
+# c.BaseFormatter.type_printers = {}
+
+#------------------------------------------------------------------------------
+# PlainTextFormatter(BaseFormatter) configuration
+#------------------------------------------------------------------------------
+## The default pretty-printer.
+#  
+#      This uses :mod:`IPython.lib.pretty` to compute the format data of
+#      the object. If the object cannot be pretty printed, :func:`repr` is 
used.
+#      See the documentation of :mod:`IPython.lib.pretty` for details on
+#      how to write pretty printers.  Here is a simple example::
+#  
+#          def dtype_pprinter(obj, p, cycle):
+#              if cycle:
+#                  return p.text('dtype(...)')
+#              if hasattr(obj, 'fields'):
+#                  if obj.fields is None:
+#                      p.text(repr(obj))
+#                  else:
+#                      p.begin_group(7, 'dtype([')
+#                      for i, field in enumerate(obj.descr):
+#                          if i > 0:
+#                              p.text(',')
+#                              p.breakable()
+#                          p.pretty(field)
+#                      p.end_group(7, '])')
+
+#  See also: BaseFormatter.deferred_printers
+# c.PlainTextFormatter.deferred_printers = {}
+
+#  Default: ''
+# c.PlainTextFormatter.float_precision = ''
+
+## Truncate large collections (lists, dicts, tuples, sets) to this size.
+#  
+#          Set to 0 to disable truncation.
+#  Default: 1000
+# c.PlainTextFormatter.max_seq_length = 1000
+
+#  Default: 79
+# c.PlainTextFormatter.max_width = 79
+
+#  Default: '\n'
+# c.PlainTextFormatter.newline = '\n'
+
+#  Default: True
+# c.PlainTextFormatter.pprint = True
+
+#  See also: BaseFormatter.singleton_printers
+# c.PlainTextFormatter.singleton_printers = {}
+
+#  See also: BaseFormatter.type_printers
+# c.PlainTextFormatter.type_printers = {}
+
+#  Default: False
+# c.PlainTextFormatter.verbose = False
+
+#------------------------------------------------------------------------------
+# Completer(Configurable) configuration
+#------------------------------------------------------------------------------
+## Enable auto-closing dictionary keys.
+#  
+#  When enabled string keys will be suffixed with a final quote (matching the
+#  opening quote), tuple keys will also receive a separating comma if needed, 
and
+#  keys which are final will receive a closing bracket (``]``).
+#  Default: False
+# c.Completer.auto_close_dict_keys = False
+
+## Enable unicode completions, e.g. \alpha<tab> . Includes completion of latex
+#  commands, unicode names, and expanding unicode characters back to latex
+#  commands.
+#  Default: True
+# c.Completer.backslash_combining_completions = True
+
+## Enable debug for the Completer. Mostly print extra information for
+#  experimental jedi integration.
+#  Default: False
+# c.Completer.debug = False
+
+## Policy for code evaluation under completion.
+#  
+#          Successive options allow to enable more eager evaluation for better
+#          completion suggestions, including for nested dictionaries, nested 
lists,
+#          or even results of function calls.
+#          Setting ``unsafe`` or higher can lead to evaluation of arbitrary 
user
+#          code on :kbd:`Tab` with potentially unwanted or dangerous side 
effects.
+#  
+#          Allowed values are:
+#  
+#          - ``forbidden``: no evaluation of code is permitted,
+#          - ``minimal``: evaluation of literals and access to built-in 
namespace;
+#            no item/attribute evaluationm no access to locals/globals,
+#            no evaluation of any operations or comparisons.
+#          - ``limited``: access to all namespaces, evaluation of hard-coded 
methods
+#            (for example: :any:`dict.keys`, :any:`object.__getattr__`,
+#            :any:`object.__getitem__`) on allow-listed objects (for example:
+#            :any:`dict`, :any:`list`, :any:`tuple`, ``pandas.Series``),
+#          - ``unsafe``: evaluation of all methods and function calls but not 
of
+#            syntax with side-effects like `del x`,
+#          - ``dangerous``: completely arbitrary evaluation.
+#  Choices: any of ['forbidden', 'minimal', 'limited', 'unsafe', 'dangerous']
+#  Default: 'limited'
+# c.Completer.evaluation = 'limited'
+
+## Activate greedy completion.
+#  
+#          .. deprecated:: 8.8
+#              Use :std:configtrait:`Completer.evaluation` and 
:std:configtrait:`Completer.auto_close_dict_keys` instead.
+#  
+#          When enabled in IPython 8.8 or newer, changes configuration as
+#  follows:
+#  
+#          - ``Completer.evaluation = 'unsafe'``
+#          - ``Completer.auto_close_dict_keys = True``
+#  Default: False
+# c.Completer.greedy = False
+
+## Experimental: restrict time (in milliseconds) during which Jedi can compute 
types.
+#          Set to 0 to stop computing types. Non-zero value lower than 100ms 
may hurt
+#          performance by preventing jedi to build its cache.
+#  Default: 400
+# c.Completer.jedi_compute_type_timeout = 400
+
+## Experimental: Use Jedi to generate autocompletions. Default to True if jedi 
is
+#  installed.
+#  Default: True
+# c.Completer.use_jedi = True
+
+#------------------------------------------------------------------------------
+# IPCompleter(Completer) configuration
+#------------------------------------------------------------------------------
+## Extension of the completer class with IPython-specific features
+
+## 
+#  See also: Completer.auto_close_dict_keys
+# c.IPCompleter.auto_close_dict_keys = False
+
+## Enable unicode completions, e.g. \alpha<tab> . Includes completion of latex
+#  commands, unicode names, and expanding unicode characters back to latex
+#  commands.
+#  See also: Completer.backslash_combining_completions
+# c.IPCompleter.backslash_combining_completions = True
+
+## Enable debug for the Completer. Mostly print extra information for
+#  experimental jedi integration.
+#  See also: Completer.debug
+# c.IPCompleter.debug = False
+
+## List of matchers to disable.
+#  
+#          The list should contain matcher identifiers (see
+#  :any:`completion_matcher`).
+#  Default: []
+# c.IPCompleter.disable_matchers = []
+
+## Policy for code evaluation under completion.
+#  See also: Completer.evaluation
+# c.IPCompleter.evaluation = 'limited'
+
+## Activate greedy completion.
+#  See also: Completer.greedy
+# c.IPCompleter.greedy = False
+
+## Experimental: restrict time (in milliseconds) during which Jedi can compute
+#  types.
+#  See also: Completer.jedi_compute_type_timeout
+# c.IPCompleter.jedi_compute_type_timeout = 400
+
+## DEPRECATED as of version 5.0.
+#  
+#  Instruct the completer to use __all__ for the completion
+#  
+#  Specifically, when completing on ``object.<tab>``.
+#  
+#  When True: only those names in obj.__all__ will be included.
+#  
+#  When False [default]: the __all__ attribute is ignored
+#  Default: False
+# c.IPCompleter.limit_to__all__ = False
+
+## Whether to merge completion results into a single list
+#  
+#          If False, only the completion results from the first non-empty
+#          completer will be returned.
+#  
+#          As of version 8.6.0, setting the value to ``False`` is an alias for:
+#          ``IPCompleter.suppress_competing_matchers = True.``.
+#  Default: True
+# c.IPCompleter.merge_completions = True
+
+## Instruct the completer to omit private method names
+#  
+#          Specifically, when completing on ``object.<tab>``.
+#  
+#          When 2 [default]: all names that start with '_' will be excluded.
+#  
+#          When 1: all 'magic' names (``__foo__``) will be excluded.
+#  
+#          When 0: nothing will be excluded.
+#  Choices: any of [0, 1, 2]
+#  Default: 2
+# c.IPCompleter.omit__names = 2
+
+## If True, emit profiling data for completion subsystem using cProfile.
+#  Default: False
+# c.IPCompleter.profile_completions = False
+
+## Template for path at which to output profile data for completions.
+#  Default: '.completion_profiles'
+# c.IPCompleter.profiler_output_dir = '.completion_profiles'
+
+## Whether to suppress completions from other *Matchers*.
+#  
+#  When set to ``None`` (default) the matchers will attempt to auto-detect
+#  whether suppression of other matchers is desirable. For example, at the
+#  beginning of a line followed by `%` we expect a magic completion to be the
+#  only applicable option, and after ``my_dict['`` we usually expect a 
completion
+#  with an existing dictionary key.
+#  
+#  If you want to disable this heuristic and see completions from all matchers,
+#  set ``IPCompleter.suppress_competing_matchers = False``. To disable the
+#  heuristic for specific matchers provide a dictionary mapping:
+#  ``IPCompleter.suppress_competing_matchers = {'IPCompleter.dict_key_matcher':
+#  False}``.
+#  
+#  Set ``IPCompleter.suppress_competing_matchers = True`` to limit completions 
to
+#  the set of matchers with the highest priority; this is equivalent to
+#  ``IPCompleter.merge_completions`` and can be beneficial for performance, but
+#  will sometimes omit relevant candidates from matchers further down the
+#  priority list.
+#  Default: None
+# c.IPCompleter.suppress_competing_matchers = None
+
+## Experimental: Use Jedi to generate autocompletions. Default to True if jedi 
is
+#  installed.
+#  See also: Completer.use_jedi
+# c.IPCompleter.use_jedi = True
+
+#------------------------------------------------------------------------------
+# ScriptMagics(Magics) configuration
+#------------------------------------------------------------------------------
+## Magics for talking to scripts
+#  
+#      This defines a base `%%script` cell magic for running a cell
+#      with a program in a subprocess, and registers a few top-level
+#      magics that call %%script with common interpreters.
+
+## Extra script cell magics to define
+#  
+#          This generates simple wrappers of `%%script foo` as `%%foo`.
+#  
+#          If you want to add script magics that aren't on your path,
+#          specify them in script_paths
+#  Default: []
+# c.ScriptMagics.script_magics = []
+
+## Dict mapping short 'ruby' names to full paths, such as 
'/opt/secret/bin/ruby'
+#  
+#          Only necessary for items in script_magics where the default path 
will not
+#          find the right interpreter.
+#  Default: {}
+# c.ScriptMagics.script_paths = {}
+
+#------------------------------------------------------------------------------
+# LoggingMagics(Magics) configuration
+#------------------------------------------------------------------------------
+## Magics related to all logging machinery.
+
+## Suppress output of log state when logging is enabled
+#  Default: False
+# c.LoggingMagics.quiet = False
+
+#------------------------------------------------------------------------------
+# StoreMagics(Magics) configuration
+#------------------------------------------------------------------------------
+## Lightweight persistence for python variables.
+#  
+#      Provides the %store magic.
+
+## If True, any %store-d variables will be automatically restored
+#          when IPython starts.
+#  Default: False
+# c.StoreMagics.autorestore = False
diff --git a/olaris-tk/cli/profile_default/startup/00-chdir.ipy 
b/olaris-tk/cli/profile_default/startup/00-chdir.ipy
new file mode 100644
index 0000000..2a994e0
--- /dev/null
+++ b/olaris-tk/cli/profile_default/startup/00-chdir.ipy
@@ -0,0 +1,8 @@
+#print("hello")
+%load_ext autoreload
+%autoreload 2
+import os
+dir = os.getenv("OPS_PWD")
+if dir:
+    #print(f"Changing directory to {dir}")
+    os.chdir(dir)
\ No newline at end of file
diff --git a/olaris-tk/cli/profile_default/startup/01-opsenv.py 
b/olaris-tk/cli/profile_default/startup/01-opsenv.py
new file mode 100644
index 0000000..632204f
--- /dev/null
+++ b/olaris-tk/cli/profile_default/startup/01-opsenv.py
@@ -0,0 +1,25 @@
+import subprocess
+from dotenv import load_dotenv
+
+if os.path.exists(os.path.expanduser("~/.wskprops")):
+    # load secrets
+    command = ["ops", "-config", "-dump"]
+    result = subprocess.run(command, capture_output=True, text=True)
+    output = result.stdout
+
+    # Parse the output and set environment variables
+    for line in output.splitlines():
+        try:
+            key, value = line.split('=', 1)
+            os.environ[key] = value
+            print("OK:", key)
+        except:
+            print("ERR:", line)
+
+# override with testenv
+load_dotenv(".env")
+load_dotenv("tests/.env", override=True)
+
+class Empty: pass
+self = Empty()
+args = {}
\ No newline at end of file
diff --git a/olaris-tk/cli/profile_default/startup/README 
b/olaris-tk/cli/profile_default/startup/README
new file mode 100644
index 0000000..61d4700
--- /dev/null
+++ b/olaris-tk/cli/profile_default/startup/README
@@ -0,0 +1,11 @@
+This is the IPython startup directory
+
+.py and .ipy files in this directory will be run *prior* to any code or files 
specified
+via the exec_lines or exec_files configurables whenever you load this profile.
+
+Files will be run in lexicographical order, so you can control the execution 
order of files
+with a prefix, e.g.::
+
+    00-first.py
+    50-middle.py
+    99-last.ipy
diff --git a/olaris-tk/cli/pyproject.toml b/olaris-tk/cli/pyproject.toml
new file mode 100644
index 0000000..ff50a5f
--- /dev/null
+++ b/olaris-tk/cli/pyproject.toml
@@ -0,0 +1,10 @@
+[project]
+name = "cli"
+version = "0.1.0"
+description = "Add your description here"
+readme = "README.md"
+requires-python = ">=3.12"
+dependencies = [
+    "ipython>=9.1.0",
+    "python-dotenv>=1.1.1",
+]
diff --git a/olaris-tk/cli/uv.lock b/olaris-tk/cli/uv.lock
new file mode 100644
index 0000000..3cc291b
--- /dev/null
+++ b/olaris-tk/cli/uv.lock
@@ -0,0 +1,223 @@
+version = 1
+revision = 2
+requires-python = ">=3.12"
+resolution-markers = [
+    "python_full_version >= '3.13' and platform_python_implementation == 
'CPython'",
+    "python_full_version >= '3.12.4' and python_full_version < '3.13' and 
platform_python_implementation == 'CPython'",
+    "python_full_version < '3.12.4' and platform_python_implementation == 
'CPython'",
+    "python_full_version >= '3.13' and platform_python_implementation == 
'PyPy'",
+    "python_full_version >= '3.12.4' and python_full_version < '3.13' and 
platform_python_implementation == 'PyPy'",
+    "python_full_version < '3.12.4' and platform_python_implementation == 
'PyPy'",
+    "python_full_version >= '3.13' and platform_python_implementation != 
'CPython' and platform_python_implementation != 'PyPy'",
+    "python_full_version >= '3.12.4' and python_full_version < '3.13' and 
platform_python_implementation != 'CPython' and platform_python_implementation 
!= 'PyPy'",
+    "python_full_version < '3.12.4' and platform_python_implementation != 
'CPython' and platform_python_implementation != 'PyPy'",
+]
+
+[[package]]
+name = "asttokens"
+version = "3.0.0"
+source = { registry = "https://pypi.org/simple"; }
+sdist = { url = 
"https://files.pythonhosted.org/packages/4a/e7/82da0a03e7ba5141f05cce0d302e6eed121ae055e0456ca228bf693984bc/asttokens-3.0.0.tar.gz";,
 hash = 
"sha256:0dcd8baa8d62b0c1d118b399b2ddba3c4aff271d0d7a9e0d4c1681c79035bbc7", size 
= 61978, upload-time = "2024-11-30T04:30:14.439Z" }
+wheels = [
+    { url = 
"https://files.pythonhosted.org/packages/25/8a/c46dcc25341b5bce5472c718902eb3d38600a903b14fa6aeecef3f21a46f/asttokens-3.0.0-py3-none-any.whl";,
 hash = 
"sha256:e3078351a059199dd5138cb1c706e6430c05eff2ff136af5eb4790f9d28932e2", size 
= 26918, upload-time = "2024-11-30T04:30:10.946Z" },
+]
+
+[[package]]
+name = "cli"
+version = "0.1.0"
+source = { virtual = "." }
+dependencies = [
+    { name = "ipython" },
+    { name = "python-dotenv" },
+]
+
+[package.metadata]
+requires-dist = [
+    { name = "ipython", specifier = ">=9.1.0" },
+    { name = "python-dotenv", specifier = ">=1.1.1" },
+]
+
+[[package]]
+name = "colorama"
+version = "0.4.6"
+source = { registry = "https://pypi.org/simple"; }
+sdist = { url = 
"https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz";,
 hash = 
"sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size 
= 27697, upload-time = "2022-10-25T02:36:22.414Z" }
+wheels = [
+    { url = 
"https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl";,
 hash = 
"sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size 
= 25335, upload-time = "2022-10-25T02:36:20.889Z" },
+]
+
+[[package]]
+name = "decorator"
+version = "5.2.1"
+source = { registry = "https://pypi.org/simple"; }
+sdist = { url = 
"https://files.pythonhosted.org/packages/43/fa/6d96a0978d19e17b68d634497769987b16c8f4cd0a7a05048bec693caa6b/decorator-5.2.1.tar.gz";,
 hash = 
"sha256:65f266143752f734b0a7cc83c46f4618af75b8c5911b00ccb61d0ac9b6da0360", size 
= 56711, upload-time = "2025-02-24T04:41:34.073Z" }
+wheels = [
+    { url = 
"https://files.pythonhosted.org/packages/4e/8c/f3147f5c4b73e7550fe5f9352eaa956ae838d5c51eb58e7a25b9f3e2643b/decorator-5.2.1-py3-none-any.whl";,
 hash = 
"sha256:d316bb415a2d9e2d2b3abcc4084c6502fc09240e292cd76a76afc106a1c8e04a", size 
= 9190, upload-time = "2025-02-24T04:41:32.565Z" },
+]
+
+[[package]]
+name = "executing"
+version = "2.2.0"
+source = { registry = "https://pypi.org/simple"; }
+sdist = { url = 
"https://files.pythonhosted.org/packages/91/50/a9d80c47ff289c611ff12e63f7c5d13942c65d68125160cefd768c73e6e4/executing-2.2.0.tar.gz";,
 hash = 
"sha256:5d108c028108fe2551d1a7b2e8b713341e2cb4fc0aa7dcf966fa4327a5226755", size 
= 978693, upload-time = "2025-01-22T15:41:29.403Z" }
+wheels = [
+    { url = 
"https://files.pythonhosted.org/packages/7b/8f/c4d9bafc34ad7ad5d8dc16dd1347ee0e507a52c3adb6bfa8887e1c6a26ba/executing-2.2.0-py2.py3-none-any.whl";,
 hash = 
"sha256:11387150cad388d62750327a53d3339fad4888b39a6fe233c3afbb54ecffd3aa", size 
= 26702, upload-time = "2025-01-22T15:41:25.929Z" },
+]
+
+[[package]]
+name = "ipython"
+version = "9.1.0"
+source = { registry = "https://pypi.org/simple"; }
+dependencies = [
+    { name = "colorama", marker = "sys_platform == 'win32'" },
+    { name = "decorator" },
+    { name = "ipython-pygments-lexers" },
+    { name = "jedi" },
+    { name = "matplotlib-inline" },
+    { name = "pexpect", marker = "sys_platform != 'emscripten' and 
sys_platform != 'win32'" },
+    { name = "prompt-toolkit" },
+    { name = "pygments" },
+    { name = "stack-data" },
+    { name = "traitlets" },
+]
+sdist = { url = 
"https://files.pythonhosted.org/packages/70/9a/6b8984bedc990f3a4aa40ba8436dea27e23d26a64527de7c2e5e12e76841/ipython-9.1.0.tar.gz";,
 hash = 
"sha256:a47e13a5e05e02f3b8e1e7a0f9db372199fe8c3763532fe7a1e0379e4e135f16", size 
= 4373688, upload-time = "2025-04-07T10:18:28.704Z" }
+wheels = [
+    { url = 
"https://files.pythonhosted.org/packages/b2/9d/4ff2adf55d1b6e3777b0303fdbe5b723f76e46cba4a53a32fe82260d2077/ipython-9.1.0-py3-none-any.whl";,
 hash = 
"sha256:2df07257ec2f84a6b346b8d83100bcf8fa501c6e01ab75cd3799b0bb253b3d2a", size 
= 604053, upload-time = "2025-04-07T10:18:24.869Z" },
+]
+
+[[package]]
+name = "ipython-pygments-lexers"
+version = "1.1.1"
+source = { registry = "https://pypi.org/simple"; }
+dependencies = [
+    { name = "pygments" },
+]
+sdist = { url = 
"https://files.pythonhosted.org/packages/ef/4c/5dd1d8af08107f88c7f741ead7a40854b8ac24ddf9ae850afbcf698aa552/ipython_pygments_lexers-1.1.1.tar.gz";,
 hash = 
"sha256:09c0138009e56b6854f9535736f4171d855c8c08a563a0dcd8022f78355c7e81", size 
= 8393, upload-time = "2025-01-17T11:24:34.505Z" }
+wheels = [
+    { url = 
"https://files.pythonhosted.org/packages/d9/33/1f075bf72b0b747cb3288d011319aaf64083cf2efef8354174e3ed4540e2/ipython_pygments_lexers-1.1.1-py3-none-any.whl";,
 hash = 
"sha256:a9462224a505ade19a605f71f8fa63c2048833ce50abc86768a0d81d876dc81c", size 
= 8074, upload-time = "2025-01-17T11:24:33.271Z" },
+]
+
+[[package]]
+name = "jedi"
+version = "0.19.2"
+source = { registry = "https://pypi.org/simple"; }
+dependencies = [
+    { name = "parso" },
+]
+sdist = { url = 
"https://files.pythonhosted.org/packages/72/3a/79a912fbd4d8dd6fbb02bf69afd3bb72cf0c729bb3063c6f4498603db17a/jedi-0.19.2.tar.gz";,
 hash = 
"sha256:4770dc3de41bde3966b02eb84fbcf557fb33cce26ad23da12c742fb50ecb11f0", size 
= 1231287, upload-time = "2024-11-11T01:41:42.873Z" }
+wheels = [
+    { url = 
"https://files.pythonhosted.org/packages/c0/5a/9cac0c82afec3d09ccd97c8b6502d48f165f9124db81b4bcb90b4af974ee/jedi-0.19.2-py2.py3-none-any.whl";,
 hash = 
"sha256:a8ef22bde8490f57fe5c7681a3c83cb58874daf72b4784de3cce5b6ef6edb5b9", size 
= 1572278, upload-time = "2024-11-11T01:41:40.175Z" },
+]
+
+[[package]]
+name = "matplotlib-inline"
+version = "0.1.7"
+source = { registry = "https://pypi.org/simple"; }
+dependencies = [
+    { name = "traitlets" },
+]
+sdist = { url = 
"https://files.pythonhosted.org/packages/99/5b/a36a337438a14116b16480db471ad061c36c3694df7c2084a0da7ba538b7/matplotlib_inline-0.1.7.tar.gz";,
 hash = 
"sha256:8423b23ec666be3d16e16b60bdd8ac4e86e840ebd1dd11a30b9f117f2fa0ab90", size 
= 8159, upload-time = "2024-04-15T13:44:44.803Z" }
+wheels = [
+    { url = 
"https://files.pythonhosted.org/packages/8f/8e/9ad090d3553c280a8060fbf6e24dc1c0c29704ee7d1c372f0c174aa59285/matplotlib_inline-0.1.7-py3-none-any.whl";,
 hash = 
"sha256:df192d39a4ff8f21b1895d72e6a13f5fcc5099f00fa84384e0ea28c2cc0653ca", size 
= 9899, upload-time = "2024-04-15T13:44:43.265Z" },
+]
+
+[[package]]
+name = "parso"
+version = "0.8.4"
+source = { registry = "https://pypi.org/simple"; }
+sdist = { url = 
"https://files.pythonhosted.org/packages/66/94/68e2e17afaa9169cf6412ab0f28623903be73d1b32e208d9e8e541bb086d/parso-0.8.4.tar.gz";,
 hash = 
"sha256:eb3a7b58240fb99099a345571deecc0f9540ea5f4dd2fe14c2a99d6b281ab92d", size 
= 400609, upload-time = "2024-04-05T09:43:55.897Z" }
+wheels = [
+    { url = 
"https://files.pythonhosted.org/packages/c6/ac/dac4a63f978e4dcb3c6d3a78c4d8e0192a113d288502a1216950c41b1027/parso-0.8.4-py2.py3-none-any.whl";,
 hash = 
"sha256:a418670a20291dacd2dddc80c377c5c3791378ee1e8d12bffc35420643d43f18", size 
= 103650, upload-time = "2024-04-05T09:43:53.299Z" },
+]
+
+[[package]]
+name = "pexpect"
+version = "4.9.0"
+source = { registry = "https://pypi.org/simple"; }
+dependencies = [
+    { name = "ptyprocess" },
+]
+sdist = { url = 
"https://files.pythonhosted.org/packages/42/92/cc564bf6381ff43ce1f4d06852fc19a2f11d180f23dc32d9588bee2f149d/pexpect-4.9.0.tar.gz";,
 hash = 
"sha256:ee7d41123f3c9911050ea2c2dac107568dc43b2d3b0c7557a33212c398ead30f", size 
= 166450, upload-time = "2023-11-25T09:07:26.339Z" }
+wheels = [
+    { url = 
"https://files.pythonhosted.org/packages/9e/c3/059298687310d527a58bb01f3b1965787ee3b40dce76752eda8b44e9a2c5/pexpect-4.9.0-py2.py3-none-any.whl";,
 hash = 
"sha256:7236d1e080e4936be2dc3e326cec0af72acf9212a7e1d060210e70a47e253523", size 
= 63772, upload-time = "2023-11-25T06:56:14.81Z" },
+]
+
+[[package]]
+name = "prompt-toolkit"
+version = "3.0.50"
+source = { registry = "https://pypi.org/simple"; }
+dependencies = [
+    { name = "wcwidth" },
+]
+sdist = { url = 
"https://files.pythonhosted.org/packages/a1/e1/bd15cb8ffdcfeeb2bdc215de3c3cffca11408d829e4b8416dcfe71ba8854/prompt_toolkit-3.0.50.tar.gz";,
 hash = 
"sha256:544748f3860a2623ca5cd6d2795e7a14f3d0e1c3c9728359013f79877fc89bab", size 
= 429087, upload-time = "2025-01-20T15:55:35.072Z" }
+wheels = [
+    { url = 
"https://files.pythonhosted.org/packages/e4/ea/d836f008d33151c7a1f62caf3d8dd782e4d15f6a43897f64480c2b8de2ad/prompt_toolkit-3.0.50-py3-none-any.whl";,
 hash = 
"sha256:9b6427eb19e479d98acff65196a307c555eb567989e6d88ebbb1b509d9779198", size 
= 387816, upload-time = "2025-01-20T15:55:29.98Z" },
+]
+
+[[package]]
+name = "ptyprocess"
+version = "0.7.0"
+source = { registry = "https://pypi.org/simple"; }
+sdist = { url = 
"https://files.pythonhosted.org/packages/20/e5/16ff212c1e452235a90aeb09066144d0c5a6a8c0834397e03f5224495c4e/ptyprocess-0.7.0.tar.gz";,
 hash = 
"sha256:5c5d0a3b48ceee0b48485e0c26037c0acd7d29765ca3fbb5cb3831d347423220", size 
= 70762, upload-time = "2020-12-28T15:15:30.155Z" }
+wheels = [
+    { url = 
"https://files.pythonhosted.org/packages/22/a6/858897256d0deac81a172289110f31629fc4cee19b6f01283303e18c8db3/ptyprocess-0.7.0-py2.py3-none-any.whl";,
 hash = 
"sha256:4b41f3967fce3af57cc7e94b888626c18bf37a083e3651ca8feeb66d492fef35", size 
= 13993, upload-time = "2020-12-28T15:15:28.35Z" },
+]
+
+[[package]]
+name = "pure-eval"
+version = "0.2.3"
+source = { registry = "https://pypi.org/simple"; }
+sdist = { url = 
"https://files.pythonhosted.org/packages/cd/05/0a34433a064256a578f1783a10da6df098ceaa4a57bbeaa96a6c0352786b/pure_eval-0.2.3.tar.gz";,
 hash = 
"sha256:5f4e983f40564c576c7c8635ae88db5956bb2229d7e9237d03b3c0b0190eaf42", size 
= 19752, upload-time = "2024-07-21T12:58:21.801Z" }
+wheels = [
+    { url = 
"https://files.pythonhosted.org/packages/8e/37/efad0257dc6e593a18957422533ff0f87ede7c9c6ea010a2177d738fb82f/pure_eval-0.2.3-py3-none-any.whl";,
 hash = 
"sha256:1db8e35b67b3d218d818ae653e27f06c3aa420901fa7b081ca98cbedc874e0d0", size 
= 11842, upload-time = "2024-07-21T12:58:20.04Z" },
+]
+
+[[package]]
+name = "pygments"
+version = "2.19.1"
+source = { registry = "https://pypi.org/simple"; }
+sdist = { url = 
"https://files.pythonhosted.org/packages/7c/2d/c3338d48ea6cc0feb8446d8e6937e1408088a72a39937982cc6111d17f84/pygments-2.19.1.tar.gz";,
 hash = 
"sha256:61c16d2a8576dc0649d9f39e089b5f02bcd27fba10d8fb4dcc28173f7a45151f", size 
= 4968581, upload-time = "2025-01-06T17:26:30.443Z" }
+wheels = [
+    { url = 
"https://files.pythonhosted.org/packages/8a/0b/9fcc47d19c48b59121088dd6da2488a49d5f72dacf8262e2790a1d2c7d15/pygments-2.19.1-py3-none-any.whl";,
 hash = 
"sha256:9ea1544ad55cecf4b8242fab6dd35a93bbce657034b0611ee383099054ab6d8c", size 
= 1225293, upload-time = "2025-01-06T17:26:25.553Z" },
+]
+
+[[package]]
+name = "python-dotenv"
+version = "1.1.1"
+source = { registry = "https://pypi.org/simple"; }
+sdist = { url = 
"https://files.pythonhosted.org/packages/f6/b0/4bc07ccd3572a2f9df7e6782f52b0c6c90dcbb803ac4a167702d7d0dfe1e/python_dotenv-1.1.1.tar.gz";,
 hash = 
"sha256:a8a6399716257f45be6a007360200409fce5cda2661e3dec71d23dc15f6189ab", size 
= 41978, upload-time = "2025-06-24T04:21:07.341Z" }
+wheels = [
+    { url = 
"https://files.pythonhosted.org/packages/5f/ed/539768cf28c661b5b068d66d96a2f155c4971a5d55684a514c1a0e0dec2f/python_dotenv-1.1.1-py3-none-any.whl";,
 hash = 
"sha256:31f23644fe2602f88ff55e1f5c79ba497e01224ee7737937930c448e4d0e24dc", size 
= 20556, upload-time = "2025-06-24T04:21:06.073Z" },
+]
+
+[[package]]
+name = "stack-data"
+version = "0.6.3"
+source = { registry = "https://pypi.org/simple"; }
+dependencies = [
+    { name = "asttokens" },
+    { name = "executing" },
+    { name = "pure-eval" },
+]
+sdist = { url = 
"https://files.pythonhosted.org/packages/28/e3/55dcc2cfbc3ca9c29519eb6884dd1415ecb53b0e934862d3559ddcb7e20b/stack_data-0.6.3.tar.gz";,
 hash = 
"sha256:836a778de4fec4dcd1dcd89ed8abff8a221f58308462e1c4aa2a3cf30148f0b9", size 
= 44707, upload-time = "2023-09-30T13:58:05.479Z" }
+wheels = [
+    { url = 
"https://files.pythonhosted.org/packages/f1/7b/ce1eafaf1a76852e2ec9b22edecf1daa58175c090266e9f6c64afcd81d91/stack_data-0.6.3-py3-none-any.whl";,
 hash = 
"sha256:d5558e0c25a4cb0853cddad3d77da9891a08cb85dd9f9f91b9f8cd66e511e695", size 
= 24521, upload-time = "2023-09-30T13:58:03.53Z" },
+]
+
+[[package]]
+name = "traitlets"
+version = "5.14.3"
+source = { registry = "https://pypi.org/simple"; }
+sdist = { url = 
"https://files.pythonhosted.org/packages/eb/79/72064e6a701c2183016abbbfedaba506d81e30e232a68c9f0d6f6fcd1574/traitlets-5.14.3.tar.gz";,
 hash = 
"sha256:9ed0579d3502c94b4b3732ac120375cda96f923114522847de4b3bb98b96b6b7", size 
= 161621, upload-time = "2024-04-19T11:11:49.746Z" }
+wheels = [
+    { url = 
"https://files.pythonhosted.org/packages/00/c0/8f5d070730d7836adc9c9b6408dec68c6ced86b304a9b26a14df072a6e8c/traitlets-5.14.3-py3-none-any.whl";,
 hash = 
"sha256:b74e89e397b1ed28cc831db7aea759ba6640cb3de13090ca145426688ff1ac4f", size 
= 85359, upload-time = "2024-04-19T11:11:46.763Z" },
+]
+
+[[package]]
+name = "wcwidth"
+version = "0.2.13"
+source = { registry = "https://pypi.org/simple"; }
+sdist = { url = 
"https://files.pythonhosted.org/packages/6c/63/53559446a878410fc5a5974feb13d31d78d752eb18aeba59c7fef1af7598/wcwidth-0.2.13.tar.gz";,
 hash = 
"sha256:72ea0c06399eb286d978fdedb6923a9eb47e1c486ce63e9b4e64fc18303972b5", size 
= 101301, upload-time = "2024-01-06T02:10:57.829Z" }
+wheels = [
+    { url = 
"https://files.pythonhosted.org/packages/fd/84/fd2ba7aafacbad3c4201d395674fc6348826569da3c0937e75505ead3528/wcwidth-0.2.13-py2.py3-none-any.whl";,
 hash = 
"sha256:3da69048e4540d84af32131829ff948f1e022c1c6bdb8d6102117aac784f6859", size 
= 34166, upload-time = "2024-01-06T02:10:55.763Z" },
+]
diff --git a/olaris-tk/docopts.md b/olaris-tk/docopts.md
new file mode 100644
index 0000000..95c0769
--- /dev/null
+++ b/olaris-tk/docopts.md
@@ -0,0 +1,13 @@
+# Plugin ops a41
+
+AI tasks to manage Agent41
+
+## Synopsis
+
+```text
+Usage:
+  tk new <action>
+  tk cli [<function>]
+  tk pg
+  tk img <action> [-f]
+```
diff --git a/olaris-tk/findimg.js b/olaris-tk/findimg.js
new file mode 100644
index 0000000..1611d70
--- /dev/null
+++ b/olaris-tk/findimg.js
@@ -0,0 +1,40 @@
+import { readFile } from "fs/promises";
+
+async function main() {
+  const [,, runtimeInput, jsonPath] = process.argv;
+
+  if (!runtimeInput || !jsonPath) {
+    console.error("Usage: bun find.js <runtime[:version|default]> 
<runtimes.json>");
+    process.exit(1);
+  }
+
+  const [language, kindOrDefault] = runtimeInput.split(":");
+
+  const content = await readFile(jsonPath, "utf-8");
+  const data = JSON.parse(content);
+
+  const runtimes = data.runtimes?.[language];
+
+  if (!runtimes) {
+    console.error(`Runtime family "${language}" not found.`);
+    process.exit(1);
+  }
+
+  let runtime;
+
+  if (!kindOrDefault || kindOrDefault === "default") {
+    runtime = runtimes.find(r => r.default);
+  } else {
+    runtime = runtimes.find(r => r.kind === `${language}:${kindOrDefault}`);
+  }
+
+  if (!runtime || !runtime.image) {
+    console.error(`Runtime for "${runtimeInput}" not found or has no image 
info.`);
+    process.exit(1);
+  }
+
+  const { prefix, name, tag } = runtime.image;
+  console.log(`${prefix}/${name}:${tag}|${language}_${tag}`);
+}
+
+main();
diff --git a/olaris-tk/opsfile.yml b/olaris-tk/opsfile.yml
new file mode 100644
index 0000000..c61c5f2
--- /dev/null
+++ b/olaris-tk/opsfile.yml
@@ -0,0 +1,172 @@
+version: 3
+
+dotenv:
+  - '{{.OPS_PWD}}/.env'
+
+tasks:
+
+  pg:
+    desc: postgres 
+    
+  cli:
+    silent: false
+    desc: cli
+    env:
+      IPYTHONDIR:
+        sh: realpath .
+    dir: cli
+    cmds:
+      - |
+        if ! test -d .venv
+        then uv venv --seed
+             export VIRTUAL_ENV="$PWD/.venv"
+             uv pip install -r requirements.in
+        fi
+      - |
+        set -a
+        test -e $OPS_PWD/.env && source $OPS_PWD/.env
+        test -e $OPS_PWD/tests/.env && source $OPS_PWD/tests/.env
+        export PYTHONPATH="$OPS_PWD/packages:$PWD:$PYTHONPATH"
+        export VIRTUAL_ENV="$PWD/.venv"
+        if test -e $OPS_PWD/requirements.txt
+        then uv pip install -r $OPS_PWD/requirements.txt 
+        fi
+        if test -z "{{._function_}}"
+        then uv run ipython
+        else uv run ipython -m '{{._function_ | replace  "/" "."}}'
+        fi
+
+  new:
+    silent: true
+    desc: generate a new python action
+    vars:
+       ACTION: 
+         sh: echo  "{{._action_}}" | cut -d/ -f2
+       PACKAGE: 
+         sh: echo  "{{._action_}}" | cut -d/ -f1
+    env:
+       ACTION: "{{.ACTION}}"
+       PACKAGE: "{{.PACKAGE}}"
+       DIR:  "packages/{{.PACKAGE}}/{{.ACTION}}"
+       FILE: "packages/{{.PACKAGE}}/{{.ACTION}}/{{.ACTION}}.py"
+    cmds:
+    #- test ! -e "$OPS_PWD/$FILE" || die "file already exists"
+    - echo $ACTION
+    - echo $PACKAGE
+    - echo $DIR
+    - echo $FILE
+    - |
+      mkdir -p $OPS_PWD/tests/$PACKAGE
+      mkdir -p $OPS_PWD/$DIR
+    - |
+      cat <<EOF >$OPS_PWD/$FILE
+      def $ACTION(args):
+        return { "output": "$ACTION" }
+      EOF
+    - |
+      cat <<EOF >$OPS_PWD/$DIR/__main__.py
+      #--kind python:default
+      #--web true
+      import $ACTION
+      def main(args):
+        return { "body": $ACTION.$ACTION(args) }
+      EOF
+    - |
+      cat <<EOF >$OPS_PWD/tests/$PACKAGE/test_${ACTION}.py
+      import sys 
+      sys.path.append("$DIR")
+      import $ACTION
+      
+      def test_$ACTION():
+          res = $ACTION.$ACTION({})
+          assert res["output"] == "$ACTION"
+      EOF
+    - |
+      cat <<EOF >$OPS_PWD/tests/$PACKAGE/test_${ACTION}_int.py
+      import os, requests as req
+      def test_$ACTION():
+          url = os.environ.get("OPSDEV_HOST") + "/api/my/$PACKAGE/$ACTION"
+          res = req.get(url).json()
+          assert res.get("output") == "$ACTION"
+      EOF
+
+  img:
+    silent: true
+    desc: build image
+    vars:
+      DIR: '{{.OPS_PWD}}/packages/{{._action_}}'
+    env:
+      NO_COLOR: "1"
+    cmds:
+    - |
+      if {{._f}}
+      then rm -f {{.DIR}}/Dockerfile
+      fi
+    - task: _img
+      vars:
+        DIR: '{{.DIR}}'
+    - |
+      if test -e "{{.DIR}}/Dockerfile"
+      then 
+        echo "Image Built. Metadata comment to use:"
+        rg -N -- '--docker' "{{.DIR}}/Dockerfile"
+      fi
+
+  _img:
+    silent: true
+    desc: build image
+    sources:
+    - "{{.DIR}}/pyproject.toml"
+    generates:
+    - "{{.DIR}}/Dockerfile"
+    cmds:
+    - test -d "{{.DIR}}" || die "{{.DIR}} not found or not a directory"
+    - test -n "$DOCKER_BUILDER" || die "create a docker builder in docker 
cloud and add to .env DOCKER_BUILDER"
+    - test -n "$DOCKER_CUSTOM_RUNTIMES" || die "set DOCKER_CUSTOM_RUNTIMES to 
the custom runtime prefix"
+    - |
+      if test -f "{{.DIR}}/pyproject.toml"
+      then
+          DIR="{{.DIR}}"
+          SHA="$(sha256sum "$DIR/pyproject.toml" | awk '{print substr($1, 0, 
12)}')"
+          KIND="$(awk '/--kind/ {print $2}' "$DIR/__main__.py")"
+          FROM_TAG="$(bun findimg.js "$KIND" "$OPS_ROOT/runtimes.json")"
+          FROM="${FROM_TAG%|*}"
+          TAG="${FROM_TAG#*|}.${SHA}"
+          #echo FROM=$FROM
+          #echo TAG=$TAG
+          if rg "$TAG" "{{.DIR}}/Dockerfile" >/dev/null
+          then echo Image already built
+          else
+            # prepare the dockerfile
+            cat <<EOF >"$DIR/Dockerfile"
+      #--docker "$DOCKER_CUSTOM_RUNTIMES:$TAG"
+      FROM $FROM
+      USER root
+      ADD pyproject.toml pyproject.toml
+      RUN pip3 install .
+      EOF
+            if rg '^#Dockerfile:\s*(.*)' -or '$1' "$DIR/pyproject.toml" 
>_dockerfile
+            then cat _dockerfile >>"$DIR/Dockerfile"
+            fi
+            echo "USER nobody" >>"$DIR/Dockerfile"
+            cat "$DIR/Dockerfile"
+            docker buildx build --platform linux/arm64,linux/amd64 --builder 
"$DOCKER_BUILDER" -t "$DOCKER_CUSTOM_RUNTIMES:$TAG" "$DIR" --push
+            if test $? -eq 0
+            then echo Built "$DOCKER_CUSTOM_RUNTIMES:$TAG"
+            else  rm "$DIR/Dockerfile"
+                  die "docker build failed, check the Dockerfile"
+            fi
+          fi
+      else
+        echo "no pyproject.toml found, skipping image build"
+      fi
+
+  #stream:
+  #  silent: true
+  #  desc: stream an action
+  #  cmds:
+  #    - |
+  #      args={{._args_}}
+  #      set -- ${args[@]}
+  #      test "$1" = "--" && shift
+  #      uv run stream.py {{._action_}} "$@"
diff --git a/olaris-tk/pg/.python-version b/olaris-tk/pg/.python-version
new file mode 100644
index 0000000..24ee5b1
--- /dev/null
+++ b/olaris-tk/pg/.python-version
@@ -0,0 +1 @@
+3.13
diff --git a/olaris-tk/pg/docopts.md b/olaris-tk/pg/docopts.md
new file mode 100644
index 0000000..f27ef78
--- /dev/null
+++ b/olaris-tk/pg/docopts.md
@@ -0,0 +1,25 @@
+# Plugin ops a41 pg
+
+AI tasks to manage Postgresql
+
+## Synopsis
+
+```text
+Usage:
+  pg cli [<command>]
+  pg lite2pg <file>
+  pg export <schema> <file>
+  pg sqload <filesql> [--group=<count>]
+  pg sql [<sql>]
+```
+
+Note you have:
+- *TEST* commands affecting the test database (pointed by tests/.env)
+- *PROD* commands affecting the production database 
+
+Local (test) commands:
+- `cli` opens a CLI to the *TEST* database
+- `lite2pg` imports a sqlite file in the *TEST*
+- `export` exports a database from the *TEST* 
+- `sqload` execute a sql file against the *PROD* - you can group `<count>` 
statements 
+- `sql` execute a single statements against the *PROD*
\ No newline at end of file
diff --git a/olaris-tk/pg/main.py b/olaris-tk/pg/main.py
new file mode 100644
index 0000000..afd9295
--- /dev/null
+++ b/olaris-tk/pg/main.py
@@ -0,0 +1,90 @@
+import sys
+from pathlib import Path
+
+def slurp_statement(file):
+    """
+    Read from the file until you find a line ending with ';' 
+    Concatenate the lines removing the newline
+    """
+    lines = []
+    while True:
+        line = file.readline()
+        if not line:
+            return None
+        line = line.rstrip('\n')
+        if line.startswith("--") or line == "":
+            continue
+        if line.startswith("SET"):
+            continue
+        if line.startswith("CREATE TABLE"):
+            line = line.replace("public.", "")
+        lines.append(line.strip())
+        if line.endswith(';'):
+            break
+    return ' '.join(lines)
+
+
+def process(filename, action, max, exec):
+    """
+    Open a file and use slurp_statement to read a statement
+    when you reach a max number of statements invoke exec passing an array of 
statements
+    """
+    with open(filename, 'r') as file:
+        statements = []
+        count = 0
+        while True:
+            count += 1
+            statement = slurp_statement(file)
+            #print(statement)
+            if statement is None:
+                if statements:
+                    exec(action, statements)
+                break
+            statements.append(statement)
+            if len(statements) >= max:
+                print(f"{count}:", end="")
+                exec(action, statements)
+                statements = []
+    
+def invoke_exec(action, statements):
+    """
+    create a temporary json file with a key "input" and a value that is the 
concatenation of all the stemements separated by newlines
+    then evecute "ops action invoke  <action> -P <the file>
+    """
+    import json
+    import tempfile
+    import subprocess
+    
+    #print("-- invoke --\n", "\n".join(statements))
+
+    # Create temporary file
+    with tempfile.NamedTemporaryFile(mode='w', suffix='.json', delete=True) as 
tmp:
+        # Write statements to JSON file
+        Path(tmp.name).write_text(json.dumps({"input": "\n".join(statements)}, 
indent=2))
+        
+        cmd = ["ops", "action", "invoke", action, "-P", tmp.name, "-r"]
+        #cmd = ["cat",  tmp.name]
+        result = subprocess.run(cmd, check=True, capture_output=True, 
text=True)
+        print(result.stdout)
+
+    # Execute ops action command
+
+def main(argv):
+    print(argv)
+    filename = argv[0]
+    
+    action = "mastrogpt/sql"
+    if argv[1] != "":
+        action = argv[1]
+
+    max = 100
+    try: max = int(argv[2])
+    except: pass
+
+    print(f"action: {action} file: {filename} max: {max}")
+    process(filename, action, max, invoke_exec)
+
+
+if __name__ == "__main__":
+    main(sys.argv[1:])
+
diff --git a/olaris-tk/pg/opsfile.yml b/olaris-tk/pg/opsfile.yml
new file mode 100644
index 0000000..2d71500
--- /dev/null
+++ b/olaris-tk/pg/opsfile.yml
@@ -0,0 +1,75 @@
+version: 3
+
+tasks:
+
+  sql:
+    desc: execute a sql query against the database - default is to list the 
tables
+    cmds:
+     - |
+        ops invoke mastrogpt/sql input='{{default "@" ._sql_}}'
+  
+  sqload:
+    desc: load a sql file for postgres
+    cmds:
+     - |
+        DIR="$(pwd)"
+        cd $OPS_PWD
+        FILE="$(realpath {{._filesql_}})"
+        uv run --directory "$DIR" python main.py "$FILE" mastrogpt/sql 
"{{.__group}}"
+
+  lite2pg:
+    desc: sqlite to postgres conversion
+    dir: $OPS_PWD
+    silent: true
+    cmds:
+    - which pg_dump || die "pg_dump not found"
+    - which pgloader || die "pgloader not found"
+    - test -n "$OPSDEV_USERNAME" || die "please login to openserverless first"
+    - |
+      source $OPS_PWD/tests/.env
+      FILE="$(realpath {{._file_}})"
+      cat > {{._file_}}.load <<EOF 
+      LOAD DATABASE
+        FROM sqlite://$FILE
+        INTO $POSTGRES_URL
+
+      WITH include no drop, create tables, create indexes, reset sequences
+
+      SET search_path to '$OPSDEV_USERNAME' 
+
+      BEFORE LOAD 
+        DO \$\$ DROP SCHEMA IF EXISTS $OPSDEV_USERNAME CASCADE; \$\$
+        DO \$\$ CREATE SCHEMA IF NOT EXISTS $OPSDEV_USERNAME; \$\$
+      ;
+      EOF
+      if pgloader {{._file_}}.load 
+      then 
+          export PGPASSWORD=$POSTGRES_PASSWORD
+          pg_dump -h "$POSTGRES_HOST" -p "$POSTGRES_PORT" -U 
"$POSTGRES_USERNAME" -d "$POSTGRES_DATABASE" -n $OPSDEV_USERNAME -F p --inserts 
--no-owner --no-privileges  -f  "{{._file_}}.sql"
+          echo "Exported {{._file_}}.sql"
+      else echo "pgloader failed"
+      fi
+
+  export:
+    desc: export database to import in openserverless
+    dir: $OPS_PWD
+    cmds:
+    - which pg_dump || die "pg_dump not found"
+    - |
+      source $OPS_PWD/tests/.env
+      export PGPASSWORD=$POSTGRES_PASSWORD
+      pg_dump -h "$POSTGRES_HOST" -p "$POSTGRES_PORT" -U "$POSTGRES_USERNAME" 
-d "$POSTGRES_DATABASE" -n $OPSDEV_USERNAME --schema="{{._schema_}}" -F p 
--inserts --no-owner --no-privileges  -f  "{{._file_}}"
+
+  cli:
+    desc: postgres psql command
+    dir: $OPS_PWD
+    silent: true
+    cmds:
+    - |
+      source $OPS_PWD/tests/.env
+      export PGPASSWORD=$POSTGRES_PASSWORD
+      if test -z "{{._command_}}"
+      then psql -h $POSTGRES_HOST -p $POSTGRES_PORT -U $POSTGRES_USERNAME -d 
$POSTGRES_DATABASE
+      else psql -h $POSTGRES_HOST -p $POSTGRES_PORT -U $POSTGRES_USERNAME -d 
$POSTGRES_DATABASE -c "{{._command_}}"
+      fi
+
diff --git a/olaris-tk/pg/pyproject.toml b/olaris-tk/pg/pyproject.toml
new file mode 100644
index 0000000..65b22bb
--- /dev/null
+++ b/olaris-tk/pg/pyproject.toml
@@ -0,0 +1,7 @@
+[project]
+name = "pg"
+version = "0.1.0"
+description = "Add your description here"
+readme = "README.md"
+requires-python = ">=3.13"
+dependencies = []
diff --git a/olaris-tk/pg/uv.lock b/olaris-tk/pg/uv.lock
new file mode 100644
index 0000000..46773a8
--- /dev/null
+++ b/olaris-tk/pg/uv.lock
@@ -0,0 +1,8 @@
+version = 1
+revision = 1
+requires-python = ">=3.13"
+
+[[package]]
+name = "pg"
+version = "0.1.0"
+source = { virtual = "." }
diff --git a/olaris-tk/prereq.yml b/olaris-tk/prereq.yml
new file mode 100644
index 0000000..11cbdc3
--- /dev/null
+++ b/olaris-tk/prereq.yml
@@ -0,0 +1,88 @@
+version: 3
+
+vars:
+  OS: "{{or .__OS OS}}"
+  ARCH: "{{or .__ARCH ARCH}}"
+  ARC: '{{if eq .OS "windows"}}.zip{{else}}.tar.gz{{end}}'
+  EXE: '{{if eq .OS "windows"}}.exe{{else}}{{end}}'
+  DRY: ""
+
+tasks:
+
+  download-and-extract:
+     requires:
+       vars: 
+       - FILE
+       - URL
+       - OUT
+     cmds:
+     - echo "{{.OUT}} {{.FILE}} {{.URL}} {{.FILETAR}}" 
+     - curl -sL "{{.URL}}" -o {{.FILE}}
+     - extract "{{.FILE}}" "{{.OUT}}{{.EXE}}"
+     - remove "{{.FILE}}"
+  
+  himalaya:
+    desc: download himalaya
+    vars:
+      VERSION: "1.1.0"
+      SUFFIX:
+        sh: |
+            case "{{.OS}}-{{.ARCH}}" in
+            linux-amd64) echo "x86_64-linux" ;;
+            linux-arm64) echo "aarch64-linux" ;;
+            darwin-amd64) echo "x86_64-darwin" ;;
+            darwin-arm64) echo "aarch64-darwin" ;; 
+            windows-*) echo "x86_64-windows" ;;
+            *) echo "unknown" ;;
+            esac
+            
#https://github.com/pimalaya/himalaya/releases/download/v1.1.0/himalaya.aarch64-darwin.tgz
+      URL:  
"https://github.com/pimalaya/himalaya/releases/download/v{{.VERSION}}/himalaya.{{.SUFFIX}}.tgz";
+      FILE: "{{base .URL}}"
+      OUT: himalaya
+    cmds:
+    - task: download-and-extract
+      vars:
+        URL: "{{.URL}}"
+        FILE: "{{.FILE}}"
+        OUT: himalaya
+  
+
+  all:
+    - task: himalaya
+    
+  test:
+    vars:
+      DIR: "{{.OPS_PWD}}/bin/{{.OS}}-{{.ARCH}}"
+    cmds:
+    - |
+      {{.DRY}} rm -Rvf {{.DIR}}
+      mkdir -p {{.DIR}}
+      cd {{.DIR}}
+      {{.DRY}} ops -task -t ../../prereq.yml -d {{.DIR}} all
+    #- task: check
+      
+  check:
+    vars:
+      DIR: "{{.OPS_PWD}}/bin/{{.OS}}-{{.ARCH}}"
+      FILETYPE:
+        sh: |
+          case "{{.OS}}" in
+          (windows) echo application/vnd.microsoft.portable-executable ;;
+          (darwin)  echo application/x-mach-binary ;;
+          (linux)   echo application/x-executable ;;
+          (*) unknown ;;
+          esac
+    sources:
+      - '{{.DIR}}/*'
+    status:
+      - false
+    cmds:
+    - for: sources
+      cmd: filetype -m {{.ITEM}} | rg {{.FILETYPE}}
+ 
+  tests:
+   - __OS=linux   __ARCH=amd64 ops -task -t prereq.yml test
+   - __OS=linux   __ARCH=arm64 ops -task -t prereq.yml test
+   - __OS=darwin  __ARCH=amd64 ops -task -t prereq.yml test
+   - __OS=darwin  __ARCH=arm64 ops -task -t prereq.yml test
+   - __OS=windows __ARCH=amd64 ops -task -t prereq.yml test
diff --git a/olaris-tk/stream.py b/olaris-tk/stream.py
new file mode 100644
index 0000000..e7f3088
--- /dev/null
+++ b/olaris-tk/stream.py
@@ -0,0 +1,30 @@
+# /// script
+# dependencies = [
+#   "requests",
+# ]
+# ///
+import sys, os, json
+import requests as req
+from urllib.parse import urlparse, urlunparse
+
+
+def main(argv):
+    #print(argv)
+    #argv=['stream/demo']
+    url = urlparse(os.getenv("OPSDEV_APIHOST"))
+    netloc = f"stream.{url.netloc}"
+    path = f"/web/{os.getenv("OPSDEV_USERNAME")}/{argv[0]}"
+    streamer = urlunparse(url._replace(netloc=netloc, path=path))
+    msg =  {"input": " ".join(argv[1:])} if len(argv)>1 else {}
+    lines = req.post(streamer, json=msg, stream=True).iter_lines()
+    for line in lines:
+        #line = next(lines)
+        msg = json.loads(line.decode("utf-8")).get("output", "")
+        if msg != "":
+            print(msg, end="", flush=True)            
+        else: 
+            print()
+            break
+   
+if __name__ == "__main__":
+    main(sys.argv[1:])
diff --git a/start-ssh.sh b/start-ssh.sh
new file mode 100755
index 0000000..dd66d9e
--- /dev/null
+++ b/start-ssh.sh
@@ -0,0 +1,10 @@
+#!/bin/bash
+sudo mkdir -p /run/sshd
+sudo ssh-keygen -A
+mkdir -p ~/.ssh
+chmod 600 ~/.ssh/authorized_keys
+if test -n "$AUTHORIZED_KEY"
+then echo "$AUTHORIZED_KEY" >>~/.ssh/authorized_keys
+fi
+ln -sf /workspace ~/workspace
+sudo chown -R 1000:1000 /workspace


Reply via email to