gitignore
diff --git a/.gitignore b/.gitignore
index c2658d7..3c3629e 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1 +1 @@
-node_modules/
+node_modules
diff --git a/spec.md b/spec.md
new file mode 100644
index 0000000..548d6fb
--- /dev/null
+++ b/spec.md
@@ -0,0 +1,294 @@
+Implement the tools as separate tools in folder tools/
+
+Rule: an action is stored folder `packages/<package>/<name>`, functions is a synonym for action.
+
+Rule: assume the packages referenced by the tools are already available and preinstalled.
+
+Rule: `action-add-secret`, `action-add-s3`, `action-add-redis`, `action-add-postgresql`, and `action-add-milvus` are idempotent — if the secret or service is already present in `__main__.py`, skip the injection silently and return a message saying it is already configured.
+
+Implement the following tools in folder tools/ using this template:
+
+```
+import { tool } from "@opencode-ai/plugin"
+
+export default tool({
+  description: "<description>",
+  args: {
+    <arg>: tool.schema.<type>().describe("<arg description>"),
+  },
+  async execute(args) {
+    <tool logic>
+    return `<results>`
+  },
+})
+```
+
+Generate each tools as standalone scripts,  avoid separate helpers files, duplicate code if needed.
+
+# tool action-new
+
+This tool creates a new action — public (API endpoint) or private (typically for initializations).
+
+## parameters
+
+Receive an <endpoint> (`name` or `package/name`, defaults to `v1` package) and an optional <public> flag (defaults to `true`). Set `public: false` to create a private action (used for initialization tasks in the `init` package).
+
+- ensure package and name only contain letters, numbers and '-' and start with a letter
+- <module> is the name with '-' replaced with '_' (example: 'new-user' becomes 'new_user')
+
+## generation
+
+- create a folder `packages/<package>/<name>`
+
+- write a file `packages/<package>/<name>/__main__.py` with the content (where `<public>` is `true` or `false`):
+
+```
+#--kind python:default
+#--web <public>
+# Note: this timeout is 5 minutes - 10 minutes is max allowed
+#--timeout 300000
+import types, os, <module>
+
+builder = []
+## build-context ##
+
+def main(args):
+  try:
+    ctx = types.SimpleNamespace()
+    for fn in builder: fn(args, ctx)
+    return { "body": <module>.main(args, ctx=ctx) }
+  except Exception as e:
+    import traceback
+    traceback.print_exc()
+    return {
+      "body": {"error": str(e) },
+      "statusCode": 500
+    }
+```
+
+- write a file `packages/<package>/<name>/<module>.py` with the content:
+
+```
+def main(args, ctx=None):
+  inp = args.get("input", "<module>")
+  out = inp
+  return out
+```
+
+## returns
+
+The resulting `<package>/<name>`.
+
+# tool action-invoke
+
+This tool invokes an api action.
+
+## parameters
+
+Receive an <endpoint> (`package/action`) and a series of key=value pairs.
+
+## executes
+
+the command `ops action invoke <endpoint> -p "<key1>" "<value1>" ... -p "<keyn>" "<valuen>"`
+
+## returns
+
+the json output of the command
+
+# tool action-add-secret
+
+## parameters
+
+Receive an <endpoint> (`name` or `package/name`) and a <secret> name.
+
+## generation
+
+This tool adds a new secret <MY_SECRET> to an endpoint/action/function.
+
+First checks the secret is available in `.env`. If not, warn the user and suggest to add it to the environment.
+
+- adds in `__main__.py` after "## build-context ##":
+
+```
+#--param <MY_SECRET> "$<MY_SECRET>"
+builder.append(lambda args, ctx: setattr(ctx, '<MY_SECRET>', args.get("<MY_SECRET>", os.getenv("<MY_SECRET>"))))
+```
+
+## returns
+
+Information on the updated context.
+
+# tool action-add-s3
+
+## parameters
+
+Receive an <endpoint> (`name` or `package/name`).
+
+## generation
+
+This tool adds S3 to the context of an endpoint/action/function, making available:
+- `ctx.S3_CLIENT` — the S3 client
+- `ctx.S3_DATA` — the S3 data bucket (private)
+- `ctx.S3_WEB` — the S3 web bucket (public)
+- `ctx.S3_PUBLIC` — the public URL to access S3
+
+- adds in `__main__.py` after "## build-context ##":
+
+```
+#--param S3_HOST "$S3_HOST"
+#--param S3_PORT "$S3_PORT"
+#--param S3_ACCESS_KEY "$S3_ACCESS_KEY"
+#--param S3_SECRET_KEY "$S3_SECRET_KEY"
+#--param S3_BUCKET_DATA "$S3_BUCKET_DATA"
+#--param S3_BUCKET_STATIC "$S3_BUCKET_STATIC"
+#--param S3_PUBLIC "$OPSDEV_S3"
+import boto3
+from botocore.client import Config
+def init_s3(args, ctx):
+  host = args.get("S3_HOST", os.getenv("S3_HOST"))
+  port = args.get("S3_PORT", os.getenv("S3_PORT"))
+  url = f"http://{host}:{port}"
+  key = args.get("S3_ACCESS_KEY", os.getenv("S3_ACCESS_KEY"))
+  sec = args.get("S3_SECRET_KEY", os.getenv("S3_SECRET_KEY"))
+  cfg = Config(signature_version='s3v4')
+  ctx.S3_CLIENT = boto3.client('s3', region_name='us-east-1', endpoint_url=url, aws_access_key_id=key, aws_secret_access_key=sec, config=cfg)
+  ctx.S3_DATA = args.get("S3_BUCKET_DATA", os.getenv("S3_BUCKET_DATA"))
+  ctx.S3_WEB = args.get("S3_BUCKET_STATIC", os.getenv("S3_BUCKET_STATIC"))
+  ctx.S3_PUBLIC = args.get("S3_PUBLIC", os.getenv("OPSDEV_S3"))
+builder.append(init_s3)
+```
+
+## returns
+
+Information on the updated context.
+
+# tool action-add-redis
+
+## parameters
+
+Receive an <endpoint> (`name` or `package/name`).
+
+## generation
+
+This tool adds a Redis connection to an endpoint/action/function, making available:
+- `ctx.REDIS` — the Redis client
+- `ctx.REDIS_PREFIX` — the key prefix
+
+- adds in `__main__.py` after "## build-context ##":
+
+```
+#--param REDIS_URL "$REDIS_URL"
+#--param REDIS_PREFIX "$REDIS_PREFIX"
+import redis
+def init_redis(args, ctx):
+  ctx.REDIS = redis.from_url(args.get("REDIS_URL", os.getenv("REDIS_URL")))
+  ctx.REDIS_PREFIX = args.get("REDIS_PREFIX", os.getenv("REDIS_PREFIX"))
+builder.append(init_redis)
+```
+
+## returns
+
+Information on the updated context.
+
+# tool action-add-postgresql
+
+## parameters
+
+Receive an <endpoint> (`name` or `package/name`).
+
+## generation
+
+This tool adds a PostgreSQL connection to an endpoint/action/function, making available:
+- `ctx.POSTGRESQL` — the psycopg connection
+
+- adds in `__main__.py` after "## build-context ##":
+
+```
+#--param POSTGRES_URL "$POSTGRES_URL"
+import psycopg
+def init_postgresql(args, ctx):
+  dburl = args.get("POSTGRES_URL", os.getenv("POSTGRES_URL"))
+  ctx.POSTGRESQL = psycopg.connect(dburl)
+builder.append(init_postgresql)
+```
+
+## returns
+
+Information on the updated context.
+
+# tool action-add-milvus
+
+## parameters
+
+Receive an <endpoint> (`name` or `package/name`).
+
+## generation
+
+This tool adds a Milvus vector DB connection to an endpoint/action/function, making available:
+- `ctx.MILVUS` — the MilvusClient instance
+
+- adds in `__main__.py` after "## build-context ##":
+
+```
+#--param MILVUS_HOST "$MILVUS_HOST"
+#--param MILVUS_PORT "$MILVUS_PORT"
+#--param MILVUS_DB_NAME "$MILVUS_DB_NAME"
+#--param MILVUS_TOKEN "$MILVUS_TOKEN"
+from pymilvus import MilvusClient
+def init_milvus(args, ctx):
+  host = args.get('MILVUS_HOST', os.getenv('MILVUS_HOST'))
+  port = args.get('MILVUS_PORT', os.getenv('MILVUS_PORT'))
+  uri = f"http://{host}:{port}"
+  token = args.get("MILVUS_TOKEN", os.getenv("MILVUS_TOKEN"))
+  db_name = args.get("MILVUS_DB_NAME", os.getenv("MILVUS_DB_NAME"))
+  ctx.MILVUS = MilvusClient(uri=uri, token=token, db_name=db_name)
+builder.append(init_milvus)
+```
+
+## returns
+
+Information on the updated context.
+
+# tool action-requirements
+
+## parameters
+
+Receive an <endpoint> (`name` or `package/name`) and a <library> name.
+
+## generation
+
+The tool will do nothing when one of the following libraries is required (use the available version), otherwise will add the library to the file
+
+packages/<package>/<name>/requirements.txt
+
+- requests
+- ollama
+- openai
+- pymilvus
+- redis
+- pyyaml
+- boto3
+- psycopg
+- beautifulsoup4
+- pillow
+- nltk
+- httplib2
+- kafka_python
+- python-dateutil
+- scrapy
+- simplejson
+- twisted
+- netifaces
+- pymongo
+- minio
+- langdetect
+- plotly
+- joblib
+- lightgbm
+- feedparser
+- numpy
+- scikit-learn
+- langchain
+- langchain-ollama
+- langchain-openai
+- bcrypt